init
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# Agent 模块
|
||||
|
||||
## 目录说明
|
||||
|
||||
`agent` 负责定义 Agent 运行时状态、节点逻辑与具体代理类型。
|
||||
|
||||
```
|
||||
agent/
|
||||
├── agents/ # 具体代理实现(ConversationAgent / ToolAgent)
|
||||
├── core/ # 状态图与共享节点
|
||||
├── utils.py # Agent 通用工具函数
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 核心能力
|
||||
|
||||
- 统一的 SQL 工作流编排
|
||||
- 多轮对话上下文维护
|
||||
- 节点级状态推进与错误收集
|
||||
|
||||
## 关键流程
|
||||
|
||||
共享 SQL 主链路:
|
||||
|
||||
`process_input -> normalize_input -> classify_query_mode -> match_table -> load_sql_prompt -> build_sql_plan -> generate_sql -> execute_sql -> generate_response`
|
||||
|
||||
对话代理会在前后追加:
|
||||
|
||||
`analyze_intent` 与 `update_context`
|
||||
|
||||
## 文件
|
||||
|
||||
- `core/base_agent.py`:共享图注册和节点编排
|
||||
- `core/nodes.py`:SQL 主流程节点实现
|
||||
- `core/state.py`:AgentState 与上下文同步
|
||||
- `agents/conversation.py`:会话型 Agent
|
||||
- `agents/tool.py`:工具调用型 Agent
|
||||
+6
-4
@@ -1,6 +1,8 @@
|
||||
from .state import AgentState
|
||||
from .graph import BaseAgent
|
||||
from .conversation import ConversationAgent
|
||||
from .tool import ToolAgent
|
||||
"""Agent 模块 - 提供智能代理功能"""
|
||||
|
||||
from agent.core.state import AgentState
|
||||
from agent.core.base_agent import BaseAgent
|
||||
from agent.agents.conversation import ConversationAgent
|
||||
from agent.agents.tool import ToolAgent
|
||||
|
||||
__all__ = ["AgentState", "BaseAgent", "ConversationAgent", "ToolAgent"]
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
"""代理实现模块 - 具体的代理实现"""
|
||||
|
||||
from .conversation import ConversationAgent
|
||||
from .tool import ToolAgent
|
||||
|
||||
__all__ = ["ConversationAgent", "ToolAgent"]
|
||||
@@ -0,0 +1,112 @@
|
||||
from typing import Dict, Any, Optional, cast
|
||||
from langchain_core.messages import HumanMessage, AIMessage
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
from agent.core.base_agent import BaseAgent
|
||||
from agent.core.state import AgentState
|
||||
from agent.core import nodes
|
||||
from config import CONVERSATION_MAX_HISTORY_MESSAGES
|
||||
|
||||
|
||||
class ConversationAgent(BaseAgent):
|
||||
"""处理多轮对话的代理"""
|
||||
|
||||
def __init__(self, model_section: Optional[str] = None):
|
||||
super().__init__(model_section)
|
||||
|
||||
def _build_graph(self) -> Any:
|
||||
"""构建对话专用图"""
|
||||
workflow = StateGraph(cast(Any, AgentState))
|
||||
|
||||
workflow.add_node("analyze_intent", cast(Any, self._analyze_intent))
|
||||
self._add_shared_sql_nodes(workflow)
|
||||
workflow.add_node("update_context", cast(Any, self._update_context))
|
||||
|
||||
workflow.add_edge("analyze_intent", "process_input")
|
||||
self._add_shared_sql_edges(workflow, start_node="process_input", end_node="generate_response")
|
||||
workflow.add_edge("generate_response", "update_context")
|
||||
workflow.add_edge("update_context", END)
|
||||
|
||||
workflow.set_entry_point("analyze_intent")
|
||||
|
||||
return workflow.compile()
|
||||
|
||||
def _analyze_intent(self, state: AgentState) -> AgentState:
|
||||
"""分析用户意图与对话上下文"""
|
||||
user_message = state.messages[-1] if state.messages else None
|
||||
|
||||
if user_message and isinstance(user_message, HumanMessage):
|
||||
content = user_message.content.lower()
|
||||
|
||||
if any(word in content for word in ["hello", "hi", "hey", "greetings"]):
|
||||
state.intent = "greeting"
|
||||
elif any(word in content for word in ["help", "assist", "support"]):
|
||||
state.intent = "help"
|
||||
elif any(token in content for token in ["sql", "查询", "统计", "汇总", "top", "eta", "so ", "soid", "订单"]):
|
||||
state.intent = "sql_query"
|
||||
elif "?" in content:
|
||||
state.intent = "question"
|
||||
else:
|
||||
state.intent = "general"
|
||||
|
||||
state.sync_context()
|
||||
state.set_current_step("intent_analyzed")
|
||||
return state
|
||||
|
||||
def _generate_response(self, state: AgentState) -> AgentState:
|
||||
"""优先返回 SQL 执行结果,其次返回生成 SQL,再回退到模型回复"""
|
||||
return nodes.generate_response(state, self.model)
|
||||
|
||||
def _update_context(self, state: AgentState) -> AgentState:
|
||||
"""更新当前会话的对话上下文与历史。"""
|
||||
conversation_history = list(state.context.get("conversation_history") or [])
|
||||
for message in state.messages:
|
||||
if isinstance(message, (HumanMessage, AIMessage)):
|
||||
conversation_history.append(message)
|
||||
|
||||
# 使用配置文件中的最大消息数限制
|
||||
max_messages = CONVERSATION_MAX_HISTORY_MESSAGES
|
||||
if len(conversation_history) > max_messages:
|
||||
conversation_history = conversation_history[-max_messages:]
|
||||
|
||||
state.context["conversation_history"] = conversation_history
|
||||
|
||||
state.sync_context()
|
||||
last_context = dict(state.context)
|
||||
last_context.pop("conversation_history", None)
|
||||
last_context.pop("last_context", None)
|
||||
state.context["last_context"] = last_context
|
||||
state.set_current_step("context_updated")
|
||||
return state
|
||||
|
||||
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||
"""运行对话,历史与上下文由调用方按会话维度传入。"""
|
||||
context = dict(kwargs)
|
||||
context["conversation_history"] = list(context.get("conversation_history") or [])
|
||||
context["last_context"] = dict(context.get("last_context") or {})
|
||||
initial_state = AgentState(
|
||||
messages=[HumanMessage(content=user_input)],
|
||||
context=context
|
||||
)
|
||||
|
||||
result = self.graph.invoke(initial_state)
|
||||
final_state = self._coerce_state(initial_state, result)
|
||||
|
||||
return {
|
||||
"messages": final_state.messages,
|
||||
"context": final_state.sync_context(),
|
||||
"conversation_history": list(final_state.context.get("conversation_history") or []),
|
||||
"final_step": final_state.current_step,
|
||||
}
|
||||
|
||||
def stream_run(self, user_input: str, **kwargs):
|
||||
"""流式运行对话;会话历史需由调用方显式传入。"""
|
||||
conversation_history = list(kwargs.get("conversation_history") or [])
|
||||
all_messages = conversation_history + [HumanMessage(content=user_input)]
|
||||
full_text = ""
|
||||
|
||||
for chunk in self.model.stream(all_messages):
|
||||
if hasattr(chunk, "content") and chunk.content:
|
||||
full_text += chunk.content
|
||||
yield chunk.content
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from typing import Dict, Any, List, Optional
|
||||
from langchain_core.messages import BaseMessage, HumanMessage
|
||||
from typing import Dict, Any, List, Optional, cast
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langchain_core.tools import BaseTool
|
||||
from langgraph.graph import StateGraph, END
|
||||
from langgraph.prebuilt import ToolNode
|
||||
|
||||
from .graph import BaseAgent
|
||||
from .state import AgentState
|
||||
from agent.core.base_agent import BaseAgent
|
||||
from agent.core.state import AgentState
|
||||
from tools.calculator import CalculatorTool
|
||||
from tools.web_search import WebSearchTool
|
||||
from tools.rest_api_tool import RestApiTool
|
||||
@@ -23,17 +23,16 @@ class ToolAgent(BaseAgent):
|
||||
self.tool_node = ToolNode(tools)
|
||||
super().__init__(model_section)
|
||||
|
||||
def _build_graph(self) -> StateGraph:
|
||||
def _build_graph(self) -> Any:
|
||||
"""构建可使用工具的图"""
|
||||
workflow = StateGraph(AgentState)
|
||||
workflow = StateGraph(cast(Any, AgentState))
|
||||
|
||||
workflow.add_node("normalize_input", self._normalize_input)
|
||||
workflow.add_node("generate_sql", self._generate_sql)
|
||||
workflow.add_node("agent", self._agent_node)
|
||||
workflow.add_node("tools", self.tool_node)
|
||||
self._add_shared_sql_nodes(workflow)
|
||||
workflow.add_node("agent", cast(Any, self._agent_node))
|
||||
workflow.add_node("tools", cast(Any, self.tool_node))
|
||||
|
||||
workflow.add_edge("normalize_input", "generate_sql")
|
||||
workflow.add_edge("generate_sql", "agent")
|
||||
workflow.set_entry_point("process_input")
|
||||
self._add_shared_sql_edges(workflow, start_node="process_input", end_node="agent")
|
||||
workflow.add_edge("tools", "agent")
|
||||
|
||||
workflow.add_conditional_edges(
|
||||
@@ -45,9 +44,7 @@ class ToolAgent(BaseAgent):
|
||||
}
|
||||
)
|
||||
|
||||
workflow.set_entry_point("normalize_input")
|
||||
|
||||
return workflow.compile()
|
||||
return cast(Any, workflow.compile())
|
||||
|
||||
def _agent_node(self, state: AgentState) -> AgentState:
|
||||
"""决定是否调用工具的代理节点"""
|
||||
@@ -67,11 +64,6 @@ class ToolAgent(BaseAgent):
|
||||
|
||||
return state
|
||||
|
||||
def _generate_sql(self, state: AgentState) -> AgentState:
|
||||
"""生成 SQL"""
|
||||
from . import nodes
|
||||
return nodes.generate_sql(state, self.model)
|
||||
|
||||
def _should_use_tools(self, state: AgentState) -> str:
|
||||
"""判断是否需要使用工具"""
|
||||
last_message = state.messages[-1]
|
||||
@@ -89,10 +81,11 @@ class ToolAgent(BaseAgent):
|
||||
)
|
||||
|
||||
result = self.graph.invoke(initial_state)
|
||||
final_state = self._coerce_state(initial_state, result)
|
||||
|
||||
return {
|
||||
"messages": result.get("messages", []),
|
||||
"context": result.get("context", {}),
|
||||
"messages": final_state.messages,
|
||||
"context": final_state.sync_context(),
|
||||
"tools_used": [tool.name for tool in self.tools],
|
||||
"final_step": result.get("current_step", "unknown")
|
||||
"final_step": final_state.current_step,
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
from typing import Dict, Any, List, Optional
|
||||
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
from .graph import BaseAgent
|
||||
from .state import AgentState
|
||||
|
||||
|
||||
class ConversationAgent(BaseAgent):
|
||||
"""处理多轮对话的代理"""
|
||||
|
||||
def __init__(self, model_section: Optional[str] = None):
|
||||
super().__init__(model_section)
|
||||
self.conversation_history: List[BaseMessage] = []
|
||||
|
||||
def _build_graph(self) -> StateGraph:
|
||||
"""构建对话专用图"""
|
||||
workflow = StateGraph(AgentState)
|
||||
|
||||
workflow.add_node("analyze_intent", self._analyze_intent)
|
||||
workflow.add_node("normalize_input", self._normalize_input)
|
||||
workflow.add_node("generate_sql", self._generate_sql)
|
||||
workflow.add_node("generate_response", self._generate_response)
|
||||
workflow.add_node("update_context", self._update_context)
|
||||
|
||||
workflow.add_edge("analyze_intent", "normalize_input")
|
||||
workflow.add_edge("normalize_input", "generate_sql")
|
||||
workflow.add_edge("generate_sql", "generate_response")
|
||||
workflow.add_edge("generate_response", "update_context")
|
||||
workflow.add_edge("update_context", END)
|
||||
|
||||
workflow.set_entry_point("analyze_intent")
|
||||
|
||||
return workflow.compile()
|
||||
|
||||
def _analyze_intent(self, state: AgentState) -> AgentState:
|
||||
"""分析用户意图与对话上下文"""
|
||||
user_message = state.messages[-1] if state.messages else None
|
||||
|
||||
if user_message and isinstance(user_message, HumanMessage):
|
||||
content = user_message.content.lower()
|
||||
|
||||
if any(word in content for word in ["hello", "hi", "hey", "greetings"]):
|
||||
state.context["intent"] = "greeting"
|
||||
elif any(word in content for word in ["help", "assist", "support"]):
|
||||
state.context["intent"] = "help"
|
||||
elif "?" in content:
|
||||
state.context["intent"] = "question"
|
||||
else:
|
||||
state.context["intent"] = "general"
|
||||
|
||||
state.current_step = "intent_analyzed"
|
||||
return state
|
||||
|
||||
def _generate_response(self, state: AgentState) -> AgentState:
|
||||
"""优先返回 SQL 执行结果,其次返回生成 SQL,再回退到模型回复"""
|
||||
from . import nodes
|
||||
state = nodes.generate_response(state, self.model)
|
||||
state.current_step = "response_generated"
|
||||
return state
|
||||
|
||||
def _generate_sql(self, state: AgentState) -> AgentState:
|
||||
"""生成 SQL"""
|
||||
from . import nodes
|
||||
return nodes.generate_sql(state, self.model)
|
||||
|
||||
def _update_context(self, state: AgentState) -> AgentState:
|
||||
"""更新对话上下文与历史"""
|
||||
for message in state.messages:
|
||||
if isinstance(message, (HumanMessage, AIMessage)):
|
||||
self.conversation_history.append(message)
|
||||
|
||||
if len(self.conversation_history) > 10:
|
||||
self.conversation_history = self.conversation_history[-10:]
|
||||
|
||||
state.current_step = "context_updated"
|
||||
return state
|
||||
|
||||
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||
"""运行对话并维护历史"""
|
||||
initial_state = AgentState(
|
||||
messages=[HumanMessage(content=user_input)],
|
||||
context=kwargs
|
||||
)
|
||||
|
||||
result = self.graph.invoke(initial_state)
|
||||
|
||||
return {
|
||||
"messages": result.get("messages", []),
|
||||
"context": result.get("context", {}),
|
||||
"conversation_history": self.conversation_history,
|
||||
"final_step": result.get("current_step", "unknown")
|
||||
}
|
||||
|
||||
def stream_run(self, user_input: str):
|
||||
"""流式运行对话并维护历史"""
|
||||
all_messages = self.conversation_history + [HumanMessage(content=user_input)]
|
||||
full_text = ""
|
||||
|
||||
for chunk in self.model.stream(all_messages):
|
||||
if hasattr(chunk, "content") and chunk.content:
|
||||
full_text += chunk.content
|
||||
yield chunk.content
|
||||
|
||||
self.conversation_history.append(HumanMessage(content=user_input))
|
||||
self.conversation_history.append(AIMessage(content=full_text))
|
||||
|
||||
if len(self.conversation_history) > 10:
|
||||
self.conversation_history = self.conversation_history[-10:]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""核心模块 - 提供代理的基础功能"""
|
||||
|
||||
from .base_agent import BaseAgent
|
||||
from .state import AgentState
|
||||
from .nodes import (
|
||||
process_input,
|
||||
normalize_input,
|
||||
classify_query_mode,
|
||||
match_table,
|
||||
load_sql_prompt,
|
||||
build_sql_plan,
|
||||
generate_sql,
|
||||
execute_sql,
|
||||
generate_response,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BaseAgent",
|
||||
"AgentState",
|
||||
"process_input",
|
||||
"normalize_input",
|
||||
"classify_query_mode",
|
||||
"match_table",
|
||||
"load_sql_prompt",
|
||||
"build_sql_plan",
|
||||
"generate_sql",
|
||||
"execute_sql",
|
||||
"generate_response",
|
||||
]
|
||||
@@ -0,0 +1,88 @@
|
||||
from typing import Any, Dict, Optional, cast
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
from services.core.llm_factory import create_chat_model
|
||||
from .state import AgentState
|
||||
from . import nodes
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
"""包含通用功能的基础代理类"""
|
||||
|
||||
def __init__(self, model_section: Optional[str] = None):
|
||||
self.model = create_chat_model(model_section)
|
||||
self.graph = self._build_graph()
|
||||
|
||||
def _build_graph(self) -> Any:
|
||||
"""构建代理状态图"""
|
||||
workflow = StateGraph(cast(Any, AgentState))
|
||||
|
||||
self._add_shared_sql_nodes(workflow)
|
||||
self._add_shared_sql_edges(workflow, start_node="process_input", end_node="generate_response")
|
||||
workflow.add_edge("generate_response", END)
|
||||
|
||||
workflow.set_entry_point("process_input")
|
||||
|
||||
return workflow.compile()
|
||||
|
||||
def _add_shared_sql_nodes(self, workflow: Any) -> None:
|
||||
"""注册 SQL 规划相关共享节点。"""
|
||||
workflow.add_node("process_input", cast(Any, nodes.process_input))
|
||||
workflow.add_node("normalize_input", cast(Any, self._normalize_input))
|
||||
workflow.add_node("classify_query_mode", cast(Any, nodes.classify_query_mode))
|
||||
workflow.add_node("match_table", cast(Any, nodes.match_table))
|
||||
workflow.add_node("load_sql_prompt", cast(Any, nodes.load_sql_prompt))
|
||||
workflow.add_node("build_sql_plan", cast(Any, nodes.build_sql_plan))
|
||||
workflow.add_node("generate_sql", cast(Any, self._generate_sql))
|
||||
workflow.add_node("execute_sql", cast(Any, nodes.execute_sql))
|
||||
workflow.add_node("check_empty_result", cast(Any, nodes.check_empty_result))
|
||||
workflow.add_node("generate_response", cast(Any, self._generate_response))
|
||||
|
||||
@staticmethod
|
||||
def _add_shared_sql_edges(workflow: Any, start_node: str, end_node: str) -> None:
|
||||
"""串联标准 SQL 工作流。"""
|
||||
workflow.add_edge(start_node, "normalize_input")
|
||||
workflow.add_edge("normalize_input", "classify_query_mode")
|
||||
workflow.add_edge("classify_query_mode", "match_table")
|
||||
workflow.add_edge("match_table", "load_sql_prompt")
|
||||
workflow.add_edge("load_sql_prompt", "build_sql_plan")
|
||||
workflow.add_edge("build_sql_plan", "generate_sql")
|
||||
workflow.add_edge("generate_sql", "execute_sql")
|
||||
workflow.add_edge("execute_sql", "check_empty_result")
|
||||
workflow.add_edge("check_empty_result", end_node)
|
||||
|
||||
def _generate_response(self, state: AgentState) -> AgentState:
|
||||
"""使用 LLM 生成回复"""
|
||||
return nodes.generate_response(state, self.model)
|
||||
|
||||
def _normalize_input(self, state: AgentState) -> AgentState:
|
||||
"""规范化用户输入"""
|
||||
return nodes.normalize_input(state, self.model)
|
||||
|
||||
def _generate_sql(self, state: AgentState) -> AgentState:
|
||||
"""生成 SQL"""
|
||||
return nodes.generate_sql(state, self.model)
|
||||
|
||||
@staticmethod
|
||||
def _coerce_state(initial_state: AgentState, result: Any) -> AgentState:
|
||||
"""兼容 LangGraph 返回 AgentState 或 dict。"""
|
||||
if isinstance(result, AgentState):
|
||||
return result
|
||||
return initial_state.apply_graph_result(result)
|
||||
|
||||
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||
"""运行代理并处理用户输入"""
|
||||
initial_state = AgentState(
|
||||
messages=[HumanMessage(content=user_input)],
|
||||
context=kwargs
|
||||
)
|
||||
|
||||
result = self.graph.invoke(initial_state)
|
||||
final_state = self._coerce_state(initial_state, result)
|
||||
|
||||
return {
|
||||
"messages": final_state.messages,
|
||||
"context": final_state.sync_context(),
|
||||
"final_step": final_state.current_step,
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
||||
|
||||
from .state import AgentState
|
||||
from config import Config
|
||||
from services.core.prompt_manager import get_prompt_manager
|
||||
from services.core.template_matcher import get_template_matcher
|
||||
from services.core.sql_prompt_manager import get_sql_prompt_manager
|
||||
from tools.sr_api_tool import SrApiQueryTool
|
||||
|
||||
|
||||
def _short(value, max_len: int = 500) -> str:
|
||||
text = str(value)
|
||||
return text if len(text) <= max_len else text[:max_len] + "..."
|
||||
|
||||
|
||||
def _trace(state: AgentState, label: str, value: Any | None = None, *, clip: bool = True) -> None:
|
||||
if not state.context.get("debug_node_trace"):
|
||||
return
|
||||
if value is None:
|
||||
print(label)
|
||||
else:
|
||||
print(label, _short(value) if clip else value)
|
||||
|
||||
|
||||
FOLLOW_UP_HINTS = (
|
||||
"那",
|
||||
"那么",
|
||||
"然后",
|
||||
"改成",
|
||||
"改为",
|
||||
"换成",
|
||||
"只看",
|
||||
"那如果",
|
||||
"how about",
|
||||
"what about",
|
||||
"same",
|
||||
"also",
|
||||
)
|
||||
|
||||
TOPN_RE = re.compile(r"\btop\s*(\d+)\b", re.IGNORECASE)
|
||||
TOPN_CN_RE = re.compile(r"前\s*(?:\d+|[一二三四五六七八九十百千万]+)")
|
||||
NUMBER_RE = re.compile(r"\b\d{8,14}\b")
|
||||
DATE_RE = re.compile(r"\b\d{4}-\d{2}-\d{2}\b|\b\d{1,2}/\d{1,2}(?:/\d{2,4})?\b")
|
||||
AGGREGATE_ENGLISH_RE = re.compile(
|
||||
r"\b(?:count|summary|summarize|aggregate|sum)\b|\bgroup\s+by\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
HISTORY_ENGLISH_RE = re.compile(r"\b(?:history|historical|changelog)\b", re.IGNORECASE)
|
||||
|
||||
|
||||
def _has_topn_hint(text: str) -> bool:
|
||||
lowered = (text or "").lower()
|
||||
if TOPN_RE.search(lowered):
|
||||
return True
|
||||
if TOPN_CN_RE.search(text or ""):
|
||||
return True
|
||||
return any(token in (text or "") for token in ["排名", "最高", "最大", "最小"])
|
||||
|
||||
|
||||
def _last_human_message(state: AgentState) -> HumanMessage | None:
|
||||
for message in reversed(state.messages):
|
||||
if isinstance(message, HumanMessage):
|
||||
return message
|
||||
return None
|
||||
|
||||
|
||||
def _looks_like_follow_up(text: str) -> bool:
|
||||
lowered = (text or "").strip().lower()
|
||||
return any(hint in lowered for hint in FOLLOW_UP_HINTS)
|
||||
|
||||
|
||||
def _detect_query_mode(text: str) -> str:
|
||||
lowered = (text or "").lower()
|
||||
if not lowered:
|
||||
return "detail"
|
||||
if HISTORY_ENGLISH_RE.search(lowered) or any(token in lowered for token in ["历史", "变更记录", "历史变更", "change record", "change log", "changes"]):
|
||||
return "history"
|
||||
if AGGREGATE_ENGLISH_RE.search(lowered) or any(token in lowered for token in ["聚合", "统计", "汇总", "计数", "数量", "多少", "几个", "分组", "求和", "总计", "合计"]):
|
||||
return "aggregate"
|
||||
if _has_topn_hint(text):
|
||||
return "topn"
|
||||
return "detail"
|
||||
|
||||
|
||||
def _extract_query_entities(text: str, prompt_data: Dict[str, Any] | None = None) -> Dict[str, Any]:
|
||||
lowered = (text or "").lower()
|
||||
entity_numbers = NUMBER_RE.findall(text or "")
|
||||
entity_dates = DATE_RE.findall(text or "")
|
||||
|
||||
top_n = None
|
||||
match = TOPN_RE.search(lowered)
|
||||
if match:
|
||||
try:
|
||||
top_n = int(match.group(1))
|
||||
except Exception:
|
||||
top_n = None
|
||||
elif _has_topn_hint(text):
|
||||
top_n = 10
|
||||
|
||||
sort_direction = "desc"
|
||||
if any(token in lowered for token in ["从小到大", "升序", "ascending", "asc"]):
|
||||
sort_direction = "asc"
|
||||
elif any(token in lowered for token in ["从大到小", "降序", "descending", "desc"]):
|
||||
sort_direction = "desc"
|
||||
|
||||
countries: List[str] = []
|
||||
regions: List[str] = []
|
||||
if prompt_data:
|
||||
additional_fields = (((prompt_data.get("field_mapping_reference") or {}).get("additional_fields") or {}))
|
||||
countries = list((((additional_fields.get("ship_to_country") or {}).get("values")) or []))
|
||||
regions = list((((additional_fields.get("region") or {}).get("values")) or []))
|
||||
|
||||
words = re.findall(r"\b[A-Z]{2,10}\b", text or "")
|
||||
matched_countries = [word for word in words if word in countries]
|
||||
matched_regions = [word for word in words if word in regions]
|
||||
|
||||
return {
|
||||
"numbers": entity_numbers,
|
||||
"dates": entity_dates,
|
||||
"top_n": top_n,
|
||||
"sort_direction": sort_direction,
|
||||
"country_codes": matched_countries,
|
||||
"regions": matched_regions,
|
||||
"mentions_eta_info": "eta信息" in lowered or "eta info" in lowered,
|
||||
"mentions_history": any(token in lowered for token in ["history", "historical", "changelog", "历史", "变更记录", "历史变更"]),
|
||||
}
|
||||
|
||||
|
||||
def _get_default_table_name() -> str | None:
|
||||
cfg = Config.get_section("ragflow")
|
||||
table_name = str(cfg.get("default_table_name") or "").strip()
|
||||
return table_name or None
|
||||
|
||||
|
||||
def _looks_like_json(text: str) -> bool:
|
||||
stripped = (text or "").strip()
|
||||
return stripped.startswith("{") or stripped.startswith("[")
|
||||
|
||||
|
||||
def _try_json_loads(value: Any) -> Any:
|
||||
if isinstance(value, (dict, list)):
|
||||
return value
|
||||
if isinstance(value, str) and _looks_like_json(value):
|
||||
try:
|
||||
return json.loads(value)
|
||||
except Exception:
|
||||
return value
|
||||
return value
|
||||
|
||||
|
||||
def _parse_sr_api_result(raw_result: Any) -> Any:
|
||||
parsed = _try_json_loads(raw_result)
|
||||
if isinstance(parsed, dict) and "text" in parsed:
|
||||
text_payload = _try_json_loads(parsed.get("text"))
|
||||
parsed = {**parsed, "text": text_payload}
|
||||
return parsed
|
||||
|
||||
|
||||
def _extract_result_rows(value: Any) -> list[Any] | None:
|
||||
if isinstance(value, list):
|
||||
return value
|
||||
|
||||
if isinstance(value, dict):
|
||||
for key in ("data", "rows", "records", "items", "list", "result", "values"):
|
||||
rows = value.get(key)
|
||||
if isinstance(rows, list):
|
||||
return rows
|
||||
|
||||
nested = value.get("text")
|
||||
if isinstance(nested, (dict, list)):
|
||||
return _extract_result_rows(nested)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _is_empty_sr_api_result(raw_result: Any) -> bool:
|
||||
parsed = _parse_sr_api_result(raw_result)
|
||||
|
||||
rows = _extract_result_rows(parsed)
|
||||
if rows is not None:
|
||||
return len(rows) == 0
|
||||
|
||||
if isinstance(parsed, dict):
|
||||
total = parsed.get("total")
|
||||
if isinstance(total, int):
|
||||
return total == 0
|
||||
|
||||
text_payload = parsed.get("text")
|
||||
if isinstance(text_payload, dict):
|
||||
total = text_payload.get("total")
|
||||
if isinstance(total, int):
|
||||
return total == 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _format_empty_result_response(query: str, llm_response: str) -> str:
|
||||
"""将空结果的 LLM 回复格式化为纯文本格式,与 endpoints._build_rich_answer_html 保持一致"""
|
||||
safe_query = html.escape((query or "").strip())
|
||||
safe_response = html.escape((llm_response or "").strip())
|
||||
|
||||
return (
|
||||
f"Question: {safe_query}\n"
|
||||
f"{safe_response}\n"
|
||||
f"Rows: 0"
|
||||
)
|
||||
|
||||
|
||||
def _default_normalizer_prompt() -> str:
|
||||
return (
|
||||
"You are a translation and normalization assistant. "
|
||||
"Convert the user's input to a clear, grammatically correct English sentence suitable for SQL intent. "
|
||||
"Preserve business identifiers, codes, country abbreviations, order numbers, and field aliases exactly when possible. "
|
||||
"Return only the final English sentence without extra explanations."
|
||||
)
|
||||
|
||||
|
||||
def process_input(state: AgentState) -> AgentState:
|
||||
"""处理用户输入"""
|
||||
_trace(state, "[process_input][in] messages=", _short(state.messages))
|
||||
last_message = _last_human_message(state)
|
||||
if last_message:
|
||||
state.original_input = str(last_message.content)
|
||||
state.context["is_follow_up"] = _looks_like_follow_up(state.original_input)
|
||||
state.set_current_step("processed")
|
||||
_trace(state, "[process_input][out] current_step=", state.current_step)
|
||||
return state
|
||||
|
||||
|
||||
def normalize_input(state: AgentState, model) -> AgentState:
|
||||
"""将用户输入规范化为保留业务标识的标准英文语句"""
|
||||
last_message = _last_human_message(state)
|
||||
if not last_message:
|
||||
return state
|
||||
|
||||
_trace(state, "[normalize_input][in] user_input=", _short(last_message.content))
|
||||
|
||||
prompt_manager = get_prompt_manager()
|
||||
normalizer_prompt = (
|
||||
prompt_manager.get("user", "english_normalizer")
|
||||
or prompt_manager.get("system", "english_normalizer")
|
||||
or _default_normalizer_prompt()
|
||||
)
|
||||
system_prompt = SystemMessage(content=normalizer_prompt)
|
||||
|
||||
try:
|
||||
response = model.invoke([system_prompt, HumanMessage(content=last_message.content)])
|
||||
normalized = response.content if hasattr(response, "content") else str(response)
|
||||
except Exception as exc:
|
||||
normalized = str(last_message.content)
|
||||
state.add_error(f"normalize_input_failed:{exc}")
|
||||
|
||||
state.original_input = str(last_message.content)
|
||||
state.normalized_input = normalized.strip() or str(last_message.content)
|
||||
state.sync_context()
|
||||
state.set_current_step("normalized")
|
||||
_trace(state, "[normalize_input][out] normalized=", _short(state.normalized_input))
|
||||
return state
|
||||
|
||||
|
||||
def classify_query_mode(state: AgentState) -> AgentState:
|
||||
"""识别查询模式:detail / aggregate / topn / history。"""
|
||||
text = "\n".join(filter(None, [state.original_input, state.normalized_input]))
|
||||
state.query_mode = _detect_query_mode(text)
|
||||
|
||||
if not state.intent:
|
||||
state.intent = "sql_query" if state.original_input else "general"
|
||||
|
||||
state.query_entities = _extract_query_entities(text)
|
||||
state.sync_context()
|
||||
state.set_current_step("query_mode_classified")
|
||||
_trace(state, "[classify_query_mode][out] query_mode=", state.query_mode)
|
||||
_trace(state, "[classify_query_mode][out] query_entities=", _short(state.query_entities))
|
||||
return state
|
||||
|
||||
|
||||
def match_table(state: AgentState) -> AgentState:
|
||||
"""根据规范化输入检索候选表,并在追问场景下回退到上一轮表或配置默认表。"""
|
||||
query = state.normalized_input or state.original_input
|
||||
if not query:
|
||||
return state
|
||||
|
||||
_trace(state, "[match_table][in] query=", _short(query))
|
||||
matcher = get_template_matcher()
|
||||
match_result = matcher.match(query)
|
||||
table_name = (match_result or {}).get("table_name")
|
||||
candidate_tables = list((match_result or {}).get("candidates") or [])
|
||||
|
||||
if not table_name and state.context.get("is_follow_up"):
|
||||
last_context = state.context.get("last_context") or {}
|
||||
fallback_table = last_context.get("table_name") or ((last_context.get("table_match") or {}).get("table_name"))
|
||||
if fallback_table:
|
||||
table_name = fallback_table
|
||||
candidate_tables = candidate_tables or [{"table_name": fallback_table, "source": "last_context"}]
|
||||
match_result = {
|
||||
"table_name": fallback_table,
|
||||
"candidates": candidate_tables,
|
||||
"raw": {"source": "last_context"},
|
||||
}
|
||||
state.context["table_match_fallback"] = "last_context"
|
||||
|
||||
if not table_name:
|
||||
default_table = _get_default_table_name()
|
||||
if default_table:
|
||||
table_name = default_table
|
||||
candidate_tables = candidate_tables or [{"table_name": default_table, "source": "config_default"}]
|
||||
match_result = {
|
||||
"table_name": default_table,
|
||||
"candidates": candidate_tables,
|
||||
"raw": {"source": "config_default"},
|
||||
}
|
||||
state.context["table_match_fallback"] = "config_default"
|
||||
state.context["default_table_name"] = default_table
|
||||
|
||||
state.table_match = dict(match_result or {})
|
||||
state.candidate_tables = candidate_tables
|
||||
state.table_name = table_name
|
||||
state.sync_context()
|
||||
state.set_current_step("table_matched")
|
||||
_trace(state, "[match_table][out] table_name=", state.table_name)
|
||||
return state
|
||||
|
||||
|
||||
def load_sql_prompt(state: AgentState) -> AgentState:
|
||||
"""加载目标表对应的 SQL prompt JSON。"""
|
||||
if not state.table_name:
|
||||
_trace(state, "[load_sql_prompt][skip] missing table_name")
|
||||
return state
|
||||
|
||||
prompt_manager = get_sql_prompt_manager()
|
||||
prompt_data, source = prompt_manager.get_prompt_with_source(state.table_name)
|
||||
if not prompt_data:
|
||||
state.add_error(f"sql_prompt_not_found:{state.table_name}")
|
||||
_trace(state, "[load_sql_prompt][skip] prompt not found for table=", state.table_name)
|
||||
return state
|
||||
|
||||
state.sql_prompt = prompt_data
|
||||
state.sql_prompt_source = source
|
||||
state.sync_context()
|
||||
state.set_current_step("sql_prompt_loaded")
|
||||
_trace(state, f"[load_sql_prompt][out] table_name={state.table_name} source={source}")
|
||||
return state
|
||||
|
||||
|
||||
def build_sql_plan(state: AgentState) -> AgentState:
|
||||
"""构建结构化 SQL 计划,为最终 SQL 生成提供显式上下文。"""
|
||||
prompt_data = state.sql_prompt or {}
|
||||
business_rules = (prompt_data.get("business_logic_rules") or {})
|
||||
data_model = (prompt_data.get("data_model_specification") or {})
|
||||
meta = (prompt_data.get("meta") or {})
|
||||
default_fields = ((data_model.get("mandatory_display_fields") or {}).get("default_fields")) or ""
|
||||
|
||||
text = "\n".join(filter(None, [state.original_input, state.normalized_input]))
|
||||
extracted = _extract_query_entities(text, prompt_data)
|
||||
if state.query_entities:
|
||||
extracted = {**state.query_entities, **{k: v for k, v in extracted.items() if v not in (None, [], {}, "")}}
|
||||
|
||||
state.query_entities = extracted
|
||||
state.sql_plan = {
|
||||
"intent": state.intent or "sql_query",
|
||||
"query_mode": state.query_mode or "detail",
|
||||
"selected_table": state.table_name,
|
||||
"candidate_tables": [item.get("table_name", item) for item in state.candidate_tables],
|
||||
"data_source": meta.get("data_source"),
|
||||
"domain": meta.get("domain"),
|
||||
"default_select_fields": default_fields,
|
||||
"default_filters": list(business_rules.get("default_filters") or []),
|
||||
"aggregate_rules": dict(business_rules.get("aggregate_rules") or {}),
|
||||
"top_n_rules": dict(business_rules.get("top_n_rules") or {}),
|
||||
"query_entities": extracted,
|
||||
"previous_context": {
|
||||
key: (state.context.get("last_context") or {}).get(key)
|
||||
for key in ("table_name", "query_mode", "final_sql", "sql_plan")
|
||||
if (state.context.get("last_context") or {}).get(key) is not None
|
||||
},
|
||||
}
|
||||
state.sync_context()
|
||||
state.set_current_step("sql_plan_built")
|
||||
_trace(state, "[build_sql_plan][out] sql_plan=", _short(state.sql_plan))
|
||||
return state
|
||||
|
||||
|
||||
def generate_sql(state: AgentState, model) -> AgentState:
|
||||
"""根据表 prompt + 结构化计划生成 SQL。"""
|
||||
if not state.table_name or not state.normalized_input:
|
||||
_trace(state, "[generate_sql][skip] missing table_name or normalized_input")
|
||||
return state
|
||||
|
||||
prompt_data = state.sql_prompt
|
||||
if not prompt_data:
|
||||
_trace(state, "[generate_sql][skip] missing sql_prompt")
|
||||
return state
|
||||
|
||||
_trace(state, "[generate_sql][in] table_name=", state.table_name)
|
||||
_trace(state, "[generate_sql][in] query_mode=", state.query_mode)
|
||||
|
||||
prompt_text = json.dumps(prompt_data, ensure_ascii=False, indent=2)
|
||||
plan_text = json.dumps(state.sql_plan or {}, ensure_ascii=False, indent=2)
|
||||
prompt_manager = get_prompt_manager()
|
||||
system_template = prompt_manager.get("system", "sql_mysql_select_only")
|
||||
system_content = system_template.format(table_prompt_json=prompt_text)
|
||||
user_content = (
|
||||
f"Original user question: {state.original_input}\n"
|
||||
f"Normalized user question: {state.normalized_input}\n"
|
||||
f"Detected query mode: {state.query_mode or 'detail'}\n"
|
||||
f"SQL planning context JSON:\n{plan_text}\n"
|
||||
"Generate the best SQL for the selected table and query mode. "
|
||||
"If the query mode is topn and the plan contains top_n, LIMIT is allowed and required. "
|
||||
"If update_date is used as a filter, do not add data_flag. "
|
||||
"Return only the final SQL."
|
||||
)
|
||||
response = model.invoke([SystemMessage(content=system_content), HumanMessage(content=user_content)])
|
||||
sql_text = response.content if hasattr(response, "content") else str(response)
|
||||
state.final_sql = sql_text.strip()
|
||||
state.sync_context()
|
||||
state.set_current_step("sql_generated")
|
||||
_trace(state, "[generate_sql][out] sql=", state.final_sql, clip=False)
|
||||
return state
|
||||
|
||||
|
||||
def execute_sql(state: AgentState) -> AgentState:
|
||||
"""在需要时执行生成后的 SQL。skip_sr_api=True 时跳过执行。"""
|
||||
if not state.final_sql:
|
||||
_trace(state, "[execute_sql][skip] missing final_sql")
|
||||
state.set_current_step("sql_execution_skipped")
|
||||
return state
|
||||
|
||||
if state.skip_sr_api:
|
||||
_trace(state, "[execute_sql][skip] skip_sr_api=true")
|
||||
state.set_current_step("sql_execution_skipped")
|
||||
return state
|
||||
|
||||
try:
|
||||
tool = SrApiQueryTool()
|
||||
state.sr_api_result = tool.run(json.dumps({"sql": state.final_sql}, ensure_ascii=False))
|
||||
_trace(state, "[execute_sql][out] sr_api_result=", _short(state.sr_api_result))
|
||||
except Exception as exc:
|
||||
state.add_error(f"sql_execution_failed:{exc}")
|
||||
_trace(state, "[execute_sql][error]", exc)
|
||||
state.sync_context()
|
||||
state.set_current_step("sql_executed")
|
||||
return state
|
||||
|
||||
|
||||
def check_empty_result(state: AgentState) -> AgentState:
|
||||
"""检查 SQL 执行结果是否为空,设置 is_empty_result 标记。"""
|
||||
sr_api_result = state.sr_api_result
|
||||
|
||||
if not sr_api_result:
|
||||
state.context["is_empty_result"] = None
|
||||
state.context["result_checked"] = False
|
||||
_trace(state, "[check_empty_result][skip] no sr_api_result")
|
||||
state.set_current_step("result_checked")
|
||||
return state
|
||||
|
||||
is_empty = _is_empty_sr_api_result(sr_api_result)
|
||||
state.context["is_empty_result"] = is_empty
|
||||
state.context["result_checked"] = True
|
||||
|
||||
if is_empty:
|
||||
_trace(state, "[check_empty_result][out] is_empty=True")
|
||||
else:
|
||||
result_rows = _extract_result_rows(sr_api_result)
|
||||
row_count = len(result_rows) if result_rows else 0
|
||||
state.context["result_row_count"] = row_count
|
||||
_trace(state, f"[check_empty_result][out] is_empty=False, row_count={row_count}")
|
||||
|
||||
state.sync_context()
|
||||
state.set_current_step("result_checked")
|
||||
return state
|
||||
|
||||
|
||||
def generate_response(state: AgentState, model) -> AgentState:
|
||||
"""使用 SQL 执行结果、SQL 本身或模型回退生成最终回复。"""
|
||||
_trace(state, "[generate_response][in] context_keys=", list((state.context or {}).keys()))
|
||||
|
||||
# 优先使用 context 中的 is_empty_result(由 check_empty_result 节点设置)
|
||||
is_empty_result = state.context.get("is_empty_result")
|
||||
sr_api_result = state.sr_api_result
|
||||
|
||||
# 如果有执行结果且标记为空
|
||||
if sr_api_result and is_empty_result is True:
|
||||
sql_plan_text = json.dumps(state.sql_plan or {}, ensure_ascii=False, indent=2)
|
||||
|
||||
fallback_system = SystemMessage(
|
||||
content=(
|
||||
"You are a friendly business query assistant. "
|
||||
"The query executed successfully but returned no data. "
|
||||
"Answer the user in a concise and helpful way. "
|
||||
"IMPORTANT RULES:\n"
|
||||
"1. DO NOT show any SQL statements, technical field names, or database terminology to the user\n"
|
||||
"2. Use business language that non-technical users can understand\n"
|
||||
"3. Clearly state that no matching data was found\n"
|
||||
"4. Provide specific suggestions about which conditions might be too restrictive\n"
|
||||
"5. Use the query context to suggest alternatives, but express them in plain language\n"
|
||||
"6. For example, say 'try removing the country filter' instead of 'remove ship_to_country condition'\n"
|
||||
"7. For example, say 'try searching all records instead of just the latest' instead of 'remove data_flag filter'"
|
||||
)
|
||||
)
|
||||
fallback_user = HumanMessage(
|
||||
content=(
|
||||
f"Original user question: {state.original_input}\n"
|
||||
f"Query mode: {state.query_mode or 'detail'}\n"
|
||||
f"SQL plan context (for your reference only, DO NOT show to user):\n{sql_plan_text}\n"
|
||||
"Please answer the user in plain business language without any SQL or technical terms."
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
response = model.invoke([fallback_system, fallback_user])
|
||||
llm_content = response.content if hasattr(response, "content") else str(response)
|
||||
state.messages.append(response)
|
||||
|
||||
formatted_html = _format_empty_result_response(
|
||||
state.original_input,
|
||||
llm_content
|
||||
)
|
||||
state.context["formatted_answer"] = formatted_html
|
||||
state.context["response_source"] = "model_empty_result_fallback"
|
||||
_trace(state, "[generate_response][out] source=model_empty_result_fallback")
|
||||
except Exception as exc:
|
||||
state.add_error(f"empty_result_fallback_failed:{exc}")
|
||||
fixed_content = "未查询到符合条件的数据,请尝试调整筛选条件后再查询。"
|
||||
state.messages.append(AIMessage(content=fixed_content))
|
||||
|
||||
formatted_html = _format_empty_result_response(
|
||||
state.original_input,
|
||||
fixed_content
|
||||
)
|
||||
state.context["formatted_answer"] = formatted_html
|
||||
state.context["response_source"] = "empty_result_fixed_fallback"
|
||||
_trace(state, "[generate_response][out] source=empty_result_fixed_fallback")
|
||||
|
||||
state.sync_context()
|
||||
state.set_current_step("response_generated")
|
||||
return state
|
||||
|
||||
# 有执行结果且不为空
|
||||
if sr_api_result:
|
||||
state.context["is_empty_result"] = False
|
||||
state.context["response_source"] = "sr_api_result"
|
||||
state.messages.append(AIMessage(content=str(sr_api_result)))
|
||||
_trace(state, "[generate_response][out] source=sr_api_result")
|
||||
state.sync_context()
|
||||
state.set_current_step("response_generated")
|
||||
return state
|
||||
|
||||
# 没有执行结果,返回 SQL(skip_sr_api=True 的情况)
|
||||
final_sql = state.final_sql
|
||||
if final_sql:
|
||||
state.context["response_source"] = "final_sql"
|
||||
state.messages.append(AIMessage(content=final_sql))
|
||||
_trace(state, "[generate_response][out] source=final_sql")
|
||||
state.sync_context()
|
||||
state.set_current_step("response_generated")
|
||||
return state
|
||||
|
||||
# 兜底:使用模型生成回复
|
||||
if state.messages:
|
||||
response = model.invoke(state.messages)
|
||||
state.messages.append(response)
|
||||
state.context["response_source"] = "model_invoke"
|
||||
_trace(state, "[generate_response][out] source=model_invoke")
|
||||
state.sync_context()
|
||||
state.set_current_step("response_generated")
|
||||
return state
|
||||
@@ -0,0 +1,124 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict, List, Optional
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentState:
|
||||
"""代理工作流的状态定义"""
|
||||
messages: List[BaseMessage] = field(default_factory=list)
|
||||
current_step: str = "start"
|
||||
context: Dict[str, Any] = field(default_factory=dict)
|
||||
intent: Optional[str] = None
|
||||
original_input: str = ""
|
||||
normalized_input: str = ""
|
||||
query_mode: str = ""
|
||||
query_entities: Dict[str, Any] = field(default_factory=dict)
|
||||
candidate_tables: List[Dict[str, Any]] = field(default_factory=list)
|
||||
table_match: Dict[str, Any] = field(default_factory=dict)
|
||||
table_name: Optional[str] = None
|
||||
sql_prompt: Dict[str, Any] = field(default_factory=dict)
|
||||
sql_prompt_source: str = ""
|
||||
sql_plan: Dict[str, Any] = field(default_factory=dict)
|
||||
final_sql: str = ""
|
||||
sr_api_result: Any = None
|
||||
skip_sr_api: bool = False
|
||||
validation_errors: List[str] = field(default_factory=list)
|
||||
errors: List[str] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.context = dict(self.context or {})
|
||||
self.messages = list(self.messages or [])
|
||||
self.intent = self.context.get("intent", self.intent)
|
||||
self.original_input = str(self.context.get("original_input") or self.original_input or "")
|
||||
self.normalized_input = str(self.context.get("normalized_input") or self.normalized_input or "")
|
||||
self.query_mode = str(self.context.get("query_mode") or self.query_mode or "")
|
||||
self.query_entities = dict(self.context.get("query_entities") or self.query_entities or {})
|
||||
self.candidate_tables = list(self.context.get("candidate_tables") or self.candidate_tables or [])
|
||||
self.table_match = dict(self.context.get("table_match") or self.table_match or {})
|
||||
self.table_name = self.context.get("table_name") or self.table_name or self.table_match.get("table_name")
|
||||
self.sql_prompt = dict(self.context.get("sql_prompt") or self.sql_prompt or {})
|
||||
self.sql_prompt_source = str(self.context.get("sql_prompt_source") or self.sql_prompt_source or "")
|
||||
self.sql_plan = dict(self.context.get("sql_plan") or self.sql_plan or {})
|
||||
self.final_sql = str(self.context.get("final_sql") or self.final_sql or "")
|
||||
self.sr_api_result = self.context.get("sr_api_result", self.sr_api_result)
|
||||
self.skip_sr_api = bool(self.context.get("skip_sr_api", self.skip_sr_api))
|
||||
self.validation_errors = list(self.context.get("validation_errors") or self.validation_errors or [])
|
||||
self.errors = list(self.context.get("errors") or self.errors or [])
|
||||
self.sync_context()
|
||||
|
||||
def sync_context(self) -> Dict[str, Any]:
|
||||
"""将显式状态字段回写到兼容 context。"""
|
||||
self.context["current_step"] = self.current_step
|
||||
self.context["skip_sr_api"] = self.skip_sr_api
|
||||
|
||||
optional_values = {
|
||||
"intent": self.intent,
|
||||
"original_input": self.original_input,
|
||||
"normalized_input": self.normalized_input,
|
||||
"query_mode": self.query_mode,
|
||||
"query_entities": self.query_entities,
|
||||
"candidate_tables": self.candidate_tables,
|
||||
"table_match": self.table_match,
|
||||
"table_name": self.table_name,
|
||||
"sql_prompt": self.sql_prompt,
|
||||
"sql_prompt_source": self.sql_prompt_source,
|
||||
"sql_plan": self.sql_plan,
|
||||
"final_sql": self.final_sql,
|
||||
"sr_api_result": self.sr_api_result,
|
||||
"validation_errors": self.validation_errors,
|
||||
"errors": self.errors,
|
||||
}
|
||||
|
||||
for key, value in optional_values.items():
|
||||
empty = value in (None, "", [], {})
|
||||
if empty:
|
||||
self.context.pop(key, None)
|
||||
else:
|
||||
self.context[key] = value
|
||||
return self.context
|
||||
|
||||
def set_current_step(self, step: str) -> None:
|
||||
self.current_step = step
|
||||
self.sync_context()
|
||||
|
||||
def add_error(self, message: str) -> None:
|
||||
if message and message not in self.errors:
|
||||
self.errors.append(message)
|
||||
self.sync_context()
|
||||
|
||||
def apply_graph_result(self, result: Any) -> "AgentState":
|
||||
"""兼容 LangGraph 返回 dict 或 AgentState 两种形式。"""
|
||||
if isinstance(result, AgentState):
|
||||
return result
|
||||
if isinstance(result, dict):
|
||||
self.messages = result.get("messages", self.messages)
|
||||
self.current_step = result.get("current_step", self.current_step)
|
||||
self.context.update(result.get("context", {}))
|
||||
self.intent = self.context.get("intent")
|
||||
self.original_input = str(self.context.get("original_input") or self.original_input)
|
||||
self.normalized_input = str(self.context.get("normalized_input") or self.normalized_input)
|
||||
self.query_mode = str(self.context.get("query_mode") or self.query_mode)
|
||||
self.query_entities = dict(self.context.get("query_entities") or self.query_entities)
|
||||
self.candidate_tables = list(self.context.get("candidate_tables") or self.candidate_tables)
|
||||
self.table_match = dict(self.context.get("table_match") or self.table_match)
|
||||
self.table_name = self.context.get("table_name") or self.table_name or self.table_match.get("table_name")
|
||||
self.sql_prompt = dict(self.context.get("sql_prompt") or self.sql_prompt)
|
||||
self.sql_prompt_source = str(self.context.get("sql_prompt_source") or self.sql_prompt_source)
|
||||
self.sql_plan = dict(self.context.get("sql_plan") or self.sql_plan)
|
||||
self.final_sql = str(self.context.get("final_sql") or self.final_sql)
|
||||
self.sr_api_result = self.context.get("sr_api_result", self.sr_api_result)
|
||||
self.skip_sr_api = bool(self.context.get("skip_sr_api", self.skip_sr_api))
|
||||
self.validation_errors = list(self.context.get("validation_errors") or self.validation_errors)
|
||||
self.errors = list(self.context.get("errors") or self.errors)
|
||||
self.sync_context()
|
||||
return self
|
||||
|
||||
def to_result(self) -> Dict[str, Any]:
|
||||
"""输出与现有 API 兼容的结果结构。"""
|
||||
self.sync_context()
|
||||
return {
|
||||
"messages": self.messages,
|
||||
"current_step": self.current_step,
|
||||
"context": self.context,
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from langchain_core.messages import HumanMessage
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
from services.llm_factory import create_chat_model
|
||||
from .state import AgentState
|
||||
from . import nodes
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
"""包含通用功能的基础代理类"""
|
||||
|
||||
def __init__(self, model_section: Optional[str] = None):
|
||||
self.model = create_chat_model(model_section)
|
||||
self.graph = self._build_graph()
|
||||
|
||||
def _build_graph(self) -> StateGraph:
|
||||
"""构建代理状态图"""
|
||||
workflow = StateGraph(AgentState)
|
||||
|
||||
workflow.add_node("process_input", nodes.process_input)
|
||||
workflow.add_node("normalize_input", self._normalize_input)
|
||||
workflow.add_node("generate_sql", self._generate_sql)
|
||||
workflow.add_node("generate_response", self._generate_response)
|
||||
|
||||
workflow.add_edge("process_input", "normalize_input")
|
||||
workflow.add_edge("normalize_input", "generate_sql")
|
||||
workflow.add_edge("generate_sql", "generate_response")
|
||||
workflow.add_edge("generate_response", END)
|
||||
|
||||
workflow.set_entry_point("process_input")
|
||||
|
||||
return workflow.compile()
|
||||
|
||||
def _generate_response(self, state: AgentState) -> AgentState:
|
||||
"""使用 LLM 生成回复"""
|
||||
return nodes.generate_response(state, self.model)
|
||||
|
||||
def _normalize_input(self, state: AgentState) -> AgentState:
|
||||
"""规范化用户输入"""
|
||||
return nodes.normalize_input(state, self.model)
|
||||
|
||||
def _generate_sql(self, state: AgentState) -> AgentState:
|
||||
"""生成 SQL"""
|
||||
return nodes.generate_sql(state, self.model)
|
||||
|
||||
def run(self, user_input: str, **kwargs) -> Dict[str, Any]:
|
||||
"""运行代理并处理用户输入"""
|
||||
initial_state = AgentState(
|
||||
messages=[HumanMessage(content=user_input)],
|
||||
context=kwargs
|
||||
)
|
||||
|
||||
result = self.graph.invoke(initial_state)
|
||||
|
||||
return {
|
||||
"messages": result.get("messages", []),
|
||||
"context": result.get("context", {}),
|
||||
"final_step": result.get("current_step", "unknown")
|
||||
}
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
import json
|
||||
|
||||
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage
|
||||
from .state import AgentState
|
||||
from services.prompt_manager import get_prompt_manager
|
||||
from services.template_matcher import get_template_matcher
|
||||
from services.sql_prompt_manager import get_sql_prompt_manager
|
||||
from tools.sr_api_tool import SrApiQueryTool
|
||||
|
||||
|
||||
def _short(value, max_len: int = 500) -> str:
|
||||
text = str(value)
|
||||
return text if len(text) <= max_len else text[:max_len] + "..."
|
||||
|
||||
|
||||
def process_input(state: AgentState) -> AgentState:
|
||||
"""处理用户输入"""
|
||||
print("[process_input][in] messages=", _short(state.messages))
|
||||
state.current_step = "processed"
|
||||
print("[process_input][out] current_step=", state.current_step)
|
||||
return state
|
||||
|
||||
|
||||
def generate_response(state: AgentState, model) -> AgentState:
|
||||
"""使用 LLM 生成回复"""
|
||||
print("[generate_response][in] context_keys=", list((state.context or {}).keys()))
|
||||
sr_api_result = state.context.get("sr_api_result")
|
||||
if sr_api_result:
|
||||
state.messages.append(AIMessage(content=str(sr_api_result)))
|
||||
print("[generate_response][out] source=sr_api_result")
|
||||
return state
|
||||
final_sql = state.context.get("final_sql")
|
||||
if final_sql:
|
||||
state.messages.append(AIMessage(content=final_sql))
|
||||
print("[generate_response][out] source=final_sql")
|
||||
return state
|
||||
if state.messages:
|
||||
response = model.invoke(state.messages)
|
||||
state.messages.append(response)
|
||||
print("[generate_response][out] source=model_invoke")
|
||||
return state
|
||||
|
||||
|
||||
def normalize_input(state: AgentState, model) -> AgentState:
|
||||
"""将用户输入规范化为标准英文语句"""
|
||||
if not state.messages:
|
||||
return state
|
||||
|
||||
last_message = state.messages[-1]
|
||||
if not isinstance(last_message, HumanMessage):
|
||||
return state
|
||||
|
||||
print("[normalize_input][in] user_input=", _short(last_message.content))
|
||||
|
||||
prompt_manager = get_prompt_manager()
|
||||
normalizer_prompt = (
|
||||
prompt_manager.get("system", "english_normalizer")
|
||||
or prompt_manager.get("user", "english_normalizer")
|
||||
)
|
||||
system_prompt = SystemMessage(content=normalizer_prompt)
|
||||
|
||||
response = model.invoke([system_prompt, HumanMessage(content=last_message.content)])
|
||||
normalized = response.content if hasattr(response, "content") else str(response)
|
||||
print("[normalize_input][out] normalized=", _short(normalized))
|
||||
|
||||
state.context["original_input"] = last_message.content
|
||||
state.context["normalized_input"] = normalized
|
||||
|
||||
matcher = get_template_matcher()
|
||||
state.context["table_match"] = matcher.match(normalized)
|
||||
print("[normalize_input][out] table_match=", _short(state.context.get("table_match")))
|
||||
return state
|
||||
|
||||
|
||||
def generate_sql(state: AgentState, model) -> AgentState:
|
||||
"""根据表名与提示词生成 SQL"""
|
||||
table_match = state.context.get("table_match") or {}
|
||||
table_name = table_match.get("table_name")
|
||||
normalized = state.context.get("normalized_input")
|
||||
|
||||
if not table_name or not normalized:
|
||||
print("[generate_sql][skip] missing table_name or normalized")
|
||||
return state
|
||||
|
||||
print("[generate_sql][in] table_name=", table_name)
|
||||
print("[generate_sql][in] normalized=", _short(normalized))
|
||||
|
||||
prompt_manager = get_sql_prompt_manager()
|
||||
prompt_data = prompt_manager.get_prompt(table_name)
|
||||
if not prompt_data:
|
||||
print("[generate_sql][skip] prompt not found for table=", table_name)
|
||||
return state
|
||||
|
||||
prompt_text = json.dumps(prompt_data, ensure_ascii=False, indent=2)
|
||||
system_template = get_prompt_manager().get("system", "sql_mysql_select_only")
|
||||
system_content = system_template.format(table_prompt_json=prompt_text)
|
||||
user_content = f"User question (normalized English): {normalized}"
|
||||
response = model.invoke([SystemMessage(content=system_content), HumanMessage(content=user_content)])
|
||||
sql_text = response.content if hasattr(response, "content") else str(response)
|
||||
print("[generate_sql][out] sql=", _short(sql_text))
|
||||
|
||||
state.context["final_sql"] = sql_text
|
||||
if not state.context.get("skip_sr_api"):
|
||||
tool = SrApiQueryTool()
|
||||
state.context["sr_api_result"] = tool.run(json.dumps({"sql": sql_text}, ensure_ascii=False))
|
||||
print("[generate_sql][out] sr_api_result=", _short(state.context.get("sr_api_result")))
|
||||
return state
|
||||
@@ -1,14 +0,0 @@
|
||||
from typing import Any, Dict, List
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
|
||||
class AgentState:
|
||||
"""代理工作流的状态定义"""
|
||||
messages: List[BaseMessage]
|
||||
current_step: str
|
||||
context: Dict[str, Any]
|
||||
|
||||
def __init__(self, messages: List[BaseMessage] = None, current_step: str = "start", context: Dict[str, Any] = None):
|
||||
self.messages = messages or []
|
||||
self.current_step = current_step
|
||||
self.context = context or {}
|
||||
Reference in New Issue
Block a user