citu_app.py 178 KB

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