feat(orchestration): add orchestration system with task scheduling
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
128
backend/app/agents/orchestration/task_graph.py
Normal file
128
backend/app/agents/orchestration/task_graph.py
Normal file
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from app.agents.schemas.orchestration import ParallelWorthiness, TaskGraph, TaskNode
|
||||
|
||||
|
||||
ROLE_KEYWORDS: list[tuple[str, tuple[str, ...]]] = [
|
||||
("librarian", ("查", "检索", "资料", "文档", "知识库", "年报", "forum", "search")),
|
||||
("analyst", ("分析", "判断", "风险", "总结", "对比", "洞察", "结论")),
|
||||
("schedule_planner", ("计划", "安排", "下周", "日程", "提醒", "优先级")),
|
||||
("executor", ("执行", "创建", "更新", "落库", "提交", "发帖")),
|
||||
]
|
||||
|
||||
|
||||
def build_bounded_task_graph(
|
||||
*,
|
||||
query_text: str,
|
||||
parallel_worthiness: ParallelWorthiness,
|
||||
max_nodes: int = 4,
|
||||
) -> TaskGraph | None:
|
||||
roles = _infer_roles(query_text)
|
||||
if not roles:
|
||||
return None
|
||||
|
||||
independent_roles = roles[: min(max_nodes - 1, max(1, parallel_worthiness.estimated_subtasks))]
|
||||
nodes: list[TaskNode] = []
|
||||
|
||||
for index, role in enumerate(independent_roles, start=1):
|
||||
node_id = f"task-{index}-{uuid4().hex[:6]}"
|
||||
nodes.append(
|
||||
TaskNode(
|
||||
node_id=node_id,
|
||||
title=_build_title(role),
|
||||
role=role,
|
||||
goal=_build_goal(role, query_text),
|
||||
depends_on=[],
|
||||
execution_mode=(
|
||||
"parallel"
|
||||
if parallel_worthiness.preferred_mode in {"collaboration", "parallel"}
|
||||
and len(independent_roles) > 1
|
||||
else "serial"
|
||||
),
|
||||
expected_evidence=_build_expected_evidence(role),
|
||||
)
|
||||
)
|
||||
|
||||
if len(nodes) > 1:
|
||||
merge_id = f"merge-{uuid4().hex[:6]}"
|
||||
nodes.append(
|
||||
TaskNode(
|
||||
node_id=merge_id,
|
||||
title="汇总并收敛最终结论",
|
||||
role="master",
|
||||
goal="汇总前置子任务结果,形成统一可验证的输出。",
|
||||
depends_on=[node.node_id for node in nodes],
|
||||
execution_mode="serial",
|
||||
expected_evidence=[{"type": "merge", "detail": "merged summary and conflict notes"}],
|
||||
)
|
||||
)
|
||||
|
||||
return TaskGraph(
|
||||
nodes=nodes,
|
||||
entry_node_ids=[node.node_id for node in nodes if not node.depends_on],
|
||||
max_parallelism=max(1, len(independent_roles)),
|
||||
rationale=_build_rationale(parallel_worthiness, independent_roles),
|
||||
)
|
||||
|
||||
|
||||
def render_task_graph_summary(task_graph: TaskGraph | None) -> str | None:
|
||||
if task_graph is None or not task_graph.nodes:
|
||||
return None
|
||||
|
||||
lines = ["- 任务图:"]
|
||||
for node in task_graph.nodes[:4]:
|
||||
deps = f" deps={','.join(node.depends_on)}" if node.depends_on else ""
|
||||
lines.append(f" - [{node.execution_mode}] {node.title} ({node.role}){deps}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _infer_roles(query_text: str) -> list[str]:
|
||||
selected: list[str] = []
|
||||
text = query_text or ""
|
||||
for role, keywords in ROLE_KEYWORDS:
|
||||
if any(keyword in text for keyword in keywords):
|
||||
selected.append(role)
|
||||
|
||||
if not selected:
|
||||
return ["analyst"]
|
||||
return selected
|
||||
|
||||
|
||||
def _build_title(role: str) -> str:
|
||||
mapping = {
|
||||
"librarian": "收集事实与外部/内部证据",
|
||||
"analyst": "形成判断与风险分析",
|
||||
"schedule_planner": "整理计划和优先级",
|
||||
"executor": "执行必要操作并回收结果",
|
||||
}
|
||||
return mapping.get(role, "处理子任务")
|
||||
|
||||
|
||||
def _build_goal(role: str, query_text: str) -> str:
|
||||
mapping = {
|
||||
"librarian": f"围绕请求收集支持结论的事实和资料:{query_text}",
|
||||
"analyst": f"基于当前请求输出结构化判断:{query_text}",
|
||||
"schedule_planner": f"把当前请求收束为计划、安排或优先级:{query_text}",
|
||||
"executor": f"基于请求执行必要动作并返回结果:{query_text}",
|
||||
}
|
||||
return mapping.get(role, query_text)
|
||||
|
||||
|
||||
def _build_expected_evidence(role: str) -> list[dict[str, str]]:
|
||||
mapping = {
|
||||
"librarian": [{"type": "evidence", "detail": "retrieval findings"}],
|
||||
"analyst": [{"type": "analysis", "detail": "structured judgment"}],
|
||||
"schedule_planner": [{"type": "plan", "detail": "explicit schedule or priorities"}],
|
||||
"executor": [{"type": "execution", "detail": "tool output or mutation result"}],
|
||||
}
|
||||
return mapping.get(role, [{"type": "summary", "detail": "task summary"}])
|
||||
|
||||
|
||||
def _build_rationale(parallel_worthiness: ParallelWorthiness, roles: list[str]) -> str:
|
||||
return (
|
||||
f"preferred_mode={parallel_worthiness.preferred_mode}; "
|
||||
f"score={parallel_worthiness.score:.2f}; "
|
||||
f"roles={','.join(roles)}"
|
||||
)
|
||||
Reference in New Issue
Block a user