Files
beaver_project/app-instance/backend/beaver/team_workflows/base.py
steven_li 520a21a027 feat(coordinator): 添加团队节点默认最大工具迭代次数配置
添加 DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS 配置项以控制团队节点的最大工具迭代次数,
并修改 LocalAgentRunner 中的逻辑来使用此默认值当 envelope 中未指定时。

fix(runtime): 修复团队节点运行成功判断逻辑

更新运行成功判断条件,将 finish_reason 为 "max_tool_iterations_finalized" 的情况
视为运行失败,并添加对原始工具调用输出的检测,避免将其误判为成功完成。

feat(mcp): 添加团队工作流MCP工具类别支持

增加新的本地MCP工具类别 "team_workflow" 及其对应的工具创建功能,
为团队工作流提供本地工具支持。

refactor(engine): 调整AgentLoop最大工具迭代次数设置

将 AgentProfile 中的默认 max_tool_iterations 从 30 增加到 100,
同时移除 TaskExecutionPlanner 构造函数中的重复参数传递。

perf(mcp): 优化MCP连接管理避免重复连接

添加 mcp_connected 标志来跟踪MCP连接状态,确保 connect_all 只执行一次,
提高性能并避免不必要的重复连接。

refactor(skills): 移除技能团队模板相关功能

移除与技能团队模板相关的代码,包括解析、存储和处理逻辑,
简化技能记录结构和加载流程。

feat(process): 增强会话过程投影器功能

添加技能激活快照事件处理,改进团队运行完成消息显示,
并增强技能激活事件的时间戳记录功能。

refactor(tasks): 简化任务尝试编排器团队执行逻辑

移除团队执行相关代码,将所有任务统一按单步执行处理,
简化任务编排器的复杂度并提升执行效率。

fix(evidence): 修复节点证据评估中需求验证逻辑

更新节点证据评估逻辑,跳过自然语言证据需求的确定性验证,
只执行机器可读的需求验证,避免因自然语言需求导致的节点失败。
2026-06-26 16:36:29 +08:00

274 lines
9.0 KiB
Python

"""Shared builders for local team workflow graph construction."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Iterable, Literal
from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode
GraphStrategy = Literal["sequence", "parallel", "dag"]
@dataclass(slots=True)
class WorkflowAgentSpec:
name: str
instruction: str
use_skill: str | None = None
skill_query: str | None = None
allowed_tool_names: list[str] | None = None
required_evidence: list[str] = field(default_factory=list)
evidence_contract: dict[str, Any] = field(default_factory=dict)
validation_rules: list[str] = field(default_factory=list)
required_for_completion: bool = True
block_downstream_on_partial: bool = False
max_tool_iterations: int | None = None
constraints: list[str] = field(default_factory=list)
expected_output: str | None = None
input_contract: dict[str, Any] = field(default_factory=dict)
output_contract: dict[str, Any] = field(default_factory=dict)
@dataclass(slots=True)
class WorkflowBuildResult:
graph: ExecutionGraph
workflow_name: str
def parse_agents(raw_agents: Iterable[WorkflowAgentSpec | dict[str, Any]]) -> list[WorkflowAgentSpec]:
agents: list[WorkflowAgentSpec] = []
for index, raw in enumerate(raw_agents, start=1):
if isinstance(raw, WorkflowAgentSpec):
spec = raw
elif isinstance(raw, dict):
spec = _agent_from_dict(raw, index=index)
else:
raise ValueError("workflow agents must be objects")
agents.append(spec)
validate_agent_names(agents)
return agents
def validate_agent_names(agents: list[WorkflowAgentSpec]) -> None:
if not agents:
raise ValueError("workflow requires at least one agent")
seen: set[str] = set()
for agent in agents:
if not agent.name:
raise ValueError("workflow agent name is required")
if not agent.instruction:
raise ValueError(f"workflow agent {agent.name!r} requires instruction")
if agent.name in seen:
raise ValueError(f"workflow agent names must be unique: {agent.name}")
seen.add(agent.name)
def agent_name_set(agents: list[WorkflowAgentSpec]) -> set[str]:
return {agent.name for agent in agents}
def build_graph_from_dependencies(
*,
workflow_name: str,
strategy: GraphStrategy,
agents: list[WorkflowAgentSpec],
dependencies: dict[str, list[str]],
) -> ExecutionGraph:
nodes = [
build_node(
workflow_name=workflow_name,
agent=agent,
depends_on=dependencies.get(agent.name, []),
)
for agent in agents
]
graph = ExecutionGraph(strategy=strategy, nodes=nodes)
graph.validate()
return graph
def build_node(
*,
workflow_name: str,
agent: WorkflowAgentSpec,
depends_on: list[str],
) -> ExecutionNode:
metadata = {
"sub_agent_kind": "generic_skill_worker",
"workflow_tool": workflow_name,
"workflow_agent_name": agent.name,
}
if agent.use_skill:
metadata["use_skill"] = agent.use_skill
if agent.skill_query:
metadata["skill_query"] = agent.skill_query
return ExecutionNode(
node_id=agent.name,
task=agent.instruction,
agent=AgentDescriptor(
name=agent.name,
role="",
system_prompt="",
metadata=metadata,
),
depends_on=list(depends_on),
constraints=list(agent.constraints),
expected_output=agent.expected_output,
input_contract=dict(agent.input_contract),
output_contract=dict(agent.output_contract),
allowed_tool_names=(
None if agent.allowed_tool_names is None else list(agent.allowed_tool_names)
),
required_evidence=list(agent.required_evidence),
evidence_contract=dict(agent.evidence_contract),
validation_rules=list(agent.validation_rules),
required_for_completion=agent.required_for_completion,
block_downstream_on_partial=agent.block_downstream_on_partial,
max_tool_iterations=agent.max_tool_iterations,
)
def edges_to_dependencies(
*,
agents: list[WorkflowAgentSpec],
edges: Iterable[tuple[str, str] | list[str]],
) -> dict[str, list[str]]:
known = agent_name_set(agents)
dependencies = {agent.name: [] for agent in agents}
for raw_edge in edges:
source, target = _parse_edge(raw_edge)
if source not in known:
raise ValueError(f"workflow edge references unknown agent: {source}")
if target not in known:
raise ValueError(f"workflow edge references unknown agent: {target}")
if source == target:
raise ValueError(f"workflow edge creates a self-cycle: {source}")
if source not in dependencies[target]:
dependencies[target].append(source)
return dependencies
def validate_output_agent(
*,
agents: list[WorkflowAgentSpec],
dependencies: dict[str, list[str]],
output_agent: str,
allow_disconnected: bool = False,
) -> None:
known = agent_name_set(agents)
if output_agent not in known:
raise ValueError(f"workflow output_agent references unknown agent: {output_agent}")
upstream = _upstream_nodes(output_agent, dependencies)
if not upstream:
raise ValueError(f"workflow output_agent {output_agent!r} must be reachable from upstream agents")
if allow_disconnected:
return
connected = set(upstream)
connected.add(output_agent)
disconnected = sorted(known - connected)
if disconnected:
raise ValueError(f"workflow has disconnected agent(s): {', '.join(disconnected)}")
def validate_no_disconnected_agents(
*,
agents: list[WorkflowAgentSpec],
dependencies: dict[str, list[str]],
) -> None:
known = agent_name_set(agents)
connected: set[str] = set()
for target, sources in dependencies.items():
if sources:
connected.add(target)
connected.update(sources)
disconnected = sorted(known - connected)
if disconnected:
raise ValueError(f"workflow has disconnected agent(s): {', '.join(disconnected)}")
def _agent_from_dict(raw: dict[str, Any], *, index: int) -> WorkflowAgentSpec:
name = _required_str(raw.get("name"), f"agents[{index}].name")
instruction = _required_str(raw.get("instruction"), f"agents[{index}].instruction")
return WorkflowAgentSpec(
name=name,
instruction=instruction,
use_skill=_optional_str(raw.get("use_skill")),
skill_query=_optional_str(raw.get("skill_query")),
allowed_tool_names=_optional_string_list(raw.get("allowed_tool_names")),
required_evidence=_string_list(raw.get("required_evidence")),
evidence_contract=_dict(raw.get("evidence_contract")),
validation_rules=_string_list(raw.get("validation_rules")),
required_for_completion=bool(raw.get("required_for_completion", True)),
block_downstream_on_partial=bool(raw.get("block_downstream_on_partial", False)),
max_tool_iterations=_optional_int(raw.get("max_tool_iterations")),
constraints=_string_list(raw.get("constraints")),
expected_output=_optional_str(raw.get("expected_output")),
input_contract=_dict(raw.get("input_contract")),
output_contract=_dict(raw.get("output_contract")),
)
def _parse_edge(raw_edge: tuple[str, str] | list[str]) -> tuple[str, str]:
if not isinstance(raw_edge, (list, tuple)) or len(raw_edge) != 2:
raise ValueError("workflow edges must be [source, target] pairs")
source = _required_str(raw_edge[0], "edge source")
target = _required_str(raw_edge[1], "edge target")
return source, target
def _upstream_nodes(node_id: str, dependencies: dict[str, list[str]]) -> set[str]:
result: set[str] = set()
def visit(current: str) -> None:
for dependency in dependencies.get(current, []):
if dependency in result:
continue
result.add(dependency)
visit(dependency)
visit(node_id)
return result
def _required_str(value: Any, label: str) -> str:
text = str(value or "").strip()
if not text:
raise ValueError(f"{label} is required")
return text
def _optional_str(value: Any) -> str | None:
text = str(value or "").strip()
return text or None
def _string_list(value: Any) -> list[str]:
if value is None:
return []
if not isinstance(value, list):
raise ValueError("expected a list of strings")
return [str(item).strip() for item in value if str(item).strip()]
def _optional_string_list(value: Any) -> list[str] | None:
if value is None:
return None
return _string_list(value)
def _dict(value: Any) -> dict[str, Any]:
return dict(value) if isinstance(value, dict) else {}
def _optional_int(value: Any) -> int | None:
if value is None:
return None
try:
return int(value)
except (TypeError, ValueError) as exc:
raise ValueError("max_tool_iterations must be an integer") from exc