citu_app.py 179 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556
  1. # 给dataops 对话助手返回结果
  2. # 初始化日志系统 - 必须在最前面
  3. from core.logging import initialize_logging, get_app_logger, set_log_context, clear_log_context
  4. initialize_logging()
  5. from vanna.flask import VannaFlaskApp
  6. from core.vanna_llm_factory import create_vanna_instance
  7. from flask import request, jsonify
  8. import pandas as pd
  9. import common.result as result
  10. from datetime import datetime, timedelta
  11. from common.session_aware_cache import ConversationAwareMemoryCache
  12. from app_config import API_MAX_RETURN_ROWS, ENABLE_RESULT_SUMMARY
  13. import re
  14. import chainlit as cl
  15. import json
  16. from flask import session # 添加session导入
  17. import sqlparse # 用于SQL语法检查
  18. from common.redis_conversation_manager import RedisConversationManager # 添加Redis对话管理器导入
  19. from common.qa_feedback_manager import QAFeedbackManager
  20. from common.result import ( # 统一导入所有需要的响应函数
  21. success_response, bad_request_response, not_found_response, internal_error_response,
  22. error_response, service_unavailable_response,
  23. agent_success_response, agent_error_response,
  24. validation_failed_response
  25. )
  26. from app_config import ( # 添加Redis相关配置导入
  27. USER_MAX_CONVERSATIONS,
  28. CONVERSATION_CONTEXT_COUNT,
  29. DEFAULT_ANONYMOUS_USER,
  30. ENABLE_QUESTION_ANSWER_CACHE
  31. )
  32. # 创建app logger
  33. logger = get_app_logger("CituApp")
  34. # 设置默认的最大返回行数
  35. DEFAULT_MAX_RETURN_ROWS = 200
  36. MAX_RETURN_ROWS = API_MAX_RETURN_ROWS if API_MAX_RETURN_ROWS is not None else DEFAULT_MAX_RETURN_ROWS
  37. vn = create_vanna_instance()
  38. # 创建对话感知的缓存
  39. conversation_cache = ConversationAwareMemoryCache()
  40. # 实例化 VannaFlaskApp,使用自定义缓存
  41. app = VannaFlaskApp(
  42. vn,
  43. cache=conversation_cache, # 使用对话感知的缓存
  44. title="辞图智能数据问答平台",
  45. logo = "https://www.citupro.com/img/logo-black-2.png",
  46. subtitle="让 AI 为你写 SQL",
  47. chart=False,
  48. allow_llm_to_see_data=True,
  49. ask_results_correct=True,
  50. followup_questions=True,
  51. debug=True
  52. )
  53. # 创建Redis对话管理器实例
  54. redis_conversation_manager = RedisConversationManager()
  55. # 修改ask接口,支持前端传递conversation_id
  56. @app.flask_app.route('/api/v0/ask', methods=['POST'])
  57. def ask_full():
  58. req = request.get_json(force=True)
  59. question = req.get("question", None)
  60. conversation_id = req.get("conversation_id", None) # 前端传递的对话ID
  61. user_id = req.get("user_id", None) # 前端传递的用户ID
  62. if not question:
  63. from common.result import bad_request_response
  64. return jsonify(bad_request_response(
  65. response_text="缺少必需参数:question",
  66. missing_params=["question"]
  67. )), 400
  68. # 如果没有传递user_id,使用默认值guest
  69. if not user_id:
  70. user_id = "guest"
  71. # 如果前端没有传递conversation_id,则生成新的
  72. if not conversation_id:
  73. conversation_id = app.cache.generate_id(question=question, user_id=user_id)
  74. try:
  75. sql, df, _ = vn.ask(
  76. question=question,
  77. print_results=False,
  78. visualize=False,
  79. allow_llm_to_see_data=True
  80. )
  81. # 关键:检查是否有LLM解释性文本(无法生成SQL的情况)
  82. if sql is None and hasattr(vn, 'last_llm_explanation') and vn.last_llm_explanation:
  83. # 在解释性文本末尾添加提示语
  84. explanation_message = vn.last_llm_explanation + "请尝试提问其它问题。"
  85. # 使用标准化错误响应
  86. from common.result import validation_failed_response
  87. return jsonify(validation_failed_response(
  88. response_text=explanation_message
  89. )), 422 # 修改HTTP状态码为422
  90. # 如果sql为None但没有解释性文本,返回通用错误
  91. if sql is None:
  92. from common.result import validation_failed_response
  93. return jsonify(validation_failed_response(
  94. response_text="无法生成SQL查询,请检查问题描述或数据表结构"
  95. )), 422
  96. # 处理返回数据 - 使用新的query_result结构
  97. query_result = {
  98. "rows": [],
  99. "columns": [],
  100. "row_count": 0,
  101. "is_limited": False,
  102. "total_row_count": 0
  103. }
  104. summary = None
  105. if isinstance(df, pd.DataFrame):
  106. query_result["columns"] = list(df.columns)
  107. if not df.empty:
  108. total_rows = len(df)
  109. limited_df = df.head(MAX_RETURN_ROWS)
  110. query_result["rows"] = limited_df.to_dict(orient="records")
  111. query_result["row_count"] = len(limited_df)
  112. query_result["total_row_count"] = total_rows
  113. query_result["is_limited"] = total_rows > MAX_RETURN_ROWS
  114. # 生成数据摘要(可通过配置控制,仅在有数据时生成)
  115. if ENABLE_RESULT_SUMMARY:
  116. try:
  117. summary = vn.generate_summary(question=question, df=df)
  118. logger.info(f"成功生成摘要: {summary}")
  119. except Exception as e:
  120. logger.warning(f"生成摘要失败: {str(e)}")
  121. summary = None
  122. # 构建返回数据
  123. response_data = {
  124. "sql": sql,
  125. "query_result": query_result,
  126. "conversation_id": conversation_id
  127. }
  128. # 添加摘要(如果启用且生成成功)
  129. if ENABLE_RESULT_SUMMARY and summary is not None:
  130. response_data["summary"] = summary
  131. response_data["response"] = summary # 同时添加response字段
  132. from common.result import success_response
  133. return jsonify(success_response(
  134. response_text="查询执行完成" if summary is None else None,
  135. data=response_data
  136. ))
  137. except Exception as e:
  138. logger.error(f"ask_full执行失败: {str(e)}")
  139. # 即使发生异常,也检查是否有业务层面的解释
  140. if hasattr(vn, 'last_llm_explanation') and vn.last_llm_explanation:
  141. # 在解释性文本末尾添加提示语
  142. explanation_message = vn.last_llm_explanation + "请尝试提问其它问题。"
  143. from common.result import validation_failed_response
  144. return jsonify(validation_failed_response(
  145. response_text=explanation_message
  146. )), 422
  147. else:
  148. # 技术错误,使用500错误码
  149. from common.result import internal_error_response
  150. return jsonify(internal_error_response(
  151. response_text="查询处理失败,请稍后重试"
  152. )), 500
  153. @app.flask_app.route('/api/v0/citu_run_sql', methods=['POST'])
  154. def citu_run_sql():
  155. req = request.get_json(force=True)
  156. sql = req.get('sql')
  157. if not sql:
  158. from common.result import bad_request_response
  159. return jsonify(bad_request_response(
  160. response_text="缺少必需参数:sql",
  161. missing_params=["sql"]
  162. )), 400
  163. try:
  164. df = vn.run_sql(sql)
  165. # 处理返回数据 - 使用新的query_result结构
  166. query_result = {
  167. "rows": [],
  168. "columns": [],
  169. "row_count": 0,
  170. "is_limited": False,
  171. "total_row_count": 0
  172. }
  173. if isinstance(df, pd.DataFrame):
  174. query_result["columns"] = list(df.columns)
  175. if not df.empty:
  176. total_rows = len(df)
  177. limited_df = df.head(MAX_RETURN_ROWS)
  178. query_result["rows"] = limited_df.to_dict(orient="records")
  179. query_result["row_count"] = len(limited_df)
  180. query_result["total_row_count"] = total_rows
  181. query_result["is_limited"] = total_rows > MAX_RETURN_ROWS
  182. from common.result import success_response
  183. return jsonify(success_response(
  184. response_text=f"SQL执行完成,共返回 {query_result['total_row_count']} 条记录" +
  185. (f",已限制显示前 {MAX_RETURN_ROWS} 条" if query_result["is_limited"] else ""),
  186. data={
  187. "sql": sql,
  188. "query_result": query_result
  189. }
  190. ))
  191. except Exception as e:
  192. logger.error(f"citu_run_sql执行失败: {str(e)}")
  193. from common.result import internal_error_response
  194. return jsonify(internal_error_response(
  195. response_text=f"SQL执行失败,请检查SQL语句是否正确"
  196. )), 500
  197. @app.flask_app.route('/api/v0/ask_cached', methods=['POST'])
  198. def ask_cached():
  199. """
  200. 带缓存功能的智能查询接口
  201. 支持会话管理和结果缓存,提高查询效率
  202. """
  203. req = request.get_json(force=True)
  204. question = req.get("question", None)
  205. conversation_id = req.get("conversation_id", None)
  206. user_id = req.get("user_id", None)
  207. if not question:
  208. from common.result import bad_request_response
  209. return jsonify(bad_request_response(
  210. response_text="缺少必需参数:question",
  211. missing_params=["question"]
  212. )), 400
  213. # 如果没有传递user_id,使用默认值guest
  214. if not user_id:
  215. user_id = "guest"
  216. try:
  217. # 生成conversation_id
  218. # 调试:查看generate_id的实际行为
  219. logger.debug(f"输入问题: '{question}'")
  220. conversation_id = app.cache.generate_id(question=question, user_id=user_id)
  221. logger.debug(f"生成的conversation_id: {conversation_id}")
  222. # 再次用相同问题测试
  223. conversation_id2 = app.cache.generate_id(question=question, user_id=user_id)
  224. logger.debug(f"再次生成的conversation_id: {conversation_id2}")
  225. logger.debug(f"两次ID是否相同: {conversation_id == conversation_id2}")
  226. # 检查缓存
  227. cached_sql = app.cache.get(id=conversation_id, field="sql")
  228. if cached_sql is not None:
  229. # 缓存命中
  230. logger.info(f"[CACHE HIT] 使用缓存结果: {conversation_id}")
  231. sql = cached_sql
  232. df = app.cache.get(id=conversation_id, field="df")
  233. summary = app.cache.get(id=conversation_id, field="summary")
  234. else:
  235. # 缓存未命中,执行新查询
  236. logger.info(f"[CACHE MISS] 执行新查询: {conversation_id}")
  237. sql, df, _ = vn.ask(
  238. question=question,
  239. print_results=False,
  240. visualize=False,
  241. allow_llm_to_see_data=True
  242. )
  243. # 检查是否有LLM解释性文本(无法生成SQL的情况)
  244. if sql is None and hasattr(vn, 'last_llm_explanation') and vn.last_llm_explanation:
  245. # 在解释性文本末尾添加提示语
  246. explanation_message = vn.last_llm_explanation + "请尝试用其它方式提问。"
  247. from common.result import validation_failed_response
  248. return jsonify(validation_failed_response(
  249. response_text=explanation_message
  250. )), 422
  251. # 如果sql为None但没有解释性文本,返回通用错误
  252. if sql is None:
  253. from common.result import validation_failed_response
  254. return jsonify(validation_failed_response(
  255. response_text="无法生成SQL查询,请检查问题描述或数据表结构"
  256. )), 422
  257. # 缓存结果
  258. app.cache.set(id=conversation_id, field="question", value=question)
  259. app.cache.set(id=conversation_id, field="sql", value=sql)
  260. app.cache.set(id=conversation_id, field="df", value=df)
  261. # 生成并缓存摘要(可通过配置控制,仅在有数据时生成)
  262. summary = None
  263. if ENABLE_RESULT_SUMMARY and isinstance(df, pd.DataFrame) and not df.empty:
  264. try:
  265. summary = vn.generate_summary(question=question, df=df)
  266. logger.info(f"成功生成摘要: {summary}")
  267. except Exception as e:
  268. logger.warning(f"生成摘要失败: {str(e)}")
  269. summary = None
  270. app.cache.set(id=conversation_id, field="summary", value=summary)
  271. # 处理返回数据 - 使用新的query_result结构
  272. query_result = {
  273. "rows": [],
  274. "columns": [],
  275. "row_count": 0,
  276. "is_limited": False,
  277. "total_row_count": 0
  278. }
  279. if isinstance(df, pd.DataFrame):
  280. query_result["columns"] = list(df.columns)
  281. if not df.empty:
  282. total_rows = len(df)
  283. limited_df = df.head(MAX_RETURN_ROWS)
  284. query_result["rows"] = limited_df.to_dict(orient="records")
  285. query_result["row_count"] = len(limited_df)
  286. query_result["total_row_count"] = total_rows
  287. query_result["is_limited"] = total_rows > MAX_RETURN_ROWS
  288. # 构建返回数据
  289. response_data = {
  290. "sql": sql,
  291. "query_result": query_result,
  292. "conversation_id": conversation_id,
  293. "cached": cached_sql is not None # 标识是否来自缓存
  294. }
  295. # 添加摘要(如果启用且生成成功)
  296. if ENABLE_RESULT_SUMMARY and summary is not None:
  297. response_data["summary"] = summary
  298. response_data["response"] = summary # 同时添加response字段
  299. from common.result import success_response
  300. return jsonify(success_response(
  301. response_text="查询执行完成" if summary is None else None,
  302. data=response_data
  303. ))
  304. except Exception as e:
  305. logger.error(f"ask_cached执行失败: {str(e)}")
  306. from common.result import internal_error_response
  307. return jsonify(internal_error_response(
  308. response_text="查询处理失败,请稍后重试"
  309. )), 500
  310. @app.flask_app.route('/api/v0/citu_train_question_sql', methods=['POST'])
  311. def citu_train_question_sql():
  312. """
  313. 训练问题-SQL对接口
  314. 此API将接收的question/sql pair写入到training库中,用于训练和改进AI模型。
  315. 支持仅传入SQL或同时传入问题和SQL进行训练。
  316. Args:
  317. question (str, optional): 用户问题
  318. sql (str, required): 对应的SQL查询语句
  319. Returns:
  320. JSON: 包含训练ID和成功消息的响应
  321. """
  322. try:
  323. req = request.get_json(force=True)
  324. question = req.get('question')
  325. sql = req.get('sql')
  326. if not sql:
  327. from common.result import bad_request_response
  328. return jsonify(bad_request_response(
  329. response_text="缺少必需参数:sql",
  330. missing_params=["sql"]
  331. )), 400
  332. # 正确的调用方式:同时传递question和sql
  333. if question:
  334. training_id = vn.train(question=question, sql=sql)
  335. logger.info(f"训练成功,训练ID为:{training_id},问题:{question},SQL:{sql}")
  336. else:
  337. training_id = vn.train(sql=sql)
  338. logger.info(f"训练成功,训练ID为:{training_id},SQL:{sql}")
  339. from common.result import success_response
  340. return jsonify(success_response(
  341. response_text="问题-SQL对训练成功",
  342. data={
  343. "training_id": training_id,
  344. "message": "Question-SQL pair trained successfully"
  345. }
  346. ))
  347. except Exception as e:
  348. from common.result import internal_error_response
  349. return jsonify(internal_error_response(
  350. response_text="训练失败,请稍后重试"
  351. )), 500
  352. # ============ LangGraph Agent 集成 ============
  353. # 全局Agent实例(单例模式)
  354. citu_langraph_agent = None
  355. def get_citu_langraph_agent():
  356. """获取LangGraph Agent实例(懒加载)"""
  357. global citu_langraph_agent
  358. if citu_langraph_agent is None:
  359. try:
  360. from agent.citu_agent import CituLangGraphAgent
  361. logger.info("开始创建LangGraph Agent实例...")
  362. citu_langraph_agent = CituLangGraphAgent()
  363. logger.info("LangGraph Agent实例创建成功")
  364. except ImportError as e:
  365. logger.critical(f"Agent模块导入失败: {str(e)}")
  366. logger.critical("请检查agent模块是否存在以及依赖是否正确安装")
  367. raise Exception(f"Agent模块导入失败: {str(e)}")
  368. except Exception as e:
  369. logger.critical(f"LangGraph Agent实例创建失败: {str(e)}")
  370. logger.critical(f"错误类型: {type(e).__name__}")
  371. # 提供更有用的错误信息
  372. if "config" in str(e).lower():
  373. logger.critical("可能是配置文件问题,请检查配置")
  374. elif "llm" in str(e).lower():
  375. logger.critical("可能是LLM连接问题,请检查LLM配置")
  376. elif "tool" in str(e).lower():
  377. logger.critical("可能是工具加载问题,请检查工具模块")
  378. raise Exception(f"Agent初始化失败: {str(e)}")
  379. return citu_langraph_agent
  380. @app.flask_app.route('/api/v0/ask_agent', methods=['POST'])
  381. def ask_agent():
  382. """
  383. 支持对话上下文的ask_agent API - 修正版
  384. """
  385. req = request.get_json(force=True)
  386. question = req.get("question", None)
  387. conversation_id_input = req.get("conversation_id", None)
  388. # 新增参数解析
  389. user_id_input = req.get("user_id", None)
  390. continue_conversation = req.get("continue_conversation", False)
  391. # 新增:路由模式参数解析和验证
  392. api_routing_mode = req.get("routing_mode", None)
  393. VALID_ROUTING_MODES = ["database_direct", "chat_direct", "hybrid", "llm_only"]
  394. if not question:
  395. return jsonify(bad_request_response(
  396. response_text="缺少必需参数:question",
  397. missing_params=["question"]
  398. )), 400
  399. # 验证routing_mode参数
  400. if api_routing_mode and api_routing_mode not in VALID_ROUTING_MODES:
  401. return jsonify(bad_request_response(
  402. response_text=f"无效的routing_mode参数值: {api_routing_mode},支持的值: {VALID_ROUTING_MODES}",
  403. invalid_params=["routing_mode"]
  404. )), 400
  405. try:
  406. # 1. 获取登录用户ID(修正:在函数中获取session信息)
  407. login_user_id = session.get('user_id') if 'user_id' in session else None
  408. # 2. 用户ID和对话ID一致性校验
  409. from common.session_aware_cache import ConversationAwareMemoryCache
  410. # 2.1 如果传递了conversation_id,从中解析user_id
  411. extracted_user_id = None
  412. if conversation_id_input:
  413. extracted_user_id = ConversationAwareMemoryCache.extract_user_id(conversation_id_input)
  414. # 如果同时传递了user_id和conversation_id,进行一致性校验
  415. if user_id_input:
  416. is_valid, error_msg = ConversationAwareMemoryCache.validate_user_id_consistency(
  417. conversation_id_input, user_id_input
  418. )
  419. if not is_valid:
  420. return jsonify(bad_request_response(
  421. response_text=error_msg,
  422. invalid_params=["user_id", "conversation_id"]
  423. )), 400
  424. # 如果没有传递user_id,但有conversation_id,则从conversation_id中解析
  425. elif not user_id_input and extracted_user_id:
  426. user_id_input = extracted_user_id
  427. logger.info(f"从conversation_id解析出user_id: {user_id_input}")
  428. # 2.2 如果没有传递user_id,使用默认值guest
  429. if not user_id_input:
  430. user_id_input = "guest"
  431. logger.info("未传递user_id,使用默认值: guest")
  432. # 3. 智能ID解析(修正:传入登录用户ID)
  433. user_id = redis_conversation_manager.resolve_user_id(
  434. user_id_input, None, request.remote_addr, login_user_id
  435. )
  436. conversation_id, conversation_status = redis_conversation_manager.resolve_conversation_id(
  437. user_id, conversation_id_input, continue_conversation
  438. )
  439. # 3. 获取上下文和上下文类型(提前到缓存检查之前)
  440. context = redis_conversation_manager.get_context(conversation_id)
  441. # 获取上下文类型:从最后一条助手消息的metadata中获取类型
  442. context_type = None
  443. if context:
  444. try:
  445. # 获取最后一条助手消息的metadata
  446. messages = redis_conversation_manager.get_messages(conversation_id, limit=10)
  447. for message in reversed(messages): # 从最新的开始找
  448. if message.get("role") == "assistant":
  449. metadata = message.get("metadata", {})
  450. context_type = metadata.get("type")
  451. if context_type:
  452. logger.info(f"[AGENT_API] 检测到上下文类型: {context_type}")
  453. break
  454. except Exception as e:
  455. logger.warning(f"获取上下文类型失败: {str(e)}")
  456. # 4. 检查缓存(新逻辑:放宽使用条件,严控存储条件)
  457. cached_answer = redis_conversation_manager.get_cached_answer(question, context)
  458. if cached_answer:
  459. logger.info(f"[AGENT_API] 使用缓存答案")
  460. # 确定缓存答案的助手回复内容(使用与非缓存相同的优先级逻辑)
  461. cached_response_type = cached_answer.get("type", "UNKNOWN")
  462. if cached_response_type == "DATABASE":
  463. # DATABASE类型:按优先级选择内容
  464. if cached_answer.get("response"):
  465. # 优先级1:错误或解释性回复(如SQL生成失败)
  466. assistant_response = cached_answer.get("response")
  467. elif cached_answer.get("summary"):
  468. # 优先级2:查询成功的摘要
  469. assistant_response = cached_answer.get("summary")
  470. elif cached_answer.get("query_result"):
  471. # 优先级3:构造简单描述
  472. query_result = cached_answer.get("query_result")
  473. row_count = query_result.get("row_count", 0)
  474. assistant_response = f"查询执行完成,共返回 {row_count} 条记录。"
  475. else:
  476. # 异常情况
  477. assistant_response = "数据库查询已处理。"
  478. else:
  479. # CHAT类型:直接使用response
  480. assistant_response = cached_answer.get("response", "")
  481. # 更新对话历史
  482. redis_conversation_manager.save_message(conversation_id, "user", question)
  483. redis_conversation_manager.save_message(
  484. conversation_id, "assistant",
  485. assistant_response,
  486. metadata={"from_cache": True}
  487. )
  488. # 添加对话信息到缓存结果
  489. cached_answer["conversation_id"] = conversation_id
  490. cached_answer["user_id"] = user_id
  491. cached_answer["from_cache"] = True
  492. cached_answer.update(conversation_status)
  493. # 使用agent_success_response返回标准格式
  494. return jsonify(agent_success_response(
  495. response_type=cached_answer.get("type", "UNKNOWN"),
  496. response=cached_answer.get("response", ""), # 修正:使用response而不是response_text
  497. sql=cached_answer.get("sql"),
  498. records=cached_answer.get("query_result"), # 修改:query_result改为records
  499. summary=cached_answer.get("summary"),
  500. conversation_id=conversation_id,
  501. execution_path=cached_answer.get("execution_path", []),
  502. classification_info=cached_answer.get("classification_info", {}),
  503. user_id=user_id,
  504. is_guest_user=(user_id == DEFAULT_ANONYMOUS_USER),
  505. context_used=bool(context),
  506. from_cache=True,
  507. conversation_status=conversation_status["status"],
  508. conversation_message=conversation_status["message"],
  509. requested_conversation_id=conversation_status.get("requested_id")
  510. ))
  511. # 5. 保存用户消息
  512. redis_conversation_manager.save_message(conversation_id, "user", question)
  513. # 6. 构建带上下文的问题
  514. if context:
  515. enhanced_question = f"\n[CONTEXT]\n{context}\n\n[CURRENT]\n{question}"
  516. logger.info(f"[AGENT_API] 使用上下文,长度: {len(context)}字符")
  517. else:
  518. enhanced_question = question
  519. logger.info(f"[AGENT_API] 新对话,无上下文")
  520. # 7. 确定最终使用的路由模式(优先级逻辑)
  521. if api_routing_mode:
  522. # API传了参数,优先使用
  523. effective_routing_mode = api_routing_mode
  524. logger.info(f"[AGENT_API] 使用API指定的路由模式: {effective_routing_mode}")
  525. else:
  526. # API没传参数,使用配置文件
  527. try:
  528. from app_config import QUESTION_ROUTING_MODE
  529. effective_routing_mode = QUESTION_ROUTING_MODE
  530. logger.info(f"[AGENT_API] 使用配置文件路由模式: {effective_routing_mode}")
  531. except ImportError:
  532. effective_routing_mode = "hybrid"
  533. logger.info(f"[AGENT_API] 配置文件读取失败,使用默认路由模式: {effective_routing_mode}")
  534. # 8. 现有Agent处理逻辑(修改为传递路由模式)
  535. try:
  536. agent = get_citu_langraph_agent()
  537. except Exception as e:
  538. logger.critical(f"Agent初始化失败: {str(e)}")
  539. return jsonify(service_unavailable_response(
  540. response_text="AI服务暂时不可用,请稍后重试",
  541. can_retry=True
  542. )), 503
  543. # 异步调用Agent处理问题
  544. import asyncio
  545. agent_result = asyncio.run(agent.process_question(
  546. question=enhanced_question, # 使用增强后的问题
  547. conversation_id=conversation_id,
  548. context_type=context_type, # 传递上下文类型
  549. routing_mode=effective_routing_mode # 新增:传递路由模式
  550. ))
  551. # 8. 处理Agent结果
  552. if agent_result.get("success", False):
  553. # 修正:直接从agent_result获取字段,因为它就是final_response
  554. response_type = agent_result.get("type", "UNKNOWN")
  555. response_text = agent_result.get("response", "")
  556. sql = agent_result.get("sql")
  557. query_result = agent_result.get("query_result")
  558. summary = agent_result.get("summary")
  559. execution_path = agent_result.get("execution_path", [])
  560. classification_info = agent_result.get("classification_info", {})
  561. # 确定助手回复内容的优先级
  562. if response_type == "DATABASE":
  563. # DATABASE类型:按优先级选择内容
  564. if response_text:
  565. # 优先级1:错误或解释性回复(如SQL生成失败)
  566. assistant_response = response_text
  567. elif summary:
  568. # 优先级2:查询成功的摘要
  569. assistant_response = summary
  570. elif query_result:
  571. # 优先级3:构造简单描述
  572. row_count = query_result.get("row_count", 0)
  573. assistant_response = f"查询执行完成,共返回 {row_count} 条记录。"
  574. else:
  575. # 异常情况
  576. assistant_response = "数据库查询已处理。"
  577. else:
  578. # CHAT类型:直接使用response
  579. assistant_response = response_text
  580. # 保存助手回复
  581. redis_conversation_manager.save_message(
  582. conversation_id, "assistant", assistant_response,
  583. metadata={
  584. "type": response_type,
  585. "sql": sql,
  586. "execution_path": execution_path
  587. }
  588. )
  589. # 缓存成功的答案(新逻辑:只缓存无上下文的问答)
  590. # 直接缓存agent_result,它已经包含所有需要的字段
  591. redis_conversation_manager.cache_answer(question, agent_result, context)
  592. # 使用agent_success_response的正确方式
  593. return jsonify(agent_success_response(
  594. response_type=response_type,
  595. response=response_text, # 修正:使用response而不是response_text
  596. sql=sql,
  597. records=query_result, # 修改:query_result改为records
  598. summary=summary,
  599. conversation_id=conversation_id,
  600. execution_path=execution_path,
  601. classification_info=classification_info,
  602. user_id=user_id,
  603. is_guest_user=(user_id == DEFAULT_ANONYMOUS_USER),
  604. context_used=bool(context),
  605. from_cache=False,
  606. conversation_status=conversation_status["status"],
  607. conversation_message=conversation_status["message"],
  608. requested_conversation_id=conversation_status.get("requested_id"),
  609. routing_mode_used=effective_routing_mode, # 新增:实际使用的路由模式
  610. routing_mode_source="api" if api_routing_mode else "config" # 新增:路由模式来源
  611. ))
  612. else:
  613. # 错误处理(修正:确保使用现有的错误响应格式)
  614. error_message = agent_result.get("error", "Agent处理失败")
  615. error_code = agent_result.get("error_code", 500)
  616. return jsonify(agent_error_response(
  617. response_text=error_message,
  618. error_type="agent_processing_failed",
  619. code=error_code,
  620. conversation_id=conversation_id,
  621. user_id=user_id
  622. )), error_code
  623. except Exception as e:
  624. logger.error(f"ask_agent执行失败: {str(e)}")
  625. return jsonify(internal_error_response(
  626. response_text="查询处理失败,请稍后重试"
  627. )), 500
  628. @app.flask_app.route('/api/v0/agent_health', methods=['GET'])
  629. def agent_health():
  630. """
  631. Agent健康检查接口
  632. 响应格式:
  633. {
  634. "success": true/false,
  635. "code": 200/503,
  636. "message": "healthy/degraded/unhealthy",
  637. "data": {
  638. "status": "healthy/degraded/unhealthy",
  639. "test_result": true/false,
  640. "workflow_compiled": true/false,
  641. "tools_count": 4,
  642. "message": "详细信息",
  643. "timestamp": "2024-01-01T12:00:00",
  644. "checks": {
  645. "agent_creation": true/false,
  646. "tools_import": true/false,
  647. "llm_connection": true/false,
  648. "classifier_ready": true/false
  649. }
  650. }
  651. }
  652. """
  653. try:
  654. # 基础健康检查
  655. health_data = {
  656. "status": "unknown",
  657. "test_result": False,
  658. "workflow_compiled": False,
  659. "tools_count": 0,
  660. "message": "",
  661. "timestamp": datetime.now().isoformat(),
  662. "checks": {
  663. "agent_creation": False,
  664. "tools_import": False,
  665. "llm_connection": False,
  666. "classifier_ready": False
  667. }
  668. }
  669. # 检查1: Agent创建
  670. try:
  671. agent = get_citu_langraph_agent()
  672. health_data["checks"]["agent_creation"] = True
  673. # 修正:Agent现在是动态创建workflow的,不再有预创建的workflow属性
  674. health_data["workflow_compiled"] = True # 动态创建,始终可用
  675. health_data["tools_count"] = len(agent.tools) if hasattr(agent, 'tools') else 0
  676. except Exception as e:
  677. health_data["message"] = f"Agent创建失败: {str(e)}"
  678. health_data["status"] = "unhealthy" # 设置状态
  679. from common.result import health_error_response
  680. return jsonify(health_error_response(**health_data)), 503
  681. # 检查2: 工具导入
  682. try:
  683. from agent.tools import TOOLS
  684. health_data["checks"]["tools_import"] = len(TOOLS) > 0
  685. except Exception as e:
  686. health_data["message"] = f"工具导入失败: {str(e)}"
  687. # 检查3: LLM连接(简单测试)
  688. try:
  689. from agent.tools.utils import get_compatible_llm
  690. llm = get_compatible_llm()
  691. health_data["checks"]["llm_connection"] = llm is not None
  692. except Exception as e:
  693. health_data["message"] = f"LLM连接失败: {str(e)}"
  694. # 检查4: 分类器准备
  695. try:
  696. from agent.classifier import QuestionClassifier
  697. classifier = QuestionClassifier()
  698. health_data["checks"]["classifier_ready"] = True
  699. except Exception as e:
  700. health_data["message"] = f"分类器失败: {str(e)}"
  701. # 检查5: 完整流程测试(可选)
  702. try:
  703. if all(health_data["checks"].values()):
  704. import asyncio
  705. # 异步调用健康检查
  706. test_result = asyncio.run(agent.health_check())
  707. health_data["test_result"] = test_result.get("status") == "healthy"
  708. health_data["status"] = test_result.get("status", "unknown")
  709. health_data["message"] = test_result.get("message", "健康检查完成")
  710. else:
  711. health_data["status"] = "degraded"
  712. health_data["message"] = "部分组件异常"
  713. except Exception as e:
  714. logger.error(f"健康检查异常: {str(e)}")
  715. import traceback
  716. logger.error(f"详细健康检查错误: {traceback.format_exc()}")
  717. health_data["status"] = "degraded"
  718. health_data["message"] = f"完整测试失败: {str(e)}"
  719. # 根据状态返回相应的HTTP代码 - 使用标准化健康检查响应
  720. from common.result import health_success_response, health_error_response
  721. if health_data["status"] == "healthy":
  722. return jsonify(health_success_response(**health_data))
  723. elif health_data["status"] == "degraded":
  724. return jsonify(health_error_response(**health_data)), 503
  725. else:
  726. # 确保状态设置为unhealthy
  727. health_data["status"] = "unhealthy"
  728. return jsonify(health_error_response(**health_data)), 503
  729. except Exception as e:
  730. logger.error(f"顶层健康检查异常: {str(e)}")
  731. import traceback
  732. logger.error(f"详细错误信息: {traceback.format_exc()}")
  733. from common.result import internal_error_response
  734. return jsonify(internal_error_response(
  735. response_text="健康检查失败,请稍后重试"
  736. )), 500
  737. # ==================== 日常管理API ====================
  738. @app.flask_app.route('/api/v0/cache_overview', methods=['GET'])
  739. def cache_overview():
  740. """日常管理:轻量概览 - 合并原cache_inspect的核心功能"""
  741. try:
  742. cache = app.cache
  743. result_data = {
  744. 'overview_summary': {
  745. 'total_conversations': 0,
  746. 'total_sessions': 0,
  747. 'query_time': datetime.now().isoformat()
  748. },
  749. 'recent_conversations': [], # 最近的对话
  750. 'session_summary': [] # 会话摘要
  751. }
  752. if hasattr(cache, 'cache') and isinstance(cache.cache, dict):
  753. result_data['overview_summary']['total_conversations'] = len(cache.cache)
  754. # 获取会话信息
  755. if hasattr(cache, 'get_all_sessions'):
  756. all_sessions = cache.get_all_sessions()
  757. result_data['overview_summary']['total_sessions'] = len(all_sessions)
  758. # 会话摘要(按最近活动排序)
  759. session_list = []
  760. for session_id, session_data in all_sessions.items():
  761. session_summary = {
  762. 'session_id': session_id,
  763. 'start_time': session_data['start_time'].isoformat(),
  764. 'conversation_count': session_data.get('conversation_count', 0),
  765. 'duration_seconds': session_data.get('session_duration_seconds', 0),
  766. 'last_activity': session_data.get('last_activity', session_data['start_time']).isoformat(),
  767. 'is_active': (datetime.now() - session_data.get('last_activity', session_data['start_time'])).total_seconds() < 1800 # 30分钟内活跃
  768. }
  769. session_list.append(session_summary)
  770. # 按最后活动时间排序
  771. session_list.sort(key=lambda x: x['last_activity'], reverse=True)
  772. result_data['session_summary'] = session_list
  773. # 最近的对话(最多显示10个)
  774. conversation_list = []
  775. for conversation_id, conversation_data in cache.cache.items():
  776. conversation_start_time = cache.conversation_start_times.get(conversation_id)
  777. conversation_info = {
  778. 'conversation_id': conversation_id,
  779. 'conversation_start_time': conversation_start_time.isoformat() if conversation_start_time else None,
  780. 'session_id': cache.conversation_to_session.get(conversation_id),
  781. 'has_question': 'question' in conversation_data,
  782. 'has_sql': 'sql' in conversation_data,
  783. 'has_data': 'df' in conversation_data and conversation_data['df'] is not None,
  784. 'question_preview': conversation_data.get('question', '')[:80] + '...' if len(conversation_data.get('question', '')) > 80 else conversation_data.get('question', ''),
  785. }
  786. # 计算对话持续时间
  787. if conversation_start_time:
  788. duration = datetime.now() - conversation_start_time
  789. conversation_info['conversation_duration_seconds'] = duration.total_seconds()
  790. conversation_list.append(conversation_info)
  791. # 按对话开始时间排序,显示最新的10个
  792. conversation_list.sort(key=lambda x: x['conversation_start_time'] or '', reverse=True)
  793. result_data['recent_conversations'] = conversation_list[:10]
  794. from common.result import success_response
  795. return jsonify(success_response(
  796. response_text="缓存概览查询完成",
  797. data=result_data
  798. ))
  799. except Exception as e:
  800. from common.result import internal_error_response
  801. return jsonify(internal_error_response(
  802. response_text="获取缓存概览失败,请稍后重试"
  803. )), 500
  804. @app.flask_app.route('/api/v0/cache_stats', methods=['GET'])
  805. def cache_stats():
  806. """日常管理:统计信息 - 合并原session_stats和cache_stats功能"""
  807. try:
  808. cache = app.cache
  809. current_time = datetime.now()
  810. stats = {
  811. 'basic_stats': {
  812. 'total_sessions': len(getattr(cache, 'session_info', {})),
  813. 'total_conversations': len(getattr(cache, 'cache', {})),
  814. 'active_sessions': 0, # 最近30分钟有活动
  815. 'average_conversations_per_session': 0
  816. },
  817. 'time_distribution': {
  818. 'sessions': {
  819. 'last_1_hour': 0,
  820. 'last_6_hours': 0,
  821. 'last_24_hours': 0,
  822. 'last_7_days': 0,
  823. 'older': 0
  824. },
  825. 'conversations': {
  826. 'last_1_hour': 0,
  827. 'last_6_hours': 0,
  828. 'last_24_hours': 0,
  829. 'last_7_days': 0,
  830. 'older': 0
  831. }
  832. },
  833. 'session_details': [],
  834. 'time_ranges': {
  835. 'oldest_session': None,
  836. 'newest_session': None,
  837. 'oldest_conversation': None,
  838. 'newest_conversation': None
  839. }
  840. }
  841. # 会话统计
  842. if hasattr(cache, 'session_info'):
  843. session_times = []
  844. total_conversations = 0
  845. for session_id, session_data in cache.session_info.items():
  846. start_time = session_data['start_time']
  847. session_times.append(start_time)
  848. conversation_count = len(session_data.get('conversations', []))
  849. total_conversations += conversation_count
  850. # 检查活跃状态
  851. last_activity = session_data.get('last_activity', session_data['start_time'])
  852. if (current_time - last_activity).total_seconds() < 1800:
  853. stats['basic_stats']['active_sessions'] += 1
  854. # 时间分布统计
  855. age_hours = (current_time - start_time).total_seconds() / 3600
  856. if age_hours <= 1:
  857. stats['time_distribution']['sessions']['last_1_hour'] += 1
  858. elif age_hours <= 6:
  859. stats['time_distribution']['sessions']['last_6_hours'] += 1
  860. elif age_hours <= 24:
  861. stats['time_distribution']['sessions']['last_24_hours'] += 1
  862. elif age_hours <= 168: # 7 days
  863. stats['time_distribution']['sessions']['last_7_days'] += 1
  864. else:
  865. stats['time_distribution']['sessions']['older'] += 1
  866. # 会话详细信息
  867. session_duration = current_time - start_time
  868. stats['session_details'].append({
  869. 'session_id': session_id,
  870. 'start_time': start_time.isoformat(),
  871. 'last_activity': last_activity.isoformat(),
  872. 'conversation_count': conversation_count,
  873. 'duration_seconds': session_duration.total_seconds(),
  874. 'duration_formatted': str(session_duration),
  875. 'is_active': (current_time - last_activity).total_seconds() < 1800,
  876. 'browser_session_id': session_data.get('browser_session_id')
  877. })
  878. # 计算平均值
  879. if len(cache.session_info) > 0:
  880. stats['basic_stats']['average_conversations_per_session'] = total_conversations / len(cache.session_info)
  881. # 时间范围
  882. if session_times:
  883. stats['time_ranges']['oldest_session'] = min(session_times).isoformat()
  884. stats['time_ranges']['newest_session'] = max(session_times).isoformat()
  885. # 对话统计
  886. if hasattr(cache, 'conversation_start_times'):
  887. conversation_times = []
  888. for conv_time in cache.conversation_start_times.values():
  889. conversation_times.append(conv_time)
  890. age_hours = (current_time - conv_time).total_seconds() / 3600
  891. if age_hours <= 1:
  892. stats['time_distribution']['conversations']['last_1_hour'] += 1
  893. elif age_hours <= 6:
  894. stats['time_distribution']['conversations']['last_6_hours'] += 1
  895. elif age_hours <= 24:
  896. stats['time_distribution']['conversations']['last_24_hours'] += 1
  897. elif age_hours <= 168:
  898. stats['time_distribution']['conversations']['last_7_days'] += 1
  899. else:
  900. stats['time_distribution']['conversations']['older'] += 1
  901. if conversation_times:
  902. stats['time_ranges']['oldest_conversation'] = min(conversation_times).isoformat()
  903. stats['time_ranges']['newest_conversation'] = max(conversation_times).isoformat()
  904. # 按最近活动排序会话详情
  905. stats['session_details'].sort(key=lambda x: x['last_activity'], reverse=True)
  906. from common.result import success_response
  907. return jsonify(success_response(
  908. response_text="缓存统计信息查询完成",
  909. data=stats
  910. ))
  911. except Exception as e:
  912. from common.result import internal_error_response
  913. return jsonify(internal_error_response(
  914. response_text="获取缓存统计失败,请稍后重试"
  915. )), 500
  916. # ==================== 高级功能API ====================
  917. @app.flask_app.route('/api/v0/cache_export', methods=['GET'])
  918. def cache_export():
  919. """高级功能:完整导出 - 保持原cache_raw_export的完整功能"""
  920. try:
  921. cache = app.cache
  922. # 验证缓存的实际结构
  923. if not hasattr(cache, 'cache'):
  924. from common.result import internal_error_response
  925. return jsonify(internal_error_response(
  926. response_text="缓存对象结构异常,请联系系统管理员"
  927. )), 500
  928. if not isinstance(cache.cache, dict):
  929. from common.result import internal_error_response
  930. return jsonify(internal_error_response(
  931. response_text="缓存数据类型异常,请联系系统管理员"
  932. )), 500
  933. # 定义JSON序列化辅助函数
  934. def make_json_serializable(obj):
  935. """将对象转换为JSON可序列化的格式"""
  936. if obj is None:
  937. return None
  938. elif isinstance(obj, (str, int, float, bool)):
  939. return obj
  940. elif isinstance(obj, (list, tuple)):
  941. return [make_json_serializable(item) for item in obj]
  942. elif isinstance(obj, dict):
  943. return {str(k): make_json_serializable(v) for k, v in obj.items()}
  944. elif hasattr(obj, 'isoformat'): # datetime objects
  945. return obj.isoformat()
  946. elif hasattr(obj, 'item'): # numpy scalars
  947. return obj.item()
  948. elif hasattr(obj, 'tolist'): # numpy arrays
  949. return obj.tolist()
  950. elif hasattr(obj, '__dict__'): # pandas dtypes and other objects
  951. return str(obj)
  952. else:
  953. return str(obj)
  954. # 获取完整的原始缓存数据
  955. raw_cache = cache.cache
  956. # 获取会话和对话时间信息
  957. conversation_times = getattr(cache, 'conversation_start_times', {})
  958. session_info = getattr(cache, 'session_info', {})
  959. conversation_to_session = getattr(cache, 'conversation_to_session', {})
  960. export_data = {
  961. 'export_metadata': {
  962. 'export_time': datetime.now().isoformat(),
  963. 'total_conversations': len(raw_cache),
  964. 'total_sessions': len(session_info),
  965. 'cache_type': type(cache).__name__,
  966. 'cache_object_info': str(cache),
  967. 'has_session_times': bool(session_info),
  968. 'has_conversation_times': bool(conversation_times)
  969. },
  970. 'session_info': {
  971. session_id: {
  972. 'start_time': session_data['start_time'].isoformat(),
  973. 'last_activity': session_data.get('last_activity', session_data['start_time']).isoformat(),
  974. 'conversations': session_data['conversations'],
  975. 'conversation_count': len(session_data['conversations']),
  976. 'browser_session_id': session_data.get('browser_session_id'),
  977. 'user_info': session_data.get('user_info', {})
  978. }
  979. for session_id, session_data in session_info.items()
  980. },
  981. 'conversation_times': {
  982. conversation_id: start_time.isoformat()
  983. for conversation_id, start_time in conversation_times.items()
  984. },
  985. 'conversation_to_session_mapping': conversation_to_session,
  986. 'conversations': {}
  987. }
  988. # 处理每个对话的完整数据
  989. for conversation_id, conversation_data in raw_cache.items():
  990. # 获取时间信息
  991. conversation_start_time = conversation_times.get(conversation_id)
  992. session_id = conversation_to_session.get(conversation_id)
  993. session_start_time = None
  994. if session_id and session_id in session_info:
  995. session_start_time = session_info[session_id]['start_time']
  996. processed_conversation = {
  997. 'conversation_id': conversation_id,
  998. 'conversation_start_time': conversation_start_time.isoformat() if conversation_start_time else None,
  999. 'session_id': session_id,
  1000. 'session_start_time': session_start_time.isoformat() if session_start_time else None,
  1001. 'field_count': len(conversation_data),
  1002. 'fields': {}
  1003. }
  1004. # 添加时间计算
  1005. if conversation_start_time:
  1006. conversation_duration = datetime.now() - conversation_start_time
  1007. processed_conversation['conversation_duration_seconds'] = conversation_duration.total_seconds()
  1008. processed_conversation['conversation_duration_formatted'] = str(conversation_duration)
  1009. if session_start_time:
  1010. session_duration = datetime.now() - session_start_time
  1011. processed_conversation['session_duration_seconds'] = session_duration.total_seconds()
  1012. processed_conversation['session_duration_formatted'] = str(session_duration)
  1013. # 处理每个字段,确保JSON序列化安全
  1014. for field_name, field_value in conversation_data.items():
  1015. field_info = {
  1016. 'field_name': field_name,
  1017. 'data_type': type(field_value).__name__,
  1018. 'is_none': field_value is None
  1019. }
  1020. try:
  1021. if field_value is None:
  1022. field_info['value'] = None
  1023. elif field_name in ['conversation_start_time', 'session_start_time']:
  1024. # 处理时间字段
  1025. field_info['content'] = make_json_serializable(field_value)
  1026. elif field_name == 'df' and field_value is not None:
  1027. # DataFrame的安全处理
  1028. if hasattr(field_value, 'to_dict'):
  1029. # 安全地处理dtypes
  1030. try:
  1031. dtypes_dict = {}
  1032. for col, dtype in field_value.dtypes.items():
  1033. dtypes_dict[col] = str(dtype)
  1034. except Exception:
  1035. dtypes_dict = {"error": "无法序列化dtypes"}
  1036. # 安全地处理内存使用
  1037. try:
  1038. memory_usage = field_value.memory_usage(deep=True)
  1039. memory_dict = {}
  1040. for idx, usage in memory_usage.items():
  1041. memory_dict[str(idx)] = int(usage) if hasattr(usage, 'item') else int(usage)
  1042. except Exception:
  1043. memory_dict = {"error": "无法获取内存使用信息"}
  1044. field_info.update({
  1045. 'dataframe_info': {
  1046. 'shape': list(field_value.shape),
  1047. 'columns': list(field_value.columns),
  1048. 'dtypes': dtypes_dict,
  1049. 'index_info': {
  1050. 'type': type(field_value.index).__name__,
  1051. 'length': len(field_value.index)
  1052. }
  1053. },
  1054. 'data': make_json_serializable(field_value.to_dict('records')),
  1055. 'memory_usage': memory_dict
  1056. })
  1057. else:
  1058. field_info['value'] = str(field_value)
  1059. field_info['note'] = 'not_standard_dataframe'
  1060. elif field_name == 'fig_json':
  1061. # 图表JSON数据处理
  1062. if isinstance(field_value, str):
  1063. try:
  1064. import json
  1065. parsed_fig = json.loads(field_value)
  1066. field_info.update({
  1067. 'json_valid': True,
  1068. 'json_size_bytes': len(field_value),
  1069. 'plotly_structure': {
  1070. 'has_data': 'data' in parsed_fig,
  1071. 'has_layout': 'layout' in parsed_fig,
  1072. 'data_traces_count': len(parsed_fig.get('data', [])),
  1073. },
  1074. 'raw_json': field_value
  1075. })
  1076. except json.JSONDecodeError:
  1077. field_info.update({
  1078. 'json_valid': False,
  1079. 'raw_content': str(field_value)
  1080. })
  1081. else:
  1082. field_info['value'] = make_json_serializable(field_value)
  1083. elif field_name == 'followup_questions':
  1084. # 后续问题列表
  1085. field_info.update({
  1086. 'content': make_json_serializable(field_value)
  1087. })
  1088. elif field_name in ['question', 'sql', 'summary']:
  1089. # 文本字段
  1090. if isinstance(field_value, str):
  1091. field_info.update({
  1092. 'text_length': len(field_value),
  1093. 'content': field_value
  1094. })
  1095. else:
  1096. field_info['value'] = make_json_serializable(field_value)
  1097. else:
  1098. # 未知字段的安全处理
  1099. field_info['content'] = make_json_serializable(field_value)
  1100. except Exception as e:
  1101. field_info.update({
  1102. 'processing_error': str(e),
  1103. 'fallback_value': str(field_value)[:500] + '...' if len(str(field_value)) > 500 else str(field_value)
  1104. })
  1105. processed_conversation['fields'][field_name] = field_info
  1106. export_data['conversations'][conversation_id] = processed_conversation
  1107. # 添加缓存统计信息
  1108. field_frequency = {}
  1109. data_types_found = set()
  1110. total_dataframes = 0
  1111. total_questions = 0
  1112. for conv_data in export_data['conversations'].values():
  1113. for field_name, field_info in conv_data['fields'].items():
  1114. field_frequency[field_name] = field_frequency.get(field_name, 0) + 1
  1115. data_types_found.add(field_info['data_type'])
  1116. if field_name == 'df' and not field_info['is_none']:
  1117. total_dataframes += 1
  1118. if field_name == 'question' and not field_info['is_none']:
  1119. total_questions += 1
  1120. export_data['cache_statistics'] = {
  1121. 'field_frequency': field_frequency,
  1122. 'data_types_found': list(data_types_found),
  1123. 'total_dataframes': total_dataframes,
  1124. 'total_questions': total_questions,
  1125. 'has_session_timing': 'session_start_time' in field_frequency,
  1126. 'has_conversation_timing': 'conversation_start_time' in field_frequency
  1127. }
  1128. from common.result import success_response
  1129. return jsonify(success_response(
  1130. response_text="缓存数据导出完成",
  1131. data=export_data
  1132. ))
  1133. except Exception as e:
  1134. import traceback
  1135. error_details = {
  1136. 'error_message': str(e),
  1137. 'error_type': type(e).__name__,
  1138. 'traceback': traceback.format_exc()
  1139. }
  1140. from common.result import internal_error_response
  1141. return jsonify(internal_error_response(
  1142. response_text="导出缓存失败,请稍后重试"
  1143. )), 500
  1144. # ==================== 清理功能API ====================
  1145. @app.flask_app.route('/api/v0/cache_preview_cleanup', methods=['POST'])
  1146. def cache_preview_cleanup():
  1147. """清理功能:预览删除操作 - 保持原功能"""
  1148. try:
  1149. req = request.get_json(force=True)
  1150. # 时间条件 - 支持三种方式
  1151. older_than_hours = req.get('older_than_hours')
  1152. older_than_days = req.get('older_than_days')
  1153. before_timestamp = req.get('before_timestamp') # YYYY-MM-DD HH:MM:SS 格式
  1154. cache = app.cache
  1155. # 计算截止时间
  1156. cutoff_time = None
  1157. time_condition = None
  1158. if older_than_hours:
  1159. cutoff_time = datetime.now() - timedelta(hours=older_than_hours)
  1160. time_condition = f"older_than_hours: {older_than_hours}"
  1161. elif older_than_days:
  1162. cutoff_time = datetime.now() - timedelta(days=older_than_days)
  1163. time_condition = f"older_than_days: {older_than_days}"
  1164. elif before_timestamp:
  1165. try:
  1166. # 支持 YYYY-MM-DD HH:MM:SS 格式
  1167. cutoff_time = datetime.strptime(before_timestamp, '%Y-%m-%d %H:%M:%S')
  1168. time_condition = f"before_timestamp: {before_timestamp}"
  1169. except ValueError:
  1170. from common.result import validation_failed_response
  1171. return jsonify(validation_failed_response(
  1172. response_text="before_timestamp格式错误,请使用 YYYY-MM-DD HH:MM:SS 格式"
  1173. )), 422
  1174. else:
  1175. from common.result import bad_request_response
  1176. return jsonify(bad_request_response(
  1177. response_text="必须提供时间条件:older_than_hours, older_than_days 或 before_timestamp (YYYY-MM-DD HH:MM:SS)",
  1178. missing_params=["older_than_hours", "older_than_days", "before_timestamp"]
  1179. )), 400
  1180. preview = {
  1181. 'time_condition': time_condition,
  1182. 'cutoff_time': cutoff_time.isoformat(),
  1183. 'will_be_removed': {
  1184. 'sessions': []
  1185. },
  1186. 'will_be_kept': {
  1187. 'sessions_count': 0,
  1188. 'conversations_count': 0
  1189. },
  1190. 'summary': {
  1191. 'sessions_to_remove': 0,
  1192. 'conversations_to_remove': 0,
  1193. 'sessions_to_keep': 0,
  1194. 'conversations_to_keep': 0
  1195. }
  1196. }
  1197. # 预览按session删除
  1198. sessions_to_remove_count = 0
  1199. conversations_to_remove_count = 0
  1200. for session_id, session_data in cache.session_info.items():
  1201. session_preview = {
  1202. 'session_id': session_id,
  1203. 'start_time': session_data['start_time'].isoformat(),
  1204. 'conversation_count': len(session_data['conversations']),
  1205. 'conversations': []
  1206. }
  1207. # 添加conversation详情
  1208. for conv_id in session_data['conversations']:
  1209. if conv_id in cache.cache:
  1210. conv_data = cache.cache[conv_id]
  1211. session_preview['conversations'].append({
  1212. 'conversation_id': conv_id,
  1213. 'question': conv_data.get('question', '')[:50] + '...' if conv_data.get('question') else '',
  1214. 'start_time': cache.conversation_start_times.get(conv_id, '').isoformat() if cache.conversation_start_times.get(conv_id) else ''
  1215. })
  1216. if session_data['start_time'] < cutoff_time:
  1217. preview['will_be_removed']['sessions'].append(session_preview)
  1218. sessions_to_remove_count += 1
  1219. conversations_to_remove_count += len(session_data['conversations'])
  1220. else:
  1221. preview['will_be_kept']['sessions_count'] += 1
  1222. preview['will_be_kept']['conversations_count'] += len(session_data['conversations'])
  1223. # 更新摘要统计
  1224. preview['summary'] = {
  1225. 'sessions_to_remove': sessions_to_remove_count,
  1226. 'conversations_to_remove': conversations_to_remove_count,
  1227. 'sessions_to_keep': preview['will_be_kept']['sessions_count'],
  1228. 'conversations_to_keep': preview['will_be_kept']['conversations_count']
  1229. }
  1230. from common.result import success_response
  1231. return jsonify(success_response(
  1232. response_text=f"清理预览完成,将删除 {sessions_to_remove_count} 个会话和 {conversations_to_remove_count} 个对话",
  1233. data=preview
  1234. ))
  1235. except Exception as e:
  1236. from common.result import internal_error_response
  1237. return jsonify(internal_error_response(
  1238. response_text="预览清理操作失败,请稍后重试"
  1239. )), 500
  1240. @app.flask_app.route('/api/v0/cache_cleanup', methods=['POST'])
  1241. def cache_cleanup():
  1242. """清理功能:实际删除缓存 - 保持原功能"""
  1243. try:
  1244. req = request.get_json(force=True)
  1245. # 时间条件 - 支持三种方式
  1246. older_than_hours = req.get('older_than_hours')
  1247. older_than_days = req.get('older_than_days')
  1248. before_timestamp = req.get('before_timestamp') # YYYY-MM-DD HH:MM:SS 格式
  1249. cache = app.cache
  1250. if not hasattr(cache, 'session_info'):
  1251. from common.result import service_unavailable_response
  1252. return jsonify(service_unavailable_response(
  1253. response_text="缓存不支持会话功能"
  1254. )), 503
  1255. # 计算截止时间
  1256. cutoff_time = None
  1257. time_condition = None
  1258. if older_than_hours:
  1259. cutoff_time = datetime.now() - timedelta(hours=older_than_hours)
  1260. time_condition = f"older_than_hours: {older_than_hours}"
  1261. elif older_than_days:
  1262. cutoff_time = datetime.now() - timedelta(days=older_than_days)
  1263. time_condition = f"older_than_days: {older_than_days}"
  1264. elif before_timestamp:
  1265. try:
  1266. # 支持 YYYY-MM-DD HH:MM:SS 格式
  1267. cutoff_time = datetime.strptime(before_timestamp, '%Y-%m-%d %H:%M:%S')
  1268. time_condition = f"before_timestamp: {before_timestamp}"
  1269. except ValueError:
  1270. from common.result import validation_failed_response
  1271. return jsonify(validation_failed_response(
  1272. response_text="before_timestamp格式错误,请使用 YYYY-MM-DD HH:MM:SS 格式"
  1273. )), 422
  1274. else:
  1275. from common.result import bad_request_response
  1276. return jsonify(bad_request_response(
  1277. response_text="必须提供时间条件:older_than_hours, older_than_days 或 before_timestamp (YYYY-MM-DD HH:MM:SS)",
  1278. missing_params=["older_than_hours", "older_than_days", "before_timestamp"]
  1279. )), 400
  1280. cleanup_stats = {
  1281. 'time_condition': time_condition,
  1282. 'cutoff_time': cutoff_time.isoformat(),
  1283. 'sessions_removed': 0,
  1284. 'conversations_removed': 0,
  1285. 'sessions_kept': 0,
  1286. 'conversations_kept': 0,
  1287. 'removed_session_ids': [],
  1288. 'removed_conversation_ids': []
  1289. }
  1290. # 按session删除
  1291. sessions_to_remove = []
  1292. for session_id, session_data in cache.session_info.items():
  1293. if session_data['start_time'] < cutoff_time:
  1294. sessions_to_remove.append(session_id)
  1295. # 删除符合条件的sessions及其所有conversations
  1296. for session_id in sessions_to_remove:
  1297. session_data = cache.session_info[session_id]
  1298. conversations_in_session = session_data['conversations'].copy()
  1299. # 删除session中的所有conversations
  1300. for conv_id in conversations_in_session:
  1301. if conv_id in cache.cache:
  1302. del cache.cache[conv_id]
  1303. cleanup_stats['conversations_removed'] += 1
  1304. cleanup_stats['removed_conversation_ids'].append(conv_id)
  1305. # 清理conversation相关的时间记录
  1306. if hasattr(cache, 'conversation_start_times') and conv_id in cache.conversation_start_times:
  1307. del cache.conversation_start_times[conv_id]
  1308. if hasattr(cache, 'conversation_to_session') and conv_id in cache.conversation_to_session:
  1309. del cache.conversation_to_session[conv_id]
  1310. # 删除session记录
  1311. del cache.session_info[session_id]
  1312. cleanup_stats['sessions_removed'] += 1
  1313. cleanup_stats['removed_session_ids'].append(session_id)
  1314. # 统计保留的sessions和conversations
  1315. cleanup_stats['sessions_kept'] = len(cache.session_info)
  1316. cleanup_stats['conversations_kept'] = len(cache.cache)
  1317. from common.result import success_response
  1318. return jsonify(success_response(
  1319. response_text=f"缓存清理完成,删除了 {cleanup_stats['sessions_removed']} 个会话和 {cleanup_stats['conversations_removed']} 个对话",
  1320. data=cleanup_stats
  1321. ))
  1322. except Exception as e:
  1323. from common.result import internal_error_response
  1324. return jsonify(internal_error_response(
  1325. response_text="缓存清理失败,请稍后重试"
  1326. )), 500
  1327. @app.flask_app.route('/api/v0/training_error_question_sql', methods=['POST'])
  1328. def training_error_question_sql():
  1329. """
  1330. 存储错误的question-sql对到error_sql集合中
  1331. 此API将接收的错误question/sql pair写入到error_sql集合中,用于记录和分析错误的SQL查询。
  1332. Args:
  1333. question (str, required): 用户问题
  1334. sql (str, required): 对应的错误SQL查询语句
  1335. Returns:
  1336. JSON: 包含训练ID和成功消息的响应
  1337. """
  1338. try:
  1339. data = request.get_json()
  1340. question = data.get('question')
  1341. sql = data.get('sql')
  1342. logger.debug(f"接收到错误SQL训练请求: question={question}, sql={sql}")
  1343. if not question or not sql:
  1344. from common.result import bad_request_response
  1345. missing_params = []
  1346. if not question:
  1347. missing_params.append("question")
  1348. if not sql:
  1349. missing_params.append("sql")
  1350. return jsonify(bad_request_response(
  1351. response_text="question和sql参数都是必需的",
  1352. missing_params=missing_params
  1353. )), 400
  1354. # 使用vn实例的train_error_sql方法存储错误SQL
  1355. id = vn.train_error_sql(question=question, sql=sql)
  1356. logger.info(f"成功存储错误SQL,ID: {id}")
  1357. from common.result import success_response
  1358. return jsonify(success_response(
  1359. response_text="错误SQL对已成功存储",
  1360. data={
  1361. "id": id,
  1362. "message": "错误SQL对已成功存储到error_sql集合"
  1363. }
  1364. ))
  1365. except Exception as e:
  1366. logger.error(f"存储错误SQL失败: {str(e)}")
  1367. from common.result import internal_error_response
  1368. return jsonify(internal_error_response(
  1369. response_text="存储错误SQL失败,请稍后重试"
  1370. )), 500
  1371. # ==================== Redis对话管理API ====================
  1372. @app.flask_app.route('/api/v0/user/<user_id>/conversations', methods=['GET'])
  1373. def get_user_conversations(user_id: str):
  1374. """获取用户的对话列表(按时间倒序)"""
  1375. try:
  1376. limit = request.args.get('limit', USER_MAX_CONVERSATIONS, type=int)
  1377. conversations = redis_conversation_manager.get_conversations(user_id, limit)
  1378. # 为每个对话动态获取标题(第一条用户消息)
  1379. for conversation in conversations:
  1380. conversation_id = conversation['conversation_id']
  1381. try:
  1382. # 获取所有消息,然后取第一条用户消息作为标题
  1383. messages = redis_conversation_manager.get_conversation_messages(conversation_id)
  1384. if messages and len(messages) > 0:
  1385. # 找到第一条用户消息(按时间顺序)
  1386. first_user_message = None
  1387. for message in messages:
  1388. if message.get('role') == 'user':
  1389. first_user_message = message
  1390. break
  1391. if first_user_message:
  1392. title = first_user_message.get('content', '对话').strip()
  1393. # 限制标题长度,保持整洁
  1394. if len(title) > 50:
  1395. conversation['conversation_title'] = title[:47] + "..."
  1396. else:
  1397. conversation['conversation_title'] = title
  1398. else:
  1399. conversation['conversation_title'] = "对话"
  1400. else:
  1401. conversation['conversation_title'] = "空对话"
  1402. except Exception as e:
  1403. logger.warning(f"获取对话标题失败 {conversation_id}: {str(e)}")
  1404. conversation['conversation_title'] = "对话"
  1405. return jsonify(success_response(
  1406. response_text="获取用户对话列表成功",
  1407. data={
  1408. "user_id": user_id,
  1409. "conversations": conversations,
  1410. "total_count": len(conversations)
  1411. }
  1412. ))
  1413. except Exception as e:
  1414. return jsonify(internal_error_response(
  1415. response_text="获取对话列表失败,请稍后重试"
  1416. )), 500
  1417. @app.flask_app.route('/api/v0/conversation/<conversation_id>/messages', methods=['GET'])
  1418. def get_conversation_messages(conversation_id: str):
  1419. """获取特定对话的消息历史"""
  1420. try:
  1421. limit = request.args.get('limit', type=int) # 可选参数
  1422. messages = redis_conversation_manager.get_conversation_messages(conversation_id, limit)
  1423. meta = redis_conversation_manager.get_conversation_meta(conversation_id)
  1424. return jsonify(success_response(
  1425. response_text="获取对话消息成功",
  1426. data={
  1427. "conversation_id": conversation_id,
  1428. "conversation_meta": meta,
  1429. "messages": messages,
  1430. "message_count": len(messages)
  1431. }
  1432. ))
  1433. except Exception as e:
  1434. return jsonify(internal_error_response(
  1435. response_text="获取对话消息失败"
  1436. )), 500
  1437. @app.flask_app.route('/api/v0/conversation/<conversation_id>/context', methods=['GET'])
  1438. def get_conversation_context(conversation_id: str):
  1439. """获取对话上下文(格式化用于LLM)"""
  1440. try:
  1441. count = request.args.get('count', CONVERSATION_CONTEXT_COUNT, type=int)
  1442. context = redis_conversation_manager.get_context_for_display(conversation_id, count)
  1443. return jsonify(success_response(
  1444. response_text="获取对话上下文成功",
  1445. data={
  1446. "conversation_id": conversation_id,
  1447. "context": context,
  1448. "context_message_count": count
  1449. }
  1450. ))
  1451. except Exception as e:
  1452. return jsonify(internal_error_response(
  1453. response_text="获取对话上下文失败"
  1454. )), 500
  1455. @app.flask_app.route('/api/v0/conversation_stats', methods=['GET'])
  1456. def conversation_stats():
  1457. """获取对话系统统计信息"""
  1458. try:
  1459. stats = redis_conversation_manager.get_stats()
  1460. return jsonify(success_response(
  1461. response_text="获取统计信息成功",
  1462. data=stats
  1463. ))
  1464. except Exception as e:
  1465. return jsonify(internal_error_response(
  1466. response_text="获取统计信息失败,请稍后重试"
  1467. )), 500
  1468. @app.flask_app.route('/api/v0/conversation_cleanup', methods=['POST'])
  1469. def conversation_cleanup():
  1470. """手动清理过期对话"""
  1471. try:
  1472. redis_conversation_manager.cleanup_expired_conversations()
  1473. return jsonify(success_response(
  1474. response_text="对话清理完成"
  1475. ))
  1476. except Exception as e:
  1477. return jsonify(internal_error_response(
  1478. response_text="对话清理失败,请稍后重试"
  1479. )), 500
  1480. @app.flask_app.route('/api/v0/user/<user_id>/conversations/full', methods=['GET'])
  1481. def get_user_conversations_with_messages(user_id: str):
  1482. """
  1483. 获取用户的完整对话数据(包含所有消息)
  1484. 一次性返回用户的所有对话和每个对话下的消息历史
  1485. Args:
  1486. user_id: 用户ID(路径参数)
  1487. conversation_limit: 对话数量限制(查询参数,可选,不传则返回所有对话)
  1488. message_limit: 每个对话的消息数限制(查询参数,可选,不传则返回所有消息)
  1489. Returns:
  1490. 包含用户所有对话和消息的完整数据
  1491. """
  1492. try:
  1493. # 获取可选参数,不传递时使用None(返回所有记录)
  1494. conversation_limit = request.args.get('conversation_limit', type=int)
  1495. message_limit = request.args.get('message_limit', type=int)
  1496. # 获取用户的对话列表
  1497. conversations = redis_conversation_manager.get_conversations(user_id, conversation_limit)
  1498. # 为每个对话获取消息历史
  1499. full_conversations = []
  1500. total_messages = 0
  1501. for conversation in conversations:
  1502. conversation_id = conversation['conversation_id']
  1503. # 获取对话消息
  1504. messages = redis_conversation_manager.get_conversation_messages(
  1505. conversation_id, message_limit
  1506. )
  1507. # 获取对话元数据
  1508. meta = redis_conversation_manager.get_conversation_meta(conversation_id)
  1509. # 组合完整数据
  1510. full_conversation = {
  1511. **conversation, # 基础对话信息
  1512. 'meta': meta, # 对话元数据
  1513. 'messages': messages, # 消息列表
  1514. 'message_count': len(messages)
  1515. }
  1516. full_conversations.append(full_conversation)
  1517. total_messages += len(messages)
  1518. return jsonify(success_response(
  1519. response_text="获取用户完整对话数据成功",
  1520. data={
  1521. "user_id": user_id,
  1522. "conversations": full_conversations,
  1523. "total_conversations": len(full_conversations),
  1524. "total_messages": total_messages,
  1525. "conversation_limit_applied": conversation_limit,
  1526. "message_limit_applied": message_limit,
  1527. "query_time": datetime.now().isoformat()
  1528. }
  1529. ))
  1530. except Exception as e:
  1531. logger.error(f"获取用户完整对话数据失败: {str(e)}")
  1532. return jsonify(internal_error_response(
  1533. response_text="获取用户对话数据失败,请稍后重试"
  1534. )), 500
  1535. # ==================== Embedding缓存管理接口 ====================
  1536. @app.flask_app.route('/api/v0/embedding_cache_stats', methods=['GET'])
  1537. def embedding_cache_stats():
  1538. """获取embedding缓存统计信息"""
  1539. try:
  1540. from common.embedding_cache_manager import get_embedding_cache_manager
  1541. cache_manager = get_embedding_cache_manager()
  1542. stats = cache_manager.get_cache_stats()
  1543. return jsonify(success_response(
  1544. response_text="获取embedding缓存统计成功",
  1545. data=stats
  1546. ))
  1547. except Exception as e:
  1548. logger.error(f"获取embedding缓存统计失败: {str(e)}")
  1549. return jsonify(internal_error_response(
  1550. response_text="获取embedding缓存统计失败,请稍后重试"
  1551. )), 500
  1552. @app.flask_app.route('/api/v0/embedding_cache_cleanup', methods=['POST'])
  1553. def embedding_cache_cleanup():
  1554. """清空所有embedding缓存"""
  1555. try:
  1556. from common.embedding_cache_manager import get_embedding_cache_manager
  1557. cache_manager = get_embedding_cache_manager()
  1558. if not cache_manager.is_available():
  1559. return jsonify(internal_error_response(
  1560. response_text="Embedding缓存功能未启用或不可用"
  1561. )), 400
  1562. success = cache_manager.clear_all_cache()
  1563. if success:
  1564. return jsonify(success_response(
  1565. response_text="所有embedding缓存已清空",
  1566. data={"cleared": True}
  1567. ))
  1568. else:
  1569. return jsonify(internal_error_response(
  1570. response_text="清空embedding缓存失败"
  1571. )), 500
  1572. except Exception as e:
  1573. logger.error(f"清空embedding缓存失败: {str(e)}")
  1574. return jsonify(internal_error_response(
  1575. response_text="清空embedding缓存失败,请稍后重试"
  1576. )), 500
  1577. # ==================== QA反馈系统接口 ====================
  1578. # 全局反馈管理器实例
  1579. qa_feedback_manager = None
  1580. def get_qa_feedback_manager():
  1581. """获取QA反馈管理器实例(懒加载)- 复用Vanna连接版本"""
  1582. global qa_feedback_manager
  1583. if qa_feedback_manager is None:
  1584. try:
  1585. # 优先尝试复用vanna连接
  1586. vanna_instance = None
  1587. try:
  1588. # 尝试获取现有的vanna实例
  1589. if 'get_citu_langraph_agent' in globals():
  1590. agent = get_citu_langraph_agent()
  1591. if hasattr(agent, 'vn'):
  1592. vanna_instance = agent.vn
  1593. elif 'vn' in globals():
  1594. vanna_instance = vn
  1595. else:
  1596. logger.info("未找到可用的vanna实例,将创建新的数据库连接")
  1597. except Exception as e:
  1598. logger.info(f"获取vanna实例失败: {e},将创建新的数据库连接")
  1599. vanna_instance = None
  1600. qa_feedback_manager = QAFeedbackManager(vanna_instance=vanna_instance)
  1601. logger.info("QA反馈管理器实例创建成功")
  1602. except Exception as e:
  1603. logger.critical(f"QA反馈管理器创建失败: {str(e)}")
  1604. raise Exception(f"QA反馈管理器初始化失败: {str(e)}")
  1605. return qa_feedback_manager
  1606. @app.flask_app.route('/api/v0/qa_feedback/query', methods=['POST'])
  1607. def qa_feedback_query():
  1608. """
  1609. 查询反馈记录API
  1610. 支持分页、筛选和排序功能
  1611. """
  1612. try:
  1613. req = request.get_json(force=True)
  1614. # 解析参数,设置默认值
  1615. page = req.get('page', 1)
  1616. page_size = req.get('page_size', 20)
  1617. is_thumb_up = req.get('is_thumb_up')
  1618. create_time_start = req.get('create_time_start')
  1619. create_time_end = req.get('create_time_end')
  1620. is_in_training_data = req.get('is_in_training_data')
  1621. sort_by = req.get('sort_by', 'create_time')
  1622. sort_order = req.get('sort_order', 'desc')
  1623. # 参数验证
  1624. if page < 1:
  1625. return jsonify(bad_request_response(
  1626. response_text="页码必须大于0",
  1627. invalid_params=["page"]
  1628. )), 400
  1629. if page_size < 1 or page_size > 100:
  1630. return jsonify(bad_request_response(
  1631. response_text="每页大小必须在1-100之间",
  1632. invalid_params=["page_size"]
  1633. )), 400
  1634. # 获取反馈管理器并查询
  1635. manager = get_qa_feedback_manager()
  1636. records, total = manager.query_feedback(
  1637. page=page,
  1638. page_size=page_size,
  1639. is_thumb_up=is_thumb_up,
  1640. create_time_start=create_time_start,
  1641. create_time_end=create_time_end,
  1642. is_in_training_data=is_in_training_data,
  1643. sort_by=sort_by,
  1644. sort_order=sort_order
  1645. )
  1646. # 计算分页信息
  1647. total_pages = (total + page_size - 1) // page_size
  1648. return jsonify(success_response(
  1649. response_text=f"查询成功,共找到 {total} 条记录",
  1650. data={
  1651. "records": records,
  1652. "pagination": {
  1653. "page": page,
  1654. "page_size": page_size,
  1655. "total": total,
  1656. "total_pages": total_pages,
  1657. "has_next": page < total_pages,
  1658. "has_prev": page > 1
  1659. }
  1660. }
  1661. ))
  1662. except Exception as e:
  1663. logger.error(f"qa_feedback_query执行失败: {str(e)}")
  1664. return jsonify(internal_error_response(
  1665. response_text="查询反馈记录失败,请稍后重试"
  1666. )), 500
  1667. @app.flask_app.route('/api/v0/qa_feedback/delete/<int:feedback_id>', methods=['DELETE'])
  1668. def qa_feedback_delete(feedback_id):
  1669. """
  1670. 删除反馈记录API
  1671. """
  1672. try:
  1673. manager = get_qa_feedback_manager()
  1674. success = manager.delete_feedback(feedback_id)
  1675. if success:
  1676. return jsonify(success_response(
  1677. response_text=f"反馈记录删除成功",
  1678. data={"deleted_id": feedback_id}
  1679. ))
  1680. else:
  1681. return jsonify(not_found_response(
  1682. response_text=f"反馈记录不存在 (ID: {feedback_id})"
  1683. )), 404
  1684. except Exception as e:
  1685. logger.error(f"qa_feedback_delete执行失败: {str(e)}")
  1686. return jsonify(internal_error_response(
  1687. response_text="删除反馈记录失败,请稍后重试"
  1688. )), 500
  1689. @app.flask_app.route('/api/v0/qa_feedback/update/<int:feedback_id>', methods=['PUT'])
  1690. def qa_feedback_update(feedback_id):
  1691. """
  1692. 更新反馈记录API
  1693. """
  1694. try:
  1695. req = request.get_json(force=True)
  1696. # 提取允许更新的字段
  1697. allowed_fields = ['question', 'sql', 'is_thumb_up', 'user_id', 'is_in_training_data']
  1698. update_data = {}
  1699. for field in allowed_fields:
  1700. if field in req:
  1701. update_data[field] = req[field]
  1702. if not update_data:
  1703. return jsonify(bad_request_response(
  1704. response_text="没有提供有效的更新字段",
  1705. missing_params=allowed_fields
  1706. )), 400
  1707. manager = get_qa_feedback_manager()
  1708. success = manager.update_feedback(feedback_id, **update_data)
  1709. if success:
  1710. return jsonify(success_response(
  1711. response_text="反馈记录更新成功",
  1712. data={
  1713. "updated_id": feedback_id,
  1714. "updated_fields": list(update_data.keys())
  1715. }
  1716. ))
  1717. else:
  1718. return jsonify(not_found_response(
  1719. response_text=f"反馈记录不存在或无变化 (ID: {feedback_id})"
  1720. )), 404
  1721. except Exception as e:
  1722. logger.error(f"qa_feedback_update执行失败: {str(e)}")
  1723. return jsonify(internal_error_response(
  1724. response_text="更新反馈记录失败,请稍后重试"
  1725. )), 500
  1726. @app.flask_app.route('/api/v0/qa_feedback/add_to_training', methods=['POST'])
  1727. def qa_feedback_add_to_training():
  1728. """
  1729. 将反馈记录添加到训练数据集API
  1730. 支持混合批量处理:正向反馈加入SQL训练集,负向反馈加入error_sql训练集
  1731. """
  1732. try:
  1733. req = request.get_json(force=True)
  1734. feedback_ids = req.get('feedback_ids', [])
  1735. if not feedback_ids or not isinstance(feedback_ids, list):
  1736. return jsonify(bad_request_response(
  1737. response_text="缺少有效的反馈ID列表",
  1738. missing_params=["feedback_ids"]
  1739. )), 400
  1740. manager = get_qa_feedback_manager()
  1741. # 获取反馈记录
  1742. records = manager.get_feedback_by_ids(feedback_ids)
  1743. if not records:
  1744. return jsonify(not_found_response(
  1745. response_text="未找到任何有效的反馈记录"
  1746. )), 404
  1747. # 分别处理正向和负向反馈
  1748. positive_count = 0 # 正向训练计数
  1749. negative_count = 0 # 负向训练计数
  1750. already_trained_count = 0 # 已训练计数
  1751. error_count = 0 # 错误计数
  1752. successfully_trained_ids = [] # 成功训练的ID列表
  1753. for record in records:
  1754. try:
  1755. # 检查是否已经在训练数据中
  1756. if record['is_in_training_data']:
  1757. already_trained_count += 1
  1758. continue
  1759. if record['is_thumb_up']:
  1760. # 正向反馈 - 加入标准SQL训练集
  1761. training_id = vn.train(
  1762. question=record['question'],
  1763. sql=record['sql']
  1764. )
  1765. positive_count += 1
  1766. logger.info(f"正向训练成功 - ID: {record['id']}, TrainingID: {training_id}")
  1767. else:
  1768. # 负向反馈 - 加入错误SQL训练集
  1769. training_id = vn.train_error_sql(
  1770. question=record['question'],
  1771. sql=record['sql']
  1772. )
  1773. negative_count += 1
  1774. logger.info(f"负向训练成功 - ID: {record['id']}, TrainingID: {training_id}")
  1775. successfully_trained_ids.append(record['id'])
  1776. except Exception as e:
  1777. logger.error(f"训练失败 - 反馈ID: {record['id']}, 错误: {e}")
  1778. error_count += 1
  1779. # 更新训练状态
  1780. if successfully_trained_ids:
  1781. updated_count = manager.mark_training_status(successfully_trained_ids, True)
  1782. logger.info(f"批量更新训练状态完成,影响 {updated_count} 条记录")
  1783. # 构建响应
  1784. total_processed = positive_count + negative_count + already_trained_count + error_count
  1785. return jsonify(success_response(
  1786. response_text=f"训练数据添加完成,成功处理 {positive_count + negative_count} 条记录",
  1787. data={
  1788. "summary": {
  1789. "total_requested": len(feedback_ids),
  1790. "total_processed": total_processed,
  1791. "positive_trained": positive_count,
  1792. "negative_trained": negative_count,
  1793. "already_trained": already_trained_count,
  1794. "errors": error_count
  1795. },
  1796. "successfully_trained_ids": successfully_trained_ids,
  1797. "training_details": {
  1798. "sql_training_count": positive_count,
  1799. "error_sql_training_count": negative_count
  1800. }
  1801. }
  1802. ))
  1803. except Exception as e:
  1804. logger.error(f"qa_feedback_add_to_training执行失败: {str(e)}")
  1805. return jsonify(internal_error_response(
  1806. response_text="添加训练数据失败,请稍后重试"
  1807. )), 500
  1808. @app.flask_app.route('/api/v0/qa_feedback/add', methods=['POST'])
  1809. def qa_feedback_add():
  1810. """
  1811. 添加反馈记录API
  1812. 用于前端直接创建反馈记录
  1813. """
  1814. try:
  1815. req = request.get_json(force=True)
  1816. question = req.get('question')
  1817. sql = req.get('sql')
  1818. is_thumb_up = req.get('is_thumb_up')
  1819. user_id = req.get('user_id', 'guest')
  1820. # 参数验证
  1821. if not question:
  1822. return jsonify(bad_request_response(
  1823. response_text="缺少必需参数:question",
  1824. missing_params=["question"]
  1825. )), 400
  1826. if not sql:
  1827. return jsonify(bad_request_response(
  1828. response_text="缺少必需参数:sql",
  1829. missing_params=["sql"]
  1830. )), 400
  1831. if is_thumb_up is None:
  1832. return jsonify(bad_request_response(
  1833. response_text="缺少必需参数:is_thumb_up",
  1834. missing_params=["is_thumb_up"]
  1835. )), 400
  1836. manager = get_qa_feedback_manager()
  1837. feedback_id = manager.add_feedback(
  1838. question=question,
  1839. sql=sql,
  1840. is_thumb_up=bool(is_thumb_up),
  1841. user_id=user_id
  1842. )
  1843. return jsonify(success_response(
  1844. response_text="反馈记录创建成功",
  1845. data={
  1846. "feedback_id": feedback_id
  1847. }
  1848. ))
  1849. except Exception as e:
  1850. logger.error(f"qa_feedback_add执行失败: {str(e)}")
  1851. return jsonify(internal_error_response(
  1852. response_text="创建反馈记录失败,请稍后重试"
  1853. )), 500
  1854. @app.flask_app.route('/api/v0/qa_feedback/stats', methods=['GET'])
  1855. def qa_feedback_stats():
  1856. """
  1857. 反馈统计API
  1858. 返回反馈数据的统计信息
  1859. """
  1860. try:
  1861. manager = get_qa_feedback_manager()
  1862. # 查询各种统计数据
  1863. all_records, total_count = manager.query_feedback(page=1, page_size=1)
  1864. positive_records, positive_count = manager.query_feedback(page=1, page_size=1, is_thumb_up=True)
  1865. negative_records, negative_count = manager.query_feedback(page=1, page_size=1, is_thumb_up=False)
  1866. trained_records, trained_count = manager.query_feedback(page=1, page_size=1, is_in_training_data=True)
  1867. untrained_records, untrained_count = manager.query_feedback(page=1, page_size=1, is_in_training_data=False)
  1868. return jsonify(success_response(
  1869. response_text="统计信息获取成功",
  1870. data={
  1871. "total_feedback": total_count,
  1872. "positive_feedback": positive_count,
  1873. "negative_feedback": negative_count,
  1874. "trained_feedback": trained_count,
  1875. "untrained_feedback": untrained_count,
  1876. "positive_rate": round(positive_count / max(total_count, 1) * 100, 2),
  1877. "training_rate": round(trained_count / max(total_count, 1) * 100, 2)
  1878. }
  1879. ))
  1880. except Exception as e:
  1881. logger.error(f"qa_feedback_stats执行失败: {str(e)}")
  1882. return jsonify(internal_error_response(
  1883. response_text="获取统计信息失败,请稍后重试"
  1884. )), 500
  1885. # ==================== 问答缓存管理接口 ====================
  1886. @app.flask_app.route('/api/v0/qa_cache_stats', methods=['GET'])
  1887. def qa_cache_stats():
  1888. """获取问答缓存统计信息"""
  1889. try:
  1890. stats = redis_conversation_manager.get_qa_cache_stats()
  1891. return jsonify(success_response(
  1892. response_text="获取问答缓存统计成功",
  1893. data=stats
  1894. ))
  1895. except Exception as e:
  1896. logger.error(f"获取问答缓存统计失败: {str(e)}")
  1897. return jsonify(internal_error_response(
  1898. response_text="获取问答缓存统计失败,请稍后重试"
  1899. )), 500
  1900. @app.flask_app.route('/api/v0/qa_cache_list', methods=['GET'])
  1901. def qa_cache_list():
  1902. """获取问答缓存列表(支持分页)"""
  1903. try:
  1904. # 获取分页参数,默认限制50条
  1905. limit = request.args.get('limit', 50, type=int)
  1906. # 限制最大返回数量,防止一次性返回过多数据
  1907. if limit > 500:
  1908. limit = 500
  1909. elif limit <= 0:
  1910. limit = 50
  1911. cache_list = redis_conversation_manager.get_qa_cache_list(limit)
  1912. return jsonify(success_response(
  1913. response_text="获取问答缓存列表成功",
  1914. data={
  1915. "cache_list": cache_list,
  1916. "total_returned": len(cache_list),
  1917. "limit_applied": limit,
  1918. "note": "按缓存时间倒序排列,最新的在前面"
  1919. }
  1920. ))
  1921. except Exception as e:
  1922. logger.error(f"获取问答缓存列表失败: {str(e)}")
  1923. return jsonify(internal_error_response(
  1924. response_text="获取问答缓存列表失败,请稍后重试"
  1925. )), 500
  1926. @app.flask_app.route('/api/v0/qa_cache_cleanup', methods=['POST'])
  1927. def qa_cache_cleanup():
  1928. """清空所有问答缓存"""
  1929. try:
  1930. if not redis_conversation_manager.is_available():
  1931. return jsonify(internal_error_response(
  1932. response_text="Redis连接不可用,无法执行清理操作"
  1933. )), 500
  1934. deleted_count = redis_conversation_manager.clear_all_qa_cache()
  1935. return jsonify(success_response(
  1936. response_text="问答缓存清理完成",
  1937. data={
  1938. "deleted_count": deleted_count,
  1939. "cleared": deleted_count > 0,
  1940. "cleanup_time": datetime.now().isoformat()
  1941. }
  1942. ))
  1943. except Exception as e:
  1944. logger.error(f"清空问答缓存失败: {str(e)}")
  1945. return jsonify(internal_error_response(
  1946. response_text="清空问答缓存失败,请稍后重试"
  1947. )), 500
  1948. # ==================== 训练数据管理接口 ====================
  1949. def validate_sql_syntax(sql: str) -> tuple[bool, str]:
  1950. """SQL语法检查(仅对sql类型)"""
  1951. try:
  1952. parsed = sqlparse.parse(sql.strip())
  1953. if not parsed or not parsed[0].tokens:
  1954. return False, "SQL语法错误:空语句"
  1955. # 基本语法检查
  1956. sql_upper = sql.strip().upper()
  1957. if not any(sql_upper.startswith(keyword) for keyword in
  1958. ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP']):
  1959. return False, "SQL语法错误:不是有效的SQL语句"
  1960. # 安全检查:禁止危险的SQL操作
  1961. dangerous_operations = ['UPDATE', 'DELETE', 'ALERT', 'DROP']
  1962. for operation in dangerous_operations:
  1963. if sql_upper.startswith(operation):
  1964. return False, f'在训练集中禁止使用"{",".join(dangerous_operations)}"'
  1965. return True, ""
  1966. except Exception as e:
  1967. return False, f"SQL语法错误:{str(e)}"
  1968. def paginate_data(data_list: list, page: int, page_size: int):
  1969. """分页处理算法"""
  1970. total = len(data_list)
  1971. start_idx = (page - 1) * page_size
  1972. end_idx = start_idx + page_size
  1973. page_data = data_list[start_idx:end_idx]
  1974. return {
  1975. "data": page_data,
  1976. "pagination": {
  1977. "page": page,
  1978. "page_size": page_size,
  1979. "total": total,
  1980. "total_pages": (total + page_size - 1) // page_size,
  1981. "has_next": end_idx < total,
  1982. "has_prev": page > 1
  1983. }
  1984. }
  1985. def filter_by_type(data_list: list, training_data_type: str):
  1986. """按类型筛选算法"""
  1987. if not training_data_type:
  1988. return data_list
  1989. return [
  1990. record for record in data_list
  1991. if record.get('training_data_type') == training_data_type
  1992. ]
  1993. def search_in_data(data_list: list, search_keyword: str):
  1994. """在数据中搜索关键词"""
  1995. if not search_keyword:
  1996. return data_list
  1997. keyword_lower = search_keyword.lower()
  1998. return [
  1999. record for record in data_list
  2000. if (record.get('question') and keyword_lower in record['question'].lower()) or
  2001. (record.get('content') and keyword_lower in record['content'].lower())
  2002. ]
  2003. def process_single_training_item(item: dict, index: int) -> dict:
  2004. """处理单个训练数据项"""
  2005. training_type = item.get('training_data_type')
  2006. if training_type == 'sql':
  2007. sql = item.get('sql')
  2008. if not sql:
  2009. raise ValueError("SQL字段是必需的")
  2010. # SQL语法检查
  2011. is_valid, error_msg = validate_sql_syntax(sql)
  2012. if not is_valid:
  2013. raise ValueError(error_msg)
  2014. question = item.get('question')
  2015. if question:
  2016. training_id = vn.train(question=question, sql=sql)
  2017. else:
  2018. training_id = vn.train(sql=sql)
  2019. elif training_type == 'error_sql':
  2020. # error_sql不需要语法检查
  2021. question = item.get('question')
  2022. sql = item.get('sql')
  2023. if not question or not sql:
  2024. raise ValueError("question和sql字段都是必需的")
  2025. training_id = vn.train_error_sql(question=question, sql=sql)
  2026. elif training_type == 'documentation':
  2027. content = item.get('content')
  2028. if not content:
  2029. raise ValueError("content字段是必需的")
  2030. training_id = vn.train(documentation=content)
  2031. elif training_type == 'ddl':
  2032. ddl = item.get('ddl')
  2033. if not ddl:
  2034. raise ValueError("ddl字段是必需的")
  2035. training_id = vn.train(ddl=ddl)
  2036. else:
  2037. raise ValueError(f"不支持的训练数据类型: {training_type}")
  2038. return {
  2039. "index": index,
  2040. "success": True,
  2041. "training_id": training_id,
  2042. "type": training_type,
  2043. "message": f"{training_type}训练数据创建成功"
  2044. }
  2045. def get_total_training_count():
  2046. """获取当前训练数据总数"""
  2047. try:
  2048. training_data = vn.get_training_data()
  2049. if training_data is not None and not training_data.empty:
  2050. return len(training_data)
  2051. return 0
  2052. except Exception as e:
  2053. logger.warning(f"获取训练数据总数失败: {e}")
  2054. return 0
  2055. @app.flask_app.route('/api/v0/training_data/query', methods=['POST'])
  2056. def training_data_query():
  2057. """
  2058. 分页查询训练数据API
  2059. 支持类型筛选、搜索和排序功能
  2060. """
  2061. try:
  2062. req = request.get_json(force=True)
  2063. # 解析参数,设置默认值
  2064. page = req.get('page', 1)
  2065. page_size = req.get('page_size', 20)
  2066. training_data_type = req.get('training_data_type')
  2067. sort_by = req.get('sort_by', 'id')
  2068. sort_order = req.get('sort_order', 'desc')
  2069. search_keyword = req.get('search_keyword')
  2070. # 参数验证
  2071. if page < 1:
  2072. return jsonify(bad_request_response(
  2073. response_text="页码必须大于0",
  2074. missing_params=["page"]
  2075. )), 400
  2076. if page_size < 1 or page_size > 100:
  2077. return jsonify(bad_request_response(
  2078. response_text="每页大小必须在1-100之间",
  2079. missing_params=["page_size"]
  2080. )), 400
  2081. if search_keyword and len(search_keyword) > 100:
  2082. return jsonify(bad_request_response(
  2083. response_text="搜索关键词最大长度为100字符",
  2084. missing_params=["search_keyword"]
  2085. )), 400
  2086. # 获取训练数据
  2087. training_data = vn.get_training_data()
  2088. if training_data is None or training_data.empty:
  2089. return jsonify(success_response(
  2090. response_text="查询成功,暂无训练数据",
  2091. data={
  2092. "records": [],
  2093. "pagination": {
  2094. "page": page,
  2095. "page_size": page_size,
  2096. "total": 0,
  2097. "total_pages": 0,
  2098. "has_next": False,
  2099. "has_prev": False
  2100. },
  2101. "filters_applied": {
  2102. "training_data_type": training_data_type,
  2103. "search_keyword": search_keyword
  2104. }
  2105. }
  2106. ))
  2107. # 转换为列表格式
  2108. records = training_data.to_dict(orient="records")
  2109. # 应用筛选条件
  2110. if training_data_type:
  2111. records = filter_by_type(records, training_data_type)
  2112. if search_keyword:
  2113. records = search_in_data(records, search_keyword)
  2114. # 排序
  2115. if sort_by in ['id', 'training_data_type']:
  2116. reverse = (sort_order.lower() == 'desc')
  2117. records.sort(key=lambda x: x.get(sort_by, ''), reverse=reverse)
  2118. # 分页
  2119. paginated_result = paginate_data(records, page, page_size)
  2120. return jsonify(success_response(
  2121. response_text=f"查询成功,共找到 {paginated_result['pagination']['total']} 条记录",
  2122. data={
  2123. "records": paginated_result["data"],
  2124. "pagination": paginated_result["pagination"],
  2125. "filters_applied": {
  2126. "training_data_type": training_data_type,
  2127. "search_keyword": search_keyword
  2128. }
  2129. }
  2130. ))
  2131. except Exception as e:
  2132. logger.error(f"training_data_query执行失败: {str(e)}")
  2133. return jsonify(internal_error_response(
  2134. response_text="查询训练数据失败,请稍后重试"
  2135. )), 500
  2136. @app.flask_app.route('/api/v0/training_data/create', methods=['POST'])
  2137. def training_data_create():
  2138. """
  2139. 创建训练数据API
  2140. 支持单条和批量创建,支持四种数据类型
  2141. """
  2142. try:
  2143. req = request.get_json(force=True)
  2144. data = req.get('data')
  2145. if not data:
  2146. return jsonify(bad_request_response(
  2147. response_text="缺少必需参数:data",
  2148. missing_params=["data"]
  2149. )), 400
  2150. # 统一处理为列表格式
  2151. if isinstance(data, dict):
  2152. data_list = [data]
  2153. elif isinstance(data, list):
  2154. data_list = data
  2155. else:
  2156. return jsonify(bad_request_response(
  2157. response_text="data字段格式错误,应为对象或数组"
  2158. )), 400
  2159. # 批量操作限制
  2160. if len(data_list) > 50:
  2161. return jsonify(bad_request_response(
  2162. response_text="批量操作最大支持50条记录"
  2163. )), 400
  2164. results = []
  2165. successful_count = 0
  2166. type_summary = {"sql": 0, "documentation": 0, "ddl": 0, "error_sql": 0}
  2167. for index, item in enumerate(data_list):
  2168. try:
  2169. result = process_single_training_item(item, index)
  2170. results.append(result)
  2171. if result['success']:
  2172. successful_count += 1
  2173. type_summary[result['type']] += 1
  2174. except Exception as e:
  2175. results.append({
  2176. "index": index,
  2177. "success": False,
  2178. "type": item.get('training_data_type', 'unknown'),
  2179. "error": str(e),
  2180. "message": "创建失败"
  2181. })
  2182. # 获取创建后的总记录数
  2183. current_total = get_total_training_count()
  2184. # 根据实际执行结果决定响应状态
  2185. failed_count = len(data_list) - successful_count
  2186. if failed_count == 0:
  2187. # 全部成功
  2188. return jsonify(success_response(
  2189. response_text="训练数据创建完成",
  2190. data={
  2191. "total_requested": len(data_list),
  2192. "successfully_created": successful_count,
  2193. "failed_count": failed_count,
  2194. "results": results,
  2195. "summary": type_summary,
  2196. "current_total_count": current_total
  2197. }
  2198. ))
  2199. elif successful_count == 0:
  2200. # 全部失败
  2201. return jsonify(error_response(
  2202. response_text="训练数据创建失败",
  2203. data={
  2204. "total_requested": len(data_list),
  2205. "successfully_created": successful_count,
  2206. "failed_count": failed_count,
  2207. "results": results,
  2208. "summary": type_summary,
  2209. "current_total_count": current_total
  2210. }
  2211. )), 400
  2212. else:
  2213. # 部分成功,部分失败
  2214. return jsonify(error_response(
  2215. response_text=f"训练数据创建部分成功,成功{successful_count}条,失败{failed_count}条",
  2216. data={
  2217. "total_requested": len(data_list),
  2218. "successfully_created": successful_count,
  2219. "failed_count": failed_count,
  2220. "results": results,
  2221. "summary": type_summary,
  2222. "current_total_count": current_total
  2223. }
  2224. )), 207
  2225. except Exception as e:
  2226. logger.error(f"training_data_create执行失败: {str(e)}")
  2227. return jsonify(internal_error_response(
  2228. response_text="创建训练数据失败,请稍后重试"
  2229. )), 500
  2230. @app.flask_app.route('/api/v0/training_data/delete', methods=['POST'])
  2231. def training_data_delete():
  2232. """
  2233. 删除训练数据API
  2234. 支持批量删除
  2235. """
  2236. try:
  2237. req = request.get_json(force=True)
  2238. ids = req.get('ids', [])
  2239. confirm = req.get('confirm', False)
  2240. if not ids or not isinstance(ids, list):
  2241. return jsonify(bad_request_response(
  2242. response_text="缺少有效的ID列表",
  2243. missing_params=["ids"]
  2244. )), 400
  2245. if not confirm:
  2246. return jsonify(bad_request_response(
  2247. response_text="删除操作需要确认,请设置confirm为true"
  2248. )), 400
  2249. # 批量操作限制
  2250. if len(ids) > 50:
  2251. return jsonify(bad_request_response(
  2252. response_text="批量删除最大支持50条记录"
  2253. )), 400
  2254. deleted_ids = []
  2255. failed_ids = []
  2256. failed_details = []
  2257. for training_id in ids:
  2258. try:
  2259. success = vn.remove_training_data(training_id)
  2260. if success:
  2261. deleted_ids.append(training_id)
  2262. else:
  2263. failed_ids.append(training_id)
  2264. failed_details.append({
  2265. "id": training_id,
  2266. "error": "记录不存在或删除失败"
  2267. })
  2268. except Exception as e:
  2269. failed_ids.append(training_id)
  2270. failed_details.append({
  2271. "id": training_id,
  2272. "error": str(e)
  2273. })
  2274. # 获取删除后的总记录数
  2275. current_total = get_total_training_count()
  2276. # 根据实际执行结果决定响应状态
  2277. failed_count = len(failed_ids)
  2278. if failed_count == 0:
  2279. # 全部成功
  2280. return jsonify(success_response(
  2281. response_text="训练数据删除完成",
  2282. data={
  2283. "total_requested": len(ids),
  2284. "successfully_deleted": len(deleted_ids),
  2285. "failed_count": failed_count,
  2286. "deleted_ids": deleted_ids,
  2287. "failed_ids": failed_ids,
  2288. "failed_details": failed_details,
  2289. "current_total_count": current_total
  2290. }
  2291. ))
  2292. elif len(deleted_ids) == 0:
  2293. # 全部失败
  2294. return jsonify(error_response(
  2295. response_text="训练数据删除失败",
  2296. data={
  2297. "total_requested": len(ids),
  2298. "successfully_deleted": len(deleted_ids),
  2299. "failed_count": failed_count,
  2300. "deleted_ids": deleted_ids,
  2301. "failed_ids": failed_ids,
  2302. "failed_details": failed_details,
  2303. "current_total_count": current_total
  2304. }
  2305. )), 400
  2306. else:
  2307. # 部分成功,部分失败
  2308. return jsonify(error_response(
  2309. response_text=f"训练数据删除部分成功,成功{len(deleted_ids)}条,失败{failed_count}条",
  2310. data={
  2311. "total_requested": len(ids),
  2312. "successfully_deleted": len(deleted_ids),
  2313. "failed_count": failed_count,
  2314. "deleted_ids": deleted_ids,
  2315. "failed_ids": failed_ids,
  2316. "failed_details": failed_details,
  2317. "current_total_count": current_total
  2318. }
  2319. )), 207
  2320. except Exception as e:
  2321. logger.error(f"training_data_delete执行失败: {str(e)}")
  2322. return jsonify(internal_error_response(
  2323. response_text="删除训练数据失败,请稍后重试"
  2324. )), 500
  2325. @app.flask_app.route('/api/v0/training_data/stats', methods=['GET'])
  2326. def training_data_stats():
  2327. """
  2328. 获取训练数据统计信息API
  2329. """
  2330. try:
  2331. training_data = vn.get_training_data()
  2332. if training_data is None or training_data.empty:
  2333. return jsonify(success_response(
  2334. response_text="统计信息获取成功",
  2335. data={
  2336. "total_count": 0,
  2337. "type_breakdown": {
  2338. "sql": 0,
  2339. "documentation": 0,
  2340. "ddl": 0,
  2341. "error_sql": 0
  2342. },
  2343. "type_percentages": {
  2344. "sql": 0.0,
  2345. "documentation": 0.0,
  2346. "ddl": 0.0,
  2347. "error_sql": 0.0
  2348. },
  2349. "last_updated": datetime.now().isoformat()
  2350. }
  2351. ))
  2352. total_count = len(training_data)
  2353. # 统计各类型数量
  2354. type_breakdown = {"sql": 0, "documentation": 0, "ddl": 0, "error_sql": 0}
  2355. if 'training_data_type' in training_data.columns:
  2356. type_counts = training_data['training_data_type'].value_counts()
  2357. for data_type, count in type_counts.items():
  2358. if data_type in type_breakdown:
  2359. type_breakdown[data_type] = int(count)
  2360. # 计算百分比
  2361. type_percentages = {}
  2362. for data_type, count in type_breakdown.items():
  2363. type_percentages[data_type] = round(count / max(total_count, 1) * 100, 2)
  2364. return jsonify(success_response(
  2365. response_text="统计信息获取成功",
  2366. data={
  2367. "total_count": total_count,
  2368. "type_breakdown": type_breakdown,
  2369. "type_percentages": type_percentages,
  2370. "last_updated": datetime.now().isoformat()
  2371. }
  2372. ))
  2373. except Exception as e:
  2374. logger.error(f"training_data_stats执行失败: {str(e)}")
  2375. return jsonify(internal_error_response(
  2376. response_text="获取统计信息失败,请稍后重试"
  2377. )), 500
  2378. @app.flask_app.route('/api/v0/cache_overview_full', methods=['GET'])
  2379. def cache_overview_full():
  2380. """获取所有缓存系统的综合概览"""
  2381. try:
  2382. from common.embedding_cache_manager import get_embedding_cache_manager
  2383. from common.vanna_instance import get_vanna_instance
  2384. # 获取现有的缓存统计
  2385. vanna_cache = get_vanna_instance()
  2386. # 直接使用应用中的缓存实例
  2387. cache = app.cache
  2388. cache_overview = {
  2389. "conversation_aware_cache": {
  2390. "enabled": True,
  2391. "total_items": len(cache.cache) if hasattr(cache, 'cache') else 0,
  2392. "sessions": list(cache.cache.keys()) if hasattr(cache, 'cache') else [],
  2393. "cache_type": type(cache).__name__
  2394. },
  2395. "question_answer_cache": redis_conversation_manager.get_qa_cache_stats() if redis_conversation_manager.is_available() else {"available": False},
  2396. "embedding_cache": get_embedding_cache_manager().get_cache_stats(),
  2397. "redis_conversation_stats": redis_conversation_manager.get_stats() if redis_conversation_manager.is_available() else None
  2398. }
  2399. return jsonify(success_response(
  2400. response_text="获取综合缓存概览成功",
  2401. data=cache_overview
  2402. ))
  2403. except Exception as e:
  2404. logger.error(f"获取综合缓存概览失败: {str(e)}")
  2405. return jsonify(internal_error_response(
  2406. response_text="获取缓存概览失败,请稍后重试"
  2407. )), 500
  2408. # 前端JavaScript示例 - 如何维持会话
  2409. """
  2410. // 前端需要维护一个会话ID
  2411. class ChatSession {
  2412. constructor() {
  2413. // 从localStorage获取或创建新的会话ID
  2414. this.sessionId = localStorage.getItem('chat_session_id') || this.generateSessionId();
  2415. localStorage.setItem('chat_session_id', this.sessionId);
  2416. }
  2417. generateSessionId() {
  2418. return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  2419. }
  2420. async askQuestion(question) {
  2421. const response = await fetch('/api/v0/ask', {
  2422. method: 'POST',
  2423. headers: {
  2424. 'Content-Type': 'application/json',
  2425. },
  2426. body: JSON.stringify({
  2427. question: question,
  2428. session_id: this.sessionId // 关键:传递会话ID
  2429. })
  2430. });
  2431. return await response.json();
  2432. }
  2433. // 开始新会话
  2434. startNewSession() {
  2435. this.sessionId = this.generateSessionId();
  2436. localStorage.setItem('chat_session_id', this.sessionId);
  2437. }
  2438. }
  2439. // 使用示例
  2440. const chatSession = new ChatSession();
  2441. chatSession.askQuestion("各年龄段客户的流失率如何?");
  2442. """
  2443. # ==================== Data Pipeline API ====================
  2444. # 导入简化的Data Pipeline模块
  2445. import asyncio
  2446. import os
  2447. from threading import Thread
  2448. from flask import send_file
  2449. from data_pipeline.api.simple_workflow import SimpleWorkflowManager
  2450. from data_pipeline.api.simple_file_manager import SimpleFileManager
  2451. # 创建简化的管理器
  2452. data_pipeline_manager = None
  2453. data_pipeline_file_manager = None
  2454. def get_data_pipeline_manager():
  2455. """获取Data Pipeline管理器单例"""
  2456. global data_pipeline_manager
  2457. if data_pipeline_manager is None:
  2458. data_pipeline_manager = SimpleWorkflowManager()
  2459. return data_pipeline_manager
  2460. def get_data_pipeline_file_manager():
  2461. """获取Data Pipeline文件管理器单例"""
  2462. global data_pipeline_file_manager
  2463. if data_pipeline_file_manager is None:
  2464. data_pipeline_file_manager = SimpleFileManager()
  2465. return data_pipeline_file_manager
  2466. # ==================== 简化的Data Pipeline API端点 ====================
  2467. @app.flask_app.route('/api/v0/data_pipeline/tasks', methods=['POST'])
  2468. def create_data_pipeline_task():
  2469. """创建数据管道任务"""
  2470. try:
  2471. req = request.get_json(force=True)
  2472. # table_list_file和business_context现在都是可选参数
  2473. # 如果未提供table_list_file,将使用文件上传模式
  2474. # 创建任务(支持可选的db_connection参数)
  2475. manager = get_data_pipeline_manager()
  2476. task_id = manager.create_task(
  2477. table_list_file=req.get('table_list_file'),
  2478. business_context=req.get('business_context'),
  2479. db_name=req.get('db_name'), # 可选参数,用于指定特定数据库名称
  2480. db_connection=req.get('db_connection'), # 可选参数,用于指定数据库连接字符串
  2481. task_name=req.get('task_name'), # 可选参数,用于指定任务名称
  2482. enable_sql_validation=req.get('enable_sql_validation', True),
  2483. enable_llm_repair=req.get('enable_llm_repair', True),
  2484. modify_original_file=req.get('modify_original_file', True),
  2485. enable_training_data_load=req.get('enable_training_data_load', True)
  2486. )
  2487. # 获取任务信息
  2488. task_info = manager.get_task_status(task_id)
  2489. response_data = {
  2490. "task_id": task_id,
  2491. "task_name": task_info.get('task_name'),
  2492. "status": task_info.get('status'),
  2493. "created_at": task_info.get('created_at').isoformat() if task_info.get('created_at') else None
  2494. }
  2495. # 检查是否为文件上传模式
  2496. file_upload_mode = not req.get('table_list_file')
  2497. response_message = "任务创建成功"
  2498. if file_upload_mode:
  2499. response_data["file_upload_mode"] = True
  2500. response_data["next_step"] = f"POST /api/v0/data_pipeline/tasks/{task_id}/upload-table-list"
  2501. response_message += ",请上传表清单文件后再执行任务"
  2502. return jsonify(success_response(
  2503. response_text=response_message,
  2504. data=response_data
  2505. )), 201
  2506. except Exception as e:
  2507. logger.error(f"创建数据管道任务失败: {str(e)}")
  2508. return jsonify(internal_error_response(
  2509. response_text="创建任务失败,请稍后重试"
  2510. )), 500
  2511. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/execute', methods=['POST'])
  2512. def execute_data_pipeline_task(task_id):
  2513. """执行数据管道任务"""
  2514. try:
  2515. req = request.get_json(force=True) if request.is_json else {}
  2516. execution_mode = req.get('execution_mode', 'complete')
  2517. step_name = req.get('step_name')
  2518. # 验证执行模式
  2519. if execution_mode not in ['complete', 'step']:
  2520. return jsonify(bad_request_response(
  2521. response_text="无效的执行模式,必须是 'complete' 或 'step'",
  2522. invalid_params=['execution_mode']
  2523. )), 400
  2524. # 如果是步骤执行模式,验证步骤名称
  2525. if execution_mode == 'step':
  2526. if not step_name:
  2527. return jsonify(bad_request_response(
  2528. response_text="步骤执行模式需要指定step_name",
  2529. missing_params=['step_name']
  2530. )), 400
  2531. valid_steps = ['ddl_generation', 'qa_generation', 'sql_validation', 'training_load']
  2532. if step_name not in valid_steps:
  2533. return jsonify(bad_request_response(
  2534. response_text=f"无效的步骤名称,支持的步骤: {', '.join(valid_steps)}",
  2535. invalid_params=['step_name']
  2536. )), 400
  2537. # 检查任务是否存在
  2538. manager = get_data_pipeline_manager()
  2539. task_info = manager.get_task_status(task_id)
  2540. if not task_info:
  2541. return jsonify(not_found_response(
  2542. response_text=f"任务不存在: {task_id}"
  2543. )), 404
  2544. # 使用subprocess启动独立进程执行任务
  2545. def run_task_subprocess():
  2546. try:
  2547. import subprocess
  2548. import sys
  2549. from pathlib import Path
  2550. # 构建执行命令
  2551. python_executable = sys.executable
  2552. script_path = Path(__file__).parent / "data_pipeline" / "task_executor.py"
  2553. cmd = [
  2554. python_executable,
  2555. str(script_path),
  2556. "--task-id", task_id,
  2557. "--execution-mode", execution_mode
  2558. ]
  2559. if step_name:
  2560. cmd.extend(["--step-name", step_name])
  2561. logger.info(f"启动任务进程: {' '.join(cmd)}")
  2562. # 启动后台进程(不等待完成)
  2563. process = subprocess.Popen(
  2564. cmd,
  2565. stdout=subprocess.PIPE,
  2566. stderr=subprocess.PIPE,
  2567. text=True,
  2568. cwd=Path(__file__).parent
  2569. )
  2570. logger.info(f"任务进程已启动: PID={process.pid}, task_id={task_id}")
  2571. except Exception as e:
  2572. logger.error(f"启动任务进程失败: {task_id}, 错误: {str(e)}")
  2573. # 在新线程中启动subprocess(避免阻塞API响应)
  2574. thread = Thread(target=run_task_subprocess, daemon=True)
  2575. thread.start()
  2576. response_data = {
  2577. "task_id": task_id,
  2578. "execution_mode": execution_mode,
  2579. "step_name": step_name if execution_mode == 'step' else None,
  2580. "message": "任务正在后台执行,请通过状态接口查询进度"
  2581. }
  2582. return jsonify(success_response(
  2583. response_text="任务执行已启动",
  2584. data=response_data
  2585. )), 202
  2586. except Exception as e:
  2587. logger.error(f"启动数据管道任务执行失败: {str(e)}")
  2588. return jsonify(internal_error_response(
  2589. response_text="启动任务执行失败,请稍后重试"
  2590. )), 500
  2591. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>', methods=['GET'])
  2592. def get_data_pipeline_task_status(task_id):
  2593. """
  2594. 获取数据管道任务状态
  2595. 响应:
  2596. {
  2597. "success": true,
  2598. "code": 200,
  2599. "message": "获取任务状态成功",
  2600. "data": {
  2601. "task_id": "task_20250627_143052",
  2602. "status": "in_progress",
  2603. "step_status": {
  2604. "ddl_generation": "completed",
  2605. "qa_generation": "running",
  2606. "sql_validation": "pending",
  2607. "training_load": "pending"
  2608. },
  2609. "created_at": "2025-06-27T14:30:52",
  2610. "started_at": "2025-06-27T14:31:00",
  2611. "parameters": {...},
  2612. "current_execution": {...},
  2613. "total_executions": 2
  2614. }
  2615. }
  2616. """
  2617. try:
  2618. manager = get_data_pipeline_manager()
  2619. task_info = manager.get_task_status(task_id)
  2620. if not task_info:
  2621. return jsonify(not_found_response(
  2622. response_text=f"任务不存在: {task_id}"
  2623. )), 404
  2624. # 获取步骤状态
  2625. steps = manager.get_task_steps(task_id)
  2626. current_step = None
  2627. for step in steps:
  2628. if step['step_status'] == 'running':
  2629. current_step = step
  2630. break
  2631. # 构建步骤状态摘要
  2632. step_status_summary = {}
  2633. for step in steps:
  2634. step_status_summary[step['step_name']] = step['step_status']
  2635. response_data = {
  2636. "task_id": task_info['task_id'],
  2637. "task_name": task_info.get('task_name'),
  2638. "status": task_info['status'],
  2639. "step_status": step_status_summary,
  2640. "created_at": task_info['created_at'].isoformat() if task_info.get('created_at') else None,
  2641. "started_at": task_info['started_at'].isoformat() if task_info.get('started_at') else None,
  2642. "completed_at": task_info['completed_at'].isoformat() if task_info.get('completed_at') else None,
  2643. "parameters": task_info.get('parameters', {}),
  2644. "result": task_info.get('result'),
  2645. "error_message": task_info.get('error_message'),
  2646. "current_step": {
  2647. "execution_id": current_step['execution_id'],
  2648. "step": current_step['step_name'],
  2649. "status": current_step['step_status'],
  2650. "started_at": current_step['started_at'].isoformat() if current_step and current_step.get('started_at') else None
  2651. } if current_step else None,
  2652. "total_steps": len(steps),
  2653. "steps": [{
  2654. "step_name": step['step_name'],
  2655. "step_status": step['step_status'],
  2656. "started_at": step['started_at'].isoformat() if step.get('started_at') else None,
  2657. "completed_at": step['completed_at'].isoformat() if step.get('completed_at') else None,
  2658. "error_message": step.get('error_message')
  2659. } for step in steps]
  2660. }
  2661. return jsonify(success_response(
  2662. response_text="获取任务状态成功",
  2663. data=response_data
  2664. ))
  2665. except Exception as e:
  2666. logger.error(f"获取数据管道任务状态失败: {str(e)}")
  2667. return jsonify(internal_error_response(
  2668. response_text="获取任务状态失败,请稍后重试"
  2669. )), 500
  2670. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/logs', methods=['GET'])
  2671. def get_data_pipeline_task_logs(task_id):
  2672. """
  2673. 获取数据管道任务日志(从任务目录文件读取)
  2674. 查询参数:
  2675. - limit: 日志行数限制,默认100
  2676. - level: 日志级别过滤,可选
  2677. 响应:
  2678. {
  2679. "success": true,
  2680. "code": 200,
  2681. "message": "获取任务日志成功",
  2682. "data": {
  2683. "task_id": "task_20250627_143052",
  2684. "logs": [
  2685. {
  2686. "timestamp": "2025-06-27 14:30:52",
  2687. "level": "INFO",
  2688. "message": "任务开始执行"
  2689. }
  2690. ],
  2691. "total": 15,
  2692. "source": "file"
  2693. }
  2694. }
  2695. """
  2696. try:
  2697. limit = request.args.get('limit', 100, type=int)
  2698. level = request.args.get('level')
  2699. # 限制最大查询数量
  2700. limit = min(limit, 1000)
  2701. manager = get_data_pipeline_manager()
  2702. # 验证任务是否存在
  2703. task_info = manager.get_task_status(task_id)
  2704. if not task_info:
  2705. return jsonify(not_found_response(
  2706. response_text=f"任务不存在: {task_id}"
  2707. )), 404
  2708. # 获取任务目录下的日志文件
  2709. import os
  2710. from pathlib import Path
  2711. # 获取项目根目录的绝对路径
  2712. project_root = Path(__file__).parent.absolute()
  2713. task_dir = project_root / "data_pipeline" / "training_data" / task_id
  2714. log_file = task_dir / "data_pipeline.log"
  2715. logs = []
  2716. if log_file.exists():
  2717. try:
  2718. # 读取日志文件的最后N行
  2719. with open(log_file, 'r', encoding='utf-8') as f:
  2720. lines = f.readlines()
  2721. # 取最后limit行
  2722. recent_lines = lines[-limit:] if len(lines) > limit else lines
  2723. # 解析日志行
  2724. import re
  2725. log_pattern = r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+?): (.+)$'
  2726. for line in recent_lines:
  2727. line = line.strip()
  2728. if not line:
  2729. continue
  2730. match = re.match(log_pattern, line)
  2731. if match:
  2732. timestamp, log_level, logger_name, message = match.groups()
  2733. # 级别过滤
  2734. if level and log_level != level.upper():
  2735. continue
  2736. logs.append({
  2737. "timestamp": timestamp,
  2738. "level": log_level,
  2739. "logger": logger_name,
  2740. "message": message
  2741. })
  2742. else:
  2743. # 处理多行日志(如异常堆栈)
  2744. if logs:
  2745. logs[-1]["message"] += f"\n{line}"
  2746. except Exception as e:
  2747. logger.error(f"读取日志文件失败: {e}")
  2748. response_data = {
  2749. "task_id": task_id,
  2750. "logs": logs,
  2751. "total": len(logs),
  2752. "source": "file",
  2753. "log_file": str(log_file) if log_file.exists() else None
  2754. }
  2755. return jsonify(success_response(
  2756. response_text="获取任务日志成功",
  2757. data=response_data
  2758. ))
  2759. except Exception as e:
  2760. logger.error(f"获取数据管道任务日志失败: {str(e)}")
  2761. return jsonify(internal_error_response(
  2762. response_text="获取任务日志失败,请稍后重试"
  2763. )), 500
  2764. @app.flask_app.route('/api/v0/data_pipeline/tasks', methods=['GET'])
  2765. def list_data_pipeline_tasks():
  2766. """获取数据管道任务列表"""
  2767. try:
  2768. limit = request.args.get('limit', 50, type=int)
  2769. offset = request.args.get('offset', 0, type=int)
  2770. status_filter = request.args.get('status')
  2771. # 限制查询数量
  2772. limit = min(limit, 100)
  2773. manager = get_data_pipeline_manager()
  2774. tasks = manager.get_tasks_list(
  2775. limit=limit,
  2776. offset=offset,
  2777. status_filter=status_filter
  2778. )
  2779. # 格式化任务列表
  2780. formatted_tasks = []
  2781. for task in tasks:
  2782. formatted_tasks.append({
  2783. "task_id": task.get('task_id'),
  2784. "task_name": task.get('task_name'),
  2785. "status": task.get('status'),
  2786. "step_status": task.get('step_status'),
  2787. "created_at": task['created_at'].isoformat() if task.get('created_at') else None,
  2788. "started_at": task['started_at'].isoformat() if task.get('started_at') else None,
  2789. "completed_at": task['completed_at'].isoformat() if task.get('completed_at') else None,
  2790. "created_by": task.get('by_user'),
  2791. "db_name": task.get('db_name'),
  2792. "business_context": task.get('parameters', {}).get('business_context') if task.get('parameters') else None,
  2793. # 新增字段
  2794. "directory_exists": task.get('directory_exists', True), # 默认为True,兼容旧数据
  2795. "updated_at": task['updated_at'].isoformat() if task.get('updated_at') else None
  2796. })
  2797. response_data = {
  2798. "tasks": formatted_tasks,
  2799. "total": len(formatted_tasks),
  2800. "limit": limit,
  2801. "offset": offset
  2802. }
  2803. return jsonify(success_response(
  2804. response_text="获取任务列表成功",
  2805. data=response_data
  2806. ))
  2807. except Exception as e:
  2808. logger.error(f"获取数据管道任务列表失败: {str(e)}")
  2809. return jsonify(internal_error_response(
  2810. response_text="获取任务列表失败,请稍后重试"
  2811. )), 500
  2812. @app.flask_app.route('/api/v0/data_pipeline/tasks/query', methods=['POST'])
  2813. def query_data_pipeline_tasks():
  2814. """
  2815. 高级查询数据管道任务列表
  2816. 支持复杂筛选、排序、分页功能
  2817. 请求体:
  2818. {
  2819. "page": 1, // 页码,必须大于0,默认1
  2820. "page_size": 20, // 每页大小,1-100之间,默认20
  2821. "status": "completed", // 可选,任务状态筛选:"pending"|"running"|"completed"|"failed"|"cancelled"
  2822. "task_name": "highway", // 可选,任务名称模糊搜索,最大100字符
  2823. "created_by": "user123", // 可选,创建者精确匹配
  2824. "db_name": "highway_db", // 可选,数据库名称精确匹配
  2825. "created_time_start": "2025-01-01T00:00:00", // 可选,创建时间范围开始
  2826. "created_time_end": "2025-12-31T23:59:59", // 可选,创建时间范围结束
  2827. "started_time_start": "2025-01-01T00:00:00", // 可选,开始时间范围开始
  2828. "started_time_end": "2025-12-31T23:59:59", // 可选,开始时间范围结束
  2829. "completed_time_start": "2025-01-01T00:00:00", // 可选,完成时间范围开始
  2830. "completed_time_end": "2025-12-31T23:59:59", // 可选,完成时间范围结束
  2831. "sort_by": "created_at", // 可选,排序字段:"created_at"|"started_at"|"completed_at"|"task_name"|"status",默认"created_at"
  2832. "sort_order": "desc" // 可选,排序方向:"asc"|"desc",默认"desc"
  2833. }
  2834. 响应:
  2835. {
  2836. "success": true,
  2837. "code": 200,
  2838. "message": "查询任务列表成功",
  2839. "data": {
  2840. "tasks": [...],
  2841. "pagination": {
  2842. "page": 1,
  2843. "page_size": 20,
  2844. "total": 150,
  2845. "total_pages": 8,
  2846. "has_next": true,
  2847. "has_prev": false
  2848. },
  2849. "filters_applied": {...},
  2850. "sort_applied": {...},
  2851. "query_time": "0.045s"
  2852. }
  2853. }
  2854. """
  2855. try:
  2856. # 获取请求数据
  2857. req = request.get_json(force=True) if request.is_json else {}
  2858. # 解析参数,设置默认值
  2859. page = req.get('page', 1)
  2860. page_size = req.get('page_size', 20)
  2861. status = req.get('status')
  2862. task_name = req.get('task_name')
  2863. created_by = req.get('created_by')
  2864. db_name = req.get('db_name')
  2865. created_time_start = req.get('created_time_start')
  2866. created_time_end = req.get('created_time_end')
  2867. started_time_start = req.get('started_time_start')
  2868. started_time_end = req.get('started_time_end')
  2869. completed_time_start = req.get('completed_time_start')
  2870. completed_time_end = req.get('completed_time_end')
  2871. sort_by = req.get('sort_by', 'created_at')
  2872. sort_order = req.get('sort_order', 'desc')
  2873. # 参数验证
  2874. # 验证分页参数
  2875. if page < 1:
  2876. return jsonify(bad_request_response(
  2877. response_text="页码必须大于0",
  2878. invalid_params=['page']
  2879. )), 400
  2880. if page_size < 1 or page_size > 100:
  2881. return jsonify(bad_request_response(
  2882. response_text="每页大小必须在1-100之间",
  2883. invalid_params=['page_size']
  2884. )), 400
  2885. # 验证任务名称长度
  2886. if task_name and len(task_name) > 100:
  2887. return jsonify(bad_request_response(
  2888. response_text="任务名称搜索关键词最大长度为100字符",
  2889. invalid_params=['task_name']
  2890. )), 400
  2891. # 验证排序参数
  2892. allowed_sort_fields = ['created_at', 'started_at', 'completed_at', 'task_name', 'status']
  2893. if sort_by not in allowed_sort_fields:
  2894. return jsonify(bad_request_response(
  2895. response_text=f"不支持的排序字段: {sort_by},支持的字段: {', '.join(allowed_sort_fields)}",
  2896. invalid_params=['sort_by']
  2897. )), 400
  2898. if sort_order.lower() not in ['asc', 'desc']:
  2899. return jsonify(bad_request_response(
  2900. response_text="排序方向必须是 'asc' 或 'desc'",
  2901. invalid_params=['sort_order']
  2902. )), 400
  2903. # 验证状态筛选
  2904. if status:
  2905. allowed_statuses = ['pending', 'running', 'completed', 'failed', 'cancelled']
  2906. if status not in allowed_statuses:
  2907. return jsonify(bad_request_response(
  2908. response_text=f"不支持的状态值: {status},支持的状态: {', '.join(allowed_statuses)}",
  2909. invalid_params=['status']
  2910. )), 400
  2911. # 调用管理器执行查询
  2912. manager = get_data_pipeline_manager()
  2913. result = manager.query_tasks_advanced(
  2914. page=page,
  2915. page_size=page_size,
  2916. status=status,
  2917. task_name=task_name,
  2918. created_by=created_by,
  2919. db_name=db_name,
  2920. created_time_start=created_time_start,
  2921. created_time_end=created_time_end,
  2922. started_time_start=started_time_start,
  2923. started_time_end=started_time_end,
  2924. completed_time_start=completed_time_start,
  2925. completed_time_end=completed_time_end,
  2926. sort_by=sort_by,
  2927. sort_order=sort_order
  2928. )
  2929. # 格式化任务列表
  2930. formatted_tasks = []
  2931. for task in result['tasks']:
  2932. formatted_tasks.append({
  2933. "task_id": task.get('task_id'),
  2934. "task_name": task.get('task_name'),
  2935. "status": task.get('status'),
  2936. "step_status": task.get('step_status'),
  2937. "created_at": task['created_at'].isoformat() if task.get('created_at') else None,
  2938. "started_at": task['started_at'].isoformat() if task.get('started_at') else None,
  2939. "completed_at": task['completed_at'].isoformat() if task.get('completed_at') else None,
  2940. "created_by": task.get('by_user'),
  2941. "db_name": task.get('db_name'),
  2942. "business_context": task.get('parameters', {}).get('business_context') if task.get('parameters') else None,
  2943. "directory_exists": task.get('directory_exists', True),
  2944. "updated_at": task['updated_at'].isoformat() if task.get('updated_at') else None
  2945. })
  2946. # 构建响应数据
  2947. response_data = {
  2948. "tasks": formatted_tasks,
  2949. "pagination": result['pagination'],
  2950. "filters_applied": {
  2951. k: v for k, v in {
  2952. "status": status,
  2953. "task_name": task_name,
  2954. "created_by": created_by,
  2955. "db_name": db_name,
  2956. "created_time_start": created_time_start,
  2957. "created_time_end": created_time_end,
  2958. "started_time_start": started_time_start,
  2959. "started_time_end": started_time_end,
  2960. "completed_time_start": completed_time_start,
  2961. "completed_time_end": completed_time_end
  2962. }.items() if v
  2963. },
  2964. "sort_applied": {
  2965. "sort_by": sort_by,
  2966. "sort_order": sort_order
  2967. },
  2968. "query_time": result.get('query_time', '0.000s')
  2969. }
  2970. return jsonify(success_response(
  2971. response_text="查询任务列表成功",
  2972. data=response_data
  2973. ))
  2974. except Exception as e:
  2975. logger.error(f"查询数据管道任务列表失败: {str(e)}")
  2976. return jsonify(internal_error_response(
  2977. response_text="查询任务列表失败,请稍后重试"
  2978. )), 500
  2979. # ==================== 表检查API端点 ====================
  2980. import asyncio
  2981. from data_pipeline.api.table_inspector_api import TableInspectorAPI
  2982. @app.flask_app.route('/api/v0/database/tables', methods=['POST'])
  2983. def get_database_tables():
  2984. """
  2985. 获取数据库表列表
  2986. 请求体:
  2987. {
  2988. "db_connection": "postgresql://postgres:postgres@192.168.67.1:5432/highway_db", // 可选,不传则使用默认配置
  2989. "schema": "public,ods", // 可选,支持多个schema用逗号分隔,默认为public
  2990. "table_name_pattern": "ods_*" // 可选,表名模式匹配,支持通配符:ods_*、*_dim、*fact*、ods_%
  2991. }
  2992. 响应:
  2993. {
  2994. "success": true,
  2995. "code": 200,
  2996. "message": "获取表列表成功",
  2997. "data": {
  2998. "tables": ["public.table1", "public.table2", "ods.table3"],
  2999. "total": 3,
  3000. "schemas": ["public", "ods"],
  3001. "table_name_pattern": "ods_*"
  3002. }
  3003. }
  3004. """
  3005. try:
  3006. req = request.get_json(force=True)
  3007. # 处理数据库连接参数(可选)
  3008. db_connection = req.get('db_connection')
  3009. if not db_connection:
  3010. # 使用app_config的默认数据库配置
  3011. import app_config
  3012. db_params = app_config.APP_DB_CONFIG
  3013. db_connection = f"postgresql://{db_params['user']}:{db_params['password']}@{db_params['host']}:{db_params['port']}/{db_params['dbname']}"
  3014. logger.info("使用默认数据库配置获取表列表")
  3015. else:
  3016. logger.info("使用用户指定的数据库配置获取表列表")
  3017. # 可选参数
  3018. schema = req.get('schema', '')
  3019. table_name_pattern = req.get('table_name_pattern')
  3020. # 创建表检查API实例
  3021. table_inspector = TableInspectorAPI()
  3022. # 使用asyncio运行异步方法
  3023. async def get_tables():
  3024. return await table_inspector.get_tables_list(db_connection, schema, table_name_pattern)
  3025. # 在新的事件循环中运行异步方法
  3026. try:
  3027. loop = asyncio.new_event_loop()
  3028. asyncio.set_event_loop(loop)
  3029. tables = loop.run_until_complete(get_tables())
  3030. finally:
  3031. loop.close()
  3032. # 解析schema信息
  3033. parsed_schemas = table_inspector._parse_schemas(schema)
  3034. response_data = {
  3035. "tables": tables,
  3036. "total": len(tables),
  3037. "schemas": parsed_schemas,
  3038. "db_connection_info": {
  3039. "database": db_connection.split('/')[-1].split('?')[0] if '/' in db_connection else "unknown"
  3040. }
  3041. }
  3042. # 如果使用了表名模式,添加到响应中
  3043. if table_name_pattern:
  3044. response_data["table_name_pattern"] = table_name_pattern
  3045. return jsonify(success_response(
  3046. response_text="获取表列表成功",
  3047. data=response_data
  3048. )), 200
  3049. except Exception as e:
  3050. logger.error(f"获取数据库表列表失败: {str(e)}")
  3051. return jsonify(internal_error_response(
  3052. response_text=f"获取表列表失败: {str(e)}"
  3053. )), 500
  3054. @app.flask_app.route('/api/v0/database/table/ddl', methods=['POST'])
  3055. def get_table_ddl():
  3056. """
  3057. 获取表的DDL语句或MD文档
  3058. 请求体:
  3059. {
  3060. "db_connection": "postgresql://postgres:postgres@192.168.67.1:5432/highway_db", // 可选,不传则使用默认配置
  3061. "table": "public.test",
  3062. "business_context": "这是高速公路服务区的相关数据", // 可选
  3063. "type": "ddl" // 可选,支持ddl/md/both,默认为ddl
  3064. }
  3065. 响应:
  3066. {
  3067. "success": true,
  3068. "code": 200,
  3069. "message": "获取表DDL成功",
  3070. "data": {
  3071. "ddl": "create table public.test (...);",
  3072. "md": "## test表...", // 仅当type为md或both时返回
  3073. "table_info": {
  3074. "table_name": "test",
  3075. "schema_name": "public",
  3076. "full_name": "public.test",
  3077. "comment": "测试表",
  3078. "field_count": 10,
  3079. "row_count": 1000
  3080. },
  3081. "fields": [...]
  3082. }
  3083. }
  3084. """
  3085. try:
  3086. req = request.get_json(force=True)
  3087. # 处理参数(table仍为必需,db_connection可选)
  3088. table = req.get('table')
  3089. db_connection = req.get('db_connection')
  3090. if not table:
  3091. return jsonify(bad_request_response(
  3092. response_text="缺少必需参数:table",
  3093. missing_params=['table']
  3094. )), 400
  3095. if not db_connection:
  3096. # 使用app_config的默认数据库配置
  3097. import app_config
  3098. db_params = app_config.APP_DB_CONFIG
  3099. db_connection = f"postgresql://{db_params['user']}:{db_params['password']}@{db_params['host']}:{db_params['port']}/{db_params['dbname']}"
  3100. logger.info("使用默认数据库配置获取表DDL")
  3101. else:
  3102. logger.info("使用用户指定的数据库配置获取表DDL")
  3103. # 可选参数
  3104. business_context = req.get('business_context', '')
  3105. output_type = req.get('type', 'ddl')
  3106. # 验证type参数
  3107. valid_types = ['ddl', 'md', 'both']
  3108. if output_type not in valid_types:
  3109. return jsonify(bad_request_response(
  3110. response_text=f"无效的type参数: {output_type},支持的值: {valid_types}",
  3111. invalid_params=['type']
  3112. )), 400
  3113. # 创建表检查API实例
  3114. table_inspector = TableInspectorAPI()
  3115. # 使用asyncio运行异步方法
  3116. async def get_ddl():
  3117. return await table_inspector.get_table_ddl(
  3118. db_connection=db_connection,
  3119. table=table,
  3120. business_context=business_context,
  3121. output_type=output_type
  3122. )
  3123. # 在新的事件循环中运行异步方法
  3124. try:
  3125. loop = asyncio.new_event_loop()
  3126. asyncio.set_event_loop(loop)
  3127. result = loop.run_until_complete(get_ddl())
  3128. finally:
  3129. loop.close()
  3130. response_data = {
  3131. **result,
  3132. "generation_info": {
  3133. "business_context": business_context,
  3134. "output_type": output_type,
  3135. "has_llm_comments": bool(business_context),
  3136. "database": db_connection.split('/')[-1].split('?')[0] if '/' in db_connection else "unknown"
  3137. }
  3138. }
  3139. return jsonify(success_response(
  3140. response_text=f"获取表{output_type.upper()}成功",
  3141. data=response_data
  3142. )), 200
  3143. except Exception as e:
  3144. logger.error(f"获取表DDL失败: {str(e)}")
  3145. return jsonify(internal_error_response(
  3146. response_text=f"获取表{output_type.upper() if 'output_type' in locals() else 'DDL'}失败: {str(e)}"
  3147. )), 500
  3148. # ==================== Data Pipeline 文件管理 API ====================
  3149. from flask import send_file
  3150. # 创建文件管理器
  3151. data_pipeline_file_manager = None
  3152. def get_data_pipeline_file_manager():
  3153. """获取Data Pipeline文件管理器单例"""
  3154. global data_pipeline_file_manager
  3155. if data_pipeline_file_manager is None:
  3156. data_pipeline_file_manager = SimpleFileManager()
  3157. return data_pipeline_file_manager
  3158. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/files', methods=['GET'])
  3159. def get_data_pipeline_task_files(task_id):
  3160. """获取任务文件列表"""
  3161. try:
  3162. file_manager = get_data_pipeline_file_manager()
  3163. # 获取任务文件
  3164. files = file_manager.get_task_files(task_id)
  3165. directory_info = file_manager.get_directory_info(task_id)
  3166. # 格式化文件信息
  3167. formatted_files = []
  3168. for file_info in files:
  3169. formatted_files.append({
  3170. "file_name": file_info['file_name'],
  3171. "file_type": file_info['file_type'],
  3172. "file_size": file_info['file_size'],
  3173. "file_size_formatted": file_info['file_size_formatted'],
  3174. "created_at": file_info['created_at'].isoformat() if file_info.get('created_at') else None,
  3175. "modified_at": file_info['modified_at'].isoformat() if file_info.get('modified_at') else None,
  3176. "is_readable": file_info['is_readable']
  3177. })
  3178. response_data = {
  3179. "task_id": task_id,
  3180. "files": formatted_files,
  3181. "directory_info": directory_info
  3182. }
  3183. return jsonify(success_response(
  3184. response_text="获取任务文件列表成功",
  3185. data=response_data
  3186. ))
  3187. except Exception as e:
  3188. logger.error(f"获取任务文件列表失败: {str(e)}")
  3189. return jsonify(internal_error_response(
  3190. response_text="获取任务文件列表失败,请稍后重试"
  3191. )), 500
  3192. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/files/<file_name>', methods=['GET'])
  3193. def download_data_pipeline_task_file(task_id, file_name):
  3194. """下载任务文件"""
  3195. try:
  3196. logger.info(f"开始下载文件: task_id={task_id}, file_name={file_name}")
  3197. # 直接构建文件路径,避免依赖数据库
  3198. from pathlib import Path
  3199. import os
  3200. # 获取项目根目录的绝对路径
  3201. project_root = Path(__file__).parent.absolute()
  3202. task_dir = project_root / "data_pipeline" / "training_data" / task_id
  3203. file_path = task_dir / file_name
  3204. logger.info(f"文件路径: {file_path}")
  3205. # 检查文件是否存在
  3206. if not file_path.exists():
  3207. logger.warning(f"文件不存在: {file_path}")
  3208. return jsonify(not_found_response(
  3209. response_text=f"文件不存在: {file_name}"
  3210. )), 404
  3211. # 检查是否为文件(而不是目录)
  3212. if not file_path.is_file():
  3213. logger.warning(f"路径不是文件: {file_path}")
  3214. return jsonify(bad_request_response(
  3215. response_text=f"路径不是有效文件: {file_name}"
  3216. )), 400
  3217. # 安全检查:确保文件在允许的目录内
  3218. try:
  3219. file_path.resolve().relative_to(task_dir.resolve())
  3220. except ValueError:
  3221. logger.warning(f"文件路径不安全: {file_path}")
  3222. return jsonify(bad_request_response(
  3223. response_text="非法的文件路径"
  3224. )), 400
  3225. # 检查文件是否可读
  3226. if not os.access(file_path, os.R_OK):
  3227. logger.warning(f"文件不可读: {file_path}")
  3228. return jsonify(bad_request_response(
  3229. response_text="文件不可读"
  3230. )), 400
  3231. logger.info(f"开始发送文件: {file_path}")
  3232. return send_file(
  3233. file_path,
  3234. as_attachment=True,
  3235. download_name=file_name
  3236. )
  3237. except Exception as e:
  3238. logger.error(f"下载任务文件失败: task_id={task_id}, file_name={file_name}, 错误: {str(e)}", exc_info=True)
  3239. return jsonify(internal_error_response(
  3240. response_text="下载文件失败,请稍后重试"
  3241. )), 500
  3242. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/upload-table-list', methods=['POST'])
  3243. def upload_table_list_file(task_id):
  3244. """
  3245. 上传表清单文件
  3246. 表单参数:
  3247. - file: 要上传的表清单文件(multipart/form-data)
  3248. 响应:
  3249. {
  3250. "success": true,
  3251. "code": 200,
  3252. "message": "表清单文件上传成功",
  3253. "data": {
  3254. "task_id": "task_20250701_123456",
  3255. "filename": "table_list.txt",
  3256. "file_size": 1024,
  3257. "file_size_formatted": "1.0 KB"
  3258. }
  3259. }
  3260. """
  3261. try:
  3262. # 验证任务是否存在
  3263. manager = get_data_pipeline_manager()
  3264. task_info = manager.get_task_status(task_id)
  3265. if not task_info:
  3266. return jsonify(not_found_response(
  3267. response_text=f"任务不存在: {task_id}"
  3268. )), 404
  3269. # 检查是否有文件上传
  3270. if 'file' not in request.files:
  3271. return jsonify(bad_request_response(
  3272. response_text="请选择要上传的表清单文件",
  3273. missing_params=['file']
  3274. )), 400
  3275. file = request.files['file']
  3276. # 验证文件名
  3277. if file.filename == '':
  3278. return jsonify(bad_request_response(
  3279. response_text="请选择有效的文件"
  3280. )), 400
  3281. try:
  3282. # 使用文件管理器上传文件
  3283. file_manager = get_data_pipeline_file_manager()
  3284. result = file_manager.upload_table_list_file(task_id, file)
  3285. response_data = {
  3286. "task_id": task_id,
  3287. "filename": result["filename"],
  3288. "file_size": result["file_size"],
  3289. "file_size_formatted": result["file_size_formatted"],
  3290. "upload_time": result["upload_time"].isoformat() if result.get("upload_time") else None
  3291. }
  3292. return jsonify(success_response(
  3293. response_text="表清单文件上传成功",
  3294. data=response_data
  3295. )), 200
  3296. except ValueError as e:
  3297. # 文件验证错误(如文件太大、空文件等)
  3298. return jsonify(bad_request_response(
  3299. response_text=str(e)
  3300. )), 400
  3301. except Exception as e:
  3302. logger.error(f"上传表清单文件失败: {str(e)}")
  3303. return jsonify(internal_error_response(
  3304. response_text="文件上传失败,请稍后重试"
  3305. )), 500
  3306. except Exception as e:
  3307. logger.error(f"处理表清单文件上传请求失败: {str(e)}")
  3308. return jsonify(internal_error_response(
  3309. response_text="处理上传请求失败,请稍后重试"
  3310. )), 500
  3311. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/table-list-info', methods=['GET'])
  3312. def get_table_list_info(task_id):
  3313. """
  3314. 获取任务的表清单文件信息
  3315. 响应:
  3316. {
  3317. "success": true,
  3318. "code": 200,
  3319. "message": "获取表清单文件信息成功",
  3320. "data": {
  3321. "task_id": "task_20250701_123456",
  3322. "has_file": true,
  3323. "filename": "table_list.txt",
  3324. "file_path": "./data_pipeline/training_data/task_20250701_123456/table_list.txt",
  3325. "file_size": 1024,
  3326. "file_size_formatted": "1.0 KB",
  3327. "uploaded_at": "2025-07-01T12:34:56",
  3328. "table_count": 5,
  3329. "table_names": ["table_name_1", "table_name_2", "table_name_3", "table_name_4", "table_name_5"],
  3330. "is_readable": true
  3331. }
  3332. }
  3333. """
  3334. try:
  3335. file_manager = get_data_pipeline_file_manager()
  3336. # 获取表清单文件信息
  3337. table_list_info = file_manager.get_table_list_file_info(task_id)
  3338. response_data = {
  3339. "task_id": task_id,
  3340. "has_file": table_list_info.get("exists", False),
  3341. **table_list_info
  3342. }
  3343. return jsonify(success_response(
  3344. response_text="获取表清单文件信息成功",
  3345. data=response_data
  3346. ))
  3347. except Exception as e:
  3348. logger.error(f"获取表清单文件信息失败: {str(e)}")
  3349. return jsonify(internal_error_response(
  3350. response_text="获取表清单文件信息失败,请稍后重试"
  3351. )), 500
  3352. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/table-list', methods=['POST'])
  3353. def create_table_list_from_names(task_id):
  3354. """
  3355. 通过POST方式提交表名列表并创建table_list.txt文件
  3356. 请求体:
  3357. {
  3358. "tables": ["table1", "schema.table2", "table3"]
  3359. }
  3360. 或者:
  3361. {
  3362. "tables": "table1,schema.table2,table3"
  3363. }
  3364. 响应:
  3365. {
  3366. "success": true,
  3367. "code": 200,
  3368. "message": "表清单已成功创建",
  3369. "data": {
  3370. "task_id": "task_20250701_123456",
  3371. "filename": "table_list.txt",
  3372. "table_count": 3,
  3373. "file_size": 45,
  3374. "file_size_formatted": "45 B",
  3375. "created_time": "2025-07-01T12:34:56"
  3376. }
  3377. }
  3378. """
  3379. try:
  3380. # 验证任务是否存在
  3381. manager = get_data_pipeline_manager()
  3382. task_info = manager.get_task_status(task_id)
  3383. if not task_info:
  3384. return jsonify(not_found_response(
  3385. response_text=f"任务不存在: {task_id}"
  3386. )), 404
  3387. # 获取请求数据
  3388. req = request.get_json(force=True)
  3389. tables_param = req.get('tables')
  3390. if not tables_param:
  3391. return jsonify(bad_request_response(
  3392. response_text="缺少必需参数:tables",
  3393. missing_params=['tables']
  3394. )), 400
  3395. # 处理不同格式的表名参数
  3396. try:
  3397. if isinstance(tables_param, str):
  3398. # 逗号分隔的字符串格式
  3399. table_names = [name.strip() for name in tables_param.split(',') if name.strip()]
  3400. elif isinstance(tables_param, list):
  3401. # 数组格式
  3402. table_names = [str(name).strip() for name in tables_param if str(name).strip()]
  3403. else:
  3404. return jsonify(bad_request_response(
  3405. response_text="tables参数格式错误,应为字符串(逗号分隔)或数组"
  3406. )), 400
  3407. if not table_names:
  3408. return jsonify(bad_request_response(
  3409. response_text="表名列表不能为空"
  3410. )), 400
  3411. except Exception as e:
  3412. return jsonify(bad_request_response(
  3413. response_text=f"解析tables参数失败: {str(e)}"
  3414. )), 400
  3415. try:
  3416. # 使用文件管理器创建表清单文件
  3417. file_manager = get_data_pipeline_file_manager()
  3418. result = file_manager.create_table_list_from_names(task_id, table_names)
  3419. response_data = {
  3420. "task_id": task_id,
  3421. "filename": result["filename"],
  3422. "table_count": result["table_count"],
  3423. "unique_table_count": result["unique_table_count"],
  3424. "file_size": result["file_size"],
  3425. "file_size_formatted": result["file_size_formatted"],
  3426. "created_time": result["created_time"].isoformat() if result.get("created_time") else None,
  3427. "original_count": len(table_names) if isinstance(table_names, list) else len(tables_param.split(','))
  3428. }
  3429. return jsonify(success_response(
  3430. response_text=f"表清单已成功创建,包含 {result['table_count']} 个表",
  3431. data=response_data
  3432. )), 200
  3433. except ValueError as e:
  3434. # 表名验证错误(如格式错误、数量限制等)
  3435. return jsonify(bad_request_response(
  3436. response_text=str(e)
  3437. )), 400
  3438. except Exception as e:
  3439. logger.error(f"创建表清单文件失败: {str(e)}")
  3440. return jsonify(internal_error_response(
  3441. response_text="创建表清单文件失败,请稍后重试"
  3442. )), 500
  3443. except Exception as e:
  3444. logger.error(f"处理表清单创建请求失败: {str(e)}")
  3445. return jsonify(internal_error_response(
  3446. response_text="处理请求失败,请稍后重试"
  3447. )), 500
  3448. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/files', methods=['POST'])
  3449. def upload_file_to_task(task_id):
  3450. """
  3451. 上传文件到指定任务目录
  3452. 表单参数:
  3453. - file: 要上传的文件(multipart/form-data)
  3454. - overwrite_mode: 重名处理模式 (backup, replace, skip),默认为backup
  3455. 支持的文件类型:
  3456. - .ddl: DDL文件
  3457. - .md: Markdown文档
  3458. - .txt: 文本文件
  3459. - .json: JSON文件
  3460. - .sql: SQL文件
  3461. - .csv: CSV文件
  3462. 重名处理模式:
  3463. - backup: 备份原文件(默认)
  3464. - replace: 直接覆盖
  3465. - skip: 跳过上传
  3466. 响应:
  3467. {
  3468. "success": true,
  3469. "code": 200,
  3470. "message": "文件上传成功",
  3471. "data": {
  3472. "task_id": "task_20250701_123456",
  3473. "uploaded_file": {
  3474. "filename": "test.ddl",
  3475. "size": 1024,
  3476. "size_formatted": "1.0 KB",
  3477. "uploaded_at": "2025-07-01T12:34:56",
  3478. "overwrite_mode": "backup"
  3479. },
  3480. "backup_info": { // 仅当overwrite_mode为backup且文件已存在时返回
  3481. "had_existing_file": true,
  3482. "backup_filename": "test.ddl_bak1",
  3483. "backup_version": 1,
  3484. "backup_created_at": "2025-07-01T12:34:56"
  3485. }
  3486. }
  3487. }
  3488. """
  3489. try:
  3490. # 验证任务是否存在
  3491. manager = get_data_pipeline_manager()
  3492. task_info = manager.get_task_status(task_id)
  3493. if not task_info:
  3494. return jsonify(not_found_response(
  3495. response_text=f"任务不存在: {task_id}"
  3496. )), 404
  3497. # 检查是否有文件上传
  3498. if 'file' not in request.files:
  3499. return jsonify(bad_request_response(
  3500. response_text="请选择要上传的文件",
  3501. missing_params=['file']
  3502. )), 400
  3503. file = request.files['file']
  3504. # 验证文件名
  3505. if file.filename == '':
  3506. return jsonify(bad_request_response(
  3507. response_text="请选择有效的文件"
  3508. )), 400
  3509. # 获取重名处理模式
  3510. overwrite_mode = request.form.get('overwrite_mode', 'backup')
  3511. # 验证重名处理模式
  3512. valid_modes = ['backup', 'replace', 'skip']
  3513. if overwrite_mode not in valid_modes:
  3514. return jsonify(bad_request_response(
  3515. response_text=f"无效的overwrite_mode参数: {overwrite_mode},支持的值: {valid_modes}",
  3516. invalid_params=['overwrite_mode']
  3517. )), 400
  3518. try:
  3519. # 使用文件管理器上传文件
  3520. file_manager = get_data_pipeline_file_manager()
  3521. result = file_manager.upload_file_to_task(task_id, file, file.filename, overwrite_mode)
  3522. # 检查是否跳过上传
  3523. if result.get('skipped'):
  3524. return jsonify(success_response(
  3525. response_text=result.get('message', '文件已存在,跳过上传'),
  3526. data=result
  3527. )), 200
  3528. return jsonify(success_response(
  3529. response_text="文件上传成功",
  3530. data=result
  3531. )), 200
  3532. except ValueError as e:
  3533. # 文件验证错误(如文件太大、空文件、不支持的类型等)
  3534. return jsonify(bad_request_response(
  3535. response_text=str(e)
  3536. )), 400
  3537. except Exception as e:
  3538. logger.error(f"上传文件失败: {str(e)}")
  3539. return jsonify(internal_error_response(
  3540. response_text="文件上传失败,请稍后重试"
  3541. )), 500
  3542. except Exception as e:
  3543. logger.error(f"处理文件上传请求失败: {str(e)}")
  3544. return jsonify(internal_error_response(
  3545. response_text="处理上传请求失败,请稍后重试"
  3546. )), 500
  3547. # ==================== 任务目录删除API ====================
  3548. import shutil
  3549. from pathlib import Path
  3550. from datetime import datetime
  3551. import psycopg2
  3552. from app_config import PGVECTOR_CONFIG
  3553. def delete_task_directory_simple(task_id, delete_database_records=False):
  3554. """
  3555. 简单的任务目录删除功能
  3556. - 删除 data_pipeline/training_data/{task_id} 目录
  3557. - 更新数据库中的 directory_exists 字段
  3558. - 可选:删除数据库记录
  3559. """
  3560. try:
  3561. # 1. 删除目录
  3562. project_root = Path(__file__).parent.absolute()
  3563. task_dir = project_root / "data_pipeline" / "training_data" / task_id
  3564. deleted_files_count = 0
  3565. deleted_size = 0
  3566. if task_dir.exists():
  3567. # 计算删除前的统计信息
  3568. for file_path in task_dir.rglob('*'):
  3569. if file_path.is_file():
  3570. deleted_files_count += 1
  3571. deleted_size += file_path.stat().st_size
  3572. # 删除目录
  3573. shutil.rmtree(task_dir)
  3574. directory_deleted = True
  3575. operation_message = "目录删除成功"
  3576. else:
  3577. directory_deleted = False
  3578. operation_message = "目录不存在,无需删除"
  3579. # 2. 更新数据库
  3580. database_records_deleted = False
  3581. try:
  3582. conn = psycopg2.connect(**PGVECTOR_CONFIG)
  3583. cur = conn.cursor()
  3584. if delete_database_records:
  3585. # 删除任务步骤记录
  3586. cur.execute("DELETE FROM data_pipeline_task_steps WHERE task_id = %s", (task_id,))
  3587. # 删除任务主记录
  3588. cur.execute("DELETE FROM data_pipeline_tasks WHERE task_id = %s", (task_id,))
  3589. database_records_deleted = True
  3590. else:
  3591. # 只更新目录状态
  3592. cur.execute("""
  3593. UPDATE data_pipeline_tasks
  3594. SET directory_exists = FALSE, updated_at = CURRENT_TIMESTAMP
  3595. WHERE task_id = %s
  3596. """, (task_id,))
  3597. conn.commit()
  3598. cur.close()
  3599. conn.close()
  3600. except Exception as db_error:
  3601. logger.error(f"数据库操作失败: {db_error}")
  3602. # 数据库失败不影响文件删除的结果
  3603. # 3. 格式化文件大小
  3604. def format_size(size_bytes):
  3605. if size_bytes < 1024:
  3606. return f"{size_bytes} B"
  3607. elif size_bytes < 1024**2:
  3608. return f"{size_bytes/1024:.1f} KB"
  3609. elif size_bytes < 1024**3:
  3610. return f"{size_bytes/(1024**2):.1f} MB"
  3611. else:
  3612. return f"{size_bytes/(1024**3):.1f} GB"
  3613. return {
  3614. "success": True,
  3615. "task_id": task_id,
  3616. "directory_deleted": directory_deleted,
  3617. "database_records_deleted": database_records_deleted,
  3618. "deleted_files_count": deleted_files_count,
  3619. "deleted_size": format_size(deleted_size),
  3620. "deleted_at": datetime.now().isoformat(),
  3621. "operation_message": operation_message # 新增:具体的操作消息
  3622. }
  3623. except Exception as e:
  3624. logger.error(f"删除任务目录失败: {task_id}, 错误: {str(e)}")
  3625. return {
  3626. "success": False,
  3627. "task_id": task_id,
  3628. "error": str(e),
  3629. "error_code": "DELETE_FAILED",
  3630. "operation_message": f"删除操作失败: {str(e)}" # 新增:失败消息
  3631. }
  3632. @app.flask_app.route('/api/v0/data_pipeline/tasks', methods=['DELETE'])
  3633. def delete_tasks():
  3634. """删除任务目录(支持单个和批量)"""
  3635. try:
  3636. # 智能获取参数:支持JSON body和URL查询参数两种方式
  3637. def get_request_parameter(param_name, array_param_name=None):
  3638. """从JSON body或URL查询参数中获取参数值"""
  3639. # 1. 优先从JSON body获取
  3640. if request.is_json:
  3641. try:
  3642. json_data = request.get_json()
  3643. if json_data and param_name in json_data:
  3644. return json_data[param_name]
  3645. except:
  3646. pass
  3647. # 2. 从URL查询参数获取
  3648. if param_name in request.args:
  3649. value = request.args.get(param_name)
  3650. # 处理布尔值
  3651. if value.lower() in ('true', '1', 'yes'):
  3652. return True
  3653. elif value.lower() in ('false', '0', 'no'):
  3654. return False
  3655. return value
  3656. # 3. 处理数组参数(如 task_ids[])
  3657. if array_param_name and array_param_name in request.args:
  3658. return request.args.getlist(array_param_name)
  3659. return None
  3660. # 获取参数
  3661. task_ids = get_request_parameter('task_ids', 'task_ids[]')
  3662. confirm = get_request_parameter('confirm')
  3663. if not task_ids:
  3664. return jsonify(bad_request_response(
  3665. response_text="缺少必需参数: task_ids",
  3666. missing_params=['task_ids']
  3667. )), 400
  3668. if not confirm:
  3669. return jsonify(bad_request_response(
  3670. response_text="缺少必需参数: confirm",
  3671. missing_params=['confirm']
  3672. )), 400
  3673. if confirm != True:
  3674. return jsonify(bad_request_response(
  3675. response_text="confirm参数必须为true以确认删除操作"
  3676. )), 400
  3677. if not isinstance(task_ids, list) or len(task_ids) == 0:
  3678. return jsonify(bad_request_response(
  3679. response_text="task_ids必须是非空的任务ID列表"
  3680. )), 400
  3681. # 获取可选参数
  3682. delete_database_records = get_request_parameter('delete_database_records') or False
  3683. continue_on_error = get_request_parameter('continue_on_error')
  3684. if continue_on_error is None:
  3685. continue_on_error = True
  3686. # 执行批量删除操作
  3687. deleted_tasks = []
  3688. failed_tasks = []
  3689. total_size_freed = 0
  3690. for task_id in task_ids:
  3691. result = delete_task_directory_simple(task_id, delete_database_records)
  3692. if result["success"]:
  3693. deleted_tasks.append(result)
  3694. # 累计释放的空间大小(这里简化处理,实际应该解析size字符串)
  3695. else:
  3696. failed_tasks.append({
  3697. "task_id": task_id,
  3698. "error": result["error"],
  3699. "error_code": result.get("error_code", "UNKNOWN")
  3700. })
  3701. if not continue_on_error:
  3702. break
  3703. # 构建响应
  3704. summary = {
  3705. "total_requested": len(task_ids),
  3706. "successfully_deleted": len(deleted_tasks),
  3707. "failed": len(failed_tasks)
  3708. }
  3709. batch_result = {
  3710. "deleted_tasks": deleted_tasks,
  3711. "failed_tasks": failed_tasks,
  3712. "summary": summary,
  3713. "deleted_at": datetime.now().isoformat()
  3714. }
  3715. # 构建智能响应消息
  3716. if len(task_ids) == 1:
  3717. # 单个删除:使用具体的操作消息
  3718. if summary["failed"] == 0:
  3719. # 从deleted_tasks中获取具体的操作消息
  3720. operation_msg = deleted_tasks[0].get('operation_message', '任务处理完成')
  3721. message = operation_msg
  3722. else:
  3723. # 从failed_tasks中获取错误消息
  3724. error_msg = failed_tasks[0].get('error', '删除失败')
  3725. message = f"任务删除失败: {error_msg}"
  3726. else:
  3727. # 批量删除:统计各种操作结果
  3728. directory_deleted_count = sum(1 for task in deleted_tasks if task.get('directory_deleted', False))
  3729. directory_not_exist_count = sum(1 for task in deleted_tasks if not task.get('directory_deleted', False))
  3730. if summary["failed"] == 0:
  3731. # 全部成功
  3732. if directory_deleted_count > 0 and directory_not_exist_count > 0:
  3733. message = f"批量操作完成:{directory_deleted_count}个目录已删除,{directory_not_exist_count}个目录不存在"
  3734. elif directory_deleted_count > 0:
  3735. message = f"批量删除完成:成功删除{directory_deleted_count}个目录"
  3736. elif directory_not_exist_count > 0:
  3737. message = f"批量操作完成:{directory_not_exist_count}个目录不存在,无需删除"
  3738. else:
  3739. message = "批量操作完成"
  3740. elif summary["successfully_deleted"] == 0:
  3741. message = f"批量删除失败:{summary['failed']}个任务处理失败"
  3742. else:
  3743. message = f"批量删除部分完成:成功{summary['successfully_deleted']}个,失败{summary['failed']}个"
  3744. return jsonify(success_response(
  3745. response_text=message,
  3746. data=batch_result
  3747. )), 200
  3748. except Exception as e:
  3749. logger.error(f"删除任务失败: 错误: {str(e)}")
  3750. return jsonify(internal_error_response(
  3751. response_text="删除任务失败,请稍后重试"
  3752. )), 500
  3753. @app.flask_app.route('/api/v0/data_pipeline/tasks/<task_id>/logs/query', methods=['POST'])
  3754. def query_data_pipeline_task_logs(task_id):
  3755. """
  3756. 高级查询数据管道任务日志
  3757. 支持复杂筛选、排序、分页功能
  3758. 请求体:
  3759. {
  3760. "page": 1, // 页码,必须大于0,默认1
  3761. "page_size": 50, // 每页大小,1-500之间,默认50
  3762. "level": "ERROR", // 可选,日志级别筛选:"DEBUG"|"INFO"|"WARNING"|"ERROR"|"CRITICAL"
  3763. "start_time": "2025-01-01 00:00:00", // 可选,开始时间范围 (YYYY-MM-DD HH:MM:SS)
  3764. "end_time": "2025-01-02 23:59:59", // 可选,结束时间范围 (YYYY-MM-DD HH:MM:SS)
  3765. "keyword": "failed", // 可选,关键字搜索(消息内容模糊匹配)
  3766. "logger_name": "DDLGenerator", // 可选,日志记录器名称精确匹配
  3767. "step_name": "ddl_generation", // 可选,执行步骤名称精确匹配
  3768. "sort_by": "timestamp", // 可选,排序字段:"timestamp"|"level"|"logger"|"step"|"line_number",默认"timestamp"
  3769. "sort_order": "desc" // 可选,排序方向:"asc"|"desc",默认"desc"
  3770. }
  3771. 响应:
  3772. {
  3773. "success": true,
  3774. "code": 200,
  3775. "message": "查询任务日志成功",
  3776. "data": {
  3777. "logs": [
  3778. {
  3779. "timestamp": "2025-07-01 14:30:52",
  3780. "level": "INFO",
  3781. "logger": "SimpleWorkflowExecutor",
  3782. "step": "ddl_generation",
  3783. "message": "开始DDL生成",
  3784. "line_number": 15
  3785. }
  3786. ],
  3787. "pagination": {
  3788. "page": 1,
  3789. "page_size": 50,
  3790. "total": 1000,
  3791. "total_pages": 20,
  3792. "has_next": true,
  3793. "has_prev": false
  3794. },
  3795. "log_file_info": {
  3796. "exists": true,
  3797. "file_path": "/path/to/log/file",
  3798. "file_size": 1024000,
  3799. "file_size_formatted": "1.0 MB",
  3800. "last_modified": "2025-07-01T14:30:52",
  3801. "total_lines": 5000
  3802. },
  3803. "query_time": "0.123s"
  3804. }
  3805. }
  3806. """
  3807. try:
  3808. # 验证任务是否存在
  3809. manager = get_data_pipeline_manager()
  3810. task_info = manager.get_task_status(task_id)
  3811. if not task_info:
  3812. return jsonify(not_found_response(
  3813. response_text=f"任务不存在: {task_id}"
  3814. )), 404
  3815. # 解析请求数据
  3816. request_data = request.get_json() or {}
  3817. # 参数验证
  3818. def _is_valid_time_format(time_str):
  3819. """验证时间格式是否有效"""
  3820. if not time_str:
  3821. return True
  3822. # 支持的时间格式
  3823. time_formats = [
  3824. '%Y-%m-%d %H:%M:%S', # 2025-01-01 00:00:00
  3825. '%Y-%m-%d', # 2025-01-01
  3826. '%Y-%m-%dT%H:%M:%S', # 2025-01-01T00:00:00
  3827. '%Y-%m-%dT%H:%M:%S.%f', # 2025-01-01T00:00:00.123456
  3828. ]
  3829. for fmt in time_formats:
  3830. try:
  3831. from datetime import datetime
  3832. datetime.strptime(time_str, fmt)
  3833. return True
  3834. except ValueError:
  3835. continue
  3836. return False
  3837. # 提取和验证参数
  3838. page = request_data.get('page', 1)
  3839. page_size = request_data.get('page_size', 50)
  3840. level = request_data.get('level')
  3841. start_time = request_data.get('start_time')
  3842. end_time = request_data.get('end_time')
  3843. keyword = request_data.get('keyword')
  3844. logger_name = request_data.get('logger_name')
  3845. step_name = request_data.get('step_name')
  3846. sort_by = request_data.get('sort_by', 'timestamp')
  3847. sort_order = request_data.get('sort_order', 'desc')
  3848. # 参数验证
  3849. if not isinstance(page, int) or page < 1:
  3850. return jsonify(bad_request_response(
  3851. response_text="页码必须是大于0的整数"
  3852. )), 400
  3853. if not isinstance(page_size, int) or page_size < 1 or page_size > 500:
  3854. return jsonify(bad_request_response(
  3855. response_text="每页大小必须是1-500之间的整数"
  3856. )), 400
  3857. # 验证日志级别
  3858. if level and level.upper() not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
  3859. return jsonify(bad_request_response(
  3860. response_text="日志级别必须是DEBUG、INFO、WARNING、ERROR、CRITICAL之一"
  3861. )), 400
  3862. # 验证时间格式
  3863. if not _is_valid_time_format(start_time):
  3864. return jsonify(bad_request_response(
  3865. response_text="开始时间格式无效,支持格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD"
  3866. )), 400
  3867. if not _is_valid_time_format(end_time):
  3868. return jsonify(bad_request_response(
  3869. response_text="结束时间格式无效,支持格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD"
  3870. )), 400
  3871. # 验证关键字长度
  3872. if keyword and len(keyword) > 200:
  3873. return jsonify(bad_request_response(
  3874. response_text="关键字长度不能超过200个字符"
  3875. )), 400
  3876. # 验证排序字段
  3877. allowed_sort_fields = ['timestamp', 'level', 'logger', 'step', 'line_number']
  3878. if sort_by not in allowed_sort_fields:
  3879. return jsonify(bad_request_response(
  3880. response_text=f"排序字段必须是以下之一: {', '.join(allowed_sort_fields)}"
  3881. )), 400
  3882. # 验证排序方向
  3883. if sort_order.lower() not in ['asc', 'desc']:
  3884. return jsonify(bad_request_response(
  3885. response_text="排序方向必须是asc或desc"
  3886. )), 400
  3887. # 创建工作流执行器并查询日志
  3888. from data_pipeline.api.simple_workflow import SimpleWorkflowExecutor
  3889. executor = SimpleWorkflowExecutor(task_id)
  3890. try:
  3891. result = executor.query_logs_advanced(
  3892. page=page,
  3893. page_size=page_size,
  3894. level=level,
  3895. start_time=start_time,
  3896. end_time=end_time,
  3897. keyword=keyword,
  3898. logger_name=logger_name,
  3899. step_name=step_name,
  3900. sort_by=sort_by,
  3901. sort_order=sort_order
  3902. )
  3903. return jsonify(success_response(
  3904. response_text="查询任务日志成功",
  3905. data=result
  3906. ))
  3907. finally:
  3908. executor.cleanup()
  3909. except Exception as e:
  3910. logger.error(f"查询数据管道任务日志失败: {str(e)}")
  3911. return jsonify(internal_error_response(
  3912. response_text="查询任务日志失败,请稍后重试"
  3913. )), 500
  3914. if __name__ == '__main__':
  3915. logger.info("启动Flask应用: http://localhost:8084")
  3916. app.run(host="0.0.0.0", port=8084, debug=True)