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): 修复节点证据评估中需求验证逻辑 更新节点证据评估逻辑,跳过自然语言证据需求的确定性验证, 只执行机器可读的需求验证,避免因自然语言需求导致的节点失败。
This commit is contained in:
2
app-instance/backend/beaver/team_workflows/__init__.py
Normal file
2
app-instance/backend/beaver/team_workflows/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Local team workflow graph builders."""
|
||||
|
||||
@ -0,0 +1,70 @@
|
||||
"""AgentRearrange graph builder using arrow/comma flow syntax."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from beaver.coordinator.models import ExecutionGraph
|
||||
|
||||
from .base import (
|
||||
WorkflowAgentSpec,
|
||||
agent_name_set,
|
||||
build_graph_from_dependencies,
|
||||
edges_to_dependencies,
|
||||
parse_agents,
|
||||
validate_no_disconnected_agents,
|
||||
)
|
||||
|
||||
WORKFLOW_NAME = "AgentRearrange"
|
||||
|
||||
|
||||
def build_graph(
|
||||
*,
|
||||
task: str,
|
||||
agents: Iterable[WorkflowAgentSpec | dict[str, Any]],
|
||||
flow: str,
|
||||
) -> ExecutionGraph:
|
||||
del task
|
||||
parsed = parse_agents(agents)
|
||||
edges = parse_flow(flow, known_agents=agent_name_set(parsed))
|
||||
dependencies = edges_to_dependencies(agents=parsed, edges=edges)
|
||||
validate_no_disconnected_agents(agents=parsed, dependencies=dependencies)
|
||||
return build_graph_from_dependencies(
|
||||
workflow_name=WORKFLOW_NAME,
|
||||
strategy="dag",
|
||||
agents=parsed,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
|
||||
|
||||
def parse_flow(flow: str, *, known_agents: set[str]) -> list[tuple[str, str]]:
|
||||
stages = _parse_stages(flow)
|
||||
edges: list[tuple[str, str]] = []
|
||||
for stage in stages:
|
||||
for name in stage:
|
||||
if name not in known_agents:
|
||||
raise ValueError(f"workflow flow references unknown agent: {name}")
|
||||
for left, right in zip(stages, stages[1:], strict=False):
|
||||
for source in left:
|
||||
for target in right:
|
||||
edge = (source, target)
|
||||
if edge not in edges:
|
||||
edges.append(edge)
|
||||
return edges
|
||||
|
||||
|
||||
def _parse_stages(flow: str) -> list[list[str]]:
|
||||
raw_flow = str(flow or "").strip()
|
||||
if not raw_flow:
|
||||
raise ValueError("workflow flow is required")
|
||||
stages: list[list[str]] = []
|
||||
for raw_stage in raw_flow.split("->"):
|
||||
names = [name.strip() for name in raw_stage.split(",") if name.strip()]
|
||||
if not names:
|
||||
raise ValueError("workflow flow contains an empty stage")
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("workflow flow contains duplicate agent names in a stage")
|
||||
stages.append(names)
|
||||
if len(stages) < 2:
|
||||
raise ValueError("workflow flow must contain at least two stages")
|
||||
return stages
|
||||
273
app-instance/backend/beaver/team_workflows/base.py
Normal file
273
app-instance/backend/beaver/team_workflows/base.py
Normal file
@ -0,0 +1,273 @@
|
||||
"""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
|
||||
26
app-instance/backend/beaver/team_workflows/concurrent.py
Normal file
26
app-instance/backend/beaver/team_workflows/concurrent.py
Normal file
@ -0,0 +1,26 @@
|
||||
"""ConcurrentWorkflow graph builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from beaver.coordinator.models import ExecutionGraph
|
||||
|
||||
from .base import WorkflowAgentSpec, build_graph_from_dependencies, parse_agents
|
||||
|
||||
WORKFLOW_NAME = "ConcurrentWorkflow"
|
||||
|
||||
|
||||
def build_graph(
|
||||
*,
|
||||
task: str,
|
||||
agents: Iterable[WorkflowAgentSpec | dict[str, Any]],
|
||||
) -> ExecutionGraph:
|
||||
del task
|
||||
parsed = parse_agents(agents)
|
||||
return build_graph_from_dependencies(
|
||||
workflow_name=WORKFLOW_NAME,
|
||||
strategy="parallel",
|
||||
agents=parsed,
|
||||
dependencies={agent.name: [] for agent in parsed},
|
||||
)
|
||||
174
app-instance/backend/beaver/team_workflows/executor.py
Normal file
174
app-instance/backend/beaver/team_workflows/executor.py
Normal file
@ -0,0 +1,174 @@
|
||||
"""Runtime bridge for local team workflow MCP tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
|
||||
from beaver.coordinator.models import ExecutionGraph, TeamRunResult
|
||||
from beaver.tools.base import ToolContext, ToolResult
|
||||
|
||||
from . import agent_rearrange, concurrent, graph, mixture_of_agents, sequential
|
||||
|
||||
GraphBuilder = Callable[..., ExecutionGraph]
|
||||
|
||||
|
||||
class TeamWorkflowExecutor:
|
||||
"""Execute workflow MCP calls inside the current Beaver runtime."""
|
||||
|
||||
_BUILDERS: dict[str, GraphBuilder] = {
|
||||
"SequentialWorkflow": sequential.build_graph,
|
||||
"ConcurrentWorkflow": concurrent.build_graph,
|
||||
"MixtureOfAgents": mixture_of_agents.build_graph,
|
||||
"AgentRearrange": agent_rearrange.build_graph,
|
||||
"GraphWorkflow": graph.build_graph,
|
||||
}
|
||||
|
||||
async def execute(
|
||||
self,
|
||||
workflow_name: str,
|
||||
arguments: dict[str, Any],
|
||||
context: ToolContext,
|
||||
*,
|
||||
tool_name: str | None = None,
|
||||
) -> ToolResult:
|
||||
exposed_name = tool_name or workflow_name
|
||||
try:
|
||||
if str(context.metadata.get("source") or "").startswith("team:"):
|
||||
raise ValueError("nested_team_workflow_not_allowed")
|
||||
builder = self._BUILDERS.get(workflow_name)
|
||||
if builder is None:
|
||||
raise ValueError(f"unknown team workflow tool: {workflow_name}")
|
||||
graph = builder(**dict(arguments or {}))
|
||||
parent_task_id = _task_id(context)
|
||||
parent_session_id = _session_id(context)
|
||||
result = await self._run_team(
|
||||
context=context,
|
||||
graph=graph,
|
||||
parent_task_id=parent_task_id,
|
||||
parent_session_id=parent_session_id,
|
||||
)
|
||||
payload = _success_payload(
|
||||
workflow_name=workflow_name,
|
||||
graph=graph,
|
||||
result=result,
|
||||
)
|
||||
return ToolResult(
|
||||
success=True,
|
||||
content=json.dumps(payload, ensure_ascii=False),
|
||||
tool_name=exposed_name,
|
||||
raw_output=payload,
|
||||
)
|
||||
except Exception as exc:
|
||||
payload = {
|
||||
"success": False,
|
||||
"workflow": workflow_name,
|
||||
"error": str(exc),
|
||||
}
|
||||
return ToolResult(
|
||||
success=False,
|
||||
content=json.dumps(payload, ensure_ascii=False),
|
||||
tool_name=exposed_name,
|
||||
error=str(exc),
|
||||
raw_output=payload,
|
||||
)
|
||||
|
||||
async def _run_team(
|
||||
self,
|
||||
*,
|
||||
context: ToolContext,
|
||||
graph: ExecutionGraph,
|
||||
parent_task_id: str,
|
||||
parent_session_id: str,
|
||||
) -> TeamRunResult:
|
||||
runner = context.services.get("agent_team_runner")
|
||||
parent_run_id = _run_id(context)
|
||||
if runner is not None:
|
||||
return await runner(
|
||||
graph,
|
||||
parent_task_id=parent_task_id,
|
||||
parent_session_id=parent_session_id,
|
||||
parent_run_id=parent_run_id,
|
||||
)
|
||||
|
||||
agent_loop = context.services.get("agent_loop")
|
||||
if agent_loop is None:
|
||||
raise ValueError("team workflow execution requires agent_loop or agent_team_runner")
|
||||
provider_bundle = context.services.get("provider_bundle")
|
||||
|
||||
def provider_bundle_factory(_node: Any) -> Any:
|
||||
return provider_bundle
|
||||
|
||||
from beaver.engine import AgentLoop
|
||||
from beaver.services.team_service import TeamService
|
||||
|
||||
loaded = context.services.get("loaded")
|
||||
team_loop = AgentLoop(profile=agent_loop.profile, loader=agent_loop.loader)
|
||||
team_loop.loaded = loaded
|
||||
return await TeamService(team_loop).run_team(
|
||||
graph,
|
||||
parent_task_id=parent_task_id,
|
||||
parent_session_id=parent_session_id,
|
||||
parent_run_id=parent_run_id,
|
||||
provider_bundle_factory=provider_bundle_factory if provider_bundle is not None else None,
|
||||
allow_candidate_generation=False,
|
||||
)
|
||||
|
||||
|
||||
def _task_id(context: ToolContext) -> str:
|
||||
value = str(context.services.get("task_id") or context.metadata.get("task_id") or "").strip()
|
||||
if not value:
|
||||
raise ValueError("team workflow execution requires task_id")
|
||||
return value
|
||||
|
||||
|
||||
def _session_id(context: ToolContext) -> str:
|
||||
value = str(context.session_id or context.services.get("session_id") or "").strip()
|
||||
if not value:
|
||||
raise ValueError("team workflow execution requires session_id")
|
||||
return value
|
||||
|
||||
|
||||
def _run_id(context: ToolContext) -> str | None:
|
||||
return str(context.services.get("run_id") or context.metadata.get("run_id") or "").strip() or None
|
||||
|
||||
|
||||
def _success_payload(
|
||||
*,
|
||||
workflow_name: str,
|
||||
graph: ExecutionGraph,
|
||||
result: TeamRunResult,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"success": result.success,
|
||||
"workflow": workflow_name,
|
||||
"summary": result.summary,
|
||||
"run_ids": list(result.run_ids),
|
||||
"session_ids": list(result.session_ids),
|
||||
"node_results": [item.to_dict() for item in result.node_results],
|
||||
"graph": _graph_to_dict(graph),
|
||||
}
|
||||
|
||||
|
||||
def _graph_to_dict(graph: ExecutionGraph) -> dict[str, Any]:
|
||||
return {
|
||||
"strategy": graph.strategy,
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": node.node_id,
|
||||
"task": node.task,
|
||||
"depends_on": list(node.depends_on),
|
||||
"allowed_tool_names": (
|
||||
None if node.allowed_tool_names is None else list(node.allowed_tool_names)
|
||||
),
|
||||
"required_evidence": list(node.required_evidence),
|
||||
"evidence_contract": dict(node.evidence_contract),
|
||||
"validation_rules": list(node.validation_rules),
|
||||
"required_for_completion": node.required_for_completion,
|
||||
"block_downstream_on_partial": node.block_downstream_on_partial,
|
||||
"max_tool_iterations": node.max_tool_iterations,
|
||||
"metadata": dict(node.agent.metadata),
|
||||
}
|
||||
for node in graph.nodes
|
||||
],
|
||||
}
|
||||
45
app-instance/backend/beaver/team_workflows/graph.py
Normal file
45
app-instance/backend/beaver/team_workflows/graph.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""GraphWorkflow explicit DAG builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from beaver.coordinator.models import ExecutionGraph
|
||||
|
||||
from .base import (
|
||||
WorkflowAgentSpec,
|
||||
build_graph_from_dependencies,
|
||||
edges_to_dependencies,
|
||||
parse_agents,
|
||||
validate_output_agent,
|
||||
)
|
||||
|
||||
WORKFLOW_NAME = "GraphWorkflow"
|
||||
|
||||
|
||||
def build_graph(
|
||||
*,
|
||||
task: str,
|
||||
agents: Iterable[WorkflowAgentSpec | dict[str, Any]],
|
||||
edges: Iterable[tuple[str, str] | list[str]],
|
||||
output_agent: str,
|
||||
allow_disconnected: bool = False,
|
||||
) -> ExecutionGraph:
|
||||
del task
|
||||
parsed = parse_agents(agents)
|
||||
edge_list = list(edges or [])
|
||||
if not edge_list:
|
||||
raise ValueError("GraphWorkflow requires edges")
|
||||
dependencies = edges_to_dependencies(agents=parsed, edges=edge_list)
|
||||
validate_output_agent(
|
||||
agents=parsed,
|
||||
dependencies=dependencies,
|
||||
output_agent=str(output_agent or "").strip(),
|
||||
allow_disconnected=allow_disconnected,
|
||||
)
|
||||
return build_graph_from_dependencies(
|
||||
workflow_name=WORKFLOW_NAME,
|
||||
strategy="dag",
|
||||
agents=parsed,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
261
app-instance/backend/beaver/team_workflows/mcp_tools.py
Normal file
261
app-instance/backend/beaver/team_workflows/mcp_tools.py
Normal file
@ -0,0 +1,261 @@
|
||||
"""MCP schema tools for local team workflow graph builders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
|
||||
from beaver.coordinator.models import ExecutionGraph
|
||||
from beaver.tools.base import BaseTool, ToolContext, ToolResult, ToolSpec
|
||||
|
||||
from . import agent_rearrange, concurrent, graph, mixture_of_agents, sequential
|
||||
|
||||
GraphBuilder = Callable[..., ExecutionGraph]
|
||||
|
||||
|
||||
def create_team_workflow_tools() -> list[BaseTool]:
|
||||
return [
|
||||
TeamWorkflowSchemaTool(
|
||||
name="SequentialWorkflow",
|
||||
description=(
|
||||
"Build a sequential Beaver team workflow graph. Use this for staged work "
|
||||
"where each agent depends on the previous agent's output."
|
||||
),
|
||||
input_schema=_sequential_schema(),
|
||||
builder=sequential.build_graph,
|
||||
),
|
||||
TeamWorkflowSchemaTool(
|
||||
name="ConcurrentWorkflow",
|
||||
description=(
|
||||
"Build a concurrent Beaver team workflow graph. Use this only when agents "
|
||||
"can work independently on the same task."
|
||||
),
|
||||
input_schema=_concurrent_schema(),
|
||||
builder=concurrent.build_graph,
|
||||
),
|
||||
TeamWorkflowSchemaTool(
|
||||
name="MixtureOfAgents",
|
||||
description=(
|
||||
"Build a mixture-of-agents Beaver team workflow graph where independent "
|
||||
"expert agents feed one aggregator agent."
|
||||
),
|
||||
input_schema=_mixture_schema(),
|
||||
builder=mixture_of_agents.build_graph,
|
||||
),
|
||||
TeamWorkflowSchemaTool(
|
||||
name="AgentRearrange",
|
||||
description=(
|
||||
"Build a Beaver team workflow graph from strict flow syntax. Use '->' for "
|
||||
"stage order and ',' for agents in the same parallel stage."
|
||||
),
|
||||
input_schema=_agent_rearrange_schema(),
|
||||
builder=agent_rearrange.build_graph,
|
||||
),
|
||||
TeamWorkflowSchemaTool(
|
||||
name="GraphWorkflow",
|
||||
description=(
|
||||
"Build an explicit Beaver DAG workflow graph. Use this advanced tool only "
|
||||
"when the dependency edges must be specified directly."
|
||||
),
|
||||
input_schema=_graph_schema(),
|
||||
builder=graph.build_graph,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class TeamWorkflowSchemaTool(BaseTool):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
description: str,
|
||||
input_schema: dict[str, Any],
|
||||
builder: GraphBuilder,
|
||||
) -> None:
|
||||
self._spec = ToolSpec(
|
||||
name=name,
|
||||
description=description,
|
||||
input_schema=input_schema,
|
||||
toolset="team_workflow",
|
||||
always_available=False,
|
||||
metadata={"category": "team_workflow"},
|
||||
)
|
||||
self._builder = builder
|
||||
|
||||
@property
|
||||
def spec(self) -> ToolSpec:
|
||||
return self._spec
|
||||
|
||||
async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult:
|
||||
del context
|
||||
try:
|
||||
graph = self._builder(**dict(arguments or {}))
|
||||
payload = {
|
||||
"success": True,
|
||||
"workflow": self.spec.name,
|
||||
"graph": _graph_to_dict(graph),
|
||||
}
|
||||
return ToolResult(
|
||||
success=True,
|
||||
content=json.dumps(payload, ensure_ascii=False),
|
||||
tool_name=self.spec.name,
|
||||
raw_output=payload,
|
||||
)
|
||||
except Exception as exc:
|
||||
payload = {"success": False, "workflow": self.spec.name, "error": str(exc)}
|
||||
return ToolResult(
|
||||
success=False,
|
||||
content=json.dumps(payload, ensure_ascii=False),
|
||||
tool_name=self.spec.name,
|
||||
error=str(exc),
|
||||
raw_output=payload,
|
||||
)
|
||||
|
||||
|
||||
def _graph_to_dict(graph: ExecutionGraph) -> dict[str, Any]:
|
||||
return {
|
||||
"strategy": graph.strategy,
|
||||
"nodes": [
|
||||
{
|
||||
"node_id": node.node_id,
|
||||
"task": node.task,
|
||||
"depends_on": list(node.depends_on),
|
||||
"allowed_tool_names": (
|
||||
None if node.allowed_tool_names is None else list(node.allowed_tool_names)
|
||||
),
|
||||
"required_evidence": list(node.required_evidence),
|
||||
"evidence_contract": dict(node.evidence_contract),
|
||||
"validation_rules": list(node.validation_rules),
|
||||
"required_for_completion": node.required_for_completion,
|
||||
"block_downstream_on_partial": node.block_downstream_on_partial,
|
||||
"max_tool_iterations": node.max_tool_iterations,
|
||||
"metadata": dict(node.agent.metadata),
|
||||
}
|
||||
for node in graph.nodes
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _sequential_schema() -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task": _task_schema(),
|
||||
"agents": _agents_schema(),
|
||||
},
|
||||
"required": ["task", "agents"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
def _concurrent_schema() -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task": _task_schema(),
|
||||
"agents": _agents_schema(),
|
||||
},
|
||||
"required": ["task", "agents"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
def _mixture_schema() -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task": _task_schema(),
|
||||
"agents": _agents_schema(description="Expert agents that run independently before aggregation."),
|
||||
"aggregator": _agent_schema(description="Aggregator agent that synthesizes expert outputs."),
|
||||
},
|
||||
"required": ["task", "agents", "aggregator"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
def _agent_rearrange_schema() -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task": _task_schema(),
|
||||
"agents": _agents_schema(),
|
||||
"flow": {
|
||||
"type": "string",
|
||||
"description": "Strict flow syntax, e.g. 'collector -> tactics, players -> synthesizer'.",
|
||||
},
|
||||
},
|
||||
"required": ["task", "agents", "flow"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
def _graph_schema() -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task": _task_schema(),
|
||||
"agents": _agents_schema(),
|
||||
"edges": {
|
||||
"type": "array",
|
||||
"description": "Directed dependency edges as [source_agent, target_agent] pairs.",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 2,
|
||||
"maxItems": 2,
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"output_agent": {
|
||||
"type": "string",
|
||||
"description": "Final output/synthesis agent. Must be reachable from upstream agents.",
|
||||
},
|
||||
"allow_disconnected": {
|
||||
"type": "boolean",
|
||||
"description": "Allow agents that are not connected to output_agent. Defaults to false.",
|
||||
},
|
||||
},
|
||||
"required": ["task", "agents", "edges", "output_agent"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
|
||||
def _task_schema() -> dict[str, Any]:
|
||||
return {
|
||||
"type": "string",
|
||||
"description": "Overall user task this workflow supports.",
|
||||
}
|
||||
|
||||
|
||||
def _agents_schema(*, description: str = "Workflow agents in the order or set used by this workflow.") -> dict[str, Any]:
|
||||
return {
|
||||
"type": "array",
|
||||
"description": description,
|
||||
"items": _agent_schema(),
|
||||
"minItems": 1,
|
||||
}
|
||||
|
||||
|
||||
def _agent_schema(*, description: str = "One workflow agent slot.") -> dict[str, Any]:
|
||||
return {
|
||||
"type": "object",
|
||||
"description": description,
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"instruction": {"type": "string"},
|
||||
"use_skill": {"type": "string"},
|
||||
"skill_query": {"type": "string"},
|
||||
"allowed_tool_names": {"type": "array", "items": {"type": "string"}},
|
||||
"required_evidence": {"type": "array", "items": {"type": "string"}},
|
||||
"evidence_contract": {"type": "object"},
|
||||
"validation_rules": {"type": "array", "items": {"type": "string"}},
|
||||
"required_for_completion": {"type": "boolean"},
|
||||
"block_downstream_on_partial": {"type": "boolean"},
|
||||
"max_tool_iterations": {"type": "integer"},
|
||||
"constraints": {"type": "array", "items": {"type": "string"}},
|
||||
"expected_output": {"type": "string"},
|
||||
"input_contract": {"type": "object"},
|
||||
"output_contract": {"type": "object"},
|
||||
},
|
||||
"required": ["name", "instruction"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
"""MixtureOfAgents graph builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from beaver.coordinator.models import ExecutionGraph
|
||||
|
||||
from .base import (
|
||||
WorkflowAgentSpec,
|
||||
build_graph_from_dependencies,
|
||||
parse_agents,
|
||||
validate_agent_names,
|
||||
)
|
||||
|
||||
WORKFLOW_NAME = "MixtureOfAgents"
|
||||
|
||||
|
||||
def build_graph(
|
||||
*,
|
||||
task: str,
|
||||
agents: Iterable[WorkflowAgentSpec | dict[str, Any]],
|
||||
aggregator: WorkflowAgentSpec | dict[str, Any],
|
||||
) -> ExecutionGraph:
|
||||
del task
|
||||
experts = parse_agents(agents)
|
||||
parsed_aggregator = parse_agents([aggregator])[0]
|
||||
all_agents = [*experts, parsed_aggregator]
|
||||
validate_agent_names(all_agents)
|
||||
dependencies = {agent.name: [] for agent in all_agents}
|
||||
dependencies[parsed_aggregator.name] = [agent.name for agent in experts]
|
||||
return build_graph_from_dependencies(
|
||||
workflow_name=WORKFLOW_NAME,
|
||||
strategy="dag",
|
||||
agents=all_agents,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
29
app-instance/backend/beaver/team_workflows/sequential.py
Normal file
29
app-instance/backend/beaver/team_workflows/sequential.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""SequentialWorkflow graph builder."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable
|
||||
|
||||
from beaver.coordinator.models import ExecutionGraph
|
||||
|
||||
from .base import WorkflowAgentSpec, build_graph_from_dependencies, parse_agents
|
||||
|
||||
WORKFLOW_NAME = "SequentialWorkflow"
|
||||
|
||||
|
||||
def build_graph(
|
||||
*,
|
||||
task: str,
|
||||
agents: Iterable[WorkflowAgentSpec | dict[str, Any]],
|
||||
) -> ExecutionGraph:
|
||||
del task
|
||||
parsed = parse_agents(agents)
|
||||
dependencies = {agent.name: [] for agent in parsed}
|
||||
for previous, current in zip(parsed, parsed[1:], strict=False):
|
||||
dependencies[current.name].append(previous.name)
|
||||
return build_graph_from_dependencies(
|
||||
workflow_name=WORKFLOW_NAME,
|
||||
strategy="sequence",
|
||||
agents=parsed,
|
||||
dependencies=dependencies,
|
||||
)
|
||||
Reference in New Issue
Block a user