unified_api.py 138 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578
  1. """
  2. 统一 API 服务
  3. 集成 citu_app.py 指定API 和 react_agent/api.py 的所有功能
  4. 提供数据库问答、Redis对话管理、QA反馈、训练数据管理、React Agent等功能
  5. 使用普通 Flask 应用 + ASGI 包装实现异步支持
  6. """
  7. import asyncio
  8. import logging
  9. import atexit
  10. import os
  11. import sys
  12. from datetime import datetime, timedelta
  13. from typing import Optional, Dict, Any, TYPE_CHECKING, Union
  14. import signal
  15. from threading import Thread
  16. if TYPE_CHECKING:
  17. from react_agent.agent import CustomReactAgent
  18. # 初始化日志系统 - 必须在最前面
  19. from core.logging import initialize_logging, get_app_logger
  20. initialize_logging()
  21. # 标准 Flask 导入
  22. from flask import Flask, request, jsonify, session, send_file
  23. import redis.asyncio as redis
  24. # 基础依赖
  25. import pandas as pd
  26. import json
  27. import sqlparse
  28. # 项目模块导入
  29. from core.vanna_llm_factory import create_vanna_instance
  30. from common.redis_conversation_manager import RedisConversationManager
  31. from common.qa_feedback_manager import QAFeedbackManager
  32. # Data Pipeline 相关导入 - 从 citu_app.py 迁移
  33. from data_pipeline.api.simple_workflow import SimpleWorkflowManager, SimpleWorkflowExecutor
  34. from data_pipeline.api.simple_file_manager import SimpleFileManager
  35. from data_pipeline.api.table_inspector_api import TableInspectorAPI
  36. from common.result import (
  37. success_response, bad_request_response, not_found_response, internal_error_response,
  38. error_response, service_unavailable_response,
  39. agent_success_response, agent_error_response,
  40. validation_failed_response
  41. )
  42. from app_config import (
  43. USER_MAX_CONVERSATIONS, CONVERSATION_CONTEXT_COUNT,
  44. DEFAULT_ANONYMOUS_USER, ENABLE_QUESTION_ANSWER_CACHE
  45. )
  46. # 创建标准 Flask 应用
  47. app = Flask(__name__)
  48. # 创建日志记录器
  49. logger = get_app_logger("UnifiedApp")
  50. # React Agent 导入
  51. try:
  52. from react_agent.agent import CustomReactAgent
  53. from react_agent.enhanced_redis_api import get_conversation_detail_from_redis
  54. except ImportError:
  55. try:
  56. from test.custom_react_agent.agent import CustomReactAgent
  57. from test.custom_react_agent.enhanced_redis_api import get_conversation_detail_from_redis
  58. except ImportError:
  59. logger.warning("无法导入 CustomReactAgent,React Agent功能将不可用")
  60. CustomReactAgent = None
  61. get_conversation_detail_from_redis = None
  62. # 初始化核心组件
  63. vn = create_vanna_instance()
  64. redis_conversation_manager = RedisConversationManager()
  65. # ==================== React Agent 全局实例管理 ====================
  66. _react_agent_instance: Optional[Any] = None
  67. _redis_client: Optional[redis.Redis] = None
  68. def validate_request_data(data: Dict[str, Any]) -> Dict[str, Any]:
  69. """验证请求数据,并支持从thread_id中推断user_id"""
  70. errors = []
  71. # 验证 question(必填)
  72. question = data.get('question', '')
  73. if not question or not question.strip():
  74. errors.append('问题不能为空')
  75. elif len(question) > 2000:
  76. errors.append('问题长度不能超过2000字符')
  77. # 优先获取 thread_id
  78. thread_id = data.get('thread_id') or data.get('conversation_id')
  79. # 获取 user_id,但暂不设置默认值
  80. user_id = data.get('user_id')
  81. # 如果没有传递 user_id,则尝试从 thread_id 中推断
  82. if not user_id:
  83. if thread_id and ':' in thread_id:
  84. inferred_user_id = thread_id.split(':', 1)[0]
  85. if inferred_user_id:
  86. user_id = inferred_user_id
  87. logger.info(f"👤 未提供user_id,从 thread_id '{thread_id}' 中推断出: '{user_id}'")
  88. else:
  89. user_id = 'guest'
  90. else:
  91. user_id = 'guest'
  92. # 验证 user_id 长度
  93. if user_id and len(user_id) > 50:
  94. errors.append('用户ID长度不能超过50字符')
  95. # 用户ID与会话ID一致性校验
  96. if thread_id:
  97. if ':' not in thread_id:
  98. errors.append('会话ID格式无效,期望格式为 user_id:timestamp')
  99. else:
  100. thread_user_id = thread_id.split(':', 1)[0]
  101. if thread_user_id != user_id:
  102. errors.append(f'会话归属验证失败:会话ID [{thread_id}] 不属于当前用户 [{user_id}]')
  103. if errors:
  104. raise ValueError('; '.join(errors))
  105. return {
  106. 'question': question.strip(),
  107. 'user_id': user_id,
  108. 'thread_id': thread_id # 可选,不传则自动生成新会话
  109. }
  110. async def get_react_agent() -> Any:
  111. """获取 React Agent 实例(懒加载)"""
  112. global _react_agent_instance, _redis_client
  113. if _react_agent_instance is None:
  114. if CustomReactAgent is None:
  115. logger.error("❌ CustomReactAgent 未能导入,无法初始化")
  116. raise ImportError("CustomReactAgent 未能导入")
  117. logger.info("🚀 正在异步初始化 Custom React Agent...")
  118. try:
  119. # 设置环境变量
  120. os.environ['REDIS_URL'] = 'redis://localhost:6379'
  121. # 初始化共享的Redis客户端
  122. _redis_client = redis.from_url('redis://localhost:6379', decode_responses=True)
  123. await _redis_client.ping()
  124. logger.info("✅ Redis客户端连接成功")
  125. _react_agent_instance = await CustomReactAgent.create()
  126. logger.info("✅ React Agent 异步初始化完成")
  127. except Exception as e:
  128. logger.error(f"❌ React Agent 异步初始化失败: {e}")
  129. raise
  130. return _react_agent_instance
  131. async def ensure_agent_ready() -> bool:
  132. """异步确保Agent实例可用"""
  133. global _react_agent_instance
  134. if _react_agent_instance is None:
  135. await get_react_agent()
  136. # 测试Agent是否还可用
  137. try:
  138. test_result = await _react_agent_instance.get_user_recent_conversations("__test__", 1)
  139. return True
  140. except Exception as e:
  141. logger.warning(f"⚠️ Agent实例不可用: {e}")
  142. _react_agent_instance = None
  143. await get_react_agent()
  144. return True
  145. def get_user_conversations_simple_sync(user_id: str, limit: int = 10):
  146. """直接从Redis获取用户对话,测试版本"""
  147. import redis
  148. import json
  149. try:
  150. # 创建Redis连接
  151. redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
  152. redis_client.ping()
  153. # 扫描用户的checkpoint keys
  154. pattern = f"checkpoint:{user_id}:*"
  155. logger.info(f"🔍 扫描模式: {pattern}")
  156. keys = []
  157. cursor = 0
  158. while True:
  159. cursor, batch = redis_client.scan(cursor=cursor, match=pattern, count=1000)
  160. keys.extend(batch)
  161. if cursor == 0:
  162. break
  163. logger.info(f"📋 找到 {len(keys)} 个keys")
  164. # 解析thread信息
  165. thread_data = {}
  166. for key in keys:
  167. try:
  168. parts = key.split(':')
  169. if len(parts) >= 4:
  170. thread_id = f"{parts[1]}:{parts[2]}" # user_id:timestamp
  171. timestamp = parts[2]
  172. if thread_id not in thread_data:
  173. thread_data[thread_id] = {
  174. "thread_id": thread_id,
  175. "timestamp": timestamp,
  176. "keys": []
  177. }
  178. thread_data[thread_id]["keys"].append(key)
  179. except Exception as e:
  180. logger.warning(f"解析key失败 {key}: {e}")
  181. continue
  182. logger.info(f"📊 找到 {len(thread_data)} 个thread")
  183. # 按时间戳排序
  184. sorted_threads = sorted(
  185. thread_data.values(),
  186. key=lambda x: x["timestamp"],
  187. reverse=True
  188. )[:limit]
  189. # 获取每个thread的详细信息
  190. conversations = []
  191. for thread_info in sorted_threads:
  192. try:
  193. thread_id = thread_info["thread_id"]
  194. # 获取最新的checkpoint数据
  195. latest_key = max(thread_info["keys"])
  196. # 先检查key的数据类型
  197. key_type = redis_client.type(latest_key)
  198. logger.info(f"🔍 Key {latest_key} 的类型: {key_type}")
  199. data = None
  200. if key_type == 'string':
  201. data = redis_client.get(latest_key)
  202. elif key_type == 'hash':
  203. # 如果是hash类型,获取所有字段
  204. hash_data = redis_client.hgetall(latest_key)
  205. logger.info(f"🔍 Hash字段: {list(hash_data.keys())}")
  206. # 尝试获取可能的数据字段
  207. for field in ['data', 'state', 'value', 'checkpoint']:
  208. if field in hash_data:
  209. data = hash_data[field]
  210. break
  211. if not data and hash_data:
  212. # 如果没找到预期字段,取第一个值试试
  213. data = list(hash_data.values())[0]
  214. elif key_type == 'list':
  215. # 如果是list类型,获取最后一个元素
  216. data = redis_client.lindex(latest_key, -1)
  217. elif key_type == 'ReJSON-RL':
  218. # 这是RedisJSON类型,使用JSON.GET命令
  219. logger.info(f"🔍 使用JSON.GET获取RedisJSON数据")
  220. try:
  221. # 使用JSON.GET命令获取整个JSON对象
  222. json_data = redis_client.execute_command('JSON.GET', latest_key)
  223. if json_data:
  224. data = json_data # JSON.GET返回的就是JSON字符串
  225. logger.info(f"🔍 JSON数据长度: {len(data)} 字符")
  226. else:
  227. logger.warning(f"⚠️ JSON.GET 返回空数据")
  228. continue
  229. except Exception as json_error:
  230. logger.error(f"❌ JSON.GET 失败: {json_error}")
  231. continue
  232. else:
  233. logger.warning(f"⚠️ 未知的key类型: {key_type}")
  234. continue
  235. if data:
  236. try:
  237. checkpoint_data = json.loads(data)
  238. # 调试:查看JSON数据结构
  239. logger.info(f"🔍 JSON顶级keys: {list(checkpoint_data.keys())}")
  240. # 根据您提供的JSON结构,消息在 checkpoint.channel_values.messages
  241. messages = []
  242. # 首先检查是否有checkpoint字段
  243. if 'checkpoint' in checkpoint_data:
  244. checkpoint = checkpoint_data['checkpoint']
  245. if isinstance(checkpoint, dict) and 'channel_values' in checkpoint:
  246. channel_values = checkpoint['channel_values']
  247. if isinstance(channel_values, dict) and 'messages' in channel_values:
  248. messages = channel_values['messages']
  249. logger.info(f"🔍 找到messages: {len(messages)} 条消息")
  250. # 如果没有checkpoint字段,尝试直接在channel_values
  251. if not messages and 'channel_values' in checkpoint_data:
  252. channel_values = checkpoint_data['channel_values']
  253. if isinstance(channel_values, dict) and 'messages' in channel_values:
  254. messages = channel_values['messages']
  255. logger.info(f"🔍 找到messages(直接路径): {len(messages)} 条消息")
  256. # 生成对话预览
  257. preview = "空对话"
  258. if messages:
  259. for msg in messages:
  260. # 处理LangChain消息格式:{"lc": 1, "type": "constructor", "id": ["langchain", "schema", "messages", "HumanMessage"], "kwargs": {"content": "...", "type": "human"}}
  261. if isinstance(msg, dict):
  262. # 检查是否是LangChain格式的HumanMessage
  263. if (msg.get('lc') == 1 and
  264. msg.get('type') == 'constructor' and
  265. 'id' in msg and
  266. isinstance(msg['id'], list) and
  267. len(msg['id']) >= 4 and
  268. msg['id'][3] == 'HumanMessage' and
  269. 'kwargs' in msg):
  270. kwargs = msg['kwargs']
  271. if kwargs.get('type') == 'human' and 'content' in kwargs:
  272. content = str(kwargs['content'])
  273. preview = content[:50] + "..." if len(content) > 50 else content
  274. break
  275. # 兼容其他格式
  276. elif msg.get('type') == 'human' and 'content' in msg:
  277. content = str(msg['content'])
  278. preview = content[:50] + "..." if len(content) > 50 else content
  279. break
  280. conversations.append({
  281. "thread_id": thread_id,
  282. "user_id": user_id,
  283. "timestamp": thread_info["timestamp"],
  284. "message_count": len(messages),
  285. "conversation_preview": preview
  286. })
  287. except json.JSONDecodeError:
  288. logger.error(f"❌ JSON解析失败,数据类型: {type(data)}, 长度: {len(str(data))}")
  289. logger.error(f"❌ 数据开头: {str(data)[:200]}...")
  290. continue
  291. except Exception as e:
  292. logger.error(f"处理thread {thread_info['thread_id']} 失败: {e}")
  293. continue
  294. redis_client.close()
  295. logger.info(f"✅ 返回 {len(conversations)} 个对话")
  296. return conversations
  297. except Exception as e:
  298. logger.error(f"❌ Redis查询失败: {e}")
  299. return []
  300. def cleanup_resources():
  301. """清理资源"""
  302. global _react_agent_instance, _redis_client
  303. async def async_cleanup():
  304. if _react_agent_instance:
  305. await _react_agent_instance.close()
  306. logger.info("✅ React Agent 资源已清理")
  307. if _redis_client:
  308. await _redis_client.aclose()
  309. logger.info("✅ Redis客户端已关闭")
  310. try:
  311. asyncio.run(async_cleanup())
  312. except Exception as e:
  313. logger.error(f"清理资源失败: {e}")
  314. atexit.register(cleanup_resources)
  315. # ==================== 基础路由 ====================
  316. @app.route("/")
  317. def index():
  318. """根路径健康检查"""
  319. return jsonify({"message": "统一API服务正在运行", "version": "1.0.0"})
  320. @app.route('/health', methods=['GET'])
  321. def health_check():
  322. """健康检查端点"""
  323. try:
  324. health_status = {
  325. "status": "healthy",
  326. "react_agent_initialized": _react_agent_instance is not None,
  327. "timestamp": datetime.now().isoformat(),
  328. "services": {
  329. "redis": redis_conversation_manager.is_available(),
  330. "vanna": vn is not None
  331. }
  332. }
  333. return jsonify(health_status), 200
  334. except Exception as e:
  335. logger.error(f"健康检查失败: {e}")
  336. return jsonify({"status": "unhealthy", "error": str(e)}), 500
  337. # ==================== React Agent API ====================
  338. @app.route("/api/v0/ask_react_agent", methods=["POST"])
  339. async def ask_react_agent():
  340. """异步React Agent智能问答接口(从 custom_react_agent 迁移,原路由:/api/chat)"""
  341. global _react_agent_instance
  342. # 确保Agent已初始化
  343. if not await ensure_agent_ready():
  344. return jsonify({
  345. "code": 503,
  346. "message": "服务未就绪",
  347. "success": False,
  348. "error": "React Agent 初始化失败"
  349. }), 503
  350. try:
  351. # 获取请求数据
  352. try:
  353. data = request.get_json(force=True)
  354. except Exception as json_error:
  355. logger.warning(f"⚠️ JSON解析失败: {json_error}")
  356. return jsonify({
  357. "code": 400,
  358. "message": "请求格式错误",
  359. "success": False,
  360. "error": "无效的JSON格式,请检查请求体中是否存在语法错误(如多余的逗号、引号不匹配等)",
  361. "details": str(json_error)
  362. }), 400
  363. if not data:
  364. return jsonify({
  365. "code": 400,
  366. "message": "请求参数错误",
  367. "success": False,
  368. "error": "请求体不能为空"
  369. }), 400
  370. # 验证请求数据
  371. validated_data = validate_request_data(data)
  372. logger.info(f"📨 收到React Agent请求 - User: {validated_data['user_id']}, Question: {validated_data['question'][:50]}...")
  373. # 异步调用处理
  374. agent_result = await _react_agent_instance.chat(
  375. message=validated_data['question'],
  376. user_id=validated_data['user_id'],
  377. thread_id=validated_data['thread_id']
  378. )
  379. if not agent_result.get("success", False):
  380. # Agent处理失败
  381. error_msg = agent_result.get("error", "React Agent处理失败")
  382. logger.error(f"❌ React Agent处理失败: {error_msg}")
  383. return jsonify({
  384. "code": 500,
  385. "message": "处理失败",
  386. "success": False,
  387. "error": error_msg,
  388. "data": {
  389. "conversation_id": agent_result.get("thread_id"),
  390. "user_id": validated_data['user_id'],
  391. "timestamp": datetime.now().isoformat()
  392. }
  393. }), 500
  394. # Agent处理成功
  395. api_data = agent_result.get("api_data", {})
  396. # 构建响应数据(按照 react_agent/api.py 的正确格式)
  397. response_data = {
  398. "response": api_data.get("response", ""),
  399. "conversation_id": agent_result.get("thread_id"),
  400. "user_id": validated_data['user_id'],
  401. "react_agent_meta": api_data.get("react_agent_meta", {
  402. "thread_id": agent_result.get("thread_id"),
  403. "agent_version": "custom_react_v1_async"
  404. }),
  405. "timestamp": datetime.now().isoformat()
  406. }
  407. # 可选字段:SQL(仅当执行SQL时存在)
  408. if "sql" in api_data:
  409. response_data["sql"] = api_data["sql"]
  410. # 可选字段:records(仅当有查询结果时存在)
  411. if "records" in api_data:
  412. response_data["records"] = api_data["records"]
  413. return jsonify({
  414. "code": 200,
  415. "message": "处理成功",
  416. "success": True,
  417. "data": response_data
  418. }), 200
  419. except ValueError as ve:
  420. # 参数验证错误
  421. logger.warning(f"⚠️ 参数验证失败: {ve}")
  422. return jsonify({
  423. "code": 400,
  424. "message": "参数验证失败",
  425. "success": False,
  426. "error": str(ve)
  427. }), 400
  428. except Exception as e:
  429. logger.error(f"❌ React Agent API 异常: {e}")
  430. return jsonify({
  431. "code": 500,
  432. "message": "内部服务错误",
  433. "success": False,
  434. "error": "服务暂时不可用,请稍后重试"
  435. }), 500
  436. # ==================== LangGraph Agent API ====================
  437. # 全局Agent实例(单例模式)
  438. citu_langraph_agent = None
  439. def get_citu_langraph_agent():
  440. """获取LangGraph Agent实例(懒加载)"""
  441. global citu_langraph_agent
  442. if citu_langraph_agent is None:
  443. try:
  444. from agent.citu_agent import CituLangGraphAgent
  445. logger.info("开始创建LangGraph Agent实例...")
  446. citu_langraph_agent = CituLangGraphAgent()
  447. logger.info("LangGraph Agent实例创建成功")
  448. except ImportError as e:
  449. logger.critical(f"Agent模块导入失败: {str(e)}")
  450. raise Exception(f"Agent模块导入失败: {str(e)}")
  451. except Exception as e:
  452. logger.critical(f"LangGraph Agent实例创建失败: {str(e)}")
  453. raise Exception(f"Agent初始化失败: {str(e)}")
  454. return citu_langraph_agent
  455. @app.route('/api/v0/ask_agent', methods=['POST'])
  456. def ask_agent():
  457. """支持对话上下文的ask_agent API"""
  458. req = request.get_json(force=True)
  459. question = req.get("question", None)
  460. browser_session_id = req.get("session_id", None)
  461. user_id_input = req.get("user_id", None)
  462. conversation_id_input = req.get("conversation_id", None)
  463. continue_conversation = req.get("continue_conversation", False)
  464. api_routing_mode = req.get("routing_mode", None)
  465. VALID_ROUTING_MODES = ["database_direct", "chat_direct", "hybrid", "llm_only"]
  466. if not question:
  467. return jsonify(bad_request_response(
  468. response_text="缺少必需参数:question",
  469. missing_params=["question"]
  470. )), 400
  471. if api_routing_mode and api_routing_mode not in VALID_ROUTING_MODES:
  472. return jsonify(bad_request_response(
  473. response_text=f"无效的routing_mode参数值: {api_routing_mode},支持的值: {VALID_ROUTING_MODES}",
  474. invalid_params=["routing_mode"]
  475. )), 400
  476. try:
  477. # 获取登录用户ID
  478. login_user_id = session.get('user_id') if 'user_id' in session else None
  479. # 智能ID解析
  480. user_id = redis_conversation_manager.resolve_user_id(
  481. user_id_input, browser_session_id, request.remote_addr, login_user_id
  482. )
  483. conversation_id, conversation_status = redis_conversation_manager.resolve_conversation_id(
  484. user_id, conversation_id_input, continue_conversation
  485. )
  486. # 获取上下文和上下文类型(提前到缓存检查之前)
  487. context = redis_conversation_manager.get_context(conversation_id)
  488. # 获取上下文类型:从最后一条助手消息的metadata中获取类型
  489. context_type = None
  490. if context:
  491. try:
  492. # 获取最后一条助手消息的metadata
  493. messages = redis_conversation_manager.get_messages(conversation_id, limit=10)
  494. for message in reversed(messages): # 从最新的开始找
  495. if message.get("role") == "assistant":
  496. metadata = message.get("metadata", {})
  497. context_type = metadata.get("type")
  498. if context_type:
  499. logger.info(f"[AGENT_API] 检测到上下文类型: {context_type}")
  500. break
  501. except Exception as e:
  502. logger.warning(f"获取上下文类型失败: {str(e)}")
  503. # 检查缓存(新逻辑:放宽使用条件,严控存储条件)
  504. cached_answer = redis_conversation_manager.get_cached_answer(question, context)
  505. if cached_answer:
  506. logger.info(f"[AGENT_API] 使用缓存答案")
  507. # 确定缓存答案的助手回复内容(使用与非缓存相同的优先级逻辑)
  508. cached_response_type = cached_answer.get("type", "UNKNOWN")
  509. if cached_response_type == "DATABASE":
  510. # DATABASE类型:按优先级选择内容
  511. if cached_answer.get("response"):
  512. # 优先级1:错误或解释性回复(如SQL生成失败)
  513. assistant_response = cached_answer.get("response")
  514. elif cached_answer.get("summary"):
  515. # 优先级2:查询成功的摘要
  516. assistant_response = cached_answer.get("summary")
  517. elif cached_answer.get("query_result"):
  518. # 优先级3:构造简单描述
  519. query_result = cached_answer.get("query_result")
  520. row_count = query_result.get("row_count", 0)
  521. assistant_response = f"查询执行完成,共返回 {row_count} 条记录。"
  522. else:
  523. # 异常情况
  524. assistant_response = "数据库查询已处理。"
  525. else:
  526. # CHAT类型:直接使用response
  527. assistant_response = cached_answer.get("response", "")
  528. # 更新对话历史
  529. redis_conversation_manager.save_message(conversation_id, "user", question)
  530. redis_conversation_manager.save_message(
  531. conversation_id, "assistant",
  532. assistant_response,
  533. metadata={"from_cache": True}
  534. )
  535. # 添加对话信息到缓存结果
  536. cached_answer["conversation_id"] = conversation_id
  537. cached_answer["user_id"] = user_id
  538. cached_answer["from_cache"] = True
  539. cached_answer.update(conversation_status)
  540. # 使用agent_success_response返回标准格式
  541. return jsonify(agent_success_response(
  542. response_type=cached_answer.get("type", "UNKNOWN"),
  543. response=cached_answer.get("response", ""),
  544. sql=cached_answer.get("sql"),
  545. records=cached_answer.get("query_result"),
  546. summary=cached_answer.get("summary"),
  547. session_id=browser_session_id,
  548. execution_path=cached_answer.get("execution_path", []),
  549. classification_info=cached_answer.get("classification_info", {}),
  550. conversation_id=conversation_id,
  551. user_id=user_id,
  552. is_guest_user=(user_id == DEFAULT_ANONYMOUS_USER),
  553. context_used=bool(context),
  554. from_cache=True,
  555. conversation_status=conversation_status["status"],
  556. conversation_message=conversation_status["message"],
  557. requested_conversation_id=conversation_status.get("requested_id")
  558. ))
  559. # 保存用户消息
  560. redis_conversation_manager.save_message(conversation_id, "user", question)
  561. # 构建带上下文的问题
  562. if context:
  563. enhanced_question = f"\n[CONTEXT]\n{context}\n\n[CURRENT]\n{question}"
  564. logger.info(f"[AGENT_API] 使用上下文,长度: {len(context)}字符")
  565. else:
  566. enhanced_question = question
  567. logger.info(f"[AGENT_API] 新对话,无上下文")
  568. # 确定最终使用的路由模式(优先级逻辑)
  569. if api_routing_mode:
  570. # API传了参数,优先使用
  571. effective_routing_mode = api_routing_mode
  572. logger.info(f"[AGENT_API] 使用API指定的路由模式: {effective_routing_mode}")
  573. else:
  574. # API没传参数,使用配置文件
  575. try:
  576. from app_config import QUESTION_ROUTING_MODE
  577. effective_routing_mode = QUESTION_ROUTING_MODE
  578. logger.info(f"[AGENT_API] 使用配置文件路由模式: {effective_routing_mode}")
  579. except ImportError:
  580. effective_routing_mode = "hybrid"
  581. logger.info(f"[AGENT_API] 配置文件读取失败,使用默认路由模式: {effective_routing_mode}")
  582. # Agent处理
  583. try:
  584. agent = get_citu_langraph_agent()
  585. except Exception as e:
  586. logger.critical(f"Agent初始化失败: {str(e)}")
  587. return jsonify(service_unavailable_response(
  588. response_text="AI服务暂时不可用,请稍后重试",
  589. can_retry=True
  590. )), 503
  591. # 异步调用Agent处理问题
  592. import asyncio
  593. agent_result = asyncio.run(agent.process_question(
  594. question=enhanced_question, # 使用增强后的问题
  595. session_id=browser_session_id,
  596. context_type=context_type, # 传递上下文类型
  597. routing_mode=effective_routing_mode # 新增:传递路由模式
  598. ))
  599. # 处理Agent结果
  600. if agent_result.get("success", False):
  601. response_type = agent_result.get("type", "UNKNOWN")
  602. response_text = agent_result.get("response", "")
  603. sql = agent_result.get("sql")
  604. query_result = agent_result.get("query_result")
  605. summary = agent_result.get("summary")
  606. execution_path = agent_result.get("execution_path", [])
  607. classification_info = agent_result.get("classification_info", {})
  608. # 确定助手回复内容的优先级
  609. if response_type == "DATABASE":
  610. if response_text:
  611. assistant_response = response_text
  612. elif summary:
  613. assistant_response = summary
  614. elif query_result:
  615. row_count = query_result.get("row_count", 0)
  616. assistant_response = f"查询执行完成,共返回 {row_count} 条记录。"
  617. else:
  618. assistant_response = "数据库查询已处理。"
  619. else:
  620. assistant_response = response_text
  621. # 保存助手回复
  622. redis_conversation_manager.save_message(
  623. conversation_id, "assistant", assistant_response,
  624. metadata={
  625. "type": response_type,
  626. "sql": sql,
  627. "execution_path": execution_path
  628. }
  629. )
  630. # 缓存成功的答案(新逻辑:只缓存无上下文的问答)
  631. # 直接缓存agent_result,它已经包含所有需要的字段
  632. redis_conversation_manager.cache_answer(question, agent_result, context)
  633. # 使用agent_success_response的正确方式
  634. return jsonify(agent_success_response(
  635. response_type=response_type,
  636. response=response_text,
  637. sql=sql,
  638. records=query_result,
  639. summary=summary,
  640. session_id=browser_session_id,
  641. execution_path=execution_path,
  642. classification_info=classification_info,
  643. conversation_id=conversation_id,
  644. user_id=user_id,
  645. is_guest_user=(user_id == DEFAULT_ANONYMOUS_USER),
  646. context_used=bool(context),
  647. from_cache=False,
  648. conversation_status=conversation_status["status"],
  649. conversation_message=conversation_status["message"],
  650. requested_conversation_id=conversation_status.get("requested_id"),
  651. routing_mode_used=effective_routing_mode, # 新增:实际使用的路由模式
  652. routing_mode_source="api" if api_routing_mode else "config" # 新增:路由模式来源
  653. ))
  654. else:
  655. # 错误处理
  656. error_message = agent_result.get("error", "Agent处理失败")
  657. error_code = agent_result.get("error_code", 500)
  658. return jsonify(agent_error_response(
  659. response_text=error_message,
  660. error_type="agent_processing_failed",
  661. code=error_code,
  662. session_id=browser_session_id,
  663. conversation_id=conversation_id,
  664. user_id=user_id
  665. )), error_code
  666. except Exception as e:
  667. logger.error(f"ask_agent执行失败: {str(e)}")
  668. return jsonify(internal_error_response(
  669. response_text="查询处理失败,请稍后重试"
  670. )), 500
  671. # ==================== QA反馈系统API ====================
  672. qa_feedback_manager = None
  673. def get_qa_feedback_manager():
  674. """获取QA反馈管理器实例(懒加载)"""
  675. global qa_feedback_manager
  676. if qa_feedback_manager is None:
  677. try:
  678. qa_feedback_manager = QAFeedbackManager(vanna_instance=vn)
  679. logger.info("QA反馈管理器实例创建成功")
  680. except Exception as e:
  681. logger.critical(f"QA反馈管理器创建失败: {str(e)}")
  682. raise Exception(f"QA反馈管理器初始化失败: {str(e)}")
  683. return qa_feedback_manager
  684. @app.route('/api/v0/qa_feedback/query', methods=['POST'])
  685. def qa_feedback_query():
  686. """
  687. 查询反馈记录API
  688. 支持分页、筛选和排序功能
  689. """
  690. try:
  691. req = request.get_json(force=True)
  692. # 解析参数,设置默认值
  693. page = req.get('page', 1)
  694. page_size = req.get('page_size', 20)
  695. is_thumb_up = req.get('is_thumb_up')
  696. create_time_start = req.get('create_time_start')
  697. create_time_end = req.get('create_time_end')
  698. is_in_training_data = req.get('is_in_training_data')
  699. sort_by = req.get('sort_by', 'create_time')
  700. sort_order = req.get('sort_order', 'desc')
  701. # 参数验证
  702. if page < 1:
  703. return jsonify(bad_request_response(
  704. response_text="页码必须大于0",
  705. invalid_params=["page"]
  706. )), 400
  707. if page_size < 1 or page_size > 100:
  708. return jsonify(bad_request_response(
  709. response_text="每页大小必须在1-100之间",
  710. invalid_params=["page_size"]
  711. )), 400
  712. # 获取反馈管理器并查询
  713. manager = get_qa_feedback_manager()
  714. records, total = manager.query_feedback(
  715. page=page,
  716. page_size=page_size,
  717. is_thumb_up=is_thumb_up,
  718. create_time_start=create_time_start,
  719. create_time_end=create_time_end,
  720. is_in_training_data=is_in_training_data,
  721. sort_by=sort_by,
  722. sort_order=sort_order
  723. )
  724. total_pages = (total + page_size - 1) // page_size
  725. return jsonify(success_response(
  726. response_text=f"查询成功,共找到 {total} 条记录",
  727. data={
  728. "records": records,
  729. "pagination": {
  730. "page": page,
  731. "page_size": page_size,
  732. "total": total,
  733. "total_pages": total_pages,
  734. "has_next": page < total_pages,
  735. "has_prev": page > 1
  736. }
  737. }
  738. ))
  739. except Exception as e:
  740. logger.error(f"qa_feedback_query执行失败: {str(e)}")
  741. return jsonify(internal_error_response(
  742. response_text="查询反馈记录失败,请稍后重试"
  743. )), 500
  744. @app.route('/api/v0/qa_feedback/delete/<int:feedback_id>', methods=['DELETE'])
  745. def qa_feedback_delete(feedback_id):
  746. """删除反馈记录API"""
  747. try:
  748. manager = get_qa_feedback_manager()
  749. success = manager.delete_feedback(feedback_id)
  750. if success:
  751. return jsonify(success_response(
  752. response_text=f"反馈记录删除成功",
  753. data={"deleted_id": feedback_id}
  754. ))
  755. else:
  756. return jsonify(not_found_response(
  757. response_text=f"反馈记录不存在 (ID: {feedback_id})"
  758. )), 404
  759. except Exception as e:
  760. logger.error(f"qa_feedback_delete执行失败: {str(e)}")
  761. return jsonify(internal_error_response(
  762. response_text="删除反馈记录失败,请稍后重试"
  763. )), 500
  764. @app.route('/api/v0/qa_feedback/update/<int:feedback_id>', methods=['PUT'])
  765. def qa_feedback_update(feedback_id):
  766. """更新反馈记录API"""
  767. try:
  768. req = request.get_json(force=True)
  769. allowed_fields = ['question', 'sql', 'is_thumb_up', 'user_id', 'is_in_training_data']
  770. update_data = {}
  771. for field in allowed_fields:
  772. if field in req:
  773. update_data[field] = req[field]
  774. if not update_data:
  775. return jsonify(bad_request_response(
  776. response_text="没有提供有效的更新字段"
  777. )), 400
  778. manager = get_qa_feedback_manager()
  779. success = manager.update_feedback(feedback_id, **update_data)
  780. if success:
  781. return jsonify(success_response(
  782. response_text="反馈记录更新成功",
  783. data={
  784. "updated_id": feedback_id,
  785. "updated_fields": list(update_data.keys())
  786. }
  787. ))
  788. else:
  789. return jsonify(not_found_response(
  790. response_text=f"反馈记录不存在或无变化 (ID: {feedback_id})"
  791. )), 404
  792. except Exception as e:
  793. logger.error(f"qa_feedback_update执行失败: {str(e)}")
  794. return jsonify(internal_error_response(
  795. response_text="更新反馈记录失败,请稍后重试"
  796. )), 500
  797. @app.route('/api/v0/qa_feedback/add_to_training', methods=['POST'])
  798. def qa_feedback_add_to_training():
  799. """将反馈记录添加到训练数据集API"""
  800. try:
  801. req = request.get_json(force=True)
  802. feedback_ids = req.get('feedback_ids', [])
  803. if not feedback_ids or not isinstance(feedback_ids, list):
  804. return jsonify(bad_request_response(
  805. response_text="缺少有效的反馈ID列表"
  806. )), 400
  807. manager = get_qa_feedback_manager()
  808. records = manager.get_feedback_by_ids(feedback_ids)
  809. if not records:
  810. return jsonify(not_found_response(
  811. response_text="未找到任何有效的反馈记录"
  812. )), 404
  813. positive_count = 0
  814. negative_count = 0
  815. successfully_trained_ids = []
  816. for record in records:
  817. try:
  818. if record['is_in_training_data']:
  819. continue
  820. if record['is_thumb_up']:
  821. training_id = vn.train(
  822. question=record['question'],
  823. sql=record['sql']
  824. )
  825. positive_count += 1
  826. else:
  827. training_id = vn.train_error_sql(
  828. question=record['question'],
  829. sql=record['sql']
  830. )
  831. negative_count += 1
  832. successfully_trained_ids.append(record['id'])
  833. except Exception as e:
  834. logger.error(f"训练失败 - 反馈ID: {record['id']}, 错误: {e}")
  835. if successfully_trained_ids:
  836. manager.mark_training_status(successfully_trained_ids, True)
  837. return jsonify(success_response(
  838. response_text=f"训练数据添加完成,成功处理 {positive_count + negative_count} 条记录",
  839. data={
  840. "positive_trained": positive_count,
  841. "negative_trained": negative_count,
  842. "successfully_trained_ids": successfully_trained_ids
  843. }
  844. ))
  845. except Exception as e:
  846. logger.error(f"qa_feedback_add_to_training执行失败: {str(e)}")
  847. return jsonify(internal_error_response(
  848. response_text="添加训练数据失败,请稍后重试"
  849. )), 500
  850. @app.route('/api/v0/qa_feedback/add', methods=['POST'])
  851. def qa_feedback_add():
  852. """添加反馈记录API"""
  853. try:
  854. req = request.get_json(force=True)
  855. question = req.get('question')
  856. sql = req.get('sql')
  857. is_thumb_up = req.get('is_thumb_up')
  858. user_id = req.get('user_id', 'guest')
  859. if not question or not sql or is_thumb_up is None:
  860. return jsonify(bad_request_response(
  861. response_text="缺少必需参数"
  862. )), 400
  863. manager = get_qa_feedback_manager()
  864. feedback_id = manager.add_feedback(
  865. question=question,
  866. sql=sql,
  867. is_thumb_up=bool(is_thumb_up),
  868. user_id=user_id
  869. )
  870. return jsonify(success_response(
  871. response_text="反馈记录创建成功",
  872. data={"feedback_id": feedback_id}
  873. ))
  874. except Exception as e:
  875. logger.error(f"qa_feedback_add执行失败: {str(e)}")
  876. return jsonify(internal_error_response(
  877. response_text="创建反馈记录失败,请稍后重试"
  878. )), 500
  879. @app.route('/api/v0/qa_feedback/stats', methods=['GET'])
  880. def qa_feedback_stats():
  881. """反馈统计API"""
  882. try:
  883. manager = get_qa_feedback_manager()
  884. all_records, total_count = manager.query_feedback(page=1, page_size=1)
  885. positive_records, positive_count = manager.query_feedback(page=1, page_size=1, is_thumb_up=True)
  886. negative_records, negative_count = manager.query_feedback(page=1, page_size=1, is_thumb_up=False)
  887. return jsonify(success_response(
  888. response_text="统计信息获取成功",
  889. data={
  890. "total_feedback": total_count,
  891. "positive_feedback": positive_count,
  892. "negative_feedback": negative_count,
  893. "positive_rate": round(positive_count / max(total_count, 1) * 100, 2)
  894. }
  895. ))
  896. except Exception as e:
  897. logger.error(f"qa_feedback_stats执行失败: {str(e)}")
  898. return jsonify(internal_error_response(
  899. response_text="获取统计信息失败,请稍后重试"
  900. )), 500
  901. # ==================== Redis对话管理API ====================
  902. @app.route('/api/v0/user/<user_id>/conversations', methods=['GET'])
  903. def get_user_conversations_redis(user_id: str):
  904. """获取用户的对话列表"""
  905. try:
  906. limit = request.args.get('limit', USER_MAX_CONVERSATIONS, type=int)
  907. conversations = redis_conversation_manager.get_conversations(user_id, limit)
  908. return jsonify(success_response(
  909. response_text="获取用户对话列表成功",
  910. data={
  911. "user_id": user_id,
  912. "conversations": conversations,
  913. "total_count": len(conversations)
  914. }
  915. ))
  916. except Exception as e:
  917. return jsonify(internal_error_response(
  918. response_text="获取对话列表失败,请稍后重试"
  919. )), 500
  920. @app.route('/api/v0/conversation/<conversation_id>/messages', methods=['GET'])
  921. def get_conversation_messages_redis(conversation_id: str):
  922. """获取特定对话的消息历史"""
  923. try:
  924. limit = request.args.get('limit', type=int)
  925. messages = redis_conversation_manager.get_conversation_messages(conversation_id, limit)
  926. meta = redis_conversation_manager.get_conversation_meta(conversation_id)
  927. return jsonify(success_response(
  928. response_text="获取对话消息成功",
  929. data={
  930. "conversation_id": conversation_id,
  931. "conversation_meta": meta,
  932. "messages": messages,
  933. "message_count": len(messages)
  934. }
  935. ))
  936. except Exception as e:
  937. return jsonify(internal_error_response(
  938. response_text="获取对话消息失败"
  939. )), 500
  940. @app.route('/api/v0/conversation_stats', methods=['GET'])
  941. def conversation_stats():
  942. """获取对话系统统计信息"""
  943. try:
  944. stats = redis_conversation_manager.get_stats()
  945. return jsonify(success_response(
  946. response_text="获取统计信息成功",
  947. data=stats
  948. ))
  949. except Exception as e:
  950. return jsonify(internal_error_response(
  951. response_text="获取统计信息失败,请稍后重试"
  952. )), 500
  953. @app.route('/api/v0/conversation_cleanup', methods=['POST'])
  954. def conversation_cleanup():
  955. """手动清理过期对话"""
  956. try:
  957. redis_conversation_manager.cleanup_expired_conversations()
  958. return jsonify(success_response(
  959. response_text="对话清理完成"
  960. ))
  961. except Exception as e:
  962. return jsonify(internal_error_response(
  963. response_text="对话清理失败,请稍后重试"
  964. )), 500
  965. @app.route('/api/v0/embedding_cache_stats', methods=['GET'])
  966. def embedding_cache_stats():
  967. """获取embedding缓存统计信息"""
  968. try:
  969. from common.embedding_cache_manager import get_embedding_cache_manager
  970. cache_manager = get_embedding_cache_manager()
  971. stats = cache_manager.get_cache_stats()
  972. return jsonify(success_response(
  973. response_text="获取embedding缓存统计成功",
  974. data=stats
  975. ))
  976. except Exception as e:
  977. logger.error(f"获取embedding缓存统计失败: {str(e)}")
  978. return jsonify(internal_error_response(
  979. response_text="获取embedding缓存统计失败,请稍后重试"
  980. )), 500
  981. @app.route('/api/v0/embedding_cache_cleanup', methods=['POST'])
  982. def embedding_cache_cleanup():
  983. """清空所有embedding缓存"""
  984. try:
  985. from common.embedding_cache_manager import get_embedding_cache_manager
  986. cache_manager = get_embedding_cache_manager()
  987. if not cache_manager.is_available():
  988. return jsonify(internal_error_response(
  989. response_text="Embedding缓存功能未启用或不可用"
  990. )), 400
  991. success = cache_manager.clear_all_cache()
  992. if success:
  993. return jsonify(success_response(
  994. response_text="所有embedding缓存已清空",
  995. data={"cleared": True}
  996. ))
  997. else:
  998. return jsonify(internal_error_response(
  999. response_text="清空embedding缓存失败"
  1000. )), 500
  1001. except Exception as e:
  1002. logger.error(f"清空embedding缓存失败: {str(e)}")
  1003. return jsonify(internal_error_response(
  1004. response_text="清空embedding缓存失败,请稍后重试"
  1005. )), 500
  1006. # ==================== 训练数据管理API ====================
  1007. def validate_sql_syntax(sql: str) -> tuple[bool, str]:
  1008. """SQL语法检查"""
  1009. try:
  1010. parsed = sqlparse.parse(sql.strip())
  1011. if not parsed or not parsed[0].tokens:
  1012. return False, "SQL语法错误:空语句"
  1013. sql_upper = sql.strip().upper()
  1014. if not any(sql_upper.startswith(keyword) for keyword in
  1015. ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP']):
  1016. return False, "SQL语法错误:不是有效的SQL语句"
  1017. return True, ""
  1018. except Exception as e:
  1019. return False, f"SQL语法错误:{str(e)}"
  1020. @app.route('/api/v0/training_data/stats', methods=['GET'])
  1021. def training_data_stats():
  1022. """获取训练数据统计信息API"""
  1023. try:
  1024. training_data = vn.get_training_data()
  1025. if training_data is None or training_data.empty:
  1026. return jsonify(success_response(
  1027. response_text="统计信息获取成功",
  1028. data={
  1029. "total_count": 0,
  1030. "last_updated": datetime.now().isoformat()
  1031. }
  1032. ))
  1033. total_count = len(training_data)
  1034. return jsonify(success_response(
  1035. response_text="统计信息获取成功",
  1036. data={
  1037. "total_count": total_count,
  1038. "last_updated": datetime.now().isoformat()
  1039. }
  1040. ))
  1041. except Exception as e:
  1042. logger.error(f"training_data_stats执行失败: {str(e)}")
  1043. return jsonify(internal_error_response(
  1044. response_text="获取统计信息失败,请稍后重试"
  1045. )), 500
  1046. @app.route('/api/v0/training_data/query', methods=['POST'])
  1047. def training_data_query():
  1048. """分页查询训练数据API"""
  1049. try:
  1050. req = request.get_json(force=True)
  1051. page = req.get('page', 1)
  1052. page_size = req.get('page_size', 20)
  1053. if page < 1 or page_size < 1 or page_size > 100:
  1054. return jsonify(bad_request_response(
  1055. response_text="参数错误"
  1056. )), 400
  1057. training_data = vn.get_training_data()
  1058. if training_data is None or training_data.empty:
  1059. return jsonify(success_response(
  1060. response_text="查询成功,暂无训练数据",
  1061. data={
  1062. "records": [],
  1063. "pagination": {
  1064. "page": page,
  1065. "page_size": page_size,
  1066. "total": 0,
  1067. "total_pages": 0,
  1068. "has_next": False,
  1069. "has_prev": False
  1070. }
  1071. }
  1072. ))
  1073. records = training_data.to_dict(orient="records")
  1074. total = len(records)
  1075. start_idx = (page - 1) * page_size
  1076. end_idx = start_idx + page_size
  1077. page_data = records[start_idx:end_idx]
  1078. total_pages = (total + page_size - 1) // page_size
  1079. return jsonify(success_response(
  1080. response_text=f"查询成功,共找到 {total} 条记录",
  1081. data={
  1082. "records": page_data,
  1083. "pagination": {
  1084. "page": page,
  1085. "page_size": page_size,
  1086. "total": total,
  1087. "total_pages": total_pages,
  1088. "has_next": end_idx < total,
  1089. "has_prev": page > 1
  1090. }
  1091. }
  1092. ))
  1093. except Exception as e:
  1094. logger.error(f"training_data_query执行失败: {str(e)}")
  1095. return jsonify(internal_error_response(
  1096. response_text="查询训练数据失败,请稍后重试"
  1097. )), 500
  1098. @app.route('/api/v0/training_data/create', methods=['POST'])
  1099. def training_data_create():
  1100. """创建训练数据API"""
  1101. try:
  1102. req = request.get_json(force=True)
  1103. data = req.get('data')
  1104. if not data:
  1105. return jsonify(bad_request_response(
  1106. response_text="缺少必需参数:data"
  1107. )), 400
  1108. if isinstance(data, dict):
  1109. data_list = [data]
  1110. elif isinstance(data, list):
  1111. data_list = data
  1112. else:
  1113. return jsonify(bad_request_response(
  1114. response_text="data字段格式错误,应为对象或数组"
  1115. )), 400
  1116. if len(data_list) > 50:
  1117. return jsonify(bad_request_response(
  1118. response_text="批量操作最大支持50条记录"
  1119. )), 400
  1120. results = []
  1121. successful_count = 0
  1122. for index, item in enumerate(data_list):
  1123. try:
  1124. training_type = item.get('training_data_type')
  1125. if training_type == 'sql':
  1126. sql = item.get('sql')
  1127. if not sql:
  1128. raise ValueError("SQL字段是必需的")
  1129. is_valid, error_msg = validate_sql_syntax(sql)
  1130. if not is_valid:
  1131. raise ValueError(error_msg)
  1132. question = item.get('question')
  1133. if question:
  1134. training_id = vn.train(question=question, sql=sql)
  1135. else:
  1136. training_id = vn.train(sql=sql)
  1137. elif training_type == 'documentation':
  1138. content = item.get('content')
  1139. if not content:
  1140. raise ValueError("content字段是必需的")
  1141. training_id = vn.train(documentation=content)
  1142. elif training_type == 'ddl':
  1143. ddl = item.get('ddl')
  1144. if not ddl:
  1145. raise ValueError("ddl字段是必需的")
  1146. training_id = vn.train(ddl=ddl)
  1147. else:
  1148. raise ValueError(f"不支持的训练数据类型: {training_type}")
  1149. results.append({
  1150. "index": index,
  1151. "success": True,
  1152. "training_id": training_id,
  1153. "type": training_type,
  1154. "message": f"{training_type}训练数据创建成功"
  1155. })
  1156. successful_count += 1
  1157. except Exception as e:
  1158. results.append({
  1159. "index": index,
  1160. "success": False,
  1161. "type": item.get('training_data_type', 'unknown'),
  1162. "error": str(e),
  1163. "message": "创建失败"
  1164. })
  1165. failed_count = len(data_list) - successful_count
  1166. if failed_count == 0:
  1167. return jsonify(success_response(
  1168. response_text="训练数据创建完成",
  1169. data={
  1170. "total_requested": len(data_list),
  1171. "successfully_created": successful_count,
  1172. "failed_count": failed_count,
  1173. "results": results
  1174. }
  1175. ))
  1176. else:
  1177. return jsonify(error_response(
  1178. response_text=f"训练数据创建部分成功,成功{successful_count}条,失败{failed_count}条",
  1179. data={
  1180. "total_requested": len(data_list),
  1181. "successfully_created": successful_count,
  1182. "failed_count": failed_count,
  1183. "results": results
  1184. }
  1185. )), 207
  1186. except Exception as e:
  1187. logger.error(f"training_data_create执行失败: {str(e)}")
  1188. return jsonify(internal_error_response(
  1189. response_text="创建训练数据失败,请稍后重试"
  1190. )), 500
  1191. @app.route('/api/v0/training_data/delete', methods=['POST'])
  1192. def training_data_delete():
  1193. """删除训练数据API"""
  1194. try:
  1195. req = request.get_json(force=True)
  1196. ids = req.get('ids', [])
  1197. confirm = req.get('confirm', False)
  1198. if not ids or not isinstance(ids, list):
  1199. return jsonify(bad_request_response(
  1200. response_text="缺少有效的ID列表"
  1201. )), 400
  1202. if not confirm:
  1203. return jsonify(bad_request_response(
  1204. response_text="删除操作需要确认,请设置confirm为true"
  1205. )), 400
  1206. if len(ids) > 50:
  1207. return jsonify(bad_request_response(
  1208. response_text="批量删除最大支持50条记录"
  1209. )), 400
  1210. deleted_ids = []
  1211. failed_ids = []
  1212. for training_id in ids:
  1213. try:
  1214. success = vn.remove_training_data(training_id)
  1215. if success:
  1216. deleted_ids.append(training_id)
  1217. else:
  1218. failed_ids.append(training_id)
  1219. except Exception as e:
  1220. failed_ids.append(training_id)
  1221. failed_count = len(failed_ids)
  1222. if failed_count == 0:
  1223. return jsonify(success_response(
  1224. response_text="训练数据删除完成",
  1225. data={
  1226. "total_requested": len(ids),
  1227. "successfully_deleted": len(deleted_ids),
  1228. "failed_count": failed_count,
  1229. "deleted_ids": deleted_ids,
  1230. "failed_ids": failed_ids
  1231. }
  1232. ))
  1233. else:
  1234. return jsonify(error_response(
  1235. response_text=f"训练数据删除部分成功,成功{len(deleted_ids)}条,失败{failed_count}条",
  1236. data={
  1237. "total_requested": len(ids),
  1238. "successfully_deleted": len(deleted_ids),
  1239. "failed_count": failed_count,
  1240. "deleted_ids": deleted_ids,
  1241. "failed_ids": failed_ids
  1242. }
  1243. )), 207
  1244. except Exception as e:
  1245. logger.error(f"training_data_delete执行失败: {str(e)}")
  1246. return jsonify(internal_error_response(
  1247. response_text="删除训练数据失败,请稍后重试"
  1248. )), 500
  1249. # ==================== React Agent 扩展API ====================
  1250. @app.route('/api/v0/react/users/<user_id>/conversations', methods=['GET'])
  1251. async def get_user_conversations_react(user_id: str):
  1252. """异步获取用户的聊天记录列表(从 custom_react_agent 迁移)"""
  1253. global _react_agent_instance
  1254. try:
  1255. # 获取查询参数
  1256. limit = request.args.get('limit', 10, type=int)
  1257. # 限制limit的范围
  1258. limit = max(1, min(limit, 50)) # 限制在1-50之间
  1259. logger.info(f"📋 异步获取用户 {user_id} 的对话列表,限制 {limit} 条")
  1260. # 确保Agent可用
  1261. if not await ensure_agent_ready():
  1262. return jsonify({
  1263. "success": False,
  1264. "error": "Agent 未就绪",
  1265. "timestamp": datetime.now().isoformat()
  1266. }), 503
  1267. # 直接调用异步方法
  1268. conversations = await _react_agent_instance.get_user_recent_conversations(user_id, limit)
  1269. return jsonify({
  1270. "success": True,
  1271. "data": {
  1272. "user_id": user_id,
  1273. "conversations": conversations,
  1274. "total_count": len(conversations),
  1275. "limit": limit
  1276. },
  1277. "timestamp": datetime.now().isoformat()
  1278. }), 200
  1279. except Exception as e:
  1280. logger.error(f"❌ 异步获取用户 {user_id} 对话列表失败: {e}")
  1281. return jsonify({
  1282. "success": False,
  1283. "error": str(e),
  1284. "timestamp": datetime.now().isoformat()
  1285. }), 500
  1286. @app.route('/api/v0/react/users/<user_id>/conversations/<thread_id>', methods=['GET'])
  1287. async def get_user_conversation_detail_react(user_id: str, thread_id: str):
  1288. """异步获取特定对话的详细历史(从 custom_react_agent 迁移)"""
  1289. global _react_agent_instance
  1290. try:
  1291. # 验证thread_id格式是否匹配user_id
  1292. if not thread_id.startswith(f"{user_id}:"):
  1293. return jsonify({
  1294. "success": False,
  1295. "error": f"Thread ID {thread_id} 不属于用户 {user_id}",
  1296. "timestamp": datetime.now().isoformat()
  1297. }), 400
  1298. logger.info(f"📖 异步获取用户 {user_id} 的对话 {thread_id} 详情")
  1299. # 确保Agent可用
  1300. if not await ensure_agent_ready():
  1301. return jsonify({
  1302. "success": False,
  1303. "error": "Agent 未就绪",
  1304. "timestamp": datetime.now().isoformat()
  1305. }), 503
  1306. # 直接调用异步方法
  1307. history = await _react_agent_instance.get_conversation_history(thread_id)
  1308. logger.info(f"✅ 异步成功获取对话历史,消息数量: {len(history)}")
  1309. if not history:
  1310. return jsonify({
  1311. "success": False,
  1312. "error": f"未找到对话 {thread_id}",
  1313. "timestamp": datetime.now().isoformat()
  1314. }), 404
  1315. return jsonify({
  1316. "success": True,
  1317. "data": {
  1318. "user_id": user_id,
  1319. "thread_id": thread_id,
  1320. "message_count": len(history),
  1321. "messages": history
  1322. },
  1323. "timestamp": datetime.now().isoformat()
  1324. }), 200
  1325. except Exception as e:
  1326. import traceback
  1327. logger.error(f"❌ 异步获取对话 {thread_id} 详情失败: {e}")
  1328. logger.error(f"❌ 详细错误信息: {traceback.format_exc()}")
  1329. return jsonify({
  1330. "success": False,
  1331. "error": str(e),
  1332. "timestamp": datetime.now().isoformat()
  1333. }), 500
  1334. @app.route('/api/test/redis', methods=['GET'])
  1335. def test_redis_connection():
  1336. """测试Redis连接和基本查询(从 custom_react_agent 迁移)"""
  1337. try:
  1338. import redis
  1339. # 创建Redis连接
  1340. r = redis.Redis(host='localhost', port=6379, decode_responses=True)
  1341. r.ping()
  1342. # 扫描checkpoint keys
  1343. pattern = "checkpoint:*"
  1344. keys = []
  1345. cursor = 0
  1346. count = 0
  1347. while True:
  1348. cursor, batch = r.scan(cursor=cursor, match=pattern, count=100)
  1349. keys.extend(batch)
  1350. count += len(batch)
  1351. if cursor == 0 or count > 500: # 限制扫描数量
  1352. break
  1353. # 统计用户
  1354. users = {}
  1355. for key in keys:
  1356. try:
  1357. parts = key.split(':')
  1358. if len(parts) >= 2:
  1359. user_id = parts[1]
  1360. users[user_id] = users.get(user_id, 0) + 1
  1361. except:
  1362. continue
  1363. r.close()
  1364. return jsonify({
  1365. "success": True,
  1366. "data": {
  1367. "redis_connected": True,
  1368. "total_checkpoint_keys": len(keys),
  1369. "users_found": list(users.keys()),
  1370. "user_key_counts": users,
  1371. "sample_keys": keys[:5] if keys else []
  1372. },
  1373. "timestamp": datetime.now().isoformat()
  1374. }), 200
  1375. except Exception as e:
  1376. logger.error(f"❌ Redis测试失败: {e}")
  1377. return jsonify({
  1378. "success": False,
  1379. "error": str(e),
  1380. "timestamp": datetime.now().isoformat()
  1381. }), 500
  1382. @app.route('/api/v0/react/direct/users/<user_id>/conversations', methods=['GET'])
  1383. def test_get_user_conversations_simple(user_id: str):
  1384. """测试简单Redis查询获取用户对话列表(从 custom_react_agent 迁移)"""
  1385. try:
  1386. limit = request.args.get('limit', 10, type=int)
  1387. limit = max(1, min(limit, 50))
  1388. logger.info(f"🧪 测试获取用户 {user_id} 的对话列表(简单Redis方式)")
  1389. # 使用简单Redis查询
  1390. conversations = get_user_conversations_simple_sync(user_id, limit)
  1391. return jsonify({
  1392. "success": True,
  1393. "method": "simple_redis_query",
  1394. "data": {
  1395. "user_id": user_id,
  1396. "conversations": conversations,
  1397. "total_count": len(conversations),
  1398. "limit": limit
  1399. },
  1400. "timestamp": datetime.now().isoformat()
  1401. }), 200
  1402. except Exception as e:
  1403. logger.error(f"❌ 测试简单Redis查询失败: {e}")
  1404. return jsonify({
  1405. "success": False,
  1406. "error": str(e),
  1407. "timestamp": datetime.now().isoformat()
  1408. }), 500
  1409. @app.route('/api/v0/react/direct/conversations/<thread_id>', methods=['GET'])
  1410. def get_conversation_detail_api(thread_id: str):
  1411. """
  1412. 获取特定对话的详细信息 - 支持include_tools开关参数(从 custom_react_agent 迁移)
  1413. Query Parameters:
  1414. - include_tools: bool, 是否包含工具调用信息,默认false
  1415. true: 返回完整对话(human/ai/tool/system)
  1416. false: 只返回human/ai消息,清理工具调用信息
  1417. - user_id: str, 可选的用户ID验证
  1418. Examples:
  1419. GET /api/conversations/wang:20250709195048728?include_tools=true # 完整模式
  1420. GET /api/conversations/wang:20250709195048728?include_tools=false # 简化模式(默认)
  1421. GET /api/conversations/wang:20250709195048728 # 简化模式(默认)
  1422. """
  1423. try:
  1424. # 获取查询参数
  1425. include_tools = request.args.get('include_tools', 'false').lower() == 'true'
  1426. user_id = request.args.get('user_id')
  1427. # 验证thread_id格式
  1428. if ':' not in thread_id:
  1429. return jsonify({
  1430. "success": False,
  1431. "error": "Invalid thread_id format. Expected format: user_id:timestamp",
  1432. "timestamp": datetime.now().isoformat()
  1433. }), 400
  1434. # 如果提供了user_id,验证thread_id是否属于该用户
  1435. thread_user_id = thread_id.split(':')[0]
  1436. if user_id and thread_user_id != user_id:
  1437. return jsonify({
  1438. "success": False,
  1439. "error": f"Thread ID {thread_id} does not belong to user {user_id}",
  1440. "timestamp": datetime.now().isoformat()
  1441. }), 400
  1442. logger.info(f"📖 获取对话详情 - Thread: {thread_id}, Include Tools: {include_tools}")
  1443. # 检查enhanced_redis_api是否可用
  1444. if get_conversation_detail_from_redis is None:
  1445. return jsonify({
  1446. "success": False,
  1447. "error": "enhanced_redis_api 模块不可用",
  1448. "timestamp": datetime.now().isoformat()
  1449. }), 503
  1450. # 从Redis获取对话详情(使用我们的新函数)
  1451. result = get_conversation_detail_from_redis(thread_id, include_tools)
  1452. if not result['success']:
  1453. logger.warning(f"⚠️ 获取对话详情失败: {result['error']}")
  1454. return jsonify({
  1455. "success": False,
  1456. "error": result['error'],
  1457. "timestamp": datetime.now().isoformat()
  1458. }), 404
  1459. # 添加API元数据
  1460. result['data']['api_metadata'] = {
  1461. "timestamp": datetime.now().isoformat(),
  1462. "api_version": "v1",
  1463. "endpoint": "get_conversation_detail",
  1464. "query_params": {
  1465. "include_tools": include_tools,
  1466. "user_id": user_id
  1467. }
  1468. }
  1469. mode_desc = "完整模式" if include_tools else "简化模式"
  1470. logger.info(f"✅ 成功获取对话详情 - Messages: {result['data']['message_count']}, Mode: {mode_desc}")
  1471. return jsonify({
  1472. "success": True,
  1473. "data": result['data'],
  1474. "timestamp": datetime.now().isoformat()
  1475. }), 200
  1476. except Exception as e:
  1477. import traceback
  1478. logger.error(f"❌ 获取对话详情异常: {e}")
  1479. logger.error(f"❌ 详细错误信息: {traceback.format_exc()}")
  1480. return jsonify({
  1481. "success": False,
  1482. "error": str(e),
  1483. "timestamp": datetime.now().isoformat()
  1484. }), 500
  1485. @app.route('/api/v0/react/direct/conversations/<thread_id>/compare', methods=['GET'])
  1486. def compare_conversation_modes_api(thread_id: str):
  1487. """
  1488. 比较完整模式和简化模式的对话内容
  1489. 用于调试和理解两种模式的差异(从 custom_react_agent 迁移)
  1490. Examples:
  1491. GET /api/conversations/wang:20250709195048728/compare
  1492. """
  1493. try:
  1494. logger.info(f"🔍 比较对话模式 - Thread: {thread_id}")
  1495. # 检查enhanced_redis_api是否可用
  1496. if get_conversation_detail_from_redis is None:
  1497. return jsonify({
  1498. "success": False,
  1499. "error": "enhanced_redis_api 模块不可用",
  1500. "timestamp": datetime.now().isoformat()
  1501. }), 503
  1502. # 获取完整模式
  1503. full_result = get_conversation_detail_from_redis(thread_id, include_tools=True)
  1504. # 获取简化模式
  1505. simple_result = get_conversation_detail_from_redis(thread_id, include_tools=False)
  1506. if not (full_result['success'] and simple_result['success']):
  1507. return jsonify({
  1508. "success": False,
  1509. "error": "无法获取对话数据进行比较",
  1510. "timestamp": datetime.now().isoformat()
  1511. }), 404
  1512. # 构建比较结果
  1513. comparison = {
  1514. "thread_id": thread_id,
  1515. "full_mode": {
  1516. "message_count": full_result['data']['message_count'],
  1517. "stats": full_result['data']['stats'],
  1518. "sample_messages": full_result['data']['messages'][:3] # 只显示前3条作为示例
  1519. },
  1520. "simple_mode": {
  1521. "message_count": simple_result['data']['message_count'],
  1522. "stats": simple_result['data']['stats'],
  1523. "sample_messages": simple_result['data']['messages'][:3] # 只显示前3条作为示例
  1524. },
  1525. "comparison_summary": {
  1526. "message_count_difference": full_result['data']['message_count'] - simple_result['data']['message_count'],
  1527. "tools_filtered_out": full_result['data']['stats'].get('tool_messages', 0),
  1528. "ai_messages_with_tools": full_result['data']['stats'].get('messages_with_tools', 0),
  1529. "filtering_effectiveness": "有效" if (full_result['data']['message_count'] - simple_result['data']['message_count']) > 0 else "无差异"
  1530. },
  1531. "metadata": {
  1532. "timestamp": datetime.now().isoformat(),
  1533. "note": "sample_messages 只显示前3条消息作为示例,完整消息请使用相应的详情API"
  1534. }
  1535. }
  1536. logger.info(f"✅ 模式比较完成 - 完整: {comparison['full_mode']['message_count']}, 简化: {comparison['simple_mode']['message_count']}")
  1537. return jsonify({
  1538. "success": True,
  1539. "data": comparison,
  1540. "timestamp": datetime.now().isoformat()
  1541. }), 200
  1542. except Exception as e:
  1543. logger.error(f"❌ 对话模式比较失败: {e}")
  1544. return jsonify({
  1545. "success": False,
  1546. "error": str(e),
  1547. "timestamp": datetime.now().isoformat()
  1548. }), 500
  1549. @app.route('/api/v0/react/direct/conversations/<thread_id>/summary', methods=['GET'])
  1550. def get_conversation_summary_api(thread_id: str):
  1551. """
  1552. 获取对话摘要信息(只包含基本统计,不返回具体消息)(从 custom_react_agent 迁移)
  1553. Query Parameters:
  1554. - include_tools: bool, 影响统计信息的计算方式
  1555. Examples:
  1556. GET /api/conversations/wang:20250709195048728/summary?include_tools=true
  1557. """
  1558. try:
  1559. include_tools = request.args.get('include_tools', 'false').lower() == 'true'
  1560. # 验证thread_id格式
  1561. if ':' not in thread_id:
  1562. return jsonify({
  1563. "success": False,
  1564. "error": "Invalid thread_id format. Expected format: user_id:timestamp",
  1565. "timestamp": datetime.now().isoformat()
  1566. }), 400
  1567. logger.info(f"📊 获取对话摘要 - Thread: {thread_id}, Include Tools: {include_tools}")
  1568. # 检查enhanced_redis_api是否可用
  1569. if get_conversation_detail_from_redis is None:
  1570. return jsonify({
  1571. "success": False,
  1572. "error": "enhanced_redis_api 模块不可用",
  1573. "timestamp": datetime.now().isoformat()
  1574. }), 503
  1575. # 获取完整对话信息
  1576. result = get_conversation_detail_from_redis(thread_id, include_tools)
  1577. if not result['success']:
  1578. return jsonify({
  1579. "success": False,
  1580. "error": result['error'],
  1581. "timestamp": datetime.now().isoformat()
  1582. }), 404
  1583. # 只返回摘要信息,不包含具体消息
  1584. data = result['data']
  1585. summary = {
  1586. "thread_id": data['thread_id'],
  1587. "user_id": data['user_id'],
  1588. "include_tools": data['include_tools'],
  1589. "message_count": data['message_count'],
  1590. "stats": data['stats'],
  1591. "metadata": data['metadata'],
  1592. "first_message_preview": None,
  1593. "last_message_preview": None,
  1594. "conversation_preview": None
  1595. }
  1596. # 添加消息预览
  1597. messages = data.get('messages', [])
  1598. if messages:
  1599. # 第一条human消息预览
  1600. for msg in messages:
  1601. if msg['type'] == 'human':
  1602. content = str(msg['content'])
  1603. summary['first_message_preview'] = content[:100] + "..." if len(content) > 100 else content
  1604. break
  1605. # 最后一条ai消息预览
  1606. for msg in reversed(messages):
  1607. if msg['type'] == 'ai' and msg.get('content', '').strip():
  1608. content = str(msg['content'])
  1609. summary['last_message_preview'] = content[:100] + "..." if len(content) > 100 else content
  1610. break
  1611. # 生成对话预览(第一条human消息)
  1612. summary['conversation_preview'] = summary['first_message_preview']
  1613. # 添加API元数据
  1614. summary['api_metadata'] = {
  1615. "timestamp": datetime.now().isoformat(),
  1616. "api_version": "v1",
  1617. "endpoint": "get_conversation_summary"
  1618. }
  1619. logger.info(f"✅ 成功获取对话摘要")
  1620. return jsonify({
  1621. "success": True,
  1622. "data": summary,
  1623. "timestamp": datetime.now().isoformat()
  1624. }), 200
  1625. except Exception as e:
  1626. logger.error(f"❌ 获取对话摘要失败: {e}")
  1627. return jsonify({
  1628. "success": False,
  1629. "error": str(e),
  1630. "timestamp": datetime.now().isoformat()
  1631. }), 500
  1632. # ==================== 启动逻辑 ====================
  1633. def signal_handler(signum, frame):
  1634. """信号处理器,优雅退出"""
  1635. logger.info(f"接收到信号 {signum},准备退出...")
  1636. cleanup_resources()
  1637. sys.exit(0)
  1638. if __name__ == '__main__':
  1639. # 注册信号处理器
  1640. signal.signal(signal.SIGINT, signal_handler)
  1641. signal.signal(signal.SIGTERM, signal_handler)
  1642. logger.info("🚀 启动统一API服务...")
  1643. logger.info("📍 服务地址: http://localhost:8084")
  1644. logger.info("🔗 健康检查: http://localhost:8084/health")
  1645. logger.info("📘 React Agent API: http://localhost:8084/api/v0/ask_react_agent")
  1646. logger.info("📘 LangGraph Agent API: http://localhost:8084/api/v0/ask_agent")
  1647. # 启动标准Flask应用(支持异步路由)
  1648. app.run(host="0.0.0.0", port=8084, debug=False, threaded=True)
  1649. # Data Pipeline 全局变量 - 从 citu_app.py 迁移
  1650. data_pipeline_manager = None
  1651. data_pipeline_file_manager = None
  1652. def get_data_pipeline_manager():
  1653. """获取Data Pipeline管理器单例(从 citu_app.py 迁移)"""
  1654. global data_pipeline_manager
  1655. if data_pipeline_manager is None:
  1656. data_pipeline_manager = SimpleWorkflowManager()
  1657. return data_pipeline_manager
  1658. def get_data_pipeline_file_manager():
  1659. """获取Data Pipeline文件管理器单例(从 citu_app.py 迁移)"""
  1660. global data_pipeline_file_manager
  1661. if data_pipeline_file_manager is None:
  1662. data_pipeline_file_manager = SimpleFileManager()
  1663. return data_pipeline_file_manager
  1664. # ==================== QA缓存管理API (从 citu_app.py 迁移) ====================
  1665. @app.route('/api/v0/qa_cache_stats', methods=['GET'])
  1666. def qa_cache_stats():
  1667. """获取问答缓存统计信息(从 citu_app.py 迁移)"""
  1668. try:
  1669. stats = redis_conversation_manager.get_qa_cache_stats()
  1670. return jsonify(success_response(
  1671. response_text="获取问答缓存统计成功",
  1672. data=stats
  1673. ))
  1674. except Exception as e:
  1675. logger.error(f"获取问答缓存统计失败: {str(e)}")
  1676. return jsonify(internal_error_response(
  1677. response_text="获取问答缓存统计失败,请稍后重试"
  1678. )), 500
  1679. @app.route('/api/v0/qa_cache_cleanup', methods=['POST'])
  1680. def qa_cache_cleanup():
  1681. """清空所有问答缓存(从 citu_app.py 迁移)"""
  1682. try:
  1683. if not redis_conversation_manager.is_available():
  1684. return jsonify(internal_error_response(
  1685. response_text="Redis连接不可用,无法执行清理操作"
  1686. )), 500
  1687. deleted_count = redis_conversation_manager.clear_all_qa_cache()
  1688. return jsonify(success_response(
  1689. response_text="问答缓存清理完成",
  1690. data={
  1691. "deleted_count": deleted_count,
  1692. "cleared": deleted_count > 0,
  1693. "cleanup_time": datetime.now().isoformat()
  1694. }
  1695. ))
  1696. except Exception as e:
  1697. logger.error(f"清空问答缓存失败: {str(e)}")
  1698. return jsonify(internal_error_response(
  1699. response_text="清空问答缓存失败,请稍后重试"
  1700. )), 500
  1701. # ==================== Database API (从 citu_app.py 迁移) ====================
  1702. @app.route('/api/v0/database/tables', methods=['POST'])
  1703. def get_database_tables():
  1704. """
  1705. 获取数据库表列表(从 citu_app.py 迁移)
  1706. 请求体:
  1707. {
  1708. "db_connection": "postgresql://postgres:postgres@192.168.67.1:5432/highway_db", // 可选,不传则使用默认配置
  1709. "schema": "public,ods", // 可选,支持多个schema用逗号分隔,默认为public
  1710. "table_name_pattern": "ods_*" // 可选,表名模式匹配,支持通配符:ods_*、*_dim、*fact*、ods_%
  1711. }
  1712. 响应:
  1713. {
  1714. "success": true,
  1715. "code": 200,
  1716. "message": "获取表列表成功",
  1717. "data": {
  1718. "tables": ["public.table1", "public.table2", "ods.table3"],
  1719. "total": 3,
  1720. "schemas": ["public", "ods"],
  1721. "table_name_pattern": "ods_*"
  1722. }
  1723. }
  1724. """
  1725. try:
  1726. req = request.get_json(force=True)
  1727. # 处理数据库连接参数(可选)
  1728. db_connection = req.get('db_connection')
  1729. if not db_connection:
  1730. # 使用app_config的默认数据库配置
  1731. import app_config
  1732. db_params = app_config.APP_DB_CONFIG
  1733. db_connection = f"postgresql://{db_params['user']}:{db_params['password']}@{db_params['host']}:{db_params['port']}/{db_params['dbname']}"
  1734. logger.info("使用默认数据库配置获取表列表")
  1735. else:
  1736. logger.info("使用用户指定的数据库配置获取表列表")
  1737. # 可选参数
  1738. schema = req.get('schema', '')
  1739. table_name_pattern = req.get('table_name_pattern')
  1740. # 创建表检查API实例
  1741. table_inspector = TableInspectorAPI()
  1742. # 使用asyncio运行异步方法
  1743. async def get_tables():
  1744. return await table_inspector.get_tables_list(db_connection, schema, table_name_pattern)
  1745. # 在新的事件循环中运行异步方法
  1746. try:
  1747. loop = asyncio.new_event_loop()
  1748. asyncio.set_event_loop(loop)
  1749. tables = loop.run_until_complete(get_tables())
  1750. finally:
  1751. loop.close()
  1752. # 解析schema信息
  1753. parsed_schemas = table_inspector._parse_schemas(schema)
  1754. response_data = {
  1755. "tables": tables,
  1756. "total": len(tables),
  1757. "schemas": parsed_schemas,
  1758. "db_connection_info": {
  1759. "database": db_connection.split('/')[-1].split('?')[0] if '/' in db_connection else "unknown"
  1760. }
  1761. }
  1762. # 如果使用了表名模式,添加到响应中
  1763. if table_name_pattern:
  1764. response_data["table_name_pattern"] = table_name_pattern
  1765. return jsonify(success_response(
  1766. response_text="获取表列表成功",
  1767. data=response_data
  1768. )), 200
  1769. except Exception as e:
  1770. logger.error(f"获取数据库表列表失败: {str(e)}")
  1771. return jsonify(internal_error_response(
  1772. response_text=f"获取表列表失败: {str(e)}"
  1773. )), 500
  1774. @app.route('/api/v0/database/table/ddl', methods=['POST'])
  1775. def get_table_ddl():
  1776. """
  1777. 获取表的DDL语句或MD文档(从 citu_app.py 迁移)
  1778. 请求体:
  1779. {
  1780. "db_connection": "postgresql://postgres:postgres@192.168.67.1:5432/highway_db", // 可选,不传则使用默认配置
  1781. "table": "public.test",
  1782. "business_context": "这是高速公路服务区的相关数据", // 可选
  1783. "type": "ddl" // 可选,支持ddl/md/both,默认为ddl
  1784. }
  1785. 响应:
  1786. {
  1787. "success": true,
  1788. "code": 200,
  1789. "message": "获取表DDL成功",
  1790. "data": {
  1791. "ddl": "create table public.test (...);",
  1792. "md": "## test表...", // 仅当type为md或both时返回
  1793. "table_info": {
  1794. "table_name": "test",
  1795. "schema_name": "public",
  1796. "full_name": "public.test",
  1797. "comment": "测试表",
  1798. "field_count": 10,
  1799. "row_count": 1000
  1800. },
  1801. "fields": [...]
  1802. }
  1803. }
  1804. """
  1805. try:
  1806. req = request.get_json(force=True)
  1807. # 处理参数(table仍为必需,db_connection可选)
  1808. table = req.get('table')
  1809. db_connection = req.get('db_connection')
  1810. if not table:
  1811. return jsonify(bad_request_response(
  1812. response_text="缺少必需参数:table",
  1813. missing_params=['table']
  1814. )), 400
  1815. if not db_connection:
  1816. # 使用app_config的默认数据库配置
  1817. import app_config
  1818. db_params = app_config.APP_DB_CONFIG
  1819. db_connection = f"postgresql://{db_params['user']}:{db_params['password']}@{db_params['host']}:{db_params['port']}/{db_params['dbname']}"
  1820. logger.info("使用默认数据库配置获取表DDL")
  1821. else:
  1822. logger.info("使用用户指定的数据库配置获取表DDL")
  1823. # 可选参数
  1824. business_context = req.get('business_context', '')
  1825. output_type = req.get('type', 'ddl')
  1826. # 验证type参数
  1827. valid_types = ['ddl', 'md', 'both']
  1828. if output_type not in valid_types:
  1829. return jsonify(bad_request_response(
  1830. response_text=f"无效的type参数: {output_type},支持的值: {valid_types}",
  1831. invalid_params=['type']
  1832. )), 400
  1833. # 创建表检查API实例
  1834. table_inspector = TableInspectorAPI()
  1835. # 使用asyncio运行异步方法
  1836. async def get_ddl():
  1837. return await table_inspector.get_table_ddl(
  1838. db_connection=db_connection,
  1839. table=table,
  1840. business_context=business_context,
  1841. output_type=output_type
  1842. )
  1843. # 在新的事件循环中运行异步方法
  1844. try:
  1845. loop = asyncio.new_event_loop()
  1846. asyncio.set_event_loop(loop)
  1847. result = loop.run_until_complete(get_ddl())
  1848. finally:
  1849. loop.close()
  1850. response_data = {
  1851. **result,
  1852. "generation_info": {
  1853. "business_context": business_context,
  1854. "output_type": output_type,
  1855. "has_llm_comments": bool(business_context),
  1856. "database": db_connection.split('/')[-1].split('?')[0] if '/' in db_connection else "unknown"
  1857. }
  1858. }
  1859. return jsonify(success_response(
  1860. response_text=f"获取表{output_type.upper()}成功",
  1861. data=response_data
  1862. )), 200
  1863. except Exception as e:
  1864. logger.error(f"获取表DDL失败: {str(e)}")
  1865. return jsonify(internal_error_response(
  1866. response_text=f"获取表{output_type.upper() if 'output_type' in locals() else 'DDL'}失败: {str(e)}"
  1867. )), 500
  1868. # ==================== Data Pipeline API (从 citu_app.py 迁移) ====================
  1869. @app.route('/api/v0/data_pipeline/tasks', methods=['POST'])
  1870. def create_data_pipeline_task():
  1871. """创建数据管道任务(从 citu_app.py 迁移)"""
  1872. try:
  1873. req = request.get_json(force=True)
  1874. # table_list_file和business_context现在都是可选参数
  1875. # 如果未提供table_list_file,将使用文件上传模式
  1876. # 创建任务(支持可选的db_connection参数)
  1877. manager = get_data_pipeline_manager()
  1878. task_id = manager.create_task(
  1879. table_list_file=req.get('table_list_file'),
  1880. business_context=req.get('business_context'),
  1881. db_name=req.get('db_name'), # 可选参数,用于指定特定数据库名称
  1882. db_connection=req.get('db_connection'), # 可选参数,用于指定数据库连接字符串
  1883. task_name=req.get('task_name'), # 可选参数,用于指定任务名称
  1884. enable_sql_validation=req.get('enable_sql_validation', True),
  1885. enable_llm_repair=req.get('enable_llm_repair', True),
  1886. modify_original_file=req.get('modify_original_file', True),
  1887. enable_training_data_load=req.get('enable_training_data_load', True)
  1888. )
  1889. # 获取任务信息
  1890. task_info = manager.get_task_status(task_id)
  1891. response_data = {
  1892. "task_id": task_id,
  1893. "task_name": task_info.get('task_name'),
  1894. "status": task_info.get('status'),
  1895. "created_at": task_info.get('created_at').isoformat() if task_info.get('created_at') else None
  1896. }
  1897. # 检查是否为文件上传模式
  1898. file_upload_mode = not req.get('table_list_file')
  1899. response_message = "任务创建成功"
  1900. if file_upload_mode:
  1901. response_data["file_upload_mode"] = True
  1902. response_data["next_step"] = f"POST /api/v0/data_pipeline/tasks/{task_id}/upload-table-list"
  1903. response_message += ",请上传表清单文件后再执行任务"
  1904. return jsonify(success_response(
  1905. response_text=response_message,
  1906. data=response_data
  1907. )), 201
  1908. except Exception as e:
  1909. logger.error(f"创建数据管道任务失败: {str(e)}")
  1910. return jsonify(internal_error_response(
  1911. response_text="创建任务失败,请稍后重试"
  1912. )), 500
  1913. @app.route('/api/v0/data_pipeline/tasks/<task_id>/execute', methods=['POST'])
  1914. def execute_data_pipeline_task(task_id):
  1915. """执行数据管道任务(从 citu_app.py 迁移)"""
  1916. try:
  1917. req = request.get_json(force=True) if request.is_json else {}
  1918. execution_mode = req.get('execution_mode', 'complete')
  1919. step_name = req.get('step_name')
  1920. # 验证执行模式
  1921. if execution_mode not in ['complete', 'step']:
  1922. return jsonify(bad_request_response(
  1923. response_text="无效的执行模式,必须是 'complete' 或 'step'",
  1924. invalid_params=['execution_mode']
  1925. )), 400
  1926. # 如果是步骤执行模式,验证步骤名称
  1927. if execution_mode == 'step':
  1928. if not step_name:
  1929. return jsonify(bad_request_response(
  1930. response_text="步骤执行模式需要指定step_name",
  1931. missing_params=['step_name']
  1932. )), 400
  1933. valid_steps = ['ddl_generation', 'qa_generation', 'sql_validation', 'training_load']
  1934. if step_name not in valid_steps:
  1935. return jsonify(bad_request_response(
  1936. response_text=f"无效的步骤名称,支持的步骤: {', '.join(valid_steps)}",
  1937. invalid_params=['step_name']
  1938. )), 400
  1939. # 检查任务是否存在
  1940. manager = get_data_pipeline_manager()
  1941. task_info = manager.get_task_status(task_id)
  1942. if not task_info:
  1943. return jsonify(not_found_response(
  1944. response_text=f"任务不存在: {task_id}"
  1945. )), 404
  1946. # 使用subprocess启动独立进程执行任务
  1947. def run_task_subprocess():
  1948. try:
  1949. import subprocess
  1950. import sys
  1951. from pathlib import Path
  1952. # 构建执行命令
  1953. python_executable = sys.executable
  1954. script_path = Path(__file__).parent / "data_pipeline" / "task_executor.py"
  1955. cmd = [
  1956. python_executable,
  1957. str(script_path),
  1958. "--task-id", task_id,
  1959. "--execution-mode", execution_mode
  1960. ]
  1961. if step_name:
  1962. cmd.extend(["--step-name", step_name])
  1963. logger.info(f"启动任务进程: {' '.join(cmd)}")
  1964. # 启动后台进程(不等待完成)
  1965. process = subprocess.Popen(
  1966. cmd,
  1967. stdout=subprocess.PIPE,
  1968. stderr=subprocess.PIPE,
  1969. text=True,
  1970. cwd=Path(__file__).parent
  1971. )
  1972. logger.info(f"任务进程已启动: PID={process.pid}, task_id={task_id}")
  1973. except Exception as e:
  1974. logger.error(f"启动任务进程失败: {task_id}, 错误: {str(e)}")
  1975. # 在新线程中启动subprocess(避免阻塞API响应)
  1976. thread = Thread(target=run_task_subprocess, daemon=True)
  1977. thread.start()
  1978. response_data = {
  1979. "task_id": task_id,
  1980. "execution_mode": execution_mode,
  1981. "step_name": step_name if execution_mode == 'step' else None,
  1982. "message": "任务正在后台执行,请通过状态接口查询进度"
  1983. }
  1984. return jsonify(success_response(
  1985. response_text="任务执行已启动",
  1986. data=response_data
  1987. )), 202
  1988. except Exception as e:
  1989. logger.error(f"启动数据管道任务执行失败: {str(e)}")
  1990. return jsonify(internal_error_response(
  1991. response_text="启动任务执行失败,请稍后重试"
  1992. )), 500
  1993. @app.route('/api/v0/data_pipeline/tasks/<task_id>', methods=['GET'])
  1994. def get_data_pipeline_task_status(task_id):
  1995. """
  1996. 获取数据管道任务状态(从 citu_app.py 迁移)
  1997. 响应:
  1998. {
  1999. "success": true,
  2000. "code": 200,
  2001. "message": "获取任务状态成功",
  2002. "data": {
  2003. "task_id": "task_20250627_143052",
  2004. "status": "in_progress",
  2005. "step_status": {
  2006. "ddl_generation": "completed",
  2007. "qa_generation": "running",
  2008. "sql_validation": "pending",
  2009. "training_load": "pending"
  2010. },
  2011. "created_at": "2025-06-27T14:30:52",
  2012. "started_at": "2025-06-27T14:31:00",
  2013. "parameters": {...},
  2014. "current_execution": {...},
  2015. "total_executions": 2
  2016. }
  2017. }
  2018. """
  2019. try:
  2020. manager = get_data_pipeline_manager()
  2021. task_info = manager.get_task_status(task_id)
  2022. if not task_info:
  2023. return jsonify(not_found_response(
  2024. response_text=f"任务不存在: {task_id}"
  2025. )), 404
  2026. # 获取步骤状态
  2027. steps = manager.get_task_steps(task_id)
  2028. current_step = None
  2029. for step in steps:
  2030. if step['step_status'] == 'running':
  2031. current_step = step
  2032. break
  2033. # 构建步骤状态摘要
  2034. step_status_summary = {}
  2035. for step in steps:
  2036. step_status_summary[step['step_name']] = step['step_status']
  2037. response_data = {
  2038. "task_id": task_info['task_id'],
  2039. "task_name": task_info.get('task_name'),
  2040. "status": task_info['status'],
  2041. "step_status": step_status_summary,
  2042. "created_at": task_info['created_at'].isoformat() if task_info.get('created_at') else None,
  2043. "started_at": task_info['started_at'].isoformat() if task_info.get('started_at') else None,
  2044. "completed_at": task_info['completed_at'].isoformat() if task_info.get('completed_at') else None,
  2045. "parameters": task_info.get('parameters', {}),
  2046. "result": task_info.get('result'),
  2047. "error_message": task_info.get('error_message'),
  2048. "current_step": {
  2049. "execution_id": current_step['execution_id'],
  2050. "step": current_step['step_name'],
  2051. "status": current_step['step_status'],
  2052. "started_at": current_step['started_at'].isoformat() if current_step and current_step.get('started_at') else None
  2053. } if current_step else None,
  2054. "total_steps": len(steps),
  2055. "steps": [{
  2056. "step_name": step['step_name'],
  2057. "step_status": step['step_status'],
  2058. "started_at": step['started_at'].isoformat() if step.get('started_at') else None,
  2059. "completed_at": step['completed_at'].isoformat() if step.get('completed_at') else None,
  2060. "error_message": step.get('error_message')
  2061. } for step in steps]
  2062. }
  2063. return jsonify(success_response(
  2064. response_text="获取任务状态成功",
  2065. data=response_data
  2066. ))
  2067. except Exception as e:
  2068. logger.error(f"获取数据管道任务状态失败: {str(e)}")
  2069. return jsonify(internal_error_response(
  2070. response_text="获取任务状态失败,请稍后重试"
  2071. )), 500
  2072. @app.route('/api/v0/data_pipeline/tasks/<task_id>/logs', methods=['GET'])
  2073. def get_data_pipeline_task_logs(task_id):
  2074. """
  2075. 获取数据管道任务日志(从任务目录文件读取)(从 citu_app.py 迁移)
  2076. 查询参数:
  2077. - limit: 日志行数限制,默认100
  2078. - level: 日志级别过滤,可选
  2079. 响应:
  2080. {
  2081. "success": true,
  2082. "code": 200,
  2083. "message": "获取任务日志成功",
  2084. "data": {
  2085. "task_id": "task_20250627_143052",
  2086. "logs": [
  2087. {
  2088. "timestamp": "2025-06-27 14:30:52",
  2089. "level": "INFO",
  2090. "message": "任务开始执行"
  2091. }
  2092. ],
  2093. "total": 15,
  2094. "source": "file"
  2095. }
  2096. }
  2097. """
  2098. try:
  2099. limit = request.args.get('limit', 100, type=int)
  2100. level = request.args.get('level')
  2101. # 限制最大查询数量
  2102. limit = min(limit, 1000)
  2103. manager = get_data_pipeline_manager()
  2104. # 验证任务是否存在
  2105. task_info = manager.get_task_status(task_id)
  2106. if not task_info:
  2107. return jsonify(not_found_response(
  2108. response_text=f"任务不存在: {task_id}"
  2109. )), 404
  2110. # 获取任务目录下的日志文件
  2111. import os
  2112. from pathlib import Path
  2113. # 获取项目根目录的绝对路径
  2114. project_root = Path(__file__).parent.absolute()
  2115. task_dir = project_root / "data_pipeline" / "training_data" / task_id
  2116. log_file = task_dir / "data_pipeline.log"
  2117. logs = []
  2118. if log_file.exists():
  2119. try:
  2120. # 读取日志文件的最后N行
  2121. with open(log_file, 'r', encoding='utf-8') as f:
  2122. lines = f.readlines()
  2123. # 取最后limit行
  2124. recent_lines = lines[-limit:] if len(lines) > limit else lines
  2125. # 解析日志行
  2126. import re
  2127. log_pattern = r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(\w+)\] (.+?): (.+)$'
  2128. for line in recent_lines:
  2129. line = line.strip()
  2130. if not line:
  2131. continue
  2132. match = re.match(log_pattern, line)
  2133. if match:
  2134. timestamp, log_level, logger_name, message = match.groups()
  2135. # 级别过滤
  2136. if level and log_level != level.upper():
  2137. continue
  2138. logs.append({
  2139. "timestamp": timestamp,
  2140. "level": log_level,
  2141. "logger": logger_name,
  2142. "message": message
  2143. })
  2144. else:
  2145. # 处理多行日志(如异常堆栈)
  2146. if logs:
  2147. logs[-1]["message"] += f"\n{line}"
  2148. except Exception as e:
  2149. logger.error(f"读取日志文件失败: {e}")
  2150. response_data = {
  2151. "task_id": task_id,
  2152. "logs": logs,
  2153. "total": len(logs),
  2154. "source": "file",
  2155. "log_file": str(log_file) if log_file.exists() else None
  2156. }
  2157. return jsonify(success_response(
  2158. response_text="获取任务日志成功",
  2159. data=response_data
  2160. ))
  2161. except Exception as e:
  2162. logger.error(f"获取数据管道任务日志失败: {str(e)}")
  2163. return jsonify(internal_error_response(
  2164. response_text="获取任务日志失败,请稍后重试"
  2165. )), 500
  2166. @app.route('/api/v0/data_pipeline/tasks', methods=['GET'])
  2167. def list_data_pipeline_tasks():
  2168. """获取数据管道任务列表(从 citu_app.py 迁移)"""
  2169. try:
  2170. limit = request.args.get('limit', 50, type=int)
  2171. offset = request.args.get('offset', 0, type=int)
  2172. status_filter = request.args.get('status')
  2173. # 限制查询数量
  2174. limit = min(limit, 100)
  2175. manager = get_data_pipeline_manager()
  2176. tasks = manager.get_tasks_list(
  2177. limit=limit,
  2178. offset=offset,
  2179. status_filter=status_filter
  2180. )
  2181. # 格式化任务列表
  2182. formatted_tasks = []
  2183. for task in tasks:
  2184. formatted_tasks.append({
  2185. "task_id": task.get('task_id'),
  2186. "task_name": task.get('task_name'),
  2187. "status": task.get('status'),
  2188. "step_status": task.get('step_status'),
  2189. "created_at": task['created_at'].isoformat() if task.get('created_at') else None,
  2190. "started_at": task['started_at'].isoformat() if task.get('started_at') else None,
  2191. "completed_at": task['completed_at'].isoformat() if task.get('completed_at') else None,
  2192. "created_by": task.get('by_user'),
  2193. "db_name": task.get('db_name'),
  2194. "business_context": task.get('parameters', {}).get('business_context') if task.get('parameters') else None,
  2195. # 新增字段
  2196. "directory_exists": task.get('directory_exists', True), # 默认为True,兼容旧数据
  2197. "updated_at": task['updated_at'].isoformat() if task.get('updated_at') else None
  2198. })
  2199. response_data = {
  2200. "tasks": formatted_tasks,
  2201. "total": len(formatted_tasks),
  2202. "limit": limit,
  2203. "offset": offset
  2204. }
  2205. return jsonify(success_response(
  2206. response_text="获取任务列表成功",
  2207. data=response_data
  2208. ))
  2209. except Exception as e:
  2210. logger.error(f"获取数据管道任务列表失败: {str(e)}")
  2211. return jsonify(internal_error_response(
  2212. response_text="获取任务列表失败,请稍后重试"
  2213. )), 500
  2214. @app.route('/api/v0/data_pipeline/tasks/query', methods=['POST'])
  2215. def query_data_pipeline_tasks():
  2216. """
  2217. 高级查询数据管道任务列表(从 citu_app.py 迁移)
  2218. 支持复杂筛选、排序、分页功能
  2219. 请求体:
  2220. {
  2221. "page": 1, // 页码,必须大于0,默认1
  2222. "page_size": 20, // 每页大小,1-100之间,默认20
  2223. "status": "completed", // 可选,任务状态筛选:"pending"|"running"|"completed"|"failed"|"cancelled"
  2224. "task_name": "highway", // 可选,任务名称模糊搜索,最大100字符
  2225. "created_by": "user123", // 可选,创建者精确匹配
  2226. "db_name": "highway_db", // 可选,数据库名称精确匹配
  2227. "created_time_start": "2025-01-01T00:00:00", // 可选,创建时间范围开始
  2228. "created_time_end": "2025-12-31T23:59:59", // 可选,创建时间范围结束
  2229. "started_time_start": "2025-01-01T00:00:00", // 可选,开始时间范围开始
  2230. "started_time_end": "2025-12-31T23:59:59", // 可选,开始时间范围结束
  2231. "completed_time_start": "2025-01-01T00:00:00", // 可选,完成时间范围开始
  2232. "completed_time_end": "2025-12-31T23:59:59", // 可选,完成时间范围结束
  2233. "sort_by": "created_at", // 可选,排序字段:"created_at"|"started_at"|"completed_at"|"task_name"|"status",默认"created_at"
  2234. "sort_order": "desc" // 可选,排序方向:"asc"|"desc",默认"desc"
  2235. }
  2236. """
  2237. try:
  2238. # 获取请求数据
  2239. req = request.get_json(force=True) if request.is_json else {}
  2240. # 解析参数,设置默认值
  2241. page = req.get('page', 1)
  2242. page_size = req.get('page_size', 20)
  2243. status = req.get('status')
  2244. task_name = req.get('task_name')
  2245. created_by = req.get('created_by')
  2246. db_name = req.get('db_name')
  2247. created_time_start = req.get('created_time_start')
  2248. created_time_end = req.get('created_time_end')
  2249. started_time_start = req.get('started_time_start')
  2250. started_time_end = req.get('started_time_end')
  2251. completed_time_start = req.get('completed_time_start')
  2252. completed_time_end = req.get('completed_time_end')
  2253. sort_by = req.get('sort_by', 'created_at')
  2254. sort_order = req.get('sort_order', 'desc')
  2255. # 参数验证
  2256. # 验证分页参数
  2257. if page < 1:
  2258. return jsonify(bad_request_response(
  2259. response_text="页码必须大于0",
  2260. invalid_params=['page']
  2261. )), 400
  2262. if page_size < 1 or page_size > 100:
  2263. return jsonify(bad_request_response(
  2264. response_text="每页大小必须在1-100之间",
  2265. invalid_params=['page_size']
  2266. )), 400
  2267. # 验证任务名称长度
  2268. if task_name and len(task_name) > 100:
  2269. return jsonify(bad_request_response(
  2270. response_text="任务名称搜索关键词最大长度为100字符",
  2271. invalid_params=['task_name']
  2272. )), 400
  2273. # 验证排序参数
  2274. allowed_sort_fields = ['created_at', 'started_at', 'completed_at', 'task_name', 'status']
  2275. if sort_by not in allowed_sort_fields:
  2276. return jsonify(bad_request_response(
  2277. response_text=f"不支持的排序字段: {sort_by},支持的字段: {', '.join(allowed_sort_fields)}",
  2278. invalid_params=['sort_by']
  2279. )), 400
  2280. if sort_order.lower() not in ['asc', 'desc']:
  2281. return jsonify(bad_request_response(
  2282. response_text="排序方向必须是 'asc' 或 'desc'",
  2283. invalid_params=['sort_order']
  2284. )), 400
  2285. # 验证状态筛选
  2286. if status:
  2287. allowed_statuses = ['pending', 'running', 'completed', 'failed', 'cancelled']
  2288. if status not in allowed_statuses:
  2289. return jsonify(bad_request_response(
  2290. response_text=f"不支持的状态值: {status},支持的状态: {', '.join(allowed_statuses)}",
  2291. invalid_params=['status']
  2292. )), 400
  2293. # 调用管理器执行查询
  2294. manager = get_data_pipeline_manager()
  2295. result = manager.query_tasks_advanced(
  2296. page=page,
  2297. page_size=page_size,
  2298. status=status,
  2299. task_name=task_name,
  2300. created_by=created_by,
  2301. db_name=db_name,
  2302. created_time_start=created_time_start,
  2303. created_time_end=created_time_end,
  2304. started_time_start=started_time_start,
  2305. started_time_end=started_time_end,
  2306. completed_time_start=completed_time_start,
  2307. completed_time_end=completed_time_end,
  2308. sort_by=sort_by,
  2309. sort_order=sort_order
  2310. )
  2311. # 格式化任务列表
  2312. formatted_tasks = []
  2313. for task in result['tasks']:
  2314. formatted_tasks.append({
  2315. "task_id": task.get('task_id'),
  2316. "task_name": task.get('task_name'),
  2317. "status": task.get('status'),
  2318. "step_status": task.get('step_status'),
  2319. "created_at": task['created_at'].isoformat() if task.get('created_at') else None,
  2320. "started_at": task['started_at'].isoformat() if task.get('started_at') else None,
  2321. "completed_at": task['completed_at'].isoformat() if task.get('completed_at') else None,
  2322. "created_by": task.get('by_user'),
  2323. "db_name": task.get('db_name'),
  2324. "business_context": task.get('parameters', {}).get('business_context') if task.get('parameters') else None,
  2325. "directory_exists": task.get('directory_exists', True),
  2326. "updated_at": task['updated_at'].isoformat() if task.get('updated_at') else None
  2327. })
  2328. # 构建响应数据
  2329. response_data = {
  2330. "tasks": formatted_tasks,
  2331. "pagination": result['pagination'],
  2332. "filters_applied": {
  2333. k: v for k, v in {
  2334. "status": status,
  2335. "task_name": task_name,
  2336. "created_by": created_by,
  2337. "db_name": db_name,
  2338. "created_time_start": created_time_start,
  2339. "created_time_end": created_time_end,
  2340. "started_time_start": started_time_start,
  2341. "started_time_end": started_time_end,
  2342. "completed_time_start": completed_time_start,
  2343. "completed_time_end": completed_time_end
  2344. }.items() if v
  2345. },
  2346. "sort_applied": {
  2347. "sort_by": sort_by,
  2348. "sort_order": sort_order
  2349. },
  2350. "query_time": result.get('query_time', '0.000s')
  2351. }
  2352. return jsonify(success_response(
  2353. response_text="查询任务列表成功",
  2354. data=response_data
  2355. ))
  2356. except Exception as e:
  2357. logger.error(f"查询数据管道任务列表失败: {str(e)}")
  2358. return jsonify(internal_error_response(
  2359. response_text="查询任务列表失败,请稍后重试"
  2360. )), 500
  2361. @app.route('/api/v0/data_pipeline/tasks/<task_id>/files', methods=['GET'])
  2362. def get_data_pipeline_task_files(task_id):
  2363. """获取任务文件列表(从 citu_app.py 迁移)"""
  2364. try:
  2365. file_manager = get_data_pipeline_file_manager()
  2366. # 获取任务文件
  2367. files = file_manager.get_task_files(task_id)
  2368. directory_info = file_manager.get_directory_info(task_id)
  2369. # 格式化文件信息
  2370. formatted_files = []
  2371. for file_info in files:
  2372. formatted_files.append({
  2373. "file_name": file_info['file_name'],
  2374. "file_type": file_info['file_type'],
  2375. "file_size": file_info['file_size'],
  2376. "file_size_formatted": file_info['file_size_formatted'],
  2377. "created_at": file_info['created_at'].isoformat() if file_info.get('created_at') else None,
  2378. "modified_at": file_info['modified_at'].isoformat() if file_info.get('modified_at') else None,
  2379. "is_readable": file_info['is_readable']
  2380. })
  2381. response_data = {
  2382. "task_id": task_id,
  2383. "files": formatted_files,
  2384. "directory_info": directory_info
  2385. }
  2386. return jsonify(success_response(
  2387. response_text="获取任务文件列表成功",
  2388. data=response_data
  2389. ))
  2390. except Exception as e:
  2391. logger.error(f"获取任务文件列表失败: {str(e)}")
  2392. return jsonify(internal_error_response(
  2393. response_text="获取任务文件列表失败,请稍后重试"
  2394. )), 500
  2395. @app.route('/api/v0/data_pipeline/tasks/<task_id>/files/<file_name>', methods=['GET'])
  2396. def download_data_pipeline_task_file(task_id, file_name):
  2397. """下载任务文件(从 citu_app.py 迁移)"""
  2398. try:
  2399. logger.info(f"开始下载文件: task_id={task_id}, file_name={file_name}")
  2400. # 直接构建文件路径,避免依赖数据库
  2401. from pathlib import Path
  2402. import os
  2403. # 获取项目根目录的绝对路径
  2404. project_root = Path(__file__).parent.absolute()
  2405. task_dir = project_root / "data_pipeline" / "training_data" / task_id
  2406. file_path = task_dir / file_name
  2407. logger.info(f"文件路径: {file_path}")
  2408. # 检查文件是否存在
  2409. if not file_path.exists():
  2410. logger.warning(f"文件不存在: {file_path}")
  2411. return jsonify(not_found_response(
  2412. response_text=f"文件不存在: {file_name}"
  2413. )), 404
  2414. # 检查是否为文件(而不是目录)
  2415. if not file_path.is_file():
  2416. logger.warning(f"路径不是文件: {file_path}")
  2417. return jsonify(bad_request_response(
  2418. response_text=f"路径不是有效文件: {file_name}"
  2419. )), 400
  2420. # 安全检查:确保文件在允许的目录内
  2421. try:
  2422. file_path.resolve().relative_to(task_dir.resolve())
  2423. except ValueError:
  2424. logger.warning(f"文件路径不安全: {file_path}")
  2425. return jsonify(bad_request_response(
  2426. response_text="非法的文件路径"
  2427. )), 400
  2428. # 检查文件是否可读
  2429. if not os.access(file_path, os.R_OK):
  2430. logger.warning(f"文件不可读: {file_path}")
  2431. return jsonify(bad_request_response(
  2432. response_text="文件不可读"
  2433. )), 400
  2434. logger.info(f"开始发送文件: {file_path}")
  2435. return send_file(
  2436. file_path,
  2437. as_attachment=True,
  2438. download_name=file_name
  2439. )
  2440. except Exception as e:
  2441. logger.error(f"下载任务文件失败: task_id={task_id}, file_name={file_name}, 错误: {str(e)}", exc_info=True)
  2442. return jsonify(internal_error_response(
  2443. response_text="下载文件失败,请稍后重试"
  2444. )), 500
  2445. @app.route('/api/v0/data_pipeline/tasks/<task_id>/upload-table-list', methods=['POST'])
  2446. def upload_table_list_file(task_id):
  2447. """
  2448. 上传表清单文件(从 citu_app.py 迁移)
  2449. 表单参数:
  2450. - file: 要上传的表清单文件(multipart/form-data)
  2451. 响应:
  2452. {
  2453. "success": true,
  2454. "code": 200,
  2455. "message": "表清单文件上传成功",
  2456. "data": {
  2457. "task_id": "task_20250701_123456",
  2458. "filename": "table_list.txt",
  2459. "file_size": 1024,
  2460. "file_size_formatted": "1.0 KB"
  2461. }
  2462. }
  2463. """
  2464. try:
  2465. # 验证任务是否存在
  2466. manager = get_data_pipeline_manager()
  2467. task_info = manager.get_task_status(task_id)
  2468. if not task_info:
  2469. return jsonify(not_found_response(
  2470. response_text=f"任务不存在: {task_id}"
  2471. )), 404
  2472. # 检查是否有文件上传
  2473. if 'file' not in request.files:
  2474. return jsonify(bad_request_response(
  2475. response_text="请选择要上传的表清单文件",
  2476. missing_params=['file']
  2477. )), 400
  2478. file = request.files['file']
  2479. # 验证文件名
  2480. if file.filename == '':
  2481. return jsonify(bad_request_response(
  2482. response_text="请选择有效的文件"
  2483. )), 400
  2484. try:
  2485. # 使用文件管理器上传文件
  2486. file_manager = get_data_pipeline_file_manager()
  2487. result = file_manager.upload_table_list_file(task_id, file)
  2488. response_data = {
  2489. "task_id": task_id,
  2490. "filename": result["filename"],
  2491. "file_size": result["file_size"],
  2492. "file_size_formatted": result["file_size_formatted"],
  2493. "upload_time": result["upload_time"].isoformat() if result.get("upload_time") else None
  2494. }
  2495. return jsonify(success_response(
  2496. response_text="表清单文件上传成功",
  2497. data=response_data
  2498. )), 200
  2499. except ValueError as e:
  2500. # 文件验证错误(如文件太大、空文件等)
  2501. return jsonify(bad_request_response(
  2502. response_text=str(e)
  2503. )), 400
  2504. except Exception as e:
  2505. logger.error(f"上传表清单文件失败: {str(e)}")
  2506. return jsonify(internal_error_response(
  2507. response_text="文件上传失败,请稍后重试"
  2508. )), 500
  2509. except Exception as e:
  2510. logger.error(f"处理表清单文件上传请求失败: {str(e)}")
  2511. return jsonify(internal_error_response(
  2512. response_text="处理上传请求失败,请稍后重试"
  2513. )), 500
  2514. @app.route('/api/v0/data_pipeline/tasks/<task_id>/table-list-info', methods=['GET'])
  2515. def get_table_list_info(task_id):
  2516. """
  2517. 获取任务的表清单文件信息(从 citu_app.py 迁移)
  2518. 响应:
  2519. {
  2520. "success": true,
  2521. "code": 200,
  2522. "message": "获取表清单文件信息成功",
  2523. "data": {
  2524. "task_id": "task_20250701_123456",
  2525. "has_file": true,
  2526. "filename": "table_list.txt",
  2527. "file_path": "./data_pipeline/training_data/task_20250701_123456/table_list.txt",
  2528. "file_size": 1024,
  2529. "file_size_formatted": "1.0 KB",
  2530. "uploaded_at": "2025-07-01T12:34:56",
  2531. "table_count": 5,
  2532. "is_readable": true
  2533. }
  2534. }
  2535. """
  2536. try:
  2537. file_manager = get_data_pipeline_file_manager()
  2538. # 获取表清单文件信息
  2539. table_list_info = file_manager.get_table_list_file_info(task_id)
  2540. response_data = {
  2541. "task_id": task_id,
  2542. "has_file": table_list_info.get("exists", False),
  2543. **table_list_info
  2544. }
  2545. return jsonify(success_response(
  2546. response_text="获取表清单文件信息成功",
  2547. data=response_data
  2548. ))
  2549. except Exception as e:
  2550. logger.error(f"获取表清单文件信息失败: {str(e)}")
  2551. return jsonify(internal_error_response(
  2552. response_text="获取表清单文件信息失败,请稍后重试"
  2553. )), 500
  2554. @app.route('/api/v0/data_pipeline/tasks/<task_id>/table-list', methods=['POST'])
  2555. def create_table_list_from_names(task_id):
  2556. """
  2557. 通过POST方式提交表名列表并创建table_list.txt文件(从 citu_app.py 迁移)
  2558. 请求体:
  2559. {
  2560. "tables": ["table1", "schema.table2", "table3"]
  2561. }
  2562. 或者:
  2563. {
  2564. "tables": "table1,schema.table2,table3"
  2565. }
  2566. 响应:
  2567. {
  2568. "success": true,
  2569. "code": 200,
  2570. "message": "表清单已成功创建",
  2571. "data": {
  2572. "task_id": "task_20250701_123456",
  2573. "filename": "table_list.txt",
  2574. "table_count": 3,
  2575. "file_size": 45,
  2576. "file_size_formatted": "45 B",
  2577. "created_time": "2025-07-01T12:34:56"
  2578. }
  2579. }
  2580. """
  2581. try:
  2582. # 验证任务是否存在
  2583. manager = get_data_pipeline_manager()
  2584. task_info = manager.get_task_status(task_id)
  2585. if not task_info:
  2586. return jsonify(not_found_response(
  2587. response_text=f"任务不存在: {task_id}"
  2588. )), 404
  2589. # 获取请求数据
  2590. req = request.get_json(force=True)
  2591. tables_param = req.get('tables')
  2592. if not tables_param:
  2593. return jsonify(bad_request_response(
  2594. response_text="缺少必需参数:tables",
  2595. missing_params=['tables']
  2596. )), 400
  2597. # 处理不同格式的表名参数
  2598. try:
  2599. if isinstance(tables_param, str):
  2600. # 逗号分隔的字符串格式
  2601. table_names = [name.strip() for name in tables_param.split(',') if name.strip()]
  2602. elif isinstance(tables_param, list):
  2603. # 数组格式
  2604. table_names = [str(name).strip() for name in tables_param if str(name).strip()]
  2605. else:
  2606. return jsonify(bad_request_response(
  2607. response_text="tables参数格式错误,应为字符串(逗号分隔)或数组"
  2608. )), 400
  2609. if not table_names:
  2610. return jsonify(bad_request_response(
  2611. response_text="表名列表不能为空"
  2612. )), 400
  2613. except Exception as e:
  2614. return jsonify(bad_request_response(
  2615. response_text=f"解析tables参数失败: {str(e)}"
  2616. )), 400
  2617. try:
  2618. # 使用文件管理器创建表清单文件
  2619. file_manager = get_data_pipeline_file_manager()
  2620. result = file_manager.create_table_list_from_names(task_id, table_names)
  2621. response_data = {
  2622. "task_id": task_id,
  2623. "filename": result["filename"],
  2624. "table_count": result["table_count"],
  2625. "unique_table_count": result["unique_table_count"],
  2626. "file_size": result["file_size"],
  2627. "file_size_formatted": result["file_size_formatted"],
  2628. "created_time": result["created_time"].isoformat() if result.get("created_time") else None,
  2629. "original_count": len(table_names) if isinstance(table_names, list) else len(tables_param.split(','))
  2630. }
  2631. return jsonify(success_response(
  2632. response_text=f"表清单已成功创建,包含 {result['table_count']} 个表",
  2633. data=response_data
  2634. )), 200
  2635. except ValueError as e:
  2636. # 表名验证错误(如格式错误、数量限制等)
  2637. return jsonify(bad_request_response(
  2638. response_text=str(e)
  2639. )), 400
  2640. except Exception as e:
  2641. logger.error(f"创建表清单文件失败: {str(e)}")
  2642. return jsonify(internal_error_response(
  2643. response_text="创建表清单文件失败,请稍后重试"
  2644. )), 500
  2645. except Exception as e:
  2646. logger.error(f"处理表清单创建请求失败: {str(e)}")
  2647. return jsonify(internal_error_response(
  2648. response_text="处理请求失败,请稍后重试"
  2649. )), 500
  2650. @app.route('/api/v0/data_pipeline/tasks/<task_id>/files', methods=['POST'])
  2651. def upload_file_to_task(task_id):
  2652. """
  2653. 上传文件到指定任务目录(从 citu_app.py 迁移)
  2654. 表单参数:
  2655. - file: 要上传的文件(multipart/form-data)
  2656. - overwrite_mode: 重名处理模式 (backup, replace, skip),默认为backup
  2657. 支持的文件类型:
  2658. - .ddl: DDL文件
  2659. - .md: Markdown文档
  2660. - .txt: 文本文件
  2661. - .json: JSON文件
  2662. - .sql: SQL文件
  2663. - .csv: CSV文件
  2664. 重名处理模式:
  2665. - backup: 备份原文件(默认)
  2666. - replace: 直接覆盖
  2667. - skip: 跳过上传
  2668. """
  2669. try:
  2670. # 验证任务是否存在
  2671. manager = get_data_pipeline_manager()
  2672. task_info = manager.get_task_status(task_id)
  2673. if not task_info:
  2674. return jsonify(not_found_response(
  2675. response_text=f"任务不存在: {task_id}"
  2676. )), 404
  2677. # 检查是否有文件上传
  2678. if 'file' not in request.files:
  2679. return jsonify(bad_request_response(
  2680. response_text="请选择要上传的文件",
  2681. missing_params=['file']
  2682. )), 400
  2683. file = request.files['file']
  2684. # 验证文件名
  2685. if file.filename == '':
  2686. return jsonify(bad_request_response(
  2687. response_text="请选择有效的文件"
  2688. )), 400
  2689. # 获取重名处理模式
  2690. overwrite_mode = request.form.get('overwrite_mode', 'backup')
  2691. # 验证重名处理模式
  2692. valid_modes = ['backup', 'replace', 'skip']
  2693. if overwrite_mode not in valid_modes:
  2694. return jsonify(bad_request_response(
  2695. response_text=f"无效的overwrite_mode参数: {overwrite_mode},支持的值: {valid_modes}",
  2696. invalid_params=['overwrite_mode']
  2697. )), 400
  2698. try:
  2699. # 使用文件管理器上传文件
  2700. file_manager = get_data_pipeline_file_manager()
  2701. result = file_manager.upload_file_to_task(task_id, file, file.filename, overwrite_mode)
  2702. # 检查是否跳过上传
  2703. if result.get('skipped'):
  2704. return jsonify(success_response(
  2705. response_text=result.get('message', '文件已存在,跳过上传'),
  2706. data=result
  2707. )), 200
  2708. return jsonify(success_response(
  2709. response_text="文件上传成功",
  2710. data=result
  2711. )), 200
  2712. except ValueError as e:
  2713. # 文件验证错误(如文件太大、空文件、不支持的类型等)
  2714. return jsonify(bad_request_response(
  2715. response_text=str(e)
  2716. )), 400
  2717. except Exception as e:
  2718. logger.error(f"上传文件失败: {str(e)}")
  2719. return jsonify(internal_error_response(
  2720. response_text="文件上传失败,请稍后重试"
  2721. )), 500
  2722. except Exception as e:
  2723. logger.error(f"处理文件上传请求失败: {str(e)}")
  2724. return jsonify(internal_error_response(
  2725. response_text="处理上传请求失败,请稍后重试"
  2726. )), 500
  2727. # 任务目录删除功能(从 citu_app.py 迁移)
  2728. import shutil
  2729. from pathlib import Path
  2730. import psycopg2
  2731. from app_config import PGVECTOR_CONFIG
  2732. def delete_task_directory_simple(task_id, delete_database_records=False):
  2733. """
  2734. 简单的任务目录删除功能(从 citu_app.py 迁移)
  2735. - 删除 data_pipeline/training_data/{task_id} 目录
  2736. - 更新数据库中的 directory_exists 字段
  2737. - 可选:删除数据库记录
  2738. """
  2739. try:
  2740. # 1. 删除目录
  2741. project_root = Path(__file__).parent.absolute()
  2742. task_dir = project_root / "data_pipeline" / "training_data" / task_id
  2743. deleted_files_count = 0
  2744. deleted_size = 0
  2745. if task_dir.exists():
  2746. # 计算删除前的统计信息
  2747. for file_path in task_dir.rglob('*'):
  2748. if file_path.is_file():
  2749. deleted_files_count += 1
  2750. deleted_size += file_path.stat().st_size
  2751. # 删除目录
  2752. shutil.rmtree(task_dir)
  2753. directory_deleted = True
  2754. operation_message = "目录删除成功"
  2755. else:
  2756. directory_deleted = False
  2757. operation_message = "目录不存在,无需删除"
  2758. # 2. 更新数据库
  2759. database_records_deleted = False
  2760. try:
  2761. conn = psycopg2.connect(**PGVECTOR_CONFIG)
  2762. cur = conn.cursor()
  2763. if delete_database_records:
  2764. # 删除任务步骤记录
  2765. cur.execute("DELETE FROM data_pipeline_task_steps WHERE task_id = %s", (task_id,))
  2766. # 删除任务主记录
  2767. cur.execute("DELETE FROM data_pipeline_tasks WHERE task_id = %s", (task_id,))
  2768. database_records_deleted = True
  2769. else:
  2770. # 只更新目录状态
  2771. cur.execute("""
  2772. UPDATE data_pipeline_tasks
  2773. SET directory_exists = FALSE, updated_at = CURRENT_TIMESTAMP
  2774. WHERE task_id = %s
  2775. """, (task_id,))
  2776. conn.commit()
  2777. cur.close()
  2778. conn.close()
  2779. except Exception as db_error:
  2780. logger.error(f"数据库操作失败: {db_error}")
  2781. # 数据库失败不影响文件删除的结果
  2782. # 3. 格式化文件大小
  2783. def format_size(size_bytes):
  2784. if size_bytes < 1024:
  2785. return f"{size_bytes} B"
  2786. elif size_bytes < 1024**2:
  2787. return f"{size_bytes/1024:.1f} KB"
  2788. elif size_bytes < 1024**3:
  2789. return f"{size_bytes/(1024**2):.1f} MB"
  2790. else:
  2791. return f"{size_bytes/(1024**3):.1f} GB"
  2792. return {
  2793. "success": True,
  2794. "task_id": task_id,
  2795. "directory_deleted": directory_deleted,
  2796. "database_records_deleted": database_records_deleted,
  2797. "deleted_files_count": deleted_files_count,
  2798. "deleted_size": format_size(deleted_size),
  2799. "deleted_at": datetime.now().isoformat(),
  2800. "operation_message": operation_message # 新增:具体的操作消息
  2801. }
  2802. except Exception as e:
  2803. logger.error(f"删除任务目录失败: {task_id}, 错误: {str(e)}")
  2804. return {
  2805. "success": False,
  2806. "task_id": task_id,
  2807. "error": str(e),
  2808. "error_code": "DELETE_FAILED",
  2809. "operation_message": f"删除操作失败: {str(e)}" # 新增:失败消息
  2810. }
  2811. @app.route('/api/v0/data_pipeline/tasks', methods=['DELETE'])
  2812. def delete_tasks():
  2813. """删除任务目录(支持单个和批量)(从 citu_app.py 迁移)"""
  2814. try:
  2815. # 智能获取参数:支持JSON body和URL查询参数两种方式
  2816. def get_request_parameter(param_name, array_param_name=None):
  2817. """从JSON body或URL查询参数中获取参数值"""
  2818. # 1. 优先从JSON body获取
  2819. if request.is_json:
  2820. try:
  2821. json_data = request.get_json()
  2822. if json_data and param_name in json_data:
  2823. return json_data[param_name]
  2824. except:
  2825. pass
  2826. # 2. 从URL查询参数获取
  2827. if param_name in request.args:
  2828. value = request.args.get(param_name)
  2829. # 处理布尔值
  2830. if value.lower() in ('true', '1', 'yes'):
  2831. return True
  2832. elif value.lower() in ('false', '0', 'no'):
  2833. return False
  2834. return value
  2835. # 3. 处理数组参数(如 task_ids[])
  2836. if array_param_name and array_param_name in request.args:
  2837. return request.args.getlist(array_param_name)
  2838. return None
  2839. # 获取参数
  2840. task_ids = get_request_parameter('task_ids', 'task_ids[]')
  2841. confirm = get_request_parameter('confirm')
  2842. if not task_ids:
  2843. return jsonify(bad_request_response(
  2844. response_text="缺少必需参数: task_ids",
  2845. missing_params=['task_ids']
  2846. )), 400
  2847. if not confirm:
  2848. return jsonify(bad_request_response(
  2849. response_text="缺少必需参数: confirm",
  2850. missing_params=['confirm']
  2851. )), 400
  2852. if confirm != True:
  2853. return jsonify(bad_request_response(
  2854. response_text="confirm参数必须为true以确认删除操作"
  2855. )), 400
  2856. if not isinstance(task_ids, list) or len(task_ids) == 0:
  2857. return jsonify(bad_request_response(
  2858. response_text="task_ids必须是非空的任务ID列表"
  2859. )), 400
  2860. # 获取可选参数
  2861. delete_database_records = get_request_parameter('delete_database_records') or False
  2862. continue_on_error = get_request_parameter('continue_on_error')
  2863. if continue_on_error is None:
  2864. continue_on_error = True
  2865. # 执行批量删除操作
  2866. deleted_tasks = []
  2867. failed_tasks = []
  2868. total_size_freed = 0
  2869. for task_id in task_ids:
  2870. result = delete_task_directory_simple(task_id, delete_database_records)
  2871. if result["success"]:
  2872. deleted_tasks.append(result)
  2873. # 累计释放的空间大小(这里简化处理,实际应该解析size字符串)
  2874. else:
  2875. failed_tasks.append({
  2876. "task_id": task_id,
  2877. "error": result["error"],
  2878. "error_code": result.get("error_code", "UNKNOWN")
  2879. })
  2880. if not continue_on_error:
  2881. break
  2882. # 构建响应
  2883. summary = {
  2884. "total_requested": len(task_ids),
  2885. "successfully_deleted": len(deleted_tasks),
  2886. "failed": len(failed_tasks)
  2887. }
  2888. batch_result = {
  2889. "deleted_tasks": deleted_tasks,
  2890. "failed_tasks": failed_tasks,
  2891. "summary": summary,
  2892. "deleted_at": datetime.now().isoformat()
  2893. }
  2894. # 构建智能响应消息
  2895. if len(task_ids) == 1:
  2896. # 单个删除:使用具体的操作消息
  2897. if summary["failed"] == 0:
  2898. # 从deleted_tasks中获取具体的操作消息
  2899. operation_msg = deleted_tasks[0].get('operation_message', '任务处理完成')
  2900. message = operation_msg
  2901. else:
  2902. # 从failed_tasks中获取错误消息
  2903. error_msg = failed_tasks[0].get('error', '删除失败')
  2904. message = f"任务删除失败: {error_msg}"
  2905. else:
  2906. # 批量删除:统计各种操作结果
  2907. directory_deleted_count = sum(1 for task in deleted_tasks if task.get('directory_deleted', False))
  2908. directory_not_exist_count = sum(1 for task in deleted_tasks if not task.get('directory_deleted', False))
  2909. if summary["failed"] == 0:
  2910. # 全部成功
  2911. if directory_deleted_count > 0 and directory_not_exist_count > 0:
  2912. message = f"批量操作完成:{directory_deleted_count}个目录已删除,{directory_not_exist_count}个目录不存在"
  2913. elif directory_deleted_count > 0:
  2914. message = f"批量删除完成:成功删除{directory_deleted_count}个目录"
  2915. elif directory_not_exist_count > 0:
  2916. message = f"批量操作完成:{directory_not_exist_count}个目录不存在,无需删除"
  2917. else:
  2918. message = "批量操作完成"
  2919. elif summary["successfully_deleted"] == 0:
  2920. message = f"批量删除失败:{summary['failed']}个任务处理失败"
  2921. else:
  2922. message = f"批量删除部分完成:成功{summary['successfully_deleted']}个,失败{summary['failed']}个"
  2923. return jsonify(success_response(
  2924. response_text=message,
  2925. data=batch_result
  2926. )), 200
  2927. except Exception as e:
  2928. logger.error(f"删除任务失败: 错误: {str(e)}")
  2929. return jsonify(internal_error_response(
  2930. response_text="删除任务失败,请稍后重试"
  2931. )), 500
  2932. @app.route('/api/v0/data_pipeline/tasks/<task_id>/logs/query', methods=['POST'])
  2933. def query_data_pipeline_task_logs(task_id):
  2934. """
  2935. 高级查询数据管道任务日志(从 citu_app.py 迁移)
  2936. 支持复杂筛选、排序、分页功能
  2937. 请求体:
  2938. {
  2939. "page": 1, // 页码,必须大于0,默认1
  2940. "page_size": 50, // 每页大小,1-500之间,默认50
  2941. "level": "ERROR", // 可选,日志级别筛选:"DEBUG"|"INFO"|"WARNING"|"ERROR"|"CRITICAL"
  2942. "start_time": "2025-01-01 00:00:00", // 可选,开始时间范围 (YYYY-MM-DD HH:MM:SS)
  2943. "end_time": "2025-01-02 23:59:59", // 可选,结束时间范围 (YYYY-MM-DD HH:MM:SS)
  2944. "keyword": "failed", // 可选,关键字搜索(消息内容模糊匹配)
  2945. "logger_name": "DDLGenerator", // 可选,日志记录器名称精确匹配
  2946. "step_name": "ddl_generation", // 可选,执行步骤名称精确匹配
  2947. "sort_by": "timestamp", // 可选,排序字段:"timestamp"|"level"|"logger"|"step"|"line_number",默认"timestamp"
  2948. "sort_order": "desc" // 可选,排序方向:"asc"|"desc",默认"desc"
  2949. }
  2950. """
  2951. try:
  2952. # 验证任务是否存在
  2953. manager = get_data_pipeline_manager()
  2954. task_info = manager.get_task_status(task_id)
  2955. if not task_info:
  2956. return jsonify(not_found_response(
  2957. response_text=f"任务不存在: {task_id}"
  2958. )), 404
  2959. # 解析请求数据
  2960. request_data = request.get_json() or {}
  2961. # 参数验证
  2962. def _is_valid_time_format(time_str):
  2963. """验证时间格式是否有效"""
  2964. if not time_str:
  2965. return True
  2966. # 支持的时间格式
  2967. time_formats = [
  2968. '%Y-%m-%d %H:%M:%S', # 2025-01-01 00:00:00
  2969. '%Y-%m-%d', # 2025-01-01
  2970. '%Y-%m-%dT%H:%M:%S', # 2025-01-01T00:00:00
  2971. '%Y-%m-%dT%H:%M:%S.%f', # 2025-01-01T00:00:00.123456
  2972. ]
  2973. for fmt in time_formats:
  2974. try:
  2975. from datetime import datetime
  2976. datetime.strptime(time_str, fmt)
  2977. return True
  2978. except ValueError:
  2979. continue
  2980. return False
  2981. # 提取和验证参数
  2982. page = request_data.get('page', 1)
  2983. page_size = request_data.get('page_size', 50)
  2984. level = request_data.get('level')
  2985. start_time = request_data.get('start_time')
  2986. end_time = request_data.get('end_time')
  2987. keyword = request_data.get('keyword')
  2988. logger_name = request_data.get('logger_name')
  2989. step_name = request_data.get('step_name')
  2990. sort_by = request_data.get('sort_by', 'timestamp')
  2991. sort_order = request_data.get('sort_order', 'desc')
  2992. # 参数验证
  2993. if not isinstance(page, int) or page < 1:
  2994. return jsonify(bad_request_response(
  2995. response_text="页码必须是大于0的整数"
  2996. )), 400
  2997. if not isinstance(page_size, int) or page_size < 1 or page_size > 500:
  2998. return jsonify(bad_request_response(
  2999. response_text="每页大小必须是1-500之间的整数"
  3000. )), 400
  3001. # 验证日志级别
  3002. if level and level.upper() not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
  3003. return jsonify(bad_request_response(
  3004. response_text="日志级别必须是DEBUG、INFO、WARNING、ERROR、CRITICAL之一"
  3005. )), 400
  3006. # 验证时间格式
  3007. if not _is_valid_time_format(start_time):
  3008. return jsonify(bad_request_response(
  3009. response_text="开始时间格式无效,支持格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD"
  3010. )), 400
  3011. if not _is_valid_time_format(end_time):
  3012. return jsonify(bad_request_response(
  3013. response_text="结束时间格式无效,支持格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD"
  3014. )), 400
  3015. # 验证关键字长度
  3016. if keyword and len(keyword) > 200:
  3017. return jsonify(bad_request_response(
  3018. response_text="关键字长度不能超过200个字符"
  3019. )), 400
  3020. # 验证排序字段
  3021. allowed_sort_fields = ['timestamp', 'level', 'logger', 'step', 'line_number']
  3022. if sort_by not in allowed_sort_fields:
  3023. return jsonify(bad_request_response(
  3024. response_text=f"排序字段必须是以下之一: {', '.join(allowed_sort_fields)}"
  3025. )), 400
  3026. # 验证排序方向
  3027. if sort_order.lower() not in ['asc', 'desc']:
  3028. return jsonify(bad_request_response(
  3029. response_text="排序方向必须是asc或desc"
  3030. )), 400
  3031. # 创建工作流执行器并查询日志
  3032. from data_pipeline.api.simple_workflow import SimpleWorkflowExecutor
  3033. executor = SimpleWorkflowExecutor(task_id)
  3034. try:
  3035. result = executor.query_logs_advanced(
  3036. page=page,
  3037. page_size=page_size,
  3038. level=level,
  3039. start_time=start_time,
  3040. end_time=end_time,
  3041. keyword=keyword,
  3042. logger_name=logger_name,
  3043. step_name=step_name,
  3044. sort_by=sort_by,
  3045. sort_order=sort_order
  3046. )
  3047. return jsonify(success_response(
  3048. response_text="查询任务日志成功",
  3049. data=result
  3050. ))
  3051. finally:
  3052. executor.cleanup()
  3053. except Exception as e:
  3054. logger.error(f"查询数据管道任务日志失败: {str(e)}")
  3055. return jsonify(internal_error_response(
  3056. response_text="查询任务日志失败,请稍后重试"
  3057. )), 500