citu_agent.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. # agent/citu_agent.py
  2. from typing import Dict, Any, Literal
  3. from langgraph.graph import StateGraph, END
  4. from langchain.agents import AgentExecutor, create_openai_tools_agent
  5. from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
  6. from langchain_core.messages import SystemMessage, HumanMessage
  7. from agent.state import AgentState
  8. from agent.classifier import QuestionClassifier
  9. from agent.tools import TOOLS, generate_sql, execute_sql, generate_summary, general_chat
  10. from agent.utils import get_compatible_llm
  11. class CituLangGraphAgent:
  12. """Citu LangGraph智能助手主类 - 使用@tool装饰器 + Agent工具调用"""
  13. def __init__(self):
  14. # 加载配置
  15. try:
  16. from agent.config import get_current_config, get_nested_config
  17. self.config = get_current_config()
  18. print("[CITU_AGENT] 加载Agent配置完成")
  19. except ImportError:
  20. self.config = {}
  21. print("[CITU_AGENT] 配置文件不可用,使用默认配置")
  22. self.classifier = QuestionClassifier()
  23. self.tools = TOOLS
  24. self.llm = get_compatible_llm()
  25. # 注意:现在使用直接工具调用模式,不再需要预创建Agent执行器
  26. print("[CITU_AGENT] 使用直接工具调用模式")
  27. self.workflow = self._create_workflow()
  28. print("[CITU_AGENT] LangGraph Agent with Direct Tools初始化完成")
  29. def _create_workflow(self) -> StateGraph:
  30. """创建LangGraph工作流"""
  31. workflow = StateGraph(AgentState)
  32. # 添加节点
  33. workflow.add_node("classify_question", self._classify_question_node)
  34. workflow.add_node("agent_chat", self._agent_chat_node)
  35. workflow.add_node("agent_database", self._agent_database_node)
  36. workflow.add_node("format_response", self._format_response_node)
  37. # 设置入口点
  38. workflow.set_entry_point("classify_question")
  39. # 添加条件边:分类后的路由
  40. # 完全信任QuestionClassifier的决策,不再进行二次判断
  41. workflow.add_conditional_edges(
  42. "classify_question",
  43. self._route_after_classification,
  44. {
  45. "DATABASE": "agent_database",
  46. "CHAT": "agent_chat" # CHAT分支处理所有非DATABASE的情况(包括UNCERTAIN)
  47. }
  48. )
  49. # 添加边
  50. workflow.add_edge("agent_chat", "format_response")
  51. workflow.add_edge("agent_database", "format_response")
  52. workflow.add_edge("format_response", END)
  53. return workflow.compile()
  54. def _classify_question_node(self, state: AgentState) -> AgentState:
  55. """问题分类节点"""
  56. try:
  57. print(f"[CLASSIFY_NODE] 开始分类问题: {state['question']}")
  58. classification_result = self.classifier.classify(state["question"])
  59. # 更新状态
  60. state["question_type"] = classification_result.question_type
  61. state["classification_confidence"] = classification_result.confidence
  62. state["classification_reason"] = classification_result.reason
  63. state["classification_method"] = classification_result.method
  64. state["current_step"] = "classified"
  65. state["execution_path"].append("classify")
  66. print(f"[CLASSIFY_NODE] 分类结果: {classification_result.question_type}, 置信度: {classification_result.confidence}")
  67. return state
  68. except Exception as e:
  69. print(f"[ERROR] 问题分类异常: {str(e)}")
  70. state["error"] = f"问题分类失败: {str(e)}"
  71. state["error_code"] = 500
  72. state["execution_path"].append("classify_error")
  73. return state
  74. def _agent_database_node(self, state: AgentState) -> AgentState:
  75. """数据库Agent节点 - 直接工具调用模式"""
  76. try:
  77. print(f"[DATABASE_AGENT] 开始处理数据库查询: {state['question']}")
  78. question = state["question"]
  79. # 步骤1:生成SQL
  80. print(f"[DATABASE_AGENT] 步骤1:生成SQL")
  81. sql_result = generate_sql.invoke({"question": question, "allow_llm_to_see_data": True})
  82. if not sql_result.get("success"):
  83. print(f"[DATABASE_AGENT] SQL生成失败: {sql_result.get('error')}")
  84. state["error"] = sql_result.get("error", "SQL生成失败")
  85. state["error_code"] = 500
  86. state["current_step"] = "database_error"
  87. state["execution_path"].append("agent_database_error")
  88. return state
  89. sql = sql_result.get("sql")
  90. state["sql"] = sql
  91. print(f"[DATABASE_AGENT] SQL生成成功: {sql}")
  92. # 步骤1.5:检查是否为解释性响应而非SQL
  93. error_type = sql_result.get("error_type")
  94. if error_type == "llm_explanation":
  95. # LLM返回了解释性文本,直接作为最终答案
  96. explanation = sql_result.get("error", "")
  97. state["summary"] = explanation + " 请尝试提问其它问题。"
  98. state["current_step"] = "database_completed"
  99. state["execution_path"].append("agent_database")
  100. print(f"[DATABASE_AGENT] 返回LLM解释性答案: {explanation}")
  101. return state
  102. # 额外验证:检查SQL格式(防止工具误判)
  103. from agent.utils import _is_valid_sql_format
  104. if not _is_valid_sql_format(sql):
  105. # 内容看起来不是SQL,当作解释性响应处理
  106. state["summary"] = sql + " 请尝试提问其它问题。"
  107. state["current_step"] = "database_completed"
  108. state["execution_path"].append("agent_database")
  109. print(f"[DATABASE_AGENT] 内容不是有效SQL,当作解释返回: {sql}")
  110. return state
  111. # 步骤2:执行SQL
  112. print(f"[DATABASE_AGENT] 步骤2:执行SQL")
  113. execute_result = execute_sql.invoke({"sql": sql, "max_rows": 200})
  114. if not execute_result.get("success"):
  115. print(f"[DATABASE_AGENT] SQL执行失败: {execute_result.get('error')}")
  116. state["error"] = execute_result.get("error", "SQL执行失败")
  117. state["error_code"] = 500
  118. state["current_step"] = "database_error"
  119. state["execution_path"].append("agent_database_error")
  120. return state
  121. data_result = execute_result.get("data_result")
  122. state["data_result"] = data_result
  123. print(f"[DATABASE_AGENT] SQL执行成功,返回 {data_result.get('row_count', 0)} 行数据")
  124. # 步骤3:生成摘要
  125. print(f"[DATABASE_AGENT] 步骤3:生成摘要")
  126. summary_result = generate_summary.invoke({
  127. "question": question,
  128. "data_result": data_result,
  129. "sql": sql
  130. })
  131. if not summary_result.get("success"):
  132. print(f"[DATABASE_AGENT] 摘要生成失败: {summary_result.get('message')}")
  133. # 摘要生成失败不是致命错误,使用默认摘要
  134. state["summary"] = f"查询执行完成,共返回 {data_result.get('row_count', 0)} 条记录。"
  135. else:
  136. state["summary"] = summary_result.get("summary")
  137. print(f"[DATABASE_AGENT] 摘要生成成功")
  138. state["current_step"] = "database_completed"
  139. state["execution_path"].append("agent_database")
  140. print(f"[DATABASE_AGENT] 数据库查询完成")
  141. return state
  142. except Exception as e:
  143. print(f"[ERROR] 数据库Agent异常: {str(e)}")
  144. import traceback
  145. print(f"[ERROR] 详细错误信息: {traceback.format_exc()}")
  146. state["error"] = f"数据库查询失败: {str(e)}"
  147. state["error_code"] = 500
  148. state["current_step"] = "database_error"
  149. state["execution_path"].append("agent_database_error")
  150. return state
  151. def _agent_chat_node(self, state: AgentState) -> AgentState:
  152. """聊天Agent节点 - 直接工具调用模式"""
  153. try:
  154. print(f"[CHAT_AGENT] 开始处理聊天: {state['question']}")
  155. question = state["question"]
  156. # 构建上下文
  157. enable_context_injection = self.config.get("chat_agent", {}).get("enable_context_injection", True)
  158. context = None
  159. if enable_context_injection and state.get("classification_reason"):
  160. context = f"分类原因: {state['classification_reason']}"
  161. # 直接调用general_chat工具
  162. print(f"[CHAT_AGENT] 调用general_chat工具")
  163. chat_result = general_chat.invoke({
  164. "question": question,
  165. "context": context
  166. })
  167. if chat_result.get("success"):
  168. state["chat_response"] = chat_result.get("response", "")
  169. print(f"[CHAT_AGENT] 聊天处理成功")
  170. else:
  171. # 处理失败,使用备用响应
  172. state["chat_response"] = chat_result.get("response", "抱歉,我暂时无法处理您的问题。请稍后再试。")
  173. print(f"[CHAT_AGENT] 聊天处理失败,使用备用响应: {chat_result.get('error')}")
  174. state["current_step"] = "chat_completed"
  175. state["execution_path"].append("agent_chat")
  176. print(f"[CHAT_AGENT] 聊天处理完成")
  177. return state
  178. except Exception as e:
  179. print(f"[ERROR] 聊天Agent异常: {str(e)}")
  180. import traceback
  181. print(f"[ERROR] 详细错误信息: {traceback.format_exc()}")
  182. state["chat_response"] = "抱歉,我暂时无法处理您的问题。请稍后再试,或者尝试询问数据相关的问题。"
  183. state["current_step"] = "chat_error"
  184. state["execution_path"].append("agent_chat_error")
  185. return state
  186. def _format_response_node(self, state: AgentState) -> AgentState:
  187. """格式化最终响应节点"""
  188. try:
  189. print(f"[FORMAT_NODE] 开始格式化响应,问题类型: {state['question_type']}")
  190. state["current_step"] = "completed"
  191. state["execution_path"].append("format_response")
  192. # 根据问题类型和执行状态格式化响应
  193. if state.get("error"):
  194. # 有错误的情况
  195. state["final_response"] = {
  196. "success": False,
  197. "error": state["error"],
  198. "error_code": state.get("error_code", 500),
  199. "question_type": state["question_type"],
  200. "execution_path": state["execution_path"],
  201. "classification_info": {
  202. "confidence": state.get("classification_confidence", 0),
  203. "reason": state.get("classification_reason", ""),
  204. "method": state.get("classification_method", "")
  205. }
  206. }
  207. elif state["question_type"] == "DATABASE":
  208. # 数据库查询类型
  209. if state.get("summary"):
  210. # 有摘要的情况(包括解释性响应和完整查询结果)
  211. state["final_response"] = {
  212. "success": True,
  213. "response": state["summary"],
  214. "type": "DATABASE",
  215. "sql": state.get("sql"),
  216. "data_result": state.get("data_result"), # 可能为None(解释性响应)
  217. "summary": state["summary"],
  218. "execution_path": state["execution_path"],
  219. "classification_info": {
  220. "confidence": state["classification_confidence"],
  221. "reason": state["classification_reason"],
  222. "method": state["classification_method"]
  223. }
  224. }
  225. else:
  226. # 数据库查询失败,没有任何结果
  227. state["final_response"] = {
  228. "success": False,
  229. "error": state.get("error", "数据库查询未完成"),
  230. "type": "DATABASE",
  231. "sql": state.get("sql"),
  232. "execution_path": state["execution_path"]
  233. }
  234. else:
  235. # 聊天类型
  236. state["final_response"] = {
  237. "success": True,
  238. "response": state.get("chat_response", ""),
  239. "type": "CHAT",
  240. "execution_path": state["execution_path"],
  241. "classification_info": {
  242. "confidence": state["classification_confidence"],
  243. "reason": state["classification_reason"],
  244. "method": state["classification_method"]
  245. }
  246. }
  247. print(f"[FORMAT_NODE] 响应格式化完成")
  248. return state
  249. except Exception as e:
  250. print(f"[ERROR] 响应格式化异常: {str(e)}")
  251. state["final_response"] = {
  252. "success": False,
  253. "error": f"响应格式化异常: {str(e)}",
  254. "error_code": 500,
  255. "execution_path": state["execution_path"]
  256. }
  257. return state
  258. def _route_after_classification(self, state: AgentState) -> Literal["DATABASE", "CHAT"]:
  259. """
  260. 分类后的路由决策
  261. 完全信任QuestionClassifier的决策:
  262. - DATABASE类型 → 数据库Agent
  263. - CHAT和UNCERTAIN类型 → 聊天Agent
  264. 这样避免了双重决策的冲突,所有分类逻辑都集中在QuestionClassifier中
  265. """
  266. question_type = state["question_type"]
  267. confidence = state["classification_confidence"]
  268. print(f"[ROUTE] 分类路由: {question_type}, 置信度: {confidence} (完全信任分类器决策)")
  269. if question_type == "DATABASE":
  270. return "DATABASE"
  271. else:
  272. # 将 "CHAT" 和 "UNCERTAIN" 类型都路由到聊天流程
  273. # 聊天Agent可以处理不确定的情况,并在必要时引导用户提供更多信息
  274. return "CHAT"
  275. def process_question(self, question: str, session_id: str = None) -> Dict[str, Any]:
  276. """
  277. 统一的问题处理入口
  278. Args:
  279. question: 用户问题
  280. session_id: 会话ID
  281. Returns:
  282. Dict包含完整的处理结果
  283. """
  284. try:
  285. print(f"[CITU_AGENT] 开始处理问题: {question}")
  286. # 初始化状态
  287. initial_state = self._create_initial_state(question, session_id)
  288. # 执行工作流
  289. final_state = self.workflow.invoke(
  290. initial_state,
  291. config={
  292. "configurable": {"session_id": session_id}
  293. } if session_id else None
  294. )
  295. # 提取最终结果
  296. result = final_state["final_response"]
  297. print(f"[CITU_AGENT] 问题处理完成: {result.get('success', False)}")
  298. return result
  299. except Exception as e:
  300. print(f"[ERROR] Agent执行异常: {str(e)}")
  301. return {
  302. "success": False,
  303. "error": f"Agent系统异常: {str(e)}",
  304. "error_code": 500,
  305. "execution_path": ["error"]
  306. }
  307. def _create_initial_state(self, question: str, session_id: str = None) -> AgentState:
  308. """创建初始状态"""
  309. return AgentState(
  310. # 输入信息
  311. question=question,
  312. session_id=session_id,
  313. # 分类结果
  314. question_type="",
  315. classification_confidence=0.0,
  316. classification_reason="",
  317. classification_method="",
  318. # 数据库查询流程状态
  319. sql=None,
  320. sql_generation_attempts=0,
  321. data_result=None,
  322. summary=None,
  323. # 聊天响应
  324. chat_response=None,
  325. # 最终输出
  326. final_response={},
  327. # 错误处理
  328. error=None,
  329. error_code=None,
  330. # 流程控制
  331. current_step="start",
  332. execution_path=[],
  333. retry_count=0,
  334. max_retries=2,
  335. # 调试信息
  336. debug_info={}
  337. )
  338. def health_check(self) -> Dict[str, Any]:
  339. """健康检查"""
  340. try:
  341. # 从配置获取健康检查参数
  342. from agent.config import get_nested_config
  343. test_question = get_nested_config(self.config, "health_check.test_question", "你好")
  344. enable_full_test = get_nested_config(self.config, "health_check.enable_full_test", True)
  345. if enable_full_test:
  346. # 完整流程测试
  347. test_result = self.process_question(test_question, "health_check")
  348. return {
  349. "status": "healthy" if test_result.get("success") else "degraded",
  350. "test_result": test_result.get("success", False),
  351. "workflow_compiled": self.workflow is not None,
  352. "tools_count": len(self.tools),
  353. "agent_reuse_enabled": False,
  354. "message": "Agent健康检查完成"
  355. }
  356. else:
  357. # 简单检查
  358. return {
  359. "status": "healthy",
  360. "test_result": True,
  361. "workflow_compiled": self.workflow is not None,
  362. "tools_count": len(self.tools),
  363. "agent_reuse_enabled": False,
  364. "message": "Agent简单健康检查完成"
  365. }
  366. except Exception as e:
  367. return {
  368. "status": "unhealthy",
  369. "error": str(e),
  370. "workflow_compiled": self.workflow is not None,
  371. "tools_count": len(self.tools) if hasattr(self, 'tools') else 0,
  372. "agent_reuse_enabled": False,
  373. "message": "Agent健康检查失败"
  374. }