citu_app.py 111 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759
  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 success_response, bad_request_response, not_found_response, internal_error_response
  21. from common.result import ( # 统一导入所有需要的响应函数
  22. bad_request_response, service_unavailable_response,
  23. agent_success_response, agent_error_response,
  24. internal_error_response, success_response,
  25. validation_failed_response
  26. )
  27. from app_config import ( # 添加Redis相关配置导入
  28. USER_MAX_CONVERSATIONS,
  29. CONVERSATION_CONTEXT_COUNT,
  30. DEFAULT_ANONYMOUS_USER,
  31. ENABLE_QUESTION_ANSWER_CACHE
  32. )
  33. # 创建app logger
  34. logger = get_app_logger("CituApp")
  35. # 设置默认的最大返回行数
  36. DEFAULT_MAX_RETURN_ROWS = 200
  37. MAX_RETURN_ROWS = API_MAX_RETURN_ROWS if API_MAX_RETURN_ROWS is not None else DEFAULT_MAX_RETURN_ROWS
  38. vn = create_vanna_instance()
  39. # 创建带时间戳的缓存
  40. timestamped_cache = WebSessionAwareMemoryCache()
  41. # 实例化 VannaFlaskApp,使用自定义缓存
  42. app = VannaFlaskApp(
  43. vn,
  44. cache=timestamped_cache, # 使用带时间戳的缓存
  45. title="辞图智能数据问答平台",
  46. logo = "https://www.citupro.com/img/logo-black-2.png",
  47. subtitle="让 AI 为你写 SQL",
  48. chart=False,
  49. allow_llm_to_see_data=True,
  50. ask_results_correct=True,
  51. followup_questions=True,
  52. debug=True
  53. )
  54. # 创建Redis对话管理器实例
  55. redis_conversation_manager = RedisConversationManager()
  56. # 修改ask接口,支持前端传递session_id
  57. @app.flask_app.route('/api/v0/ask', methods=['POST'])
  58. def ask_full():
  59. req = request.get_json(force=True)
  60. question = req.get("question", None)
  61. browser_session_id = req.get("session_id", None) # 前端传递的会话ID
  62. if not question:
  63. from common.result import bad_request_response
  64. return jsonify(bad_request_response(
  65. response_text="缺少必需参数:question",
  66. missing_params=["question"]
  67. )), 400
  68. # 如果使用WebSessionAwareMemoryCache
  69. if hasattr(app.cache, 'generate_id_with_browser_session') and browser_session_id:
  70. # 这里需要修改vanna的ask方法来支持传递session_id
  71. # 或者预先调用generate_id来建立会话关联
  72. conversation_id = app.cache.generate_id_with_browser_session(
  73. question=question,
  74. browser_session_id=browser_session_id
  75. )
  76. try:
  77. sql, df, _ = vn.ask(
  78. question=question,
  79. print_results=False,
  80. visualize=False,
  81. allow_llm_to_see_data=True
  82. )
  83. # 关键:检查是否有LLM解释性文本(无法生成SQL的情况)
  84. if sql is None and hasattr(vn, 'last_llm_explanation') and vn.last_llm_explanation:
  85. # 在解释性文本末尾添加提示语
  86. explanation_message = vn.last_llm_explanation + "请尝试提问其它问题。"
  87. # 使用标准化错误响应
  88. from common.result import validation_failed_response
  89. return jsonify(validation_failed_response(
  90. response_text=explanation_message
  91. )), 422 # 修改HTTP状态码为422
  92. # 如果sql为None但没有解释性文本,返回通用错误
  93. if sql is None:
  94. from common.result import validation_failed_response
  95. return jsonify(validation_failed_response(
  96. response_text="无法生成SQL查询,请检查问题描述或数据表结构"
  97. )), 422
  98. # 处理返回数据 - 使用新的query_result结构
  99. query_result = {
  100. "rows": [],
  101. "columns": [],
  102. "row_count": 0,
  103. "is_limited": False,
  104. "total_row_count": 0
  105. }
  106. summary = None
  107. if isinstance(df, pd.DataFrame):
  108. query_result["columns"] = list(df.columns)
  109. if not df.empty:
  110. total_rows = len(df)
  111. limited_df = df.head(MAX_RETURN_ROWS)
  112. query_result["rows"] = limited_df.to_dict(orient="records")
  113. query_result["row_count"] = len(limited_df)
  114. query_result["total_row_count"] = total_rows
  115. query_result["is_limited"] = total_rows > MAX_RETURN_ROWS
  116. # 生成数据摘要(可通过配置控制,仅在有数据时生成)
  117. if ENABLE_RESULT_SUMMARY:
  118. try:
  119. summary = vn.generate_summary(question=question, df=df)
  120. logger.info(f"成功生成摘要: {summary}")
  121. except Exception as e:
  122. logger.warning(f"生成摘要失败: {str(e)}")
  123. summary = None
  124. # 构建返回数据
  125. response_data = {
  126. "sql": sql,
  127. "query_result": query_result,
  128. "conversation_id": conversation_id if 'conversation_id' in locals() else None,
  129. "session_id": browser_session_id
  130. }
  131. # 添加摘要(如果启用且生成成功)
  132. if ENABLE_RESULT_SUMMARY and summary is not None:
  133. response_data["summary"] = summary
  134. response_data["response"] = summary # 同时添加response字段
  135. from common.result import success_response
  136. return jsonify(success_response(
  137. response_text="查询执行完成" if summary is None else None,
  138. data=response_data
  139. ))
  140. except Exception as e:
  141. logger.error(f"ask_full执行失败: {str(e)}")
  142. # 即使发生异常,也检查是否有业务层面的解释
  143. if hasattr(vn, 'last_llm_explanation') and vn.last_llm_explanation:
  144. # 在解释性文本末尾添加提示语
  145. explanation_message = vn.last_llm_explanation + "请尝试提问其它问题。"
  146. from common.result import validation_failed_response
  147. return jsonify(validation_failed_response(
  148. response_text=explanation_message
  149. )), 422
  150. else:
  151. # 技术错误,使用500错误码
  152. from common.result import internal_error_response
  153. return jsonify(internal_error_response(
  154. response_text="查询处理失败,请稍后重试"
  155. )), 500
  156. @app.flask_app.route('/api/v0/citu_run_sql', methods=['POST'])
  157. def citu_run_sql():
  158. req = request.get_json(force=True)
  159. sql = req.get('sql')
  160. if not sql:
  161. from common.result import bad_request_response
  162. return jsonify(bad_request_response(
  163. response_text="缺少必需参数:sql",
  164. missing_params=["sql"]
  165. )), 400
  166. try:
  167. df = vn.run_sql(sql)
  168. # 处理返回数据 - 使用新的query_result结构
  169. query_result = {
  170. "rows": [],
  171. "columns": [],
  172. "row_count": 0,
  173. "is_limited": False,
  174. "total_row_count": 0
  175. }
  176. if isinstance(df, pd.DataFrame):
  177. query_result["columns"] = list(df.columns)
  178. if not df.empty:
  179. total_rows = len(df)
  180. limited_df = df.head(MAX_RETURN_ROWS)
  181. query_result["rows"] = limited_df.to_dict(orient="records")
  182. query_result["row_count"] = len(limited_df)
  183. query_result["total_row_count"] = total_rows
  184. query_result["is_limited"] = total_rows > MAX_RETURN_ROWS
  185. from common.result import success_response
  186. return jsonify(success_response(
  187. response_text=f"SQL执行完成,共返回 {query_result['total_row_count']} 条记录" +
  188. (f",已限制显示前 {MAX_RETURN_ROWS} 条" if query_result["is_limited"] else ""),
  189. data={
  190. "sql": sql,
  191. "query_result": query_result
  192. }
  193. ))
  194. except Exception as e:
  195. logger.error(f"citu_run_sql执行失败: {str(e)}")
  196. from common.result import internal_error_response
  197. return jsonify(internal_error_response(
  198. response_text=f"SQL执行失败,请检查SQL语句是否正确"
  199. )), 500
  200. @app.flask_app.route('/api/v0/ask_cached', methods=['POST'])
  201. def ask_cached():
  202. """
  203. 带缓存功能的智能查询接口
  204. 支持会话管理和结果缓存,提高查询效率
  205. """
  206. req = request.get_json(force=True)
  207. question = req.get("question", None)
  208. browser_session_id = req.get("session_id", None)
  209. if not question:
  210. from common.result import bad_request_response
  211. return jsonify(bad_request_response(
  212. response_text="缺少必需参数:question",
  213. missing_params=["question"]
  214. )), 400
  215. try:
  216. # 生成conversation_id
  217. # 调试:查看generate_id的实际行为
  218. logger.debug(f"输入问题: '{question}'")
  219. conversation_id = app.cache.generate_id(question=question)
  220. logger.debug(f"生成的conversation_id: {conversation_id}")
  221. # 再次用相同问题测试
  222. conversation_id2 = app.cache.generate_id(question=question)
  223. logger.debug(f"再次生成的conversation_id: {conversation_id2}")
  224. logger.debug(f"两次ID是否相同: {conversation_id == conversation_id2}")
  225. # 检查缓存
  226. cached_sql = app.cache.get(id=conversation_id, field="sql")
  227. if cached_sql is not None:
  228. # 缓存命中
  229. logger.info(f"[CACHE HIT] 使用缓存结果: {conversation_id}")
  230. sql = cached_sql
  231. df = app.cache.get(id=conversation_id, field="df")
  232. summary = app.cache.get(id=conversation_id, field="summary")
  233. else:
  234. # 缓存未命中,执行新查询
  235. logger.info(f"[CACHE MISS] 执行新查询: {conversation_id}")
  236. sql, df, _ = vn.ask(
  237. question=question,
  238. print_results=False,
  239. visualize=False,
  240. allow_llm_to_see_data=True
  241. )
  242. # 检查是否有LLM解释性文本(无法生成SQL的情况)
  243. if sql is None and hasattr(vn, 'last_llm_explanation') and vn.last_llm_explanation:
  244. # 在解释性文本末尾添加提示语
  245. explanation_message = vn.last_llm_explanation + "请尝试用其它方式提问。"
  246. from common.result import validation_failed_response
  247. return jsonify(validation_failed_response(
  248. response_text=explanation_message
  249. )), 422
  250. # 如果sql为None但没有解释性文本,返回通用错误
  251. if sql is None:
  252. from common.result import validation_failed_response
  253. return jsonify(validation_failed_response(
  254. response_text="无法生成SQL查询,请检查问题描述或数据表结构"
  255. )), 422
  256. # 缓存结果
  257. app.cache.set(id=conversation_id, field="question", value=question)
  258. app.cache.set(id=conversation_id, field="sql", value=sql)
  259. app.cache.set(id=conversation_id, field="df", value=df)
  260. # 生成并缓存摘要(可通过配置控制,仅在有数据时生成)
  261. summary = None
  262. if ENABLE_RESULT_SUMMARY and isinstance(df, pd.DataFrame) and not df.empty:
  263. try:
  264. summary = vn.generate_summary(question=question, df=df)
  265. logger.info(f"成功生成摘要: {summary}")
  266. except Exception as e:
  267. logger.warning(f"生成摘要失败: {str(e)}")
  268. summary = None
  269. app.cache.set(id=conversation_id, field="summary", value=summary)
  270. # 处理返回数据 - 使用新的query_result结构
  271. query_result = {
  272. "rows": [],
  273. "columns": [],
  274. "row_count": 0,
  275. "is_limited": False,
  276. "total_row_count": 0
  277. }
  278. if isinstance(df, pd.DataFrame):
  279. query_result["columns"] = list(df.columns)
  280. if not df.empty:
  281. total_rows = len(df)
  282. limited_df = df.head(MAX_RETURN_ROWS)
  283. query_result["rows"] = limited_df.to_dict(orient="records")
  284. query_result["row_count"] = len(limited_df)
  285. query_result["total_row_count"] = total_rows
  286. query_result["is_limited"] = total_rows > MAX_RETURN_ROWS
  287. # 构建返回数据
  288. response_data = {
  289. "sql": sql,
  290. "query_result": query_result,
  291. "conversation_id": conversation_id,
  292. "session_id": browser_session_id,
  293. "cached": cached_sql is not None # 标识是否来自缓存
  294. }
  295. # 添加摘要(如果启用且生成成功)
  296. if ENABLE_RESULT_SUMMARY and summary is not None:
  297. response_data["summary"] = summary
  298. response_data["response"] = summary # 同时添加response字段
  299. from common.result import success_response
  300. return jsonify(success_response(
  301. response_text="查询执行完成" if summary is None else None,
  302. data=response_data
  303. ))
  304. except Exception as e:
  305. logger.error(f"ask_cached执行失败: {str(e)}")
  306. from common.result import internal_error_response
  307. return jsonify(internal_error_response(
  308. response_text="查询处理失败,请稍后重试"
  309. )), 500
  310. @app.flask_app.route('/api/v0/citu_train_question_sql', methods=['POST'])
  311. def citu_train_question_sql():
  312. """
  313. 训练问题-SQL对接口
  314. 此API将接收的question/sql pair写入到training库中,用于训练和改进AI模型。
  315. 支持仅传入SQL或同时传入问题和SQL进行训练。
  316. Args:
  317. question (str, optional): 用户问题
  318. sql (str, required): 对应的SQL查询语句
  319. Returns:
  320. JSON: 包含训练ID和成功消息的响应
  321. """
  322. try:
  323. req = request.get_json(force=True)
  324. question = req.get('question')
  325. sql = req.get('sql')
  326. if not sql:
  327. from common.result import bad_request_response
  328. return jsonify(bad_request_response(
  329. response_text="缺少必需参数:sql",
  330. missing_params=["sql"]
  331. )), 400
  332. # 正确的调用方式:同时传递question和sql
  333. if question:
  334. training_id = vn.train(question=question, sql=sql)
  335. logger.info(f"训练成功,训练ID为:{training_id},问题:{question},SQL:{sql}")
  336. else:
  337. training_id = vn.train(sql=sql)
  338. logger.info(f"训练成功,训练ID为:{training_id},SQL:{sql}")
  339. from common.result import success_response
  340. return jsonify(success_response(
  341. response_text="问题-SQL对训练成功",
  342. data={
  343. "training_id": training_id,
  344. "message": "Question-SQL pair trained successfully"
  345. }
  346. ))
  347. except Exception as e:
  348. from common.result import internal_error_response
  349. return jsonify(internal_error_response(
  350. response_text="训练失败,请稍后重试"
  351. )), 500
  352. # ============ LangGraph Agent 集成 ============
  353. # 全局Agent实例(单例模式)
  354. citu_langraph_agent = None
  355. def get_citu_langraph_agent():
  356. """获取LangGraph Agent实例(懒加载)"""
  357. global citu_langraph_agent
  358. if citu_langraph_agent is None:
  359. try:
  360. from agent.citu_agent import CituLangGraphAgent
  361. logger.info("开始创建LangGraph Agent实例...")
  362. citu_langraph_agent = CituLangGraphAgent()
  363. logger.info("LangGraph Agent实例创建成功")
  364. except ImportError as e:
  365. logger.critical(f"Agent模块导入失败: {str(e)}")
  366. logger.critical("请检查agent模块是否存在以及依赖是否正确安装")
  367. raise Exception(f"Agent模块导入失败: {str(e)}")
  368. except Exception as e:
  369. logger.critical(f"LangGraph Agent实例创建失败: {str(e)}")
  370. logger.critical(f"错误类型: {type(e).__name__}")
  371. # 提供更有用的错误信息
  372. if "config" in str(e).lower():
  373. logger.critical("可能是配置文件问题,请检查配置")
  374. elif "llm" in str(e).lower():
  375. logger.critical("可能是LLM连接问题,请检查LLM配置")
  376. elif "tool" in str(e).lower():
  377. logger.critical("可能是工具加载问题,请检查工具模块")
  378. raise Exception(f"Agent初始化失败: {str(e)}")
  379. return citu_langraph_agent
  380. @app.flask_app.route('/api/v0/ask_agent', methods=['POST'])
  381. def ask_agent():
  382. """
  383. 支持对话上下文的ask_agent API - 修正版
  384. """
  385. req = request.get_json(force=True)
  386. question = req.get("question", None)
  387. browser_session_id = req.get("session_id", None)
  388. # 新增参数解析
  389. user_id_input = req.get("user_id", None)
  390. conversation_id_input = req.get("conversation_id", None)
  391. continue_conversation = req.get("continue_conversation", False)
  392. # 新增:路由模式参数解析和验证
  393. api_routing_mode = req.get("routing_mode", None)
  394. VALID_ROUTING_MODES = ["database_direct", "chat_direct", "hybrid", "llm_only"]
  395. if not question:
  396. return jsonify(bad_request_response(
  397. response_text="缺少必需参数:question",
  398. missing_params=["question"]
  399. )), 400
  400. # 验证routing_mode参数
  401. if api_routing_mode and api_routing_mode not in VALID_ROUTING_MODES:
  402. return jsonify(bad_request_response(
  403. response_text=f"无效的routing_mode参数值: {api_routing_mode},支持的值: {VALID_ROUTING_MODES}",
  404. invalid_params=["routing_mode"]
  405. )), 400
  406. try:
  407. # 1. 获取登录用户ID(修正:在函数中获取session信息)
  408. login_user_id = session.get('user_id') if 'user_id' in session else None
  409. # 2. 智能ID解析(修正:传入登录用户ID)
  410. user_id = redis_conversation_manager.resolve_user_id(
  411. user_id_input, browser_session_id, request.remote_addr, login_user_id
  412. )
  413. conversation_id, conversation_status = redis_conversation_manager.resolve_conversation_id(
  414. user_id, conversation_id_input, continue_conversation
  415. )
  416. # 3. 获取上下文和上下文类型(提前到缓存检查之前)
  417. context = redis_conversation_manager.get_context(conversation_id)
  418. # 获取上下文类型:从最后一条助手消息的metadata中获取类型
  419. context_type = None
  420. if context:
  421. try:
  422. # 获取最后一条助手消息的metadata
  423. messages = redis_conversation_manager.get_messages(conversation_id, limit=10)
  424. for message in reversed(messages): # 从最新的开始找
  425. if message.get("role") == "assistant":
  426. metadata = message.get("metadata", {})
  427. context_type = metadata.get("type")
  428. if context_type:
  429. logger.info(f"[AGENT_API] 检测到上下文类型: {context_type}")
  430. break
  431. except Exception as e:
  432. logger.warning(f"获取上下文类型失败: {str(e)}")
  433. # 4. 检查缓存(新逻辑:放宽使用条件,严控存储条件)
  434. cached_answer = redis_conversation_manager.get_cached_answer(question, context)
  435. if cached_answer:
  436. logger.info(f"[AGENT_API] 使用缓存答案")
  437. # 确定缓存答案的助手回复内容(使用与非缓存相同的优先级逻辑)
  438. cached_response_type = cached_answer.get("type", "UNKNOWN")
  439. if cached_response_type == "DATABASE":
  440. # DATABASE类型:按优先级选择内容
  441. if cached_answer.get("response"):
  442. # 优先级1:错误或解释性回复(如SQL生成失败)
  443. assistant_response = cached_answer.get("response")
  444. elif cached_answer.get("summary"):
  445. # 优先级2:查询成功的摘要
  446. assistant_response = cached_answer.get("summary")
  447. elif cached_answer.get("query_result"):
  448. # 优先级3:构造简单描述
  449. query_result = cached_answer.get("query_result")
  450. row_count = query_result.get("row_count", 0)
  451. assistant_response = f"查询执行完成,共返回 {row_count} 条记录。"
  452. else:
  453. # 异常情况
  454. assistant_response = "数据库查询已处理。"
  455. else:
  456. # CHAT类型:直接使用response
  457. assistant_response = cached_answer.get("response", "")
  458. # 更新对话历史
  459. redis_conversation_manager.save_message(conversation_id, "user", question)
  460. redis_conversation_manager.save_message(
  461. conversation_id, "assistant",
  462. assistant_response,
  463. metadata={"from_cache": True}
  464. )
  465. # 添加对话信息到缓存结果
  466. cached_answer["conversation_id"] = conversation_id
  467. cached_answer["user_id"] = user_id
  468. cached_answer["from_cache"] = True
  469. cached_answer.update(conversation_status)
  470. # 使用agent_success_response返回标准格式
  471. return jsonify(agent_success_response(
  472. response_type=cached_answer.get("type", "UNKNOWN"),
  473. response=cached_answer.get("response", ""), # 修正:使用response而不是response_text
  474. sql=cached_answer.get("sql"),
  475. records=cached_answer.get("query_result"), # 修改:query_result改为records
  476. summary=cached_answer.get("summary"),
  477. session_id=browser_session_id,
  478. execution_path=cached_answer.get("execution_path", []),
  479. classification_info=cached_answer.get("classification_info", {}),
  480. conversation_id=conversation_id,
  481. user_id=user_id,
  482. is_guest_user=(user_id == DEFAULT_ANONYMOUS_USER),
  483. context_used=bool(context),
  484. from_cache=True,
  485. conversation_status=conversation_status["status"],
  486. conversation_message=conversation_status["message"],
  487. requested_conversation_id=conversation_status.get("requested_id")
  488. ))
  489. # 5. 保存用户消息
  490. redis_conversation_manager.save_message(conversation_id, "user", question)
  491. # 6. 构建带上下文的问题
  492. if context:
  493. enhanced_question = f"\n[CONTEXT]\n{context}\n\n[CURRENT]\n{question}"
  494. logger.info(f"[AGENT_API] 使用上下文,长度: {len(context)}字符")
  495. else:
  496. enhanced_question = question
  497. logger.info(f"[AGENT_API] 新对话,无上下文")
  498. # 7. 确定最终使用的路由模式(优先级逻辑)
  499. if api_routing_mode:
  500. # API传了参数,优先使用
  501. effective_routing_mode = api_routing_mode
  502. logger.info(f"[AGENT_API] 使用API指定的路由模式: {effective_routing_mode}")
  503. else:
  504. # API没传参数,使用配置文件
  505. try:
  506. from app_config import QUESTION_ROUTING_MODE
  507. effective_routing_mode = QUESTION_ROUTING_MODE
  508. logger.info(f"[AGENT_API] 使用配置文件路由模式: {effective_routing_mode}")
  509. except ImportError:
  510. effective_routing_mode = "hybrid"
  511. logger.info(f"[AGENT_API] 配置文件读取失败,使用默认路由模式: {effective_routing_mode}")
  512. # 8. 现有Agent处理逻辑(修改为传递路由模式)
  513. try:
  514. agent = get_citu_langraph_agent()
  515. except Exception as e:
  516. logger.critical(f"Agent初始化失败: {str(e)}")
  517. return jsonify(service_unavailable_response(
  518. response_text="AI服务暂时不可用,请稍后重试",
  519. can_retry=True
  520. )), 503
  521. # 异步调用Agent处理问题
  522. import asyncio
  523. agent_result = asyncio.run(agent.process_question(
  524. question=enhanced_question, # 使用增强后的问题
  525. session_id=browser_session_id,
  526. context_type=context_type, # 传递上下文类型
  527. routing_mode=effective_routing_mode # 新增:传递路由模式
  528. ))
  529. # 8. 处理Agent结果
  530. if agent_result.get("success", False):
  531. # 修正:直接从agent_result获取字段,因为它就是final_response
  532. response_type = agent_result.get("type", "UNKNOWN")
  533. response_text = agent_result.get("response", "")
  534. sql = agent_result.get("sql")
  535. query_result = agent_result.get("query_result")
  536. summary = agent_result.get("summary")
  537. execution_path = agent_result.get("execution_path", [])
  538. classification_info = agent_result.get("classification_info", {})
  539. # 确定助手回复内容的优先级
  540. if response_type == "DATABASE":
  541. # DATABASE类型:按优先级选择内容
  542. if response_text:
  543. # 优先级1:错误或解释性回复(如SQL生成失败)
  544. assistant_response = response_text
  545. elif summary:
  546. # 优先级2:查询成功的摘要
  547. assistant_response = summary
  548. elif query_result:
  549. # 优先级3:构造简单描述
  550. row_count = query_result.get("row_count", 0)
  551. assistant_response = f"查询执行完成,共返回 {row_count} 条记录。"
  552. else:
  553. # 异常情况
  554. assistant_response = "数据库查询已处理。"
  555. else:
  556. # CHAT类型:直接使用response
  557. assistant_response = response_text
  558. # 保存助手回复
  559. redis_conversation_manager.save_message(
  560. conversation_id, "assistant", assistant_response,
  561. metadata={
  562. "type": response_type,
  563. "sql": sql,
  564. "execution_path": execution_path
  565. }
  566. )
  567. # 缓存成功的答案(新逻辑:只缓存无上下文的问答)
  568. # 直接缓存agent_result,它已经包含所有需要的字段
  569. redis_conversation_manager.cache_answer(question, agent_result, context)
  570. # 使用agent_success_response的正确方式
  571. return jsonify(agent_success_response(
  572. response_type=response_type,
  573. response=response_text, # 修正:使用response而不是response_text
  574. sql=sql,
  575. records=query_result, # 修改:query_result改为records
  576. summary=summary,
  577. session_id=browser_session_id,
  578. execution_path=execution_path,
  579. classification_info=classification_info,
  580. conversation_id=conversation_id,
  581. user_id=user_id,
  582. is_guest_user=(user_id == DEFAULT_ANONYMOUS_USER),
  583. context_used=bool(context),
  584. from_cache=False,
  585. conversation_status=conversation_status["status"],
  586. conversation_message=conversation_status["message"],
  587. requested_conversation_id=conversation_status.get("requested_id"),
  588. routing_mode_used=effective_routing_mode, # 新增:实际使用的路由模式
  589. routing_mode_source="api" if api_routing_mode else "config" # 新增:路由模式来源
  590. ))
  591. else:
  592. # 错误处理(修正:确保使用现有的错误响应格式)
  593. error_message = agent_result.get("error", "Agent处理失败")
  594. error_code = agent_result.get("error_code", 500)
  595. return jsonify(agent_error_response(
  596. response_text=error_message,
  597. error_type="agent_processing_failed",
  598. code=error_code,
  599. session_id=browser_session_id,
  600. conversation_id=conversation_id,
  601. user_id=user_id
  602. )), error_code
  603. except Exception as e:
  604. logger.error(f"ask_agent执行失败: {str(e)}")
  605. return jsonify(internal_error_response(
  606. response_text="查询处理失败,请稍后重试"
  607. )), 500
  608. @app.flask_app.route('/api/v0/agent_health', methods=['GET'])
  609. def agent_health():
  610. """
  611. Agent健康检查接口
  612. 响应格式:
  613. {
  614. "success": true/false,
  615. "code": 200/503,
  616. "message": "healthy/degraded/unhealthy",
  617. "data": {
  618. "status": "healthy/degraded/unhealthy",
  619. "test_result": true/false,
  620. "workflow_compiled": true/false,
  621. "tools_count": 4,
  622. "message": "详细信息",
  623. "timestamp": "2024-01-01T12:00:00",
  624. "checks": {
  625. "agent_creation": true/false,
  626. "tools_import": true/false,
  627. "llm_connection": true/false,
  628. "classifier_ready": true/false
  629. }
  630. }
  631. }
  632. """
  633. try:
  634. # 基础健康检查
  635. health_data = {
  636. "status": "unknown",
  637. "test_result": False,
  638. "workflow_compiled": False,
  639. "tools_count": 0,
  640. "message": "",
  641. "timestamp": datetime.now().isoformat(),
  642. "checks": {
  643. "agent_creation": False,
  644. "tools_import": False,
  645. "llm_connection": False,
  646. "classifier_ready": False
  647. }
  648. }
  649. # 检查1: Agent创建
  650. try:
  651. agent = get_citu_langraph_agent()
  652. health_data["checks"]["agent_creation"] = True
  653. # 修正:Agent现在是动态创建workflow的,不再有预创建的workflow属性
  654. health_data["workflow_compiled"] = True # 动态创建,始终可用
  655. health_data["tools_count"] = len(agent.tools) if hasattr(agent, 'tools') else 0
  656. except Exception as e:
  657. health_data["message"] = f"Agent创建失败: {str(e)}"
  658. health_data["status"] = "unhealthy" # 设置状态
  659. from common.result import health_error_response
  660. return jsonify(health_error_response(**health_data)), 503
  661. # 检查2: 工具导入
  662. try:
  663. from agent.tools import TOOLS
  664. health_data["checks"]["tools_import"] = len(TOOLS) > 0
  665. except Exception as e:
  666. health_data["message"] = f"工具导入失败: {str(e)}"
  667. # 检查3: LLM连接(简单测试)
  668. try:
  669. from agent.tools.utils import get_compatible_llm
  670. llm = get_compatible_llm()
  671. health_data["checks"]["llm_connection"] = llm is not None
  672. except Exception as e:
  673. health_data["message"] = f"LLM连接失败: {str(e)}"
  674. # 检查4: 分类器准备
  675. try:
  676. from agent.classifier import QuestionClassifier
  677. classifier = QuestionClassifier()
  678. health_data["checks"]["classifier_ready"] = True
  679. except Exception as e:
  680. health_data["message"] = f"分类器失败: {str(e)}"
  681. # 检查5: 完整流程测试(可选)
  682. try:
  683. if all(health_data["checks"].values()):
  684. import asyncio
  685. # 异步调用健康检查
  686. test_result = asyncio.run(agent.health_check())
  687. health_data["test_result"] = test_result.get("status") == "healthy"
  688. health_data["status"] = test_result.get("status", "unknown")
  689. health_data["message"] = test_result.get("message", "健康检查完成")
  690. else:
  691. health_data["status"] = "degraded"
  692. health_data["message"] = "部分组件异常"
  693. except Exception as e:
  694. logger.error(f"健康检查异常: {str(e)}")
  695. import traceback
  696. logger.error(f"详细健康检查错误: {traceback.format_exc()}")
  697. health_data["status"] = "degraded"
  698. health_data["message"] = f"完整测试失败: {str(e)}"
  699. # 根据状态返回相应的HTTP代码 - 使用标准化健康检查响应
  700. from common.result import health_success_response, health_error_response
  701. if health_data["status"] == "healthy":
  702. return jsonify(health_success_response(**health_data))
  703. elif health_data["status"] == "degraded":
  704. return jsonify(health_error_response(**health_data)), 503
  705. else:
  706. # 确保状态设置为unhealthy
  707. health_data["status"] = "unhealthy"
  708. return jsonify(health_error_response(**health_data)), 503
  709. except Exception as e:
  710. logger.error(f"顶层健康检查异常: {str(e)}")
  711. import traceback
  712. logger.error(f"详细错误信息: {traceback.format_exc()}")
  713. from common.result import internal_error_response
  714. return jsonify(internal_error_response(
  715. response_text="健康检查失败,请稍后重试"
  716. )), 500
  717. # ==================== 日常管理API ====================
  718. @app.flask_app.route('/api/v0/cache_overview', methods=['GET'])
  719. def cache_overview():
  720. """日常管理:轻量概览 - 合并原cache_inspect的核心功能"""
  721. try:
  722. cache = app.cache
  723. result_data = {
  724. 'overview_summary': {
  725. 'total_conversations': 0,
  726. 'total_sessions': 0,
  727. 'query_time': datetime.now().isoformat()
  728. },
  729. 'recent_conversations': [], # 最近的对话
  730. 'session_summary': [] # 会话摘要
  731. }
  732. if hasattr(cache, 'cache') and isinstance(cache.cache, dict):
  733. result_data['overview_summary']['total_conversations'] = len(cache.cache)
  734. # 获取会话信息
  735. if hasattr(cache, 'get_all_sessions'):
  736. all_sessions = cache.get_all_sessions()
  737. result_data['overview_summary']['total_sessions'] = len(all_sessions)
  738. # 会话摘要(按最近活动排序)
  739. session_list = []
  740. for session_id, session_data in all_sessions.items():
  741. session_summary = {
  742. 'session_id': session_id,
  743. 'start_time': session_data['start_time'].isoformat(),
  744. 'conversation_count': session_data.get('conversation_count', 0),
  745. 'duration_seconds': session_data.get('session_duration_seconds', 0),
  746. 'last_activity': session_data.get('last_activity', session_data['start_time']).isoformat(),
  747. 'is_active': (datetime.now() - session_data.get('last_activity', session_data['start_time'])).total_seconds() < 1800 # 30分钟内活跃
  748. }
  749. session_list.append(session_summary)
  750. # 按最后活动时间排序
  751. session_list.sort(key=lambda x: x['last_activity'], reverse=True)
  752. result_data['session_summary'] = session_list
  753. # 最近的对话(最多显示10个)
  754. conversation_list = []
  755. for conversation_id, conversation_data in cache.cache.items():
  756. conversation_start_time = cache.conversation_start_times.get(conversation_id)
  757. conversation_info = {
  758. 'conversation_id': conversation_id,
  759. 'conversation_start_time': conversation_start_time.isoformat() if conversation_start_time else None,
  760. 'session_id': cache.conversation_to_session.get(conversation_id),
  761. 'has_question': 'question' in conversation_data,
  762. 'has_sql': 'sql' in conversation_data,
  763. 'has_data': 'df' in conversation_data and conversation_data['df'] is not None,
  764. 'question_preview': conversation_data.get('question', '')[:80] + '...' if len(conversation_data.get('question', '')) > 80 else conversation_data.get('question', ''),
  765. }
  766. # 计算对话持续时间
  767. if conversation_start_time:
  768. duration = datetime.now() - conversation_start_time
  769. conversation_info['conversation_duration_seconds'] = duration.total_seconds()
  770. conversation_list.append(conversation_info)
  771. # 按对话开始时间排序,显示最新的10个
  772. conversation_list.sort(key=lambda x: x['conversation_start_time'] or '', reverse=True)
  773. result_data['recent_conversations'] = conversation_list[:10]
  774. from common.result import success_response
  775. return jsonify(success_response(
  776. response_text="缓存概览查询完成",
  777. data=result_data
  778. ))
  779. except Exception as e:
  780. from common.result import internal_error_response
  781. return jsonify(internal_error_response(
  782. response_text="获取缓存概览失败,请稍后重试"
  783. )), 500
  784. @app.flask_app.route('/api/v0/cache_stats', methods=['GET'])
  785. def cache_stats():
  786. """日常管理:统计信息 - 合并原session_stats和cache_stats功能"""
  787. try:
  788. cache = app.cache
  789. current_time = datetime.now()
  790. stats = {
  791. 'basic_stats': {
  792. 'total_sessions': len(getattr(cache, 'session_info', {})),
  793. 'total_conversations': len(getattr(cache, 'cache', {})),
  794. 'active_sessions': 0, # 最近30分钟有活动
  795. 'average_conversations_per_session': 0
  796. },
  797. 'time_distribution': {
  798. 'sessions': {
  799. 'last_1_hour': 0,
  800. 'last_6_hours': 0,
  801. 'last_24_hours': 0,
  802. 'last_7_days': 0,
  803. 'older': 0
  804. },
  805. 'conversations': {
  806. 'last_1_hour': 0,
  807. 'last_6_hours': 0,
  808. 'last_24_hours': 0,
  809. 'last_7_days': 0,
  810. 'older': 0
  811. }
  812. },
  813. 'session_details': [],
  814. 'time_ranges': {
  815. 'oldest_session': None,
  816. 'newest_session': None,
  817. 'oldest_conversation': None,
  818. 'newest_conversation': None
  819. }
  820. }
  821. # 会话统计
  822. if hasattr(cache, 'session_info'):
  823. session_times = []
  824. total_conversations = 0
  825. for session_id, session_data in cache.session_info.items():
  826. start_time = session_data['start_time']
  827. session_times.append(start_time)
  828. conversation_count = len(session_data.get('conversations', []))
  829. total_conversations += conversation_count
  830. # 检查活跃状态
  831. last_activity = session_data.get('last_activity', session_data['start_time'])
  832. if (current_time - last_activity).total_seconds() < 1800:
  833. stats['basic_stats']['active_sessions'] += 1
  834. # 时间分布统计
  835. age_hours = (current_time - start_time).total_seconds() / 3600
  836. if age_hours <= 1:
  837. stats['time_distribution']['sessions']['last_1_hour'] += 1
  838. elif age_hours <= 6:
  839. stats['time_distribution']['sessions']['last_6_hours'] += 1
  840. elif age_hours <= 24:
  841. stats['time_distribution']['sessions']['last_24_hours'] += 1
  842. elif age_hours <= 168: # 7 days
  843. stats['time_distribution']['sessions']['last_7_days'] += 1
  844. else:
  845. stats['time_distribution']['sessions']['older'] += 1
  846. # 会话详细信息
  847. session_duration = current_time - start_time
  848. stats['session_details'].append({
  849. 'session_id': session_id,
  850. 'start_time': start_time.isoformat(),
  851. 'last_activity': last_activity.isoformat(),
  852. 'conversation_count': conversation_count,
  853. 'duration_seconds': session_duration.total_seconds(),
  854. 'duration_formatted': str(session_duration),
  855. 'is_active': (current_time - last_activity).total_seconds() < 1800,
  856. 'browser_session_id': session_data.get('browser_session_id')
  857. })
  858. # 计算平均值
  859. if len(cache.session_info) > 0:
  860. stats['basic_stats']['average_conversations_per_session'] = total_conversations / len(cache.session_info)
  861. # 时间范围
  862. if session_times:
  863. stats['time_ranges']['oldest_session'] = min(session_times).isoformat()
  864. stats['time_ranges']['newest_session'] = max(session_times).isoformat()
  865. # 对话统计
  866. if hasattr(cache, 'conversation_start_times'):
  867. conversation_times = []
  868. for conv_time in cache.conversation_start_times.values():
  869. conversation_times.append(conv_time)
  870. age_hours = (current_time - conv_time).total_seconds() / 3600
  871. if age_hours <= 1:
  872. stats['time_distribution']['conversations']['last_1_hour'] += 1
  873. elif age_hours <= 6:
  874. stats['time_distribution']['conversations']['last_6_hours'] += 1
  875. elif age_hours <= 24:
  876. stats['time_distribution']['conversations']['last_24_hours'] += 1
  877. elif age_hours <= 168:
  878. stats['time_distribution']['conversations']['last_7_days'] += 1
  879. else:
  880. stats['time_distribution']['conversations']['older'] += 1
  881. if conversation_times:
  882. stats['time_ranges']['oldest_conversation'] = min(conversation_times).isoformat()
  883. stats['time_ranges']['newest_conversation'] = max(conversation_times).isoformat()
  884. # 按最近活动排序会话详情
  885. stats['session_details'].sort(key=lambda x: x['last_activity'], reverse=True)
  886. from common.result import success_response
  887. return jsonify(success_response(
  888. response_text="缓存统计信息查询完成",
  889. data=stats
  890. ))
  891. except Exception as e:
  892. from common.result import internal_error_response
  893. return jsonify(internal_error_response(
  894. response_text="获取缓存统计失败,请稍后重试"
  895. )), 500
  896. # ==================== 高级功能API ====================
  897. @app.flask_app.route('/api/v0/cache_export', methods=['GET'])
  898. def cache_export():
  899. """高级功能:完整导出 - 保持原cache_raw_export的完整功能"""
  900. try:
  901. cache = app.cache
  902. # 验证缓存的实际结构
  903. if not hasattr(cache, 'cache'):
  904. from common.result import internal_error_response
  905. return jsonify(internal_error_response(
  906. response_text="缓存对象结构异常,请联系系统管理员"
  907. )), 500
  908. if not isinstance(cache.cache, dict):
  909. from common.result import internal_error_response
  910. return jsonify(internal_error_response(
  911. response_text="缓存数据类型异常,请联系系统管理员"
  912. )), 500
  913. # 定义JSON序列化辅助函数
  914. def make_json_serializable(obj):
  915. """将对象转换为JSON可序列化的格式"""
  916. if obj is None:
  917. return None
  918. elif isinstance(obj, (str, int, float, bool)):
  919. return obj
  920. elif isinstance(obj, (list, tuple)):
  921. return [make_json_serializable(item) for item in obj]
  922. elif isinstance(obj, dict):
  923. return {str(k): make_json_serializable(v) for k, v in obj.items()}
  924. elif hasattr(obj, 'isoformat'): # datetime objects
  925. return obj.isoformat()
  926. elif hasattr(obj, 'item'): # numpy scalars
  927. return obj.item()
  928. elif hasattr(obj, 'tolist'): # numpy arrays
  929. return obj.tolist()
  930. elif hasattr(obj, '__dict__'): # pandas dtypes and other objects
  931. return str(obj)
  932. else:
  933. return str(obj)
  934. # 获取完整的原始缓存数据
  935. raw_cache = cache.cache
  936. # 获取会话和对话时间信息
  937. conversation_times = getattr(cache, 'conversation_start_times', {})
  938. session_info = getattr(cache, 'session_info', {})
  939. conversation_to_session = getattr(cache, 'conversation_to_session', {})
  940. export_data = {
  941. 'export_metadata': {
  942. 'export_time': datetime.now().isoformat(),
  943. 'total_conversations': len(raw_cache),
  944. 'total_sessions': len(session_info),
  945. 'cache_type': type(cache).__name__,
  946. 'cache_object_info': str(cache),
  947. 'has_session_times': bool(session_info),
  948. 'has_conversation_times': bool(conversation_times)
  949. },
  950. 'session_info': {
  951. session_id: {
  952. 'start_time': session_data['start_time'].isoformat(),
  953. 'last_activity': session_data.get('last_activity', session_data['start_time']).isoformat(),
  954. 'conversations': session_data['conversations'],
  955. 'conversation_count': len(session_data['conversations']),
  956. 'browser_session_id': session_data.get('browser_session_id'),
  957. 'user_info': session_data.get('user_info', {})
  958. }
  959. for session_id, session_data in session_info.items()
  960. },
  961. 'conversation_times': {
  962. conversation_id: start_time.isoformat()
  963. for conversation_id, start_time in conversation_times.items()
  964. },
  965. 'conversation_to_session_mapping': conversation_to_session,
  966. 'conversations': {}
  967. }
  968. # 处理每个对话的完整数据
  969. for conversation_id, conversation_data in raw_cache.items():
  970. # 获取时间信息
  971. conversation_start_time = conversation_times.get(conversation_id)
  972. session_id = conversation_to_session.get(conversation_id)
  973. session_start_time = None
  974. if session_id and session_id in session_info:
  975. session_start_time = session_info[session_id]['start_time']
  976. processed_conversation = {
  977. 'conversation_id': conversation_id,
  978. 'conversation_start_time': conversation_start_time.isoformat() if conversation_start_time else None,
  979. 'session_id': session_id,
  980. 'session_start_time': session_start_time.isoformat() if session_start_time else None,
  981. 'field_count': len(conversation_data),
  982. 'fields': {}
  983. }
  984. # 添加时间计算
  985. if conversation_start_time:
  986. conversation_duration = datetime.now() - conversation_start_time
  987. processed_conversation['conversation_duration_seconds'] = conversation_duration.total_seconds()
  988. processed_conversation['conversation_duration_formatted'] = str(conversation_duration)
  989. if session_start_time:
  990. session_duration = datetime.now() - session_start_time
  991. processed_conversation['session_duration_seconds'] = session_duration.total_seconds()
  992. processed_conversation['session_duration_formatted'] = str(session_duration)
  993. # 处理每个字段,确保JSON序列化安全
  994. for field_name, field_value in conversation_data.items():
  995. field_info = {
  996. 'field_name': field_name,
  997. 'data_type': type(field_value).__name__,
  998. 'is_none': field_value is None
  999. }
  1000. try:
  1001. if field_value is None:
  1002. field_info['value'] = None
  1003. elif field_name in ['conversation_start_time', 'session_start_time']:
  1004. # 处理时间字段
  1005. field_info['content'] = make_json_serializable(field_value)
  1006. elif field_name == 'df' and field_value is not None:
  1007. # DataFrame的安全处理
  1008. if hasattr(field_value, 'to_dict'):
  1009. # 安全地处理dtypes
  1010. try:
  1011. dtypes_dict = {}
  1012. for col, dtype in field_value.dtypes.items():
  1013. dtypes_dict[col] = str(dtype)
  1014. except Exception:
  1015. dtypes_dict = {"error": "无法序列化dtypes"}
  1016. # 安全地处理内存使用
  1017. try:
  1018. memory_usage = field_value.memory_usage(deep=True)
  1019. memory_dict = {}
  1020. for idx, usage in memory_usage.items():
  1021. memory_dict[str(idx)] = int(usage) if hasattr(usage, 'item') else int(usage)
  1022. except Exception:
  1023. memory_dict = {"error": "无法获取内存使用信息"}
  1024. field_info.update({
  1025. 'dataframe_info': {
  1026. 'shape': list(field_value.shape),
  1027. 'columns': list(field_value.columns),
  1028. 'dtypes': dtypes_dict,
  1029. 'index_info': {
  1030. 'type': type(field_value.index).__name__,
  1031. 'length': len(field_value.index)
  1032. }
  1033. },
  1034. 'data': make_json_serializable(field_value.to_dict('records')),
  1035. 'memory_usage': memory_dict
  1036. })
  1037. else:
  1038. field_info['value'] = str(field_value)
  1039. field_info['note'] = 'not_standard_dataframe'
  1040. elif field_name == 'fig_json':
  1041. # 图表JSON数据处理
  1042. if isinstance(field_value, str):
  1043. try:
  1044. import json
  1045. parsed_fig = json.loads(field_value)
  1046. field_info.update({
  1047. 'json_valid': True,
  1048. 'json_size_bytes': len(field_value),
  1049. 'plotly_structure': {
  1050. 'has_data': 'data' in parsed_fig,
  1051. 'has_layout': 'layout' in parsed_fig,
  1052. 'data_traces_count': len(parsed_fig.get('data', [])),
  1053. },
  1054. 'raw_json': field_value
  1055. })
  1056. except json.JSONDecodeError:
  1057. field_info.update({
  1058. 'json_valid': False,
  1059. 'raw_content': str(field_value)
  1060. })
  1061. else:
  1062. field_info['value'] = make_json_serializable(field_value)
  1063. elif field_name == 'followup_questions':
  1064. # 后续问题列表
  1065. field_info.update({
  1066. 'content': make_json_serializable(field_value)
  1067. })
  1068. elif field_name in ['question', 'sql', 'summary']:
  1069. # 文本字段
  1070. if isinstance(field_value, str):
  1071. field_info.update({
  1072. 'text_length': len(field_value),
  1073. 'content': field_value
  1074. })
  1075. else:
  1076. field_info['value'] = make_json_serializable(field_value)
  1077. else:
  1078. # 未知字段的安全处理
  1079. field_info['content'] = make_json_serializable(field_value)
  1080. except Exception as e:
  1081. field_info.update({
  1082. 'processing_error': str(e),
  1083. 'fallback_value': str(field_value)[:500] + '...' if len(str(field_value)) > 500 else str(field_value)
  1084. })
  1085. processed_conversation['fields'][field_name] = field_info
  1086. export_data['conversations'][conversation_id] = processed_conversation
  1087. # 添加缓存统计信息
  1088. field_frequency = {}
  1089. data_types_found = set()
  1090. total_dataframes = 0
  1091. total_questions = 0
  1092. for conv_data in export_data['conversations'].values():
  1093. for field_name, field_info in conv_data['fields'].items():
  1094. field_frequency[field_name] = field_frequency.get(field_name, 0) + 1
  1095. data_types_found.add(field_info['data_type'])
  1096. if field_name == 'df' and not field_info['is_none']:
  1097. total_dataframes += 1
  1098. if field_name == 'question' and not field_info['is_none']:
  1099. total_questions += 1
  1100. export_data['cache_statistics'] = {
  1101. 'field_frequency': field_frequency,
  1102. 'data_types_found': list(data_types_found),
  1103. 'total_dataframes': total_dataframes,
  1104. 'total_questions': total_questions,
  1105. 'has_session_timing': 'session_start_time' in field_frequency,
  1106. 'has_conversation_timing': 'conversation_start_time' in field_frequency
  1107. }
  1108. from common.result import success_response
  1109. return jsonify(success_response(
  1110. response_text="缓存数据导出完成",
  1111. data=export_data
  1112. ))
  1113. except Exception as e:
  1114. import traceback
  1115. error_details = {
  1116. 'error_message': str(e),
  1117. 'error_type': type(e).__name__,
  1118. 'traceback': traceback.format_exc()
  1119. }
  1120. from common.result import internal_error_response
  1121. return jsonify(internal_error_response(
  1122. response_text="导出缓存失败,请稍后重试"
  1123. )), 500
  1124. # ==================== 清理功能API ====================
  1125. @app.flask_app.route('/api/v0/cache_preview_cleanup', methods=['POST'])
  1126. def cache_preview_cleanup():
  1127. """清理功能:预览删除操作 - 保持原功能"""
  1128. try:
  1129. req = request.get_json(force=True)
  1130. # 时间条件 - 支持三种方式
  1131. older_than_hours = req.get('older_than_hours')
  1132. older_than_days = req.get('older_than_days')
  1133. before_timestamp = req.get('before_timestamp') # YYYY-MM-DD HH:MM:SS 格式
  1134. cache = app.cache
  1135. # 计算截止时间
  1136. cutoff_time = None
  1137. time_condition = None
  1138. if older_than_hours:
  1139. cutoff_time = datetime.now() - timedelta(hours=older_than_hours)
  1140. time_condition = f"older_than_hours: {older_than_hours}"
  1141. elif older_than_days:
  1142. cutoff_time = datetime.now() - timedelta(days=older_than_days)
  1143. time_condition = f"older_than_days: {older_than_days}"
  1144. elif before_timestamp:
  1145. try:
  1146. # 支持 YYYY-MM-DD HH:MM:SS 格式
  1147. cutoff_time = datetime.strptime(before_timestamp, '%Y-%m-%d %H:%M:%S')
  1148. time_condition = f"before_timestamp: {before_timestamp}"
  1149. except ValueError:
  1150. from common.result import validation_failed_response
  1151. return jsonify(validation_failed_response(
  1152. response_text="before_timestamp格式错误,请使用 YYYY-MM-DD HH:MM:SS 格式"
  1153. )), 422
  1154. else:
  1155. from common.result import bad_request_response
  1156. return jsonify(bad_request_response(
  1157. response_text="必须提供时间条件:older_than_hours, older_than_days 或 before_timestamp (YYYY-MM-DD HH:MM:SS)",
  1158. missing_params=["older_than_hours", "older_than_days", "before_timestamp"]
  1159. )), 400
  1160. preview = {
  1161. 'time_condition': time_condition,
  1162. 'cutoff_time': cutoff_time.isoformat(),
  1163. 'will_be_removed': {
  1164. 'sessions': []
  1165. },
  1166. 'will_be_kept': {
  1167. 'sessions_count': 0,
  1168. 'conversations_count': 0
  1169. },
  1170. 'summary': {
  1171. 'sessions_to_remove': 0,
  1172. 'conversations_to_remove': 0,
  1173. 'sessions_to_keep': 0,
  1174. 'conversations_to_keep': 0
  1175. }
  1176. }
  1177. # 预览按session删除
  1178. sessions_to_remove_count = 0
  1179. conversations_to_remove_count = 0
  1180. for session_id, session_data in cache.session_info.items():
  1181. session_preview = {
  1182. 'session_id': session_id,
  1183. 'start_time': session_data['start_time'].isoformat(),
  1184. 'conversation_count': len(session_data['conversations']),
  1185. 'conversations': []
  1186. }
  1187. # 添加conversation详情
  1188. for conv_id in session_data['conversations']:
  1189. if conv_id in cache.cache:
  1190. conv_data = cache.cache[conv_id]
  1191. session_preview['conversations'].append({
  1192. 'conversation_id': conv_id,
  1193. 'question': conv_data.get('question', '')[:50] + '...' if conv_data.get('question') else '',
  1194. 'start_time': cache.conversation_start_times.get(conv_id, '').isoformat() if cache.conversation_start_times.get(conv_id) else ''
  1195. })
  1196. if session_data['start_time'] < cutoff_time:
  1197. preview['will_be_removed']['sessions'].append(session_preview)
  1198. sessions_to_remove_count += 1
  1199. conversations_to_remove_count += len(session_data['conversations'])
  1200. else:
  1201. preview['will_be_kept']['sessions_count'] += 1
  1202. preview['will_be_kept']['conversations_count'] += len(session_data['conversations'])
  1203. # 更新摘要统计
  1204. preview['summary'] = {
  1205. 'sessions_to_remove': sessions_to_remove_count,
  1206. 'conversations_to_remove': conversations_to_remove_count,
  1207. 'sessions_to_keep': preview['will_be_kept']['sessions_count'],
  1208. 'conversations_to_keep': preview['will_be_kept']['conversations_count']
  1209. }
  1210. from common.result import success_response
  1211. return jsonify(success_response(
  1212. response_text=f"清理预览完成,将删除 {sessions_to_remove_count} 个会话和 {conversations_to_remove_count} 个对话",
  1213. data=preview
  1214. ))
  1215. except Exception as e:
  1216. from common.result import internal_error_response
  1217. return jsonify(internal_error_response(
  1218. response_text="预览清理操作失败,请稍后重试"
  1219. )), 500
  1220. @app.flask_app.route('/api/v0/cache_cleanup', methods=['POST'])
  1221. def cache_cleanup():
  1222. """清理功能:实际删除缓存 - 保持原功能"""
  1223. try:
  1224. req = request.get_json(force=True)
  1225. # 时间条件 - 支持三种方式
  1226. older_than_hours = req.get('older_than_hours')
  1227. older_than_days = req.get('older_than_days')
  1228. before_timestamp = req.get('before_timestamp') # YYYY-MM-DD HH:MM:SS 格式
  1229. cache = app.cache
  1230. if not hasattr(cache, 'session_info'):
  1231. from common.result import service_unavailable_response
  1232. return jsonify(service_unavailable_response(
  1233. response_text="缓存不支持会话功能"
  1234. )), 503
  1235. # 计算截止时间
  1236. cutoff_time = None
  1237. time_condition = None
  1238. if older_than_hours:
  1239. cutoff_time = datetime.now() - timedelta(hours=older_than_hours)
  1240. time_condition = f"older_than_hours: {older_than_hours}"
  1241. elif older_than_days:
  1242. cutoff_time = datetime.now() - timedelta(days=older_than_days)
  1243. time_condition = f"older_than_days: {older_than_days}"
  1244. elif before_timestamp:
  1245. try:
  1246. # 支持 YYYY-MM-DD HH:MM:SS 格式
  1247. cutoff_time = datetime.strptime(before_timestamp, '%Y-%m-%d %H:%M:%S')
  1248. time_condition = f"before_timestamp: {before_timestamp}"
  1249. except ValueError:
  1250. from common.result import validation_failed_response
  1251. return jsonify(validation_failed_response(
  1252. response_text="before_timestamp格式错误,请使用 YYYY-MM-DD HH:MM:SS 格式"
  1253. )), 422
  1254. else:
  1255. from common.result import bad_request_response
  1256. return jsonify(bad_request_response(
  1257. response_text="必须提供时间条件:older_than_hours, older_than_days 或 before_timestamp (YYYY-MM-DD HH:MM:SS)",
  1258. missing_params=["older_than_hours", "older_than_days", "before_timestamp"]
  1259. )), 400
  1260. cleanup_stats = {
  1261. 'time_condition': time_condition,
  1262. 'cutoff_time': cutoff_time.isoformat(),
  1263. 'sessions_removed': 0,
  1264. 'conversations_removed': 0,
  1265. 'sessions_kept': 0,
  1266. 'conversations_kept': 0,
  1267. 'removed_session_ids': [],
  1268. 'removed_conversation_ids': []
  1269. }
  1270. # 按session删除
  1271. sessions_to_remove = []
  1272. for session_id, session_data in cache.session_info.items():
  1273. if session_data['start_time'] < cutoff_time:
  1274. sessions_to_remove.append(session_id)
  1275. # 删除符合条件的sessions及其所有conversations
  1276. for session_id in sessions_to_remove:
  1277. session_data = cache.session_info[session_id]
  1278. conversations_in_session = session_data['conversations'].copy()
  1279. # 删除session中的所有conversations
  1280. for conv_id in conversations_in_session:
  1281. if conv_id in cache.cache:
  1282. del cache.cache[conv_id]
  1283. cleanup_stats['conversations_removed'] += 1
  1284. cleanup_stats['removed_conversation_ids'].append(conv_id)
  1285. # 清理conversation相关的时间记录
  1286. if hasattr(cache, 'conversation_start_times') and conv_id in cache.conversation_start_times:
  1287. del cache.conversation_start_times[conv_id]
  1288. if hasattr(cache, 'conversation_to_session') and conv_id in cache.conversation_to_session:
  1289. del cache.conversation_to_session[conv_id]
  1290. # 删除session记录
  1291. del cache.session_info[session_id]
  1292. cleanup_stats['sessions_removed'] += 1
  1293. cleanup_stats['removed_session_ids'].append(session_id)
  1294. # 统计保留的sessions和conversations
  1295. cleanup_stats['sessions_kept'] = len(cache.session_info)
  1296. cleanup_stats['conversations_kept'] = len(cache.cache)
  1297. from common.result import success_response
  1298. return jsonify(success_response(
  1299. response_text=f"缓存清理完成,删除了 {cleanup_stats['sessions_removed']} 个会话和 {cleanup_stats['conversations_removed']} 个对话",
  1300. data=cleanup_stats
  1301. ))
  1302. except Exception as e:
  1303. from common.result import internal_error_response
  1304. return jsonify(internal_error_response(
  1305. response_text="缓存清理失败,请稍后重试"
  1306. )), 500
  1307. @app.flask_app.route('/api/v0/training_error_question_sql', methods=['POST'])
  1308. def training_error_question_sql():
  1309. """
  1310. 存储错误的question-sql对到error_sql集合中
  1311. 此API将接收的错误question/sql pair写入到error_sql集合中,用于记录和分析错误的SQL查询。
  1312. Args:
  1313. question (str, required): 用户问题
  1314. sql (str, required): 对应的错误SQL查询语句
  1315. Returns:
  1316. JSON: 包含训练ID和成功消息的响应
  1317. """
  1318. try:
  1319. data = request.get_json()
  1320. question = data.get('question')
  1321. sql = data.get('sql')
  1322. logger.debug(f"接收到错误SQL训练请求: question={question}, sql={sql}")
  1323. if not question or not sql:
  1324. from common.result import bad_request_response
  1325. missing_params = []
  1326. if not question:
  1327. missing_params.append("question")
  1328. if not sql:
  1329. missing_params.append("sql")
  1330. return jsonify(bad_request_response(
  1331. response_text="question和sql参数都是必需的",
  1332. missing_params=missing_params
  1333. )), 400
  1334. # 使用vn实例的train_error_sql方法存储错误SQL
  1335. id = vn.train_error_sql(question=question, sql=sql)
  1336. logger.info(f"成功存储错误SQL,ID: {id}")
  1337. from common.result import success_response
  1338. return jsonify(success_response(
  1339. response_text="错误SQL对已成功存储",
  1340. data={
  1341. "id": id,
  1342. "message": "错误SQL对已成功存储到error_sql集合"
  1343. }
  1344. ))
  1345. except Exception as e:
  1346. logger.error(f"存储错误SQL失败: {str(e)}")
  1347. from common.result import internal_error_response
  1348. return jsonify(internal_error_response(
  1349. response_text="存储错误SQL失败,请稍后重试"
  1350. )), 500
  1351. # ==================== Redis对话管理API ====================
  1352. @app.flask_app.route('/api/v0/user/<user_id>/conversations', methods=['GET'])
  1353. def get_user_conversations(user_id: str):
  1354. """获取用户的对话列表(按时间倒序)"""
  1355. try:
  1356. limit = request.args.get('limit', USER_MAX_CONVERSATIONS, type=int)
  1357. conversations = redis_conversation_manager.get_conversations(user_id, limit)
  1358. # 为每个对话动态获取标题(第一条用户消息)
  1359. for conversation in conversations:
  1360. conversation_id = conversation['conversation_id']
  1361. try:
  1362. # 获取所有消息,然后取第一条用户消息作为标题
  1363. messages = redis_conversation_manager.get_conversation_messages(conversation_id)
  1364. if messages and len(messages) > 0:
  1365. # 找到第一条用户消息(按时间顺序)
  1366. first_user_message = None
  1367. for message in messages:
  1368. if message.get('role') == 'user':
  1369. first_user_message = message
  1370. break
  1371. if first_user_message:
  1372. title = first_user_message.get('content', '对话').strip()
  1373. # 限制标题长度,保持整洁
  1374. if len(title) > 50:
  1375. conversation['conversation_title'] = title[:47] + "..."
  1376. else:
  1377. conversation['conversation_title'] = title
  1378. else:
  1379. conversation['conversation_title'] = "对话"
  1380. else:
  1381. conversation['conversation_title'] = "空对话"
  1382. except Exception as e:
  1383. logger.warning(f"获取对话标题失败 {conversation_id}: {str(e)}")
  1384. conversation['conversation_title'] = "对话"
  1385. return jsonify(success_response(
  1386. response_text="获取用户对话列表成功",
  1387. data={
  1388. "user_id": user_id,
  1389. "conversations": conversations,
  1390. "total_count": len(conversations)
  1391. }
  1392. ))
  1393. except Exception as e:
  1394. return jsonify(internal_error_response(
  1395. response_text="获取对话列表失败,请稍后重试"
  1396. )), 500
  1397. @app.flask_app.route('/api/v0/conversation/<conversation_id>/messages', methods=['GET'])
  1398. def get_conversation_messages(conversation_id: str):
  1399. """获取特定对话的消息历史"""
  1400. try:
  1401. limit = request.args.get('limit', type=int) # 可选参数
  1402. messages = redis_conversation_manager.get_conversation_messages(conversation_id, limit)
  1403. meta = redis_conversation_manager.get_conversation_meta(conversation_id)
  1404. return jsonify(success_response(
  1405. response_text="获取对话消息成功",
  1406. data={
  1407. "conversation_id": conversation_id,
  1408. "conversation_meta": meta,
  1409. "messages": messages,
  1410. "message_count": len(messages)
  1411. }
  1412. ))
  1413. except Exception as e:
  1414. return jsonify(internal_error_response(
  1415. response_text="获取对话消息失败"
  1416. )), 500
  1417. @app.flask_app.route('/api/v0/conversation/<conversation_id>/context', methods=['GET'])
  1418. def get_conversation_context(conversation_id: str):
  1419. """获取对话上下文(格式化用于LLM)"""
  1420. try:
  1421. count = request.args.get('count', CONVERSATION_CONTEXT_COUNT, type=int)
  1422. context = redis_conversation_manager.get_context_for_display(conversation_id, count)
  1423. return jsonify(success_response(
  1424. response_text="获取对话上下文成功",
  1425. data={
  1426. "conversation_id": conversation_id,
  1427. "context": context,
  1428. "context_message_count": count
  1429. }
  1430. ))
  1431. except Exception as e:
  1432. return jsonify(internal_error_response(
  1433. response_text="获取对话上下文失败"
  1434. )), 500
  1435. @app.flask_app.route('/api/v0/conversation_stats', methods=['GET'])
  1436. def conversation_stats():
  1437. """获取对话系统统计信息"""
  1438. try:
  1439. stats = redis_conversation_manager.get_stats()
  1440. return jsonify(success_response(
  1441. response_text="获取统计信息成功",
  1442. data=stats
  1443. ))
  1444. except Exception as e:
  1445. return jsonify(internal_error_response(
  1446. response_text="获取统计信息失败,请稍后重试"
  1447. )), 500
  1448. @app.flask_app.route('/api/v0/conversation_cleanup', methods=['POST'])
  1449. def conversation_cleanup():
  1450. """手动清理过期对话"""
  1451. try:
  1452. redis_conversation_manager.cleanup_expired_conversations()
  1453. return jsonify(success_response(
  1454. response_text="对话清理完成"
  1455. ))
  1456. except Exception as e:
  1457. return jsonify(internal_error_response(
  1458. response_text="对话清理失败,请稍后重试"
  1459. )), 500
  1460. @app.flask_app.route('/api/v0/user/<user_id>/conversations/full', methods=['GET'])
  1461. def get_user_conversations_with_messages(user_id: str):
  1462. """
  1463. 获取用户的完整对话数据(包含所有消息)
  1464. 一次性返回用户的所有对话和每个对话下的消息历史
  1465. Args:
  1466. user_id: 用户ID(路径参数)
  1467. conversation_limit: 对话数量限制(查询参数,可选,不传则返回所有对话)
  1468. message_limit: 每个对话的消息数限制(查询参数,可选,不传则返回所有消息)
  1469. Returns:
  1470. 包含用户所有对话和消息的完整数据
  1471. """
  1472. try:
  1473. # 获取可选参数,不传递时使用None(返回所有记录)
  1474. conversation_limit = request.args.get('conversation_limit', type=int)
  1475. message_limit = request.args.get('message_limit', type=int)
  1476. # 获取用户的对话列表
  1477. conversations = redis_conversation_manager.get_conversations(user_id, conversation_limit)
  1478. # 为每个对话获取消息历史
  1479. full_conversations = []
  1480. total_messages = 0
  1481. for conversation in conversations:
  1482. conversation_id = conversation['conversation_id']
  1483. # 获取对话消息
  1484. messages = redis_conversation_manager.get_conversation_messages(
  1485. conversation_id, message_limit
  1486. )
  1487. # 获取对话元数据
  1488. meta = redis_conversation_manager.get_conversation_meta(conversation_id)
  1489. # 组合完整数据
  1490. full_conversation = {
  1491. **conversation, # 基础对话信息
  1492. 'meta': meta, # 对话元数据
  1493. 'messages': messages, # 消息列表
  1494. 'message_count': len(messages)
  1495. }
  1496. full_conversations.append(full_conversation)
  1497. total_messages += len(messages)
  1498. return jsonify(success_response(
  1499. response_text="获取用户完整对话数据成功",
  1500. data={
  1501. "user_id": user_id,
  1502. "conversations": full_conversations,
  1503. "total_conversations": len(full_conversations),
  1504. "total_messages": total_messages,
  1505. "conversation_limit_applied": conversation_limit,
  1506. "message_limit_applied": message_limit,
  1507. "query_time": datetime.now().isoformat()
  1508. }
  1509. ))
  1510. except Exception as e:
  1511. logger.error(f"获取用户完整对话数据失败: {str(e)}")
  1512. return jsonify(internal_error_response(
  1513. response_text="获取用户对话数据失败,请稍后重试"
  1514. )), 500
  1515. # ==================== Embedding缓存管理接口 ====================
  1516. @app.flask_app.route('/api/v0/embedding_cache_stats', methods=['GET'])
  1517. def embedding_cache_stats():
  1518. """获取embedding缓存统计信息"""
  1519. try:
  1520. from common.embedding_cache_manager import get_embedding_cache_manager
  1521. cache_manager = get_embedding_cache_manager()
  1522. stats = cache_manager.get_cache_stats()
  1523. return jsonify(success_response(
  1524. response_text="获取embedding缓存统计成功",
  1525. data=stats
  1526. ))
  1527. except Exception as e:
  1528. logger.error(f"获取embedding缓存统计失败: {str(e)}")
  1529. return jsonify(internal_error_response(
  1530. response_text="获取embedding缓存统计失败,请稍后重试"
  1531. )), 500
  1532. @app.flask_app.route('/api/v0/embedding_cache_cleanup', methods=['POST'])
  1533. def embedding_cache_cleanup():
  1534. """清空所有embedding缓存"""
  1535. try:
  1536. from common.embedding_cache_manager import get_embedding_cache_manager
  1537. cache_manager = get_embedding_cache_manager()
  1538. if not cache_manager.is_available():
  1539. return jsonify(internal_error_response(
  1540. response_text="Embedding缓存功能未启用或不可用"
  1541. )), 400
  1542. success = cache_manager.clear_all_cache()
  1543. if success:
  1544. return jsonify(success_response(
  1545. response_text="所有embedding缓存已清空",
  1546. data={"cleared": True}
  1547. ))
  1548. else:
  1549. return jsonify(internal_error_response(
  1550. response_text="清空embedding缓存失败"
  1551. )), 500
  1552. except Exception as e:
  1553. logger.error(f"清空embedding缓存失败: {str(e)}")
  1554. return jsonify(internal_error_response(
  1555. response_text="清空embedding缓存失败,请稍后重试"
  1556. )), 500
  1557. # ==================== QA反馈系统接口 ====================
  1558. # 全局反馈管理器实例
  1559. qa_feedback_manager = None
  1560. def get_qa_feedback_manager():
  1561. """获取QA反馈管理器实例(懒加载)- 复用Vanna连接版本"""
  1562. global qa_feedback_manager
  1563. if qa_feedback_manager is None:
  1564. try:
  1565. # 优先尝试复用vanna连接
  1566. vanna_instance = None
  1567. try:
  1568. # 尝试获取现有的vanna实例
  1569. if 'get_citu_langraph_agent' in globals():
  1570. agent = get_citu_langraph_agent()
  1571. if hasattr(agent, 'vn'):
  1572. vanna_instance = agent.vn
  1573. elif 'vn' in globals():
  1574. vanna_instance = vn
  1575. else:
  1576. logger.info("未找到可用的vanna实例,将创建新的数据库连接")
  1577. except Exception as e:
  1578. logger.info(f"获取vanna实例失败: {e},将创建新的数据库连接")
  1579. vanna_instance = None
  1580. qa_feedback_manager = QAFeedbackManager(vanna_instance=vanna_instance)
  1581. logger.info("QA反馈管理器实例创建成功")
  1582. except Exception as e:
  1583. logger.critical(f"QA反馈管理器创建失败: {str(e)}")
  1584. raise Exception(f"QA反馈管理器初始化失败: {str(e)}")
  1585. return qa_feedback_manager
  1586. @app.flask_app.route('/api/v0/qa_feedback/query', methods=['POST'])
  1587. def qa_feedback_query():
  1588. """
  1589. 查询反馈记录API
  1590. 支持分页、筛选和排序功能
  1591. """
  1592. try:
  1593. req = request.get_json(force=True)
  1594. # 解析参数,设置默认值
  1595. page = req.get('page', 1)
  1596. page_size = req.get('page_size', 20)
  1597. is_thumb_up = req.get('is_thumb_up')
  1598. create_time_start = req.get('create_time_start')
  1599. create_time_end = req.get('create_time_end')
  1600. is_in_training_data = req.get('is_in_training_data')
  1601. sort_by = req.get('sort_by', 'create_time')
  1602. sort_order = req.get('sort_order', 'desc')
  1603. # 参数验证
  1604. if page < 1:
  1605. return jsonify(bad_request_response(
  1606. response_text="页码必须大于0",
  1607. invalid_params=["page"]
  1608. )), 400
  1609. if page_size < 1 or page_size > 100:
  1610. return jsonify(bad_request_response(
  1611. response_text="每页大小必须在1-100之间",
  1612. invalid_params=["page_size"]
  1613. )), 400
  1614. # 获取反馈管理器并查询
  1615. manager = get_qa_feedback_manager()
  1616. records, total = manager.query_feedback(
  1617. page=page,
  1618. page_size=page_size,
  1619. is_thumb_up=is_thumb_up,
  1620. create_time_start=create_time_start,
  1621. create_time_end=create_time_end,
  1622. is_in_training_data=is_in_training_data,
  1623. sort_by=sort_by,
  1624. sort_order=sort_order
  1625. )
  1626. # 计算分页信息
  1627. total_pages = (total + page_size - 1) // page_size
  1628. return jsonify(success_response(
  1629. response_text=f"查询成功,共找到 {total} 条记录",
  1630. data={
  1631. "records": records,
  1632. "pagination": {
  1633. "page": page,
  1634. "page_size": page_size,
  1635. "total": total,
  1636. "total_pages": total_pages,
  1637. "has_next": page < total_pages,
  1638. "has_prev": page > 1
  1639. }
  1640. }
  1641. ))
  1642. except Exception as e:
  1643. logger.error(f"qa_feedback_query执行失败: {str(e)}")
  1644. return jsonify(internal_error_response(
  1645. response_text="查询反馈记录失败,请稍后重试"
  1646. )), 500
  1647. @app.flask_app.route('/api/v0/qa_feedback/delete/<int:feedback_id>', methods=['DELETE'])
  1648. def qa_feedback_delete(feedback_id):
  1649. """
  1650. 删除反馈记录API
  1651. """
  1652. try:
  1653. manager = get_qa_feedback_manager()
  1654. success = manager.delete_feedback(feedback_id)
  1655. if success:
  1656. return jsonify(success_response(
  1657. response_text=f"反馈记录删除成功",
  1658. data={"deleted_id": feedback_id}
  1659. ))
  1660. else:
  1661. return jsonify(not_found_response(
  1662. response_text=f"反馈记录不存在 (ID: {feedback_id})"
  1663. )), 404
  1664. except Exception as e:
  1665. logger.error(f"qa_feedback_delete执行失败: {str(e)}")
  1666. return jsonify(internal_error_response(
  1667. response_text="删除反馈记录失败,请稍后重试"
  1668. )), 500
  1669. @app.flask_app.route('/api/v0/qa_feedback/update/<int:feedback_id>', methods=['PUT'])
  1670. def qa_feedback_update(feedback_id):
  1671. """
  1672. 更新反馈记录API
  1673. """
  1674. try:
  1675. req = request.get_json(force=True)
  1676. # 提取允许更新的字段
  1677. allowed_fields = ['question', 'sql', 'is_thumb_up', 'user_id', 'is_in_training_data']
  1678. update_data = {}
  1679. for field in allowed_fields:
  1680. if field in req:
  1681. update_data[field] = req[field]
  1682. if not update_data:
  1683. return jsonify(bad_request_response(
  1684. response_text="没有提供有效的更新字段",
  1685. missing_params=allowed_fields
  1686. )), 400
  1687. manager = get_qa_feedback_manager()
  1688. success = manager.update_feedback(feedback_id, **update_data)
  1689. if success:
  1690. return jsonify(success_response(
  1691. response_text="反馈记录更新成功",
  1692. data={
  1693. "updated_id": feedback_id,
  1694. "updated_fields": list(update_data.keys())
  1695. }
  1696. ))
  1697. else:
  1698. return jsonify(not_found_response(
  1699. response_text=f"反馈记录不存在或无变化 (ID: {feedback_id})"
  1700. )), 404
  1701. except Exception as e:
  1702. logger.error(f"qa_feedback_update执行失败: {str(e)}")
  1703. return jsonify(internal_error_response(
  1704. response_text="更新反馈记录失败,请稍后重试"
  1705. )), 500
  1706. @app.flask_app.route('/api/v0/qa_feedback/add_to_training', methods=['POST'])
  1707. def qa_feedback_add_to_training():
  1708. """
  1709. 将反馈记录添加到训练数据集API
  1710. 支持混合批量处理:正向反馈加入SQL训练集,负向反馈加入error_sql训练集
  1711. """
  1712. try:
  1713. req = request.get_json(force=True)
  1714. feedback_ids = req.get('feedback_ids', [])
  1715. if not feedback_ids or not isinstance(feedback_ids, list):
  1716. return jsonify(bad_request_response(
  1717. response_text="缺少有效的反馈ID列表",
  1718. missing_params=["feedback_ids"]
  1719. )), 400
  1720. manager = get_qa_feedback_manager()
  1721. # 获取反馈记录
  1722. records = manager.get_feedback_by_ids(feedback_ids)
  1723. if not records:
  1724. return jsonify(not_found_response(
  1725. response_text="未找到任何有效的反馈记录"
  1726. )), 404
  1727. # 分别处理正向和负向反馈
  1728. positive_count = 0 # 正向训练计数
  1729. negative_count = 0 # 负向训练计数
  1730. already_trained_count = 0 # 已训练计数
  1731. error_count = 0 # 错误计数
  1732. successfully_trained_ids = [] # 成功训练的ID列表
  1733. for record in records:
  1734. try:
  1735. # 检查是否已经在训练数据中
  1736. if record['is_in_training_data']:
  1737. already_trained_count += 1
  1738. continue
  1739. if record['is_thumb_up']:
  1740. # 正向反馈 - 加入标准SQL训练集
  1741. training_id = vn.train(
  1742. question=record['question'],
  1743. sql=record['sql']
  1744. )
  1745. positive_count += 1
  1746. logger.info(f"正向训练成功 - ID: {record['id']}, TrainingID: {training_id}")
  1747. else:
  1748. # 负向反馈 - 加入错误SQL训练集
  1749. training_id = vn.train_error_sql(
  1750. question=record['question'],
  1751. sql=record['sql']
  1752. )
  1753. negative_count += 1
  1754. logger.info(f"负向训练成功 - ID: {record['id']}, TrainingID: {training_id}")
  1755. successfully_trained_ids.append(record['id'])
  1756. except Exception as e:
  1757. logger.error(f"训练失败 - 反馈ID: {record['id']}, 错误: {e}")
  1758. error_count += 1
  1759. # 更新训练状态
  1760. if successfully_trained_ids:
  1761. updated_count = manager.mark_training_status(successfully_trained_ids, True)
  1762. logger.info(f"批量更新训练状态完成,影响 {updated_count} 条记录")
  1763. # 构建响应
  1764. total_processed = positive_count + negative_count + already_trained_count + error_count
  1765. return jsonify(success_response(
  1766. response_text=f"训练数据添加完成,成功处理 {positive_count + negative_count} 条记录",
  1767. data={
  1768. "summary": {
  1769. "total_requested": len(feedback_ids),
  1770. "total_processed": total_processed,
  1771. "positive_trained": positive_count,
  1772. "negative_trained": negative_count,
  1773. "already_trained": already_trained_count,
  1774. "errors": error_count
  1775. },
  1776. "successfully_trained_ids": successfully_trained_ids,
  1777. "training_details": {
  1778. "sql_training_count": positive_count,
  1779. "error_sql_training_count": negative_count
  1780. }
  1781. }
  1782. ))
  1783. except Exception as e:
  1784. logger.error(f"qa_feedback_add_to_training执行失败: {str(e)}")
  1785. return jsonify(internal_error_response(
  1786. response_text="添加训练数据失败,请稍后重试"
  1787. )), 500
  1788. @app.flask_app.route('/api/v0/qa_feedback/add', methods=['POST'])
  1789. def qa_feedback_add():
  1790. """
  1791. 添加反馈记录API
  1792. 用于前端直接创建反馈记录
  1793. """
  1794. try:
  1795. req = request.get_json(force=True)
  1796. question = req.get('question')
  1797. sql = req.get('sql')
  1798. is_thumb_up = req.get('is_thumb_up')
  1799. user_id = req.get('user_id', 'guest')
  1800. # 参数验证
  1801. if not question:
  1802. return jsonify(bad_request_response(
  1803. response_text="缺少必需参数:question",
  1804. missing_params=["question"]
  1805. )), 400
  1806. if not sql:
  1807. return jsonify(bad_request_response(
  1808. response_text="缺少必需参数:sql",
  1809. missing_params=["sql"]
  1810. )), 400
  1811. if is_thumb_up is None:
  1812. return jsonify(bad_request_response(
  1813. response_text="缺少必需参数:is_thumb_up",
  1814. missing_params=["is_thumb_up"]
  1815. )), 400
  1816. manager = get_qa_feedback_manager()
  1817. feedback_id = manager.add_feedback(
  1818. question=question,
  1819. sql=sql,
  1820. is_thumb_up=bool(is_thumb_up),
  1821. user_id=user_id
  1822. )
  1823. return jsonify(success_response(
  1824. response_text="反馈记录创建成功",
  1825. data={
  1826. "feedback_id": feedback_id
  1827. }
  1828. ))
  1829. except Exception as e:
  1830. logger.error(f"qa_feedback_add执行失败: {str(e)}")
  1831. return jsonify(internal_error_response(
  1832. response_text="创建反馈记录失败,请稍后重试"
  1833. )), 500
  1834. @app.flask_app.route('/api/v0/qa_feedback/stats', methods=['GET'])
  1835. def qa_feedback_stats():
  1836. """
  1837. 反馈统计API
  1838. 返回反馈数据的统计信息
  1839. """
  1840. try:
  1841. manager = get_qa_feedback_manager()
  1842. # 查询各种统计数据
  1843. all_records, total_count = manager.query_feedback(page=1, page_size=1)
  1844. positive_records, positive_count = manager.query_feedback(page=1, page_size=1, is_thumb_up=True)
  1845. negative_records, negative_count = manager.query_feedback(page=1, page_size=1, is_thumb_up=False)
  1846. trained_records, trained_count = manager.query_feedback(page=1, page_size=1, is_in_training_data=True)
  1847. untrained_records, untrained_count = manager.query_feedback(page=1, page_size=1, is_in_training_data=False)
  1848. return jsonify(success_response(
  1849. response_text="统计信息获取成功",
  1850. data={
  1851. "total_feedback": total_count,
  1852. "positive_feedback": positive_count,
  1853. "negative_feedback": negative_count,
  1854. "trained_feedback": trained_count,
  1855. "untrained_feedback": untrained_count,
  1856. "positive_rate": round(positive_count / max(total_count, 1) * 100, 2),
  1857. "training_rate": round(trained_count / max(total_count, 1) * 100, 2)
  1858. }
  1859. ))
  1860. except Exception as e:
  1861. logger.error(f"qa_feedback_stats执行失败: {str(e)}")
  1862. return jsonify(internal_error_response(
  1863. response_text="获取统计信息失败,请稍后重试"
  1864. )), 500
  1865. # ==================== 问答缓存管理接口 ====================
  1866. @app.flask_app.route('/api/v0/qa_cache_stats', methods=['GET'])
  1867. def qa_cache_stats():
  1868. """获取问答缓存统计信息"""
  1869. try:
  1870. stats = redis_conversation_manager.get_qa_cache_stats()
  1871. return jsonify(success_response(
  1872. response_text="获取问答缓存统计成功",
  1873. data=stats
  1874. ))
  1875. except Exception as e:
  1876. logger.error(f"获取问答缓存统计失败: {str(e)}")
  1877. return jsonify(internal_error_response(
  1878. response_text="获取问答缓存统计失败,请稍后重试"
  1879. )), 500
  1880. @app.flask_app.route('/api/v0/qa_cache_list', methods=['GET'])
  1881. def qa_cache_list():
  1882. """获取问答缓存列表(支持分页)"""
  1883. try:
  1884. # 获取分页参数,默认限制50条
  1885. limit = request.args.get('limit', 50, type=int)
  1886. # 限制最大返回数量,防止一次性返回过多数据
  1887. if limit > 500:
  1888. limit = 500
  1889. elif limit <= 0:
  1890. limit = 50
  1891. cache_list = redis_conversation_manager.get_qa_cache_list(limit)
  1892. return jsonify(success_response(
  1893. response_text="获取问答缓存列表成功",
  1894. data={
  1895. "cache_list": cache_list,
  1896. "total_returned": len(cache_list),
  1897. "limit_applied": limit,
  1898. "note": "按缓存时间倒序排列,最新的在前面"
  1899. }
  1900. ))
  1901. except Exception as e:
  1902. logger.error(f"获取问答缓存列表失败: {str(e)}")
  1903. return jsonify(internal_error_response(
  1904. response_text="获取问答缓存列表失败,请稍后重试"
  1905. )), 500
  1906. @app.flask_app.route('/api/v0/qa_cache_cleanup', methods=['POST'])
  1907. def qa_cache_cleanup():
  1908. """清空所有问答缓存"""
  1909. try:
  1910. if not redis_conversation_manager.is_available():
  1911. return jsonify(internal_error_response(
  1912. response_text="Redis连接不可用,无法执行清理操作"
  1913. )), 500
  1914. deleted_count = redis_conversation_manager.clear_all_qa_cache()
  1915. return jsonify(success_response(
  1916. response_text="问答缓存清理完成",
  1917. data={
  1918. "deleted_count": deleted_count,
  1919. "cleared": deleted_count > 0,
  1920. "cleanup_time": datetime.now().isoformat()
  1921. }
  1922. ))
  1923. except Exception as e:
  1924. logger.error(f"清空问答缓存失败: {str(e)}")
  1925. return jsonify(internal_error_response(
  1926. response_text="清空问答缓存失败,请稍后重试"
  1927. )), 500
  1928. # ==================== 训练数据管理接口 ====================
  1929. def validate_sql_syntax(sql: str) -> tuple[bool, str]:
  1930. """SQL语法检查(仅对sql类型)"""
  1931. try:
  1932. parsed = sqlparse.parse(sql.strip())
  1933. if not parsed or not parsed[0].tokens:
  1934. return False, "SQL语法错误:空语句"
  1935. # 基本语法检查
  1936. sql_upper = sql.strip().upper()
  1937. if not any(sql_upper.startswith(keyword) for keyword in
  1938. ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP']):
  1939. return False, "SQL语法错误:不是有效的SQL语句"
  1940. # 安全检查:禁止危险的SQL操作
  1941. dangerous_operations = ['UPDATE', 'DELETE', 'ALERT', 'DROP']
  1942. for operation in dangerous_operations:
  1943. if sql_upper.startswith(operation):
  1944. return False, f'在训练集中禁止使用"{",".join(dangerous_operations)}"'
  1945. return True, ""
  1946. except Exception as e:
  1947. return False, f"SQL语法错误:{str(e)}"
  1948. def paginate_data(data_list: list, page: int, page_size: int):
  1949. """分页处理算法"""
  1950. total = len(data_list)
  1951. start_idx = (page - 1) * page_size
  1952. end_idx = start_idx + page_size
  1953. page_data = data_list[start_idx:end_idx]
  1954. return {
  1955. "data": page_data,
  1956. "pagination": {
  1957. "page": page,
  1958. "page_size": page_size,
  1959. "total": total,
  1960. "total_pages": (total + page_size - 1) // page_size,
  1961. "has_next": end_idx < total,
  1962. "has_prev": page > 1
  1963. }
  1964. }
  1965. def filter_by_type(data_list: list, training_data_type: str):
  1966. """按类型筛选算法"""
  1967. if not training_data_type:
  1968. return data_list
  1969. return [
  1970. record for record in data_list
  1971. if record.get('training_data_type') == training_data_type
  1972. ]
  1973. def search_in_data(data_list: list, search_keyword: str):
  1974. """在数据中搜索关键词"""
  1975. if not search_keyword:
  1976. return data_list
  1977. keyword_lower = search_keyword.lower()
  1978. return [
  1979. record for record in data_list
  1980. if (record.get('question') and keyword_lower in record['question'].lower()) or
  1981. (record.get('content') and keyword_lower in record['content'].lower())
  1982. ]
  1983. def process_single_training_item(item: dict, index: int) -> dict:
  1984. """处理单个训练数据项"""
  1985. training_type = item.get('training_data_type')
  1986. if training_type == 'sql':
  1987. sql = item.get('sql')
  1988. if not sql:
  1989. raise ValueError("SQL字段是必需的")
  1990. # SQL语法检查
  1991. is_valid, error_msg = validate_sql_syntax(sql)
  1992. if not is_valid:
  1993. raise ValueError(error_msg)
  1994. question = item.get('question')
  1995. if question:
  1996. training_id = vn.train(question=question, sql=sql)
  1997. else:
  1998. training_id = vn.train(sql=sql)
  1999. elif training_type == 'error_sql':
  2000. # error_sql不需要语法检查
  2001. question = item.get('question')
  2002. sql = item.get('sql')
  2003. if not question or not sql:
  2004. raise ValueError("question和sql字段都是必需的")
  2005. training_id = vn.train_error_sql(question=question, sql=sql)
  2006. elif training_type == 'documentation':
  2007. content = item.get('content')
  2008. if not content:
  2009. raise ValueError("content字段是必需的")
  2010. training_id = vn.train(documentation=content)
  2011. elif training_type == 'ddl':
  2012. ddl = item.get('ddl')
  2013. if not ddl:
  2014. raise ValueError("ddl字段是必需的")
  2015. training_id = vn.train(ddl=ddl)
  2016. else:
  2017. raise ValueError(f"不支持的训练数据类型: {training_type}")
  2018. return {
  2019. "index": index,
  2020. "success": True,
  2021. "training_id": training_id,
  2022. "type": training_type,
  2023. "message": f"{training_type}训练数据创建成功"
  2024. }
  2025. def get_total_training_count():
  2026. """获取当前训练数据总数"""
  2027. try:
  2028. training_data = vn.get_training_data()
  2029. if training_data is not None and not training_data.empty:
  2030. return len(training_data)
  2031. return 0
  2032. except Exception as e:
  2033. logger.warning(f"获取训练数据总数失败: {e}")
  2034. return 0
  2035. @app.flask_app.route('/api/v0/training_data/query', methods=['POST'])
  2036. def training_data_query():
  2037. """
  2038. 分页查询训练数据API
  2039. 支持类型筛选、搜索和排序功能
  2040. """
  2041. try:
  2042. req = request.get_json(force=True)
  2043. # 解析参数,设置默认值
  2044. page = req.get('page', 1)
  2045. page_size = req.get('page_size', 20)
  2046. training_data_type = req.get('training_data_type')
  2047. sort_by = req.get('sort_by', 'id')
  2048. sort_order = req.get('sort_order', 'desc')
  2049. search_keyword = req.get('search_keyword')
  2050. # 参数验证
  2051. if page < 1:
  2052. return jsonify(bad_request_response(
  2053. response_text="页码必须大于0",
  2054. missing_params=["page"]
  2055. )), 400
  2056. if page_size < 1 or page_size > 100:
  2057. return jsonify(bad_request_response(
  2058. response_text="每页大小必须在1-100之间",
  2059. missing_params=["page_size"]
  2060. )), 400
  2061. if search_keyword and len(search_keyword) > 100:
  2062. return jsonify(bad_request_response(
  2063. response_text="搜索关键词最大长度为100字符",
  2064. missing_params=["search_keyword"]
  2065. )), 400
  2066. # 获取训练数据
  2067. training_data = vn.get_training_data()
  2068. if training_data is None or training_data.empty:
  2069. return jsonify(success_response(
  2070. response_text="查询成功,暂无训练数据",
  2071. data={
  2072. "records": [],
  2073. "pagination": {
  2074. "page": page,
  2075. "page_size": page_size,
  2076. "total": 0,
  2077. "total_pages": 0,
  2078. "has_next": False,
  2079. "has_prev": False
  2080. },
  2081. "filters_applied": {
  2082. "training_data_type": training_data_type,
  2083. "search_keyword": search_keyword
  2084. }
  2085. }
  2086. ))
  2087. # 转换为列表格式
  2088. records = training_data.to_dict(orient="records")
  2089. # 应用筛选条件
  2090. if training_data_type:
  2091. records = filter_by_type(records, training_data_type)
  2092. if search_keyword:
  2093. records = search_in_data(records, search_keyword)
  2094. # 排序
  2095. if sort_by in ['id', 'training_data_type']:
  2096. reverse = (sort_order.lower() == 'desc')
  2097. records.sort(key=lambda x: x.get(sort_by, ''), reverse=reverse)
  2098. # 分页
  2099. paginated_result = paginate_data(records, page, page_size)
  2100. return jsonify(success_response(
  2101. response_text=f"查询成功,共找到 {paginated_result['pagination']['total']} 条记录",
  2102. data={
  2103. "records": paginated_result["data"],
  2104. "pagination": paginated_result["pagination"],
  2105. "filters_applied": {
  2106. "training_data_type": training_data_type,
  2107. "search_keyword": search_keyword
  2108. }
  2109. }
  2110. ))
  2111. except Exception as e:
  2112. logger.error(f"training_data_query执行失败: {str(e)}")
  2113. return jsonify(internal_error_response(
  2114. response_text="查询训练数据失败,请稍后重试"
  2115. )), 500
  2116. @app.flask_app.route('/api/v0/training_data/create', methods=['POST'])
  2117. def training_data_create():
  2118. """
  2119. 创建训练数据API
  2120. 支持单条和批量创建,支持四种数据类型
  2121. """
  2122. try:
  2123. req = request.get_json(force=True)
  2124. data = req.get('data')
  2125. if not data:
  2126. return jsonify(bad_request_response(
  2127. response_text="缺少必需参数:data",
  2128. missing_params=["data"]
  2129. )), 400
  2130. # 统一处理为列表格式
  2131. if isinstance(data, dict):
  2132. data_list = [data]
  2133. elif isinstance(data, list):
  2134. data_list = data
  2135. else:
  2136. return jsonify(bad_request_response(
  2137. response_text="data字段格式错误,应为对象或数组"
  2138. )), 400
  2139. # 批量操作限制
  2140. if len(data_list) > 50:
  2141. return jsonify(bad_request_response(
  2142. response_text="批量操作最大支持50条记录"
  2143. )), 400
  2144. results = []
  2145. successful_count = 0
  2146. type_summary = {"sql": 0, "documentation": 0, "ddl": 0, "error_sql": 0}
  2147. for index, item in enumerate(data_list):
  2148. try:
  2149. result = process_single_training_item(item, index)
  2150. results.append(result)
  2151. if result['success']:
  2152. successful_count += 1
  2153. type_summary[result['type']] += 1
  2154. except Exception as e:
  2155. results.append({
  2156. "index": index,
  2157. "success": False,
  2158. "type": item.get('training_data_type', 'unknown'),
  2159. "error": str(e),
  2160. "message": "创建失败"
  2161. })
  2162. # 获取创建后的总记录数
  2163. current_total = get_total_training_count()
  2164. return jsonify(success_response(
  2165. response_text="训练数据创建完成",
  2166. data={
  2167. "total_requested": len(data_list),
  2168. "successfully_created": successful_count,
  2169. "failed_count": len(data_list) - successful_count,
  2170. "results": results,
  2171. "summary": type_summary,
  2172. "current_total_count": current_total
  2173. }
  2174. ))
  2175. except Exception as e:
  2176. logger.error(f"training_data_create执行失败: {str(e)}")
  2177. return jsonify(internal_error_response(
  2178. response_text="创建训练数据失败,请稍后重试"
  2179. )), 500
  2180. @app.flask_app.route('/api/v0/training_data/delete', methods=['POST'])
  2181. def training_data_delete():
  2182. """
  2183. 删除训练数据API
  2184. 支持批量删除
  2185. """
  2186. try:
  2187. req = request.get_json(force=True)
  2188. ids = req.get('ids', [])
  2189. confirm = req.get('confirm', False)
  2190. if not ids or not isinstance(ids, list):
  2191. return jsonify(bad_request_response(
  2192. response_text="缺少有效的ID列表",
  2193. missing_params=["ids"]
  2194. )), 400
  2195. if not confirm:
  2196. return jsonify(bad_request_response(
  2197. response_text="删除操作需要确认,请设置confirm为true"
  2198. )), 400
  2199. # 批量操作限制
  2200. if len(ids) > 50:
  2201. return jsonify(bad_request_response(
  2202. response_text="批量删除最大支持50条记录"
  2203. )), 400
  2204. deleted_ids = []
  2205. failed_ids = []
  2206. failed_details = []
  2207. for training_id in ids:
  2208. try:
  2209. success = vn.remove_training_data(training_id)
  2210. if success:
  2211. deleted_ids.append(training_id)
  2212. else:
  2213. failed_ids.append(training_id)
  2214. failed_details.append({
  2215. "id": training_id,
  2216. "error": "记录不存在或删除失败"
  2217. })
  2218. except Exception as e:
  2219. failed_ids.append(training_id)
  2220. failed_details.append({
  2221. "id": training_id,
  2222. "error": str(e)
  2223. })
  2224. # 获取删除后的总记录数
  2225. current_total = get_total_training_count()
  2226. return jsonify(success_response(
  2227. response_text="训练数据删除完成",
  2228. data={
  2229. "total_requested": len(ids),
  2230. "successfully_deleted": len(deleted_ids),
  2231. "failed_count": len(failed_ids),
  2232. "deleted_ids": deleted_ids,
  2233. "failed_ids": failed_ids,
  2234. "failed_details": failed_details,
  2235. "current_total_count": current_total
  2236. }
  2237. ))
  2238. except Exception as e:
  2239. logger.error(f"training_data_delete执行失败: {str(e)}")
  2240. return jsonify(internal_error_response(
  2241. response_text="删除训练数据失败,请稍后重试"
  2242. )), 500
  2243. @app.flask_app.route('/api/v0/training_data/stats', methods=['GET'])
  2244. def training_data_stats():
  2245. """
  2246. 获取训练数据统计信息API
  2247. """
  2248. try:
  2249. training_data = vn.get_training_data()
  2250. if training_data is None or training_data.empty:
  2251. return jsonify(success_response(
  2252. response_text="统计信息获取成功",
  2253. data={
  2254. "total_count": 0,
  2255. "type_breakdown": {
  2256. "sql": 0,
  2257. "documentation": 0,
  2258. "ddl": 0,
  2259. "error_sql": 0
  2260. },
  2261. "type_percentages": {
  2262. "sql": 0.0,
  2263. "documentation": 0.0,
  2264. "ddl": 0.0,
  2265. "error_sql": 0.0
  2266. },
  2267. "last_updated": datetime.now().isoformat()
  2268. }
  2269. ))
  2270. total_count = len(training_data)
  2271. # 统计各类型数量
  2272. type_breakdown = {"sql": 0, "documentation": 0, "ddl": 0, "error_sql": 0}
  2273. if 'training_data_type' in training_data.columns:
  2274. type_counts = training_data['training_data_type'].value_counts()
  2275. for data_type, count in type_counts.items():
  2276. if data_type in type_breakdown:
  2277. type_breakdown[data_type] = int(count)
  2278. # 计算百分比
  2279. type_percentages = {}
  2280. for data_type, count in type_breakdown.items():
  2281. type_percentages[data_type] = round(count / max(total_count, 1) * 100, 2)
  2282. return jsonify(success_response(
  2283. response_text="统计信息获取成功",
  2284. data={
  2285. "total_count": total_count,
  2286. "type_breakdown": type_breakdown,
  2287. "type_percentages": type_percentages,
  2288. "last_updated": datetime.now().isoformat()
  2289. }
  2290. ))
  2291. except Exception as e:
  2292. logger.error(f"training_data_stats执行失败: {str(e)}")
  2293. return jsonify(internal_error_response(
  2294. response_text="获取统计信息失败,请稍后重试"
  2295. )), 500
  2296. @app.flask_app.route('/api/v0/cache_overview_full', methods=['GET'])
  2297. def cache_overview_full():
  2298. """获取所有缓存系统的综合概览"""
  2299. try:
  2300. from common.embedding_cache_manager import get_embedding_cache_manager
  2301. from common.vanna_instance import get_vanna_instance
  2302. # 获取现有的缓存统计
  2303. vanna_cache = get_vanna_instance()
  2304. # 直接使用应用中的缓存实例
  2305. cache = app.cache
  2306. cache_overview = {
  2307. "conversation_aware_cache": {
  2308. "enabled": True,
  2309. "total_items": len(cache.cache) if hasattr(cache, 'cache') else 0,
  2310. "sessions": list(cache.cache.keys()) if hasattr(cache, 'cache') else [],
  2311. "cache_type": type(cache).__name__
  2312. },
  2313. "question_answer_cache": redis_conversation_manager.get_qa_cache_stats() if redis_conversation_manager.is_available() else {"available": False},
  2314. "embedding_cache": get_embedding_cache_manager().get_cache_stats(),
  2315. "redis_conversation_stats": redis_conversation_manager.get_stats() if redis_conversation_manager.is_available() else None
  2316. }
  2317. return jsonify(success_response(
  2318. response_text="获取综合缓存概览成功",
  2319. data=cache_overview
  2320. ))
  2321. except Exception as e:
  2322. logger.error(f"获取综合缓存概览失败: {str(e)}")
  2323. return jsonify(internal_error_response(
  2324. response_text="获取缓存概览失败,请稍后重试"
  2325. )), 500
  2326. # 前端JavaScript示例 - 如何维持会话
  2327. """
  2328. // 前端需要维护一个会话ID
  2329. class ChatSession {
  2330. constructor() {
  2331. // 从localStorage获取或创建新的会话ID
  2332. this.sessionId = localStorage.getItem('chat_session_id') || this.generateSessionId();
  2333. localStorage.setItem('chat_session_id', this.sessionId);
  2334. }
  2335. generateSessionId() {
  2336. return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  2337. }
  2338. async askQuestion(question) {
  2339. const response = await fetch('/api/v0/ask', {
  2340. method: 'POST',
  2341. headers: {
  2342. 'Content-Type': 'application/json',
  2343. },
  2344. body: JSON.stringify({
  2345. question: question,
  2346. session_id: this.sessionId // 关键:传递会话ID
  2347. })
  2348. });
  2349. return await response.json();
  2350. }
  2351. // 开始新会话
  2352. startNewSession() {
  2353. this.sessionId = this.generateSessionId();
  2354. localStorage.setItem('chat_session_id', this.sessionId);
  2355. }
  2356. }
  2357. // 使用示例
  2358. const chatSession = new ChatSession();
  2359. chatSession.askQuestion("各年龄段客户的流失率如何?");
  2360. """
  2361. logger.info("正在启动Flask应用: http://localhost:8084")
  2362. app.run(host="0.0.0.0", port=8084, debug=True)