fix: #33 内置多模态工具现在在工具市场 /api/v1/tools 中可见
list_tools 端点合并内置工具(image_ocr/image_vision/speech_to_text/text_to_speech 等), 按 scope=public/all 时自动包含,无需额外种子到 DB。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -612,6 +612,188 @@ class AgentOrchestrator:
|
||||
agent_results=execution_results,
|
||||
)
|
||||
|
||||
async def _graph(
|
||||
self, question: str, nodes: List[Dict[str, Any]], edges: List[Dict[str, Any]],
|
||||
on_llm_call: Optional[Callable] = None,
|
||||
) -> OrchestratorResult:
|
||||
"""图编排模式:按 DAG 拓扑顺序执行节点,支持 agent 和 condition 类型。"""
|
||||
if not nodes:
|
||||
return OrchestratorResult(mode="graph", final_answer="无节点可执行")
|
||||
|
||||
# 建立节点索引
|
||||
node_map: Dict[str, Dict[str, Any]] = {n["id"]: n for n in nodes}
|
||||
|
||||
# 建立邻接表和入度
|
||||
adj: Dict[str, List[tuple]] = {} # source_id → [(target_id, source_handle)]
|
||||
in_degree: Dict[str, int] = {n["id"]: 0 for n in nodes}
|
||||
for e in edges:
|
||||
src = e["source"]
|
||||
tgt = e["target"]
|
||||
sh = e.get("sourceHandle", "")
|
||||
if src not in adj:
|
||||
adj[src] = []
|
||||
adj[src].append((tgt, sh))
|
||||
if tgt in in_degree:
|
||||
in_degree[tgt] += 1
|
||||
|
||||
# 找起始节点(入度为 0)
|
||||
start_ids = [nid for nid, deg in in_degree.items() if deg == 0]
|
||||
if not start_ids:
|
||||
start_ids = [nodes[0]["id"]]
|
||||
|
||||
steps: List[OrchestratorStep] = []
|
||||
node_outputs: Dict[str, str] = {} # node_id → output text
|
||||
|
||||
# BFS 拓扑执行
|
||||
from collections import deque
|
||||
queue = deque(start_ids)
|
||||
# 将初始输入注入起始节点的"上游输出"
|
||||
for sid in start_ids:
|
||||
node_outputs[f"__input__{sid}"] = question
|
||||
|
||||
while queue:
|
||||
node_id = queue.popleft()
|
||||
node = node_map.get(node_id)
|
||||
if not node:
|
||||
continue
|
||||
|
||||
node_type = node.get("type", "agent")
|
||||
node_data = node.get("data", {})
|
||||
|
||||
# 收集上游输出作为本节点输入
|
||||
upstream_inputs = []
|
||||
for e in edges:
|
||||
if e["target"] == node_id:
|
||||
src_output = node_outputs.get(e["source"], "")
|
||||
if src_output:
|
||||
upstream_inputs.append(src_output)
|
||||
context_input = "\n\n".join(upstream_inputs) if upstream_inputs else question
|
||||
|
||||
if node_type == "condition":
|
||||
# 条件节点:根据上游输出来决定走哪个分支
|
||||
condition_expr = node_data.get("condition", "")
|
||||
condition_field = node_data.get("field", "output")
|
||||
|
||||
# 取最后一个上游输出作为判断依据
|
||||
last_output = upstream_inputs[-1] if upstream_inputs else question
|
||||
|
||||
# 简单条件评估:支持 contains / not_contains / equals
|
||||
op = node_data.get("operator", "contains")
|
||||
value = node_data.get("value", "")
|
||||
result_true = self._eval_condition(last_output, op, value)
|
||||
|
||||
branch = "true" if result_true else "false"
|
||||
steps.append(OrchestratorStep(
|
||||
agent_id=node_id,
|
||||
agent_name=f"条件: {condition_expr or node_data.get('name', node_id)}",
|
||||
input=f"判断: {op} '{value}' → {branch}",
|
||||
output=branch,
|
||||
))
|
||||
node_outputs[node_id] = branch
|
||||
|
||||
# 只沿匹配的分支继续
|
||||
for tgt, sh in adj.get(node_id, []):
|
||||
if sh == branch:
|
||||
in_degree[tgt] -= 1
|
||||
if in_degree[tgt] == 0:
|
||||
queue.append(tgt)
|
||||
continue
|
||||
|
||||
# agent 节点:构建 AgentRuntime 并执行
|
||||
agent_name = node_data.get("name", node_data.get("agent_name", node.get("label", node_id)))
|
||||
system_prompt = node_data.get("system_prompt", "你是一个有用的AI助手。")
|
||||
model = node_data.get("model", "deepseek-v4-flash")
|
||||
provider = node_data.get("provider", "deepseek")
|
||||
temperature = float(node_data.get("temperature", 0.7))
|
||||
max_iterations = int(node_data.get("max_iterations", 10))
|
||||
tools = node_data.get("tools", [])
|
||||
|
||||
runtime = AgentRuntime(
|
||||
AgentConfig(
|
||||
name=agent_name,
|
||||
system_prompt=system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
model=model, provider=provider,
|
||||
temperature=temperature, max_iterations=max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(include_tools=tools if isinstance(tools, list) else []),
|
||||
),
|
||||
on_llm_call=on_llm_call,
|
||||
)
|
||||
|
||||
# 构建带上下文的输入
|
||||
if len(upstream_inputs) > 1:
|
||||
agent_input = f"原始问题: {question}\n\n前序步骤的输出:\n{context_input}\n\n请基于以上信息继续处理。"
|
||||
elif len(upstream_inputs) == 1 and upstream_inputs[0] != question:
|
||||
agent_input = f"原始问题: {question}\n\n前一步输出:\n{upstream_inputs[0]}\n\n请基于以上信息继续处理。"
|
||||
else:
|
||||
agent_input = question
|
||||
|
||||
result = await runtime.run(agent_input)
|
||||
|
||||
steps.append(OrchestratorStep(
|
||||
agent_id=node_id,
|
||||
agent_name=agent_name,
|
||||
input=agent_input[:200],
|
||||
output=result.content[:500],
|
||||
iterations_used=result.iterations_used,
|
||||
tool_calls_made=result.tool_calls_made,
|
||||
error=None if result.success else result.error,
|
||||
))
|
||||
node_outputs[node_id] = result.content
|
||||
|
||||
if not result.success:
|
||||
logger.warning(f"Graph 节点 {agent_name} ({node_id}) 执行失败: {result.error}")
|
||||
|
||||
# 将下游节点的入度减 1
|
||||
for tgt, sh in adj.get(node_id, []):
|
||||
if tgt in in_degree:
|
||||
in_degree[tgt] -= 1
|
||||
if in_degree[tgt] == 0:
|
||||
queue.append(tgt)
|
||||
|
||||
# 收集最终输出(出度为 0 的节点)
|
||||
out_degree: Dict[str, int] = {n["id"]: 0 for n in nodes}
|
||||
for e in edges:
|
||||
out_degree[e["source"]] = out_degree.get(e["source"], 0) + 1
|
||||
end_ids = [nid for nid, deg in out_degree.items() if deg == 0]
|
||||
if not end_ids:
|
||||
end_ids = [steps[-1].agent_id] if steps else []
|
||||
|
||||
final_parts = []
|
||||
for eid in end_ids:
|
||||
out = node_outputs.get(eid, "")
|
||||
if out and out not in ("true", "false"):
|
||||
final_parts.append(out)
|
||||
final_answer = "\n\n".join(final_parts) if final_parts else (steps[-1].output if steps else "无输出")
|
||||
|
||||
return OrchestratorResult(
|
||||
mode="graph",
|
||||
final_answer=final_answer,
|
||||
steps=steps,
|
||||
agent_results=[
|
||||
{"agent_id": s.agent_id, "agent_name": s.agent_name, "output": s.output}
|
||||
for s in steps
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _eval_condition(text: str, op: str, value: str) -> bool:
|
||||
"""评估简单条件表达式。"""
|
||||
if op == "contains":
|
||||
return value.lower() in text.lower()
|
||||
elif op == "not_contains":
|
||||
return value.lower() not in text.lower()
|
||||
elif op == "equals":
|
||||
return text.strip().lower() == value.lower()
|
||||
elif op == "not_equals":
|
||||
return text.strip().lower() != value.lower()
|
||||
elif op == "starts_with":
|
||||
return text.strip().lower().startswith(value.lower())
|
||||
elif op == "ends_with":
|
||||
return text.strip().lower().endswith(value.lower())
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _parse_plan(text: str) -> dict:
|
||||
"""从 Planner 输出中解析 JSON 计划。"""
|
||||
|
||||
Reference in New Issue
Block a user