增加智能体
This commit is contained in:
1 parent
39def1c20b
commit
37bd8c6591
10 files changed
+517
-7
No files matched your search
@@ -0,0 +1,222 @@
|
|||||||
|
# 问答模式路由智能体设计方案
|
||||||
|
|
||||||
|
## 1. 背景与目标
|
||||||
|
|
||||||
|
路由器智能体(`f0e75f44f7594053ba1567ed89258f35`)根据用户问题判断最合适的问答模式(fast/rag/bigMode/moreModes/taskChat/hxchat/deerFlow),选择后需要调用相应子 Agent 执行,路由器对执行结果进行**完整性质检**,通过则流式输出到前端,不通过则重新匹配模式并再次执行。
|
||||||
|
|
||||||
|
## 2. 整体流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户问题
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ 路由器智能体 │
|
||||||
|
│ (已有 SOUL.md) │
|
||||||
|
│ ① 判断最佳模式 │
|
||||||
|
└─────────────┬────────────────┘
|
||||||
|
│ 选择模式
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ 子 Agent 执行 │
|
||||||
|
│ (完整执行完毕,结果缓存) │
|
||||||
|
└─────────────┬────────────────┘
|
||||||
|
│ 执行完毕
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────┐
|
||||||
|
│ 路由器质检 │
|
||||||
|
│ LLM 判断结果是否完整覆盖用户问题 │
|
||||||
|
└─────────────┬────────────────┘
|
||||||
|
│
|
||||||
|
③ 质检结果
|
||||||
|
┌────┴────┐
|
||||||
|
│通过 │不通过
|
||||||
|
▼ ▼
|
||||||
|
流式输出 重新匹配模式
|
||||||
|
到前端 (最多重试2次)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **最大重试次数**:每个问题最多 2 次重试(总共最多 3 次执行),超过后强制输出当前结果并标注"结果可能不完整"
|
||||||
|
|
||||||
|
## 3. 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.deer-flow/agents/f0e75f44f7594053ba1567ed89258f35/
|
||||||
|
├── SOUL.md ← 路由器智能体(已有)
|
||||||
|
├── config.yaml ← 路由器配置
|
||||||
|
└── subagents/
|
||||||
|
├── fast/
|
||||||
|
│ ├── SOUL.md ← 快速问答子 Agent
|
||||||
|
│ └── config.yaml ← tool_groups 声明
|
||||||
|
├── rag/
|
||||||
|
│ ├── SOUL.md
|
||||||
|
│ └── config.yaml
|
||||||
|
├── bigMode/
|
||||||
|
│ ├── SOUL.md
|
||||||
|
│ └── config.yaml
|
||||||
|
├── moreModes/
|
||||||
|
│ ├── SOUL.md
|
||||||
|
│ └── config.yaml
|
||||||
|
├── taskChat/
|
||||||
|
│ ├── SOUL.md
|
||||||
|
│ └── config.yaml
|
||||||
|
├── hxchat/
|
||||||
|
│ ├── SOUL.md
|
||||||
|
│ └── config.yaml
|
||||||
|
└── deerFlow/
|
||||||
|
├── SOUL.md
|
||||||
|
└── config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
**父子关系**:子 Agent 目录放在路由器下二级 `subagents/`,通过目录层级体现父子结构。
|
||||||
|
|
||||||
|
## 4. 子 Agent 配置
|
||||||
|
|
||||||
|
### 4.1 config.yaml 格式
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
id: rag
|
||||||
|
name: 知识库问答子Agent
|
||||||
|
description: 从知识库检索并返回引用
|
||||||
|
tool_groups:
|
||||||
|
- knowledge # 知识库检索工具组
|
||||||
|
- file:read # 只读文件工具
|
||||||
|
skills: []
|
||||||
|
model: deepseek-chat
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 tool_groups 说明
|
||||||
|
|
||||||
|
- tool_groups 在系统级 `config.yaml` 中统一定义(`tool_groups` 列表)
|
||||||
|
- 子 Agent 的 config.yaml 通过 `tool_groups` 字段声明自己可以使用哪些工具组
|
||||||
|
- 工具组示例:web(网页搜索)、knowledge(知识库检索)、file:read、file:write、bash 等
|
||||||
|
|
||||||
|
## 5. 调用方式
|
||||||
|
|
||||||
|
### 5.1 复用 Gateway API(方案 A — 推荐)
|
||||||
|
|
||||||
|
路由器通过 DeerFlow 现有 `/api/threads/{thread_id}/runs/stream` 接口调用子 Agent:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/threads/{thread_id}/runs/stream
|
||||||
|
{
|
||||||
|
"assistant_id": "f0e75f44f7594053ba1567ed89258f35",
|
||||||
|
"context": {
|
||||||
|
"subagent_mode": "rag" ← 路由器注入子模式
|
||||||
|
},
|
||||||
|
"input": {
|
||||||
|
"messages": [...]
|
||||||
|
},
|
||||||
|
"stream_mode": ["values"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**执行步骤**:
|
||||||
|
1. 路由器启动一个 run,assistant_id 指向路由器本身
|
||||||
|
2. 路由器通过 `context.subagent_mode` 告知 DeerFlow 运行时本次应加载哪个子 Agent
|
||||||
|
3. DeerFlow 运行时加载 `subagents/{subagent_mode}/SOUL.md` 和对应的 tool_groups
|
||||||
|
4. 子 Agent 完整执行,结果缓存在 StreamBridge 中
|
||||||
|
5. 执行完毕后,路由器读取完整结果
|
||||||
|
|
||||||
|
**context.subagent_mode 注入机制**:
|
||||||
|
- `build_run_config` 中新增 `subagent_mode` 到 `_CONTEXT_CONFIGURABLE_KEYS`
|
||||||
|
- `make_lead_agent` 读取 `configurable.subagent_mode`,如果存在则加载对应子 Agent 的 SOUL.md 和工具配置
|
||||||
|
- 如果 `subagent_mode` 不存在,则正常运行路由器自己的 SOUL.md
|
||||||
|
|
||||||
|
### 5.2 质检提示词
|
||||||
|
|
||||||
|
路由器在子 Agent 执行完毕后,用 LLM 进行完整性质检:
|
||||||
|
|
||||||
|
```
|
||||||
|
你是一个质检员。上游子 Agent 已根据问题执行完毕。
|
||||||
|
请判断以下结果是否完整覆盖了用户问题的所有子问题。
|
||||||
|
|
||||||
|
用户原始问题:{original_question}
|
||||||
|
|
||||||
|
子 Agent 执行结果:
|
||||||
|
{agent_output}
|
||||||
|
|
||||||
|
请只返回 JSON 格式:
|
||||||
|
{
|
||||||
|
"passed": true/false,
|
||||||
|
"reason": "判断理由(10字内)",
|
||||||
|
"missing_parts": ["缺失的子问题1", "缺失的子问题2"] // 如果 passed=false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**质检维度**:完整性(结果是否覆盖了用户问题的所有子问题)
|
||||||
|
|
||||||
|
## 6. 关键代码改动
|
||||||
|
|
||||||
|
### 6.1 DeerFlow 运行时改动
|
||||||
|
|
||||||
|
**文件**:`packages/harness/deerflow/agents/lead_agent/agent.py`
|
||||||
|
|
||||||
|
改动点:
|
||||||
|
- `make_lead_agent` 读取 `configurable.subagent_mode`
|
||||||
|
- 如果存在,加载 `.deer-flow/agents/{agent_id}/subagents/{subagent_mode}/SOUL.md` 和对应 `config.yaml`
|
||||||
|
- 工具绑定使用子 Agent 的 tool_groups,而非父 Agent 的
|
||||||
|
|
||||||
|
### 6.2 build_run_config 改动
|
||||||
|
|
||||||
|
**文件**:`app/gateway/services.py`
|
||||||
|
|
||||||
|
改动点:
|
||||||
|
- `_CONTEXT_CONFIGURABLE_KEYS` 新增 `subagent_mode`
|
||||||
|
- `merge_run_context_overrides` 将 `subagent_mode` 透传到 configurable
|
||||||
|
|
||||||
|
### 6.3 路由器 SOUL.md 改动
|
||||||
|
|
||||||
|
路由器 SOUL.md 新增功能描述:
|
||||||
|
- 角色扩展:增加"在确定模式后,调用相应子 Agent 执行并对结果进行质检"的职责
|
||||||
|
- 工具描述:新增一个质检工具(路由器自己调用 LLM 执行质检)
|
||||||
|
|
||||||
|
### 6.4 新增路由器质检工具
|
||||||
|
|
||||||
|
**文件**:`packages/harness/deerflow/tools/builtins/router_quality_tool.py`(新建)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@tool
|
||||||
|
def router_quality_check(original_question: str, agent_output: str) -> str:
|
||||||
|
"""质检工具 — 判断子 Agent 输出是否完整覆盖用户问题
|
||||||
|
|
||||||
|
返回 JSON:{"passed": bool, "reason": str, "missing_parts": list[str]}
|
||||||
|
"""
|
||||||
|
# 调用 LLM 执行质检逻辑
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
用户问题
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
路由器 SOUL.md 判断模式
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
启动子 Agent Run(context.subagent_mode = rag)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
子 Agent SOUL.md + tool_groups 执行
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
StreamBridge 缓存完整结果
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
路由器调用 router_quality_check 质检
|
||||||
|
│
|
||||||
|
├── passed=true → 流式输出到前端
|
||||||
|
└── passed=false → 重新匹配模式(最多2次重试)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 测试计划
|
||||||
|
|
||||||
|
- **单元测试**:`tests/test_router_subagent.py` — 测试模式判断、质检逻辑、重试机制
|
||||||
|
- **集成测试**:手动调用 `/api/threads/{thread_id}/runs/stream`,验证完整流程
|
||||||
|
- **质检提示词测试**:针对不同问题类型测试质检结果的准确性
|
||||||
|
|
||||||
|
## 9. 依赖项
|
||||||
|
|
||||||
|
- 路由器复用现有 Gateway API,无需新建接口
|
||||||
|
- 质检使用 LLM(deepseek-chat 或配置中指定的模型)
|
||||||
|
- tool_groups 机制依赖系统级 config.yaml 中的工具组定义
|
||||||
@@ -160,6 +160,11 @@ _CONTEXT_CONFIGURABLE_KEYS: frozenset[str] = frozenset(
|
|||||||
"max_concurrent_subagents",
|
"max_concurrent_subagents",
|
||||||
"agent_name",
|
"agent_name",
|
||||||
"is_bootstrap",
|
"is_bootstrap",
|
||||||
|
"subagent_mode",
|
||||||
|
"search_space_id",
|
||||||
|
"mentioned_document_ids",
|
||||||
|
"pic_query",
|
||||||
|
"subagent_model_name",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -302,12 +307,16 @@ def _collect_requested_agent_ids(body: Any) -> set[str]:
|
|||||||
|
|
||||||
async def _sync_legacy_agent_records(store) -> None:
|
async def _sync_legacy_agent_records(store) -> None:
|
||||||
"""Import existing folder-backed agents as built-in records."""
|
"""Import existing folder-backed agents as built-in records."""
|
||||||
|
import sys
|
||||||
from deerflow.config.agents_config import list_custom_agents
|
from deerflow.config.agents_config import list_custom_agents
|
||||||
|
|
||||||
|
print(f"[_sync_legacy_agent_records] store type: {type(store).__name__}, id: {id(store)}", file=sys.stderr)
|
||||||
|
logger.info("_sync_legacy_agent_records: starting sync")
|
||||||
for agent_cfg in list_custom_agents():
|
for agent_cfg in list_custom_agents():
|
||||||
agent_id = agent_cfg.id or agent_cfg.name
|
agent_id = agent_cfg.id or agent_cfg.name
|
||||||
if not agent_id:
|
if not agent_id:
|
||||||
continue
|
continue
|
||||||
|
logger.info(f" Syncing agent: {agent_id}")
|
||||||
await store.ensure_builtin(
|
await store.ensure_builtin(
|
||||||
{
|
{
|
||||||
"id": agent_id,
|
"id": agent_id,
|
||||||
@@ -316,6 +325,7 @@ async def _sync_legacy_agent_records(store) -> None:
|
|||||||
"published": True,
|
"published": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
logger.info("_sync_legacy_agent_records: done")
|
||||||
|
|
||||||
|
|
||||||
async def _enforce_agent_access(body: Any, request: Request) -> None:
|
async def _enforce_agent_access(body: Any, request: Request) -> None:
|
||||||
@@ -324,12 +334,20 @@ async def _enforce_agent_access(body: Any, request: Request) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
from app.gateway.deps import get_agent_store
|
from app.gateway.deps import get_agent_store
|
||||||
from deerflow.config.agents_config import validate_agent_id
|
from deerflow.config.agents_config import validate_agent_id, list_custom_agents
|
||||||
from deerflow.runtime.user_context import get_effective_user_id
|
from deerflow.runtime.user_context import get_effective_user_id
|
||||||
|
|
||||||
store = get_agent_store(request)
|
store = get_agent_store(request)
|
||||||
|
import sys
|
||||||
|
print(f"[_enforce_agent_access] store type: {type(store).__name__}, id: {id(store)}", file=sys.stderr)
|
||||||
await _sync_legacy_agent_records(store)
|
await _sync_legacy_agent_records(store)
|
||||||
user_id = get_effective_user_id()
|
user_id = get_effective_user_id()
|
||||||
|
logger.info(f"_enforce_agent_access: user_id={user_id}, agent_ids={agent_ids}")
|
||||||
|
|
||||||
|
# Debug: list all agents from list_custom_agents
|
||||||
|
all_agents = list_custom_agents()
|
||||||
|
logger.info(f" list_custom_agents() returned {len(all_agents)} agents: {[a.id for a in all_agents]}")
|
||||||
|
|
||||||
for agent_id in agent_ids:
|
for agent_id in agent_ids:
|
||||||
try:
|
try:
|
||||||
normalized = validate_agent_id(agent_id)
|
normalized = validate_agent_id(agent_id)
|
||||||
@@ -337,7 +355,13 @@ async def _enforce_agent_access(body: Any, request: Request) -> None:
|
|||||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||||
if normalized is None:
|
if normalized is None:
|
||||||
continue
|
continue
|
||||||
if await store.get_visible(normalized, user_id) is None:
|
record = await store.get_visible(normalized, user_id)
|
||||||
|
logger.info(f" get_visible({normalized}, {user_id}) = {record}")
|
||||||
|
# Also check get_any
|
||||||
|
any_record = await store.get_any(normalized)
|
||||||
|
import sys
|
||||||
|
print(f"[_enforce_agent_access] get_any({normalized}) = {any_record}", file=sys.stderr)
|
||||||
|
if record is None:
|
||||||
raise HTTPException(status_code=404, detail=f"Agent {normalized} not found")
|
raise HTTPException(status_code=404, detail=f"Agent {normalized} not found")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+43
-3
@@ -360,8 +360,43 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
logger.warning(f"Thinking mode is enabled but model '{model_name}' does not support it; fallback to non-thinking mode.")
|
logger.warning(f"Thinking mode is enabled but model '{model_name}' does not support it; fallback to non-thinking mode.")
|
||||||
thinking_enabled = False
|
thinking_enabled = False
|
||||||
|
|
||||||
|
# Check if we're in subagent mode (router agent delegating to a subagent)
|
||||||
|
subagent_mode = cfg.get("subagent_mode")
|
||||||
|
if subagent_mode and agent_id:
|
||||||
|
# Load subagent config and SOUL.md from subagents/{subagent_mode}/
|
||||||
|
from deerflow.config.paths import get_paths
|
||||||
|
|
||||||
|
paths = get_paths()
|
||||||
|
subagent_dir = paths.agent_dir(agent_id) / "subagents" / subagent_mode
|
||||||
|
subagent_soul_path = subagent_dir / "SOUL.md"
|
||||||
|
subagent_config_path = subagent_dir / "config.yaml"
|
||||||
|
|
||||||
|
if subagent_soul_path.exists():
|
||||||
|
subagent_soul = subagent_soul_path.read_text(encoding="utf-8").strip()
|
||||||
|
else:
|
||||||
|
subagent_soul = None
|
||||||
|
|
||||||
|
subagent_tool_groups = None
|
||||||
|
if subagent_config_path.exists():
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
with open(subagent_config_path, encoding="utf-8") as f:
|
||||||
|
subagent_data = yaml.safe_load(f) or {}
|
||||||
|
subagent_tool_groups = subagent_data.get("tool_groups")
|
||||||
|
except yaml.YAMLError:
|
||||||
|
logger.warning(f"Failed to parse subagent config: {subagent_config_path}")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Subagent mode(%s) for router(%s) -> subagent_soul: %s, tool_groups: %s",
|
||||||
|
subagent_mode,
|
||||||
|
agent_id,
|
||||||
|
"found" if subagent_soul else "not found",
|
||||||
|
subagent_tool_groups,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Create Agent(%s) -> thinking_enabled: %s, reasoning_effort: %s, model_name: %s, is_plan_mode: %s, subagent_enabled: %s, max_concurrent_subagents: %s, writing_mode: %s",
|
"Create Agent(%s) -> thinking_enabled: %s, reasoning_effort: %s, model_name: %s, is_plan_mode: %s, subagent_enabled: %s, max_concurrent_subagents: %s, writing_mode: %s, subagent_mode: %s",
|
||||||
agent_id or "default",
|
agent_id or "default",
|
||||||
thinking_enabled,
|
thinking_enabled,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
@@ -370,6 +405,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
subagent_enabled,
|
subagent_enabled,
|
||||||
max_concurrent_subagents,
|
max_concurrent_subagents,
|
||||||
writing_mode,
|
writing_mode,
|
||||||
|
subagent_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Inject run metadata for LangSmith trace tagging
|
# Inject run metadata for LangSmith trace tagging
|
||||||
@@ -396,7 +432,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
"subagent_enabled": subagent_enabled,
|
"subagent_enabled": subagent_enabled,
|
||||||
"writing_mode": writing_mode,
|
"writing_mode": writing_mode,
|
||||||
"writing_artifact_path": writing_artifact_path,
|
"writing_artifact_path": writing_artifact_path,
|
||||||
"tool_groups": agent_config.tool_groups if agent_config else None,
|
"tool_groups": subagent_tool_groups if subagent_mode else (agent_config.tool_groups if agent_config else None),
|
||||||
"available_skills": runtime_available_skills,
|
"available_skills": runtime_available_skills,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -425,11 +461,14 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Default lead agent (unchanged behavior)
|
# Default lead agent (unchanged behavior)
|
||||||
|
# Use subagent tool groups if in subagent mode, otherwise use parent agent config
|
||||||
|
effective_tool_groups = subagent_tool_groups if subagent_mode else (agent_config.tool_groups if agent_config else None)
|
||||||
|
|
||||||
return create_agent(
|
return create_agent(
|
||||||
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, app_config=resolved_app_config),
|
model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, reasoning_effort=reasoning_effort, app_config=resolved_app_config),
|
||||||
tools=get_available_tools(
|
tools=get_available_tools(
|
||||||
model_name=model_name,
|
model_name=model_name,
|
||||||
groups=agent_config.tool_groups if agent_config else None,
|
groups=effective_tool_groups,
|
||||||
subagent_enabled=subagent_enabled,
|
subagent_enabled=subagent_enabled,
|
||||||
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
is_scheduled_run=cfg.get("is_scheduled_run", False),
|
||||||
app_config=resolved_app_config,
|
app_config=resolved_app_config,
|
||||||
@@ -444,6 +483,7 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig):
|
|||||||
writing_artifact_path=writing_artifact_path,
|
writing_artifact_path=writing_artifact_path,
|
||||||
available_skills=(set(agent_config.skills or []) if agent_config else None),
|
available_skills=(set(agent_config.skills or []) if agent_config else None),
|
||||||
app_config=resolved_app_config,
|
app_config=resolved_app_config,
|
||||||
|
soul_override=subagent_soul if subagent_mode else None,
|
||||||
),
|
),
|
||||||
state_schema=ThreadState,
|
state_schema=ThreadState,
|
||||||
)
|
)
|
||||||
+2
-1
@@ -823,6 +823,7 @@ def apply_prompt_template(
|
|||||||
writing_artifact_path: str | None = None,
|
writing_artifact_path: str | None = None,
|
||||||
available_skills: set[str] | None = None,
|
available_skills: set[str] | None = None,
|
||||||
app_config: AppConfig | None = None,
|
app_config: AppConfig | None = None,
|
||||||
|
soul_override: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
storage_agent_id = agent_id if agent_id is not None else agent_name
|
storage_agent_id = agent_id if agent_id is not None else agent_name
|
||||||
|
|
||||||
@@ -894,7 +895,7 @@ Writing mode is enabled for this run.
|
|||||||
# Format the prompt with dynamic skills and memory
|
# Format the prompt with dynamic skills and memory
|
||||||
prompt = SYSTEM_PROMPT_TEMPLATE.format(
|
prompt = SYSTEM_PROMPT_TEMPLATE.format(
|
||||||
agent_name=agent_name or storage_agent_id or "CM 助手",
|
agent_name=agent_name or storage_agent_id or "CM 助手",
|
||||||
soul=get_agent_soul(storage_agent_id),
|
soul=f"<soul>\n{soul_override}\n</soul>\n" if soul_override else get_agent_soul(storage_agent_id),
|
||||||
skills_section=skills_section,
|
skills_section=skills_section,
|
||||||
deferred_tools_section=deferred_tools_section,
|
deferred_tools_section=deferred_tools_section,
|
||||||
writing_mode_section=writing_mode_section,
|
writing_mode_section=writing_mode_section,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from .clarification_tool import ask_clarification_tool
|
from .clarification_tool import ask_clarification_tool
|
||||||
|
from .fast_chat_tool import fast_chat_tool, fast_llm_answer
|
||||||
from .present_file_tool import present_file_tool
|
from .present_file_tool import present_file_tool
|
||||||
from .scheduled_task_tool import scheduled_task_tool
|
from .scheduled_task_tool import scheduled_task_tool
|
||||||
from .setup_agent_tool import setup_agent
|
from .setup_agent_tool import setup_agent
|
||||||
@@ -12,4 +13,6 @@ __all__ = [
|
|||||||
"scheduled_task_tool",
|
"scheduled_task_tool",
|
||||||
"view_image_tool",
|
"view_image_tool",
|
||||||
"task_tool",
|
"task_tool",
|
||||||
|
"fast_chat_tool",
|
||||||
|
"fast_llm_answer",
|
||||||
]
|
]
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
"""Fast chat tool - call new_chat API for fast Q&A mode."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Annotated, Any
|
||||||
|
|
||||||
|
from langchain.tools import InjectedToolCallId, ToolRuntime, tool
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
from langgraph.typing import ContextT
|
||||||
|
|
||||||
|
from deerflow.agents.thread_state import ThreadState
|
||||||
|
from deerflow.config.app_config import get_app_config
|
||||||
|
from deerflow.models import create_chat_model
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_runtime_value(runtime: ToolRuntime[ContextT, ThreadState], key: str, default: Any = None) -> Any:
|
||||||
|
"""Get value from runtime context or config."""
|
||||||
|
if runtime.context and isinstance(runtime.context, dict):
|
||||||
|
value = runtime.context.get(key)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
runtime_config = getattr(runtime, "config", None) or {}
|
||||||
|
configurable = runtime_config.get("configurable", {})
|
||||||
|
if isinstance(configurable, dict):
|
||||||
|
value = configurable.get(key)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
@tool("fast_chat", parse_docstring=True, return_direct=True)
|
||||||
|
async def fast_chat_tool(
|
||||||
|
runtime: ToolRuntime[ContextT, ThreadState],
|
||||||
|
prompt: str,
|
||||||
|
tool_call_id: Annotated[str, InjectedToolCallId],
|
||||||
|
search_space_id: int | None = None,
|
||||||
|
mentioned_document_ids: list[int] | None = None,
|
||||||
|
pic_query: list[dict[str, Any]] | None = None,
|
||||||
|
model_name: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Fast chat tool - 快速问答模式调用后端聊天接口.
|
||||||
|
|
||||||
|
用于 fast 子 Agent 直接调用后端 new_chat 接口进行快速问答.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
runtime: 运行时上下文,自动注入.
|
||||||
|
prompt: 用户问题内容.
|
||||||
|
tool_call_id: 工具调用 ID,自动注入.
|
||||||
|
search_space_id: 搜索空间 ID (可选,从 context.subagent_search_space_id 获取).
|
||||||
|
mentioned_document_ids: 引用的文档 ID 列表 (可选).
|
||||||
|
pic_query: 图片查询列表 (可选).
|
||||||
|
model_name: 使用的模型名称 (默认使用 context.subagent_model_name 或 OpenAI3).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
API 响应结果的 JSON 字符串.
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 从 context 读取参数,支持外部传入或运行时获取
|
||||||
|
thread_id = _get_runtime_value(runtime, "thread_id")
|
||||||
|
if thread_id is None:
|
||||||
|
return json.dumps({"error": "thread_id not found in context", "passed": False})
|
||||||
|
|
||||||
|
# 参数优先级:显式传入 > context 注入
|
||||||
|
effective_search_space_id = search_space_id if search_space_id is not None else _get_runtime_value(runtime, "subagent_search_space_id")
|
||||||
|
effective_model_name = model_name or _get_runtime_value(runtime, "subagent_model_name", "OpenAI3")
|
||||||
|
effective_mentioned_ids = mentioned_document_ids or _get_runtime_value(runtime, "subagent_mentioned_document_ids") or []
|
||||||
|
effective_pic_query = pic_query or _get_runtime_value(runtime, "subagent_pic_query") or []
|
||||||
|
|
||||||
|
app_config = get_app_config()
|
||||||
|
backend_url = getattr(app_config, "backend_url", "http://localhost:8001")
|
||||||
|
|
||||||
|
messages = [{"role": "user", "content": prompt}]
|
||||||
|
|
||||||
|
req_body = {
|
||||||
|
"chat_id": thread_id,
|
||||||
|
"user_query": prompt,
|
||||||
|
"search_space_id": effective_search_space_id,
|
||||||
|
"messages": messages,
|
||||||
|
"mentioned_document_ids": effective_mentioned_ids,
|
||||||
|
"pic_query": effective_pic_query,
|
||||||
|
"model_name": effective_model_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
req_url = f"{backend_url}/api/v1/new_chat"
|
||||||
|
|
||||||
|
def _make_request():
|
||||||
|
data = json.dumps(req_body).encode("utf-8")
|
||||||
|
req = urllib.request.Request(
|
||||||
|
req_url,
|
||||||
|
data=data,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as response:
|
||||||
|
return response.read().decode("utf-8")
|
||||||
|
|
||||||
|
result = await asyncio.to_thread(_make_request)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except urllib.error.HTTPError as exc:
|
||||||
|
logger.warning(f"Fast chat API HTTP error: {exc.code} {exc.reason}")
|
||||||
|
return json.dumps({"error": f"HTTP {exc.code}: {exc.reason}", "passed": False})
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Fast chat API failed: {exc}")
|
||||||
|
return json.dumps({"error": str(exc), "passed": False})
|
||||||
|
|
||||||
|
|
||||||
|
@tool("fast_llm_answer", parse_docstring=True, return_direct=True)
|
||||||
|
def fast_llm_answer(prompt: str) -> str:
|
||||||
|
"""Fast LLM answer tool - 使用 LLM 直接回答简单问题.
|
||||||
|
|
||||||
|
当 fast 模式不需要调用外部 API 时,直接使用 LLM 回答简单事实查询.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt: 用户问题.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LLM 回答内容.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app_config = get_app_config()
|
||||||
|
model_name = app_config.models[0].name if app_config.models else None
|
||||||
|
model = create_chat_model(name=model_name, thinking_enabled=False, app_config=app_config)
|
||||||
|
|
||||||
|
response = model.invoke([HumanMessage(content=prompt)], config=RunnableConfig())
|
||||||
|
content = response.content if hasattr(response, "content") else str(response)
|
||||||
|
return content
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Fast LLM answer failed: {exc}")
|
||||||
|
return f"抱歉,处理您的问题时出现错误: {str(exc)}"
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
"""Router quality check tool — validates sub-agent output completeness."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from langchain_core.messages import HumanMessage
|
||||||
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
|
||||||
|
from deerflow.config.app_config import get_app_config
|
||||||
|
from deerflow.models import create_chat_model
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
QUALITY_CHECK_PROMPT = """你是一个质检员。上游子 Agent 已根据问题执行完毕。
|
||||||
|
请判断以下结果是否完整覆盖了用户问题的所有子问题。
|
||||||
|
|
||||||
|
用户原始问题:{original_question}
|
||||||
|
|
||||||
|
子 Agent 执行结果:
|
||||||
|
{agent_output}
|
||||||
|
|
||||||
|
请只返回 JSON 格式:
|
||||||
|
{{
|
||||||
|
"passed": true/false,
|
||||||
|
"reason": "判断理由(10字内)",
|
||||||
|
"missing_parts": ["缺失的子问题1", "缺失的子问题2"] // 如果 passed=false
|
||||||
|
}}"""
|
||||||
|
|
||||||
|
|
||||||
|
@tool("router_quality_check", parse_docstring=True, return_direct=True)
|
||||||
|
def router_quality_check(original_question: str, agent_output: str) -> str:
|
||||||
|
"""质检工具 — 判断子 Agent 输出是否完整覆盖用户问题。
|
||||||
|
|
||||||
|
路由器在子 Agent 执行完毕后调用此工具进行完整性质检。
|
||||||
|
质检通过则流式输出到前端,不通过则重新匹配模式并再次执行。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
original_question: 用户原始问题。
|
||||||
|
agent_output: 子 Agent 的执行结果。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON 格式的质检结果:{{"passed": bool, "reason": str, "missing_parts": list[str]}}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
app_config = get_app_config()
|
||||||
|
model_name = app_config.models[0].name if app_config.models else None
|
||||||
|
model = create_chat_model(name=model_name, thinking_enabled=False, app_config=app_config)
|
||||||
|
|
||||||
|
prompt = QUALITY_CHECK_PROMPT.format(
|
||||||
|
original_question=original_question,
|
||||||
|
agent_output=agent_output,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = model.invoke([HumanMessage(content=prompt)], config=RunnableConfig())
|
||||||
|
content = response.content if hasattr(response, "content") else str(response)
|
||||||
|
|
||||||
|
# Try to parse JSON from the response
|
||||||
|
# The model might wrap it in ```json or just return raw JSON
|
||||||
|
json_str = content.strip()
|
||||||
|
if json_str.startswith("```json"):
|
||||||
|
json_str = json_str[7:]
|
||||||
|
if json_str.startswith("```"):
|
||||||
|
json_str = json_str[3:]
|
||||||
|
if json_str.endswith("```"):
|
||||||
|
json_str = json_str[:-3]
|
||||||
|
json_str = json_str.strip()
|
||||||
|
|
||||||
|
result = json.loads(json_str)
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Router quality check failed: {exc}")
|
||||||
|
# Fallback: pass with unknown reason on error
|
||||||
|
return json.dumps({"passed": True, "reason": "质检异常默认通过", "missing_parts": []})
|
||||||
@@ -6,7 +6,7 @@ from deerflow.config import get_app_config
|
|||||||
from deerflow.config.app_config import AppConfig
|
from deerflow.config.app_config import AppConfig
|
||||||
from deerflow.reflection import resolve_variable
|
from deerflow.reflection import resolve_variable
|
||||||
from deerflow.sandbox.security import is_host_bash_allowed
|
from deerflow.sandbox.security import is_host_bash_allowed
|
||||||
from deerflow.tools.builtins import ask_clarification_tool, present_file_tool, scheduled_task_tool, task_tool, view_image_tool
|
from deerflow.tools.builtins import ask_clarification_tool, fast_chat_tool, fast_llm_answer, present_file_tool, scheduled_task_tool, task_tool, view_image_tool
|
||||||
from deerflow.tools.builtins.tool_search import reset_deferred_registry
|
from deerflow.tools.builtins.tool_search import reset_deferred_registry
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -14,6 +14,8 @@ logger = logging.getLogger(__name__)
|
|||||||
BUILTIN_TOOLS = [
|
BUILTIN_TOOLS = [
|
||||||
present_file_tool,
|
present_file_tool,
|
||||||
ask_clarification_tool,
|
ask_clarification_tool,
|
||||||
|
fast_chat_tool,
|
||||||
|
fast_llm_answer,
|
||||||
]
|
]
|
||||||
|
|
||||||
SUBAGENT_TOOLS = [
|
SUBAGENT_TOOLS = [
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ dependencies = [
|
|||||||
"pyjwt>=2.9.0",
|
"pyjwt>=2.9.0",
|
||||||
"email-validator>=2.0.0",
|
"email-validator>=2.0.0",
|
||||||
"croniter>=6.2.2",
|
"croniter>=6.2.2",
|
||||||
|
"greenlet>=3.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
Generated
+2
@@ -750,6 +750,7 @@ dependencies = [
|
|||||||
{ name = "dingtalk-stream" },
|
{ name = "dingtalk-stream" },
|
||||||
{ name = "email-validator" },
|
{ name = "email-validator" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "greenlet" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "langgraph-sdk" },
|
{ name = "langgraph-sdk" },
|
||||||
{ name = "lark-oapi" },
|
{ name = "lark-oapi" },
|
||||||
@@ -785,6 +786,7 @@ requires-dist = [
|
|||||||
{ name = "dingtalk-stream", specifier = ">=0.24.3" },
|
{ name = "dingtalk-stream", specifier = ">=0.24.3" },
|
||||||
{ name = "email-validator", specifier = ">=2.0.0" },
|
{ name = "email-validator", specifier = ">=2.0.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.115.0" },
|
{ name = "fastapi", specifier = ">=0.115.0" },
|
||||||
|
{ name = "greenlet", specifier = ">=3.4.0" },
|
||||||
{ name = "httpx", specifier = ">=0.28.0" },
|
{ name = "httpx", specifier = ">=0.28.0" },
|
||||||
{ name = "langgraph-sdk", specifier = ">=0.1.51" },
|
{ name = "langgraph-sdk", specifier = ">=0.1.51" },
|
||||||
{ name = "lark-oapi", specifier = ">=1.4.0" },
|
{ name = "lark-oapi", specifier = ">=1.4.0" },
|
||||||
|
|||||||
Reference in new issue
Block a user