zero-agent-core-multi-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_core_multi_agent/__init__.py +68 -0
- agent_core_multi_agent/adapters.py +139 -0
- agent_core_multi_agent/errors.py +39 -0
- agent_core_multi_agent/protocols.py +49 -0
- agent_core_multi_agent/py.typed +1 -0
- agent_core_multi_agent/registry.py +133 -0
- agent_core_multi_agent/routing.py +124 -0
- agent_core_multi_agent/storage.py +110 -0
- agent_core_multi_agent/supervisor.py +554 -0
- agent_core_multi_agent/types.py +450 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/METADATA +72 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/RECORD +14 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/WHEEL +4 -0
- zero_agent_core_multi_agent-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Handwritten Agent Core 的多 Agent 编排扩展。"""
|
|
2
|
+
|
|
3
|
+
from agent_core_multi_agent.adapters import (
|
|
4
|
+
AgentFunction,
|
|
5
|
+
CallableAgentInvoker,
|
|
6
|
+
RuntimeAgentInvoker,
|
|
7
|
+
RuntimeResultMapper,
|
|
8
|
+
)
|
|
9
|
+
from agent_core_multi_agent.errors import (
|
|
10
|
+
AgentAccessDeniedError,
|
|
11
|
+
AgentInvocationError,
|
|
12
|
+
AgentNotFoundError,
|
|
13
|
+
AgentRegistrationError,
|
|
14
|
+
AgentRoutingError,
|
|
15
|
+
CoordinationBudgetExceededError,
|
|
16
|
+
CoordinationBusyError,
|
|
17
|
+
CoordinationLeaseLostError,
|
|
18
|
+
MultiAgentError,
|
|
19
|
+
)
|
|
20
|
+
from agent_core_multi_agent.protocols import AgentInvoker, AgentRouter, CoordinationStore
|
|
21
|
+
from agent_core_multi_agent.registry import AgentRegistry
|
|
22
|
+
from agent_core_multi_agent.routing import CapabilityRouter, ModelAgentRouter
|
|
23
|
+
from agent_core_multi_agent.storage import WorkflowCoordinationStore
|
|
24
|
+
from agent_core_multi_agent.supervisor import SupervisorAgent
|
|
25
|
+
from agent_core_multi_agent.types import (
|
|
26
|
+
AgentDescriptor,
|
|
27
|
+
AgentResult,
|
|
28
|
+
AgentResultStatus,
|
|
29
|
+
AgentTask,
|
|
30
|
+
CoordinationExecution,
|
|
31
|
+
CoordinationPolicy,
|
|
32
|
+
CoordinationStatus,
|
|
33
|
+
HandoffRequest,
|
|
34
|
+
RoutingDecision,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AgentAccessDeniedError",
|
|
39
|
+
"AgentDescriptor",
|
|
40
|
+
"AgentFunction",
|
|
41
|
+
"AgentInvocationError",
|
|
42
|
+
"AgentInvoker",
|
|
43
|
+
"AgentNotFoundError",
|
|
44
|
+
"AgentRegistrationError",
|
|
45
|
+
"AgentRegistry",
|
|
46
|
+
"AgentResult",
|
|
47
|
+
"AgentResultStatus",
|
|
48
|
+
"AgentRouter",
|
|
49
|
+
"AgentRoutingError",
|
|
50
|
+
"AgentTask",
|
|
51
|
+
"CallableAgentInvoker",
|
|
52
|
+
"CapabilityRouter",
|
|
53
|
+
"CoordinationBudgetExceededError",
|
|
54
|
+
"CoordinationBusyError",
|
|
55
|
+
"CoordinationExecution",
|
|
56
|
+
"CoordinationLeaseLostError",
|
|
57
|
+
"CoordinationPolicy",
|
|
58
|
+
"CoordinationStatus",
|
|
59
|
+
"CoordinationStore",
|
|
60
|
+
"HandoffRequest",
|
|
61
|
+
"ModelAgentRouter",
|
|
62
|
+
"MultiAgentError",
|
|
63
|
+
"RoutingDecision",
|
|
64
|
+
"RuntimeAgentInvoker",
|
|
65
|
+
"RuntimeResultMapper",
|
|
66
|
+
"SupervisorAgent",
|
|
67
|
+
"WorkflowCoordinationStore",
|
|
68
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""函数和 Core AgentRuntime 的调用适配器。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
from agent_core import AgentRuntime, RunContext
|
|
9
|
+
|
|
10
|
+
from agent_core_multi_agent.types import AgentResult, AgentResultStatus, AgentTask
|
|
11
|
+
|
|
12
|
+
AgentFunction = Callable[
|
|
13
|
+
[AgentTask],
|
|
14
|
+
AgentResult | str | Awaitable[AgentResult | str],
|
|
15
|
+
]
|
|
16
|
+
RuntimeResultMapper = Callable[
|
|
17
|
+
[RunContext, AgentTask],
|
|
18
|
+
AgentResult | Awaitable[AgentResult],
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CallableAgentInvoker:
|
|
23
|
+
"""把一个异步函数包装成 AgentInvoker。"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, agent_id: str, function: AgentFunction) -> None:
|
|
26
|
+
if not agent_id.strip():
|
|
27
|
+
raise ValueError("agent_id 不能为空")
|
|
28
|
+
self._agent_id = agent_id
|
|
29
|
+
self._function = function
|
|
30
|
+
|
|
31
|
+
async def invoke(self, task: AgentTask) -> AgentResult:
|
|
32
|
+
called = self._function(task)
|
|
33
|
+
value = await called if inspect.isawaitable(called) else called
|
|
34
|
+
if isinstance(value, AgentResult):
|
|
35
|
+
return value
|
|
36
|
+
return AgentResult(
|
|
37
|
+
task_id=task.task_id,
|
|
38
|
+
agent_id=self._agent_id,
|
|
39
|
+
status="completed",
|
|
40
|
+
output=str(value),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RuntimeAgentInvoker:
|
|
45
|
+
"""把现有 ``AgentRuntime`` 接入多 Agent 注册表。"""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
agent_id: str,
|
|
50
|
+
runtime: AgentRuntime,
|
|
51
|
+
*,
|
|
52
|
+
result_mapper: RuntimeResultMapper | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
if not agent_id.strip():
|
|
55
|
+
raise ValueError("agent_id 不能为空")
|
|
56
|
+
self._agent_id = agent_id
|
|
57
|
+
self._runtime = runtime
|
|
58
|
+
self._result_mapper = result_mapper
|
|
59
|
+
|
|
60
|
+
async def invoke(self, task: AgentTask) -> AgentResult:
|
|
61
|
+
access = task.access
|
|
62
|
+
context = await self._runtime.run(
|
|
63
|
+
task.thread_id,
|
|
64
|
+
task.input,
|
|
65
|
+
user_id=access.user_id if access else None,
|
|
66
|
+
tenant_id=access.tenant_id if access else None,
|
|
67
|
+
parent_run_id=task.parent_run_id or task.coordination_id,
|
|
68
|
+
idempotency_key=task.idempotency_key,
|
|
69
|
+
metadata={
|
|
70
|
+
**task.metadata,
|
|
71
|
+
"multi_agent_execution_id": task.coordination_id,
|
|
72
|
+
"multi_agent_task_id": task.task_id,
|
|
73
|
+
"multi_agent_context": task.context,
|
|
74
|
+
"multi_agent_lineage": list(task.lineage),
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
if (
|
|
78
|
+
task.resume_requested
|
|
79
|
+
and context.status not in {"completed", "cancelled"}
|
|
80
|
+
and context.checkpoint
|
|
81
|
+
):
|
|
82
|
+
await self._runtime.resume(
|
|
83
|
+
task.thread_id,
|
|
84
|
+
context.run_id,
|
|
85
|
+
access=access,
|
|
86
|
+
)
|
|
87
|
+
context = await self._runtime.wait(
|
|
88
|
+
task.thread_id,
|
|
89
|
+
context.run_id,
|
|
90
|
+
access=access,
|
|
91
|
+
)
|
|
92
|
+
if self._result_mapper is not None:
|
|
93
|
+
mapped = self._result_mapper(context, task)
|
|
94
|
+
return await mapped if inspect.isawaitable(mapped) else mapped
|
|
95
|
+
return self._default_result(context, task)
|
|
96
|
+
|
|
97
|
+
def _default_result(self, context: RunContext, task: AgentTask) -> AgentResult:
|
|
98
|
+
status = _runtime_status(context.status)
|
|
99
|
+
output = ""
|
|
100
|
+
error: str | None = None
|
|
101
|
+
usage: dict[str, float] = {}
|
|
102
|
+
for event in context.events:
|
|
103
|
+
for key, value in event.usage.items():
|
|
104
|
+
if isinstance(value, int | float) and not isinstance(value, bool):
|
|
105
|
+
usage[key] = usage.get(key, 0.0) + float(value)
|
|
106
|
+
if event.event_type == "run_completed":
|
|
107
|
+
output = str(event.payload.get("answer", ""))
|
|
108
|
+
if event.event_type == "run_failed":
|
|
109
|
+
error = event.error or str(event.payload.get("error", "Agent 运行失败"))
|
|
110
|
+
if status == "failed" and not error:
|
|
111
|
+
error = "Agent 运行失败"
|
|
112
|
+
return AgentResult(
|
|
113
|
+
task_id=task.task_id,
|
|
114
|
+
agent_id=self._agent_id,
|
|
115
|
+
status=status,
|
|
116
|
+
output=output,
|
|
117
|
+
run_id=context.run_id,
|
|
118
|
+
error=error,
|
|
119
|
+
usage=usage,
|
|
120
|
+
metadata={"runtime_status": context.status},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _runtime_status(status: str) -> AgentResultStatus:
|
|
125
|
+
if status == "completed":
|
|
126
|
+
return "completed"
|
|
127
|
+
if status == "failed":
|
|
128
|
+
return "failed"
|
|
129
|
+
if status == "cancelled":
|
|
130
|
+
return "cancelled"
|
|
131
|
+
return "interrupted"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = [
|
|
135
|
+
"AgentFunction",
|
|
136
|
+
"CallableAgentInvoker",
|
|
137
|
+
"RuntimeAgentInvoker",
|
|
138
|
+
"RuntimeResultMapper",
|
|
139
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""多 Agent 编排扩展错误。"""
|
|
2
|
+
|
|
3
|
+
from agent_core import AgentError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MultiAgentError(AgentError):
|
|
7
|
+
"""多 Agent 扩展基础错误。"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentRegistrationError(MultiAgentError):
|
|
11
|
+
"""Agent 注册信息无效或发生名称冲突。"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentNotFoundError(MultiAgentError):
|
|
15
|
+
"""注册表中不存在目标 Agent。"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentAccessDeniedError(MultiAgentError):
|
|
19
|
+
"""当前访问身份无权调用目标 Agent。"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgentRoutingError(MultiAgentError):
|
|
23
|
+
"""无法为任务选择唯一且可访问的 Agent。"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentInvocationError(MultiAgentError):
|
|
27
|
+
"""子 Agent 调用失败或返回了无效结果。"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CoordinationBudgetExceededError(MultiAgentError):
|
|
31
|
+
"""协调实例超过 handoff、调用次数、访问次数或使用量预算。"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CoordinationBusyError(MultiAgentError):
|
|
35
|
+
"""协调实例已经由其他 Worker 持有。"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CoordinationLeaseLostError(MultiAgentError):
|
|
39
|
+
"""执行过程中失去协调实例租约。"""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""多 Agent 扩展的依赖倒置协议。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from agent_core import AccessContext
|
|
8
|
+
|
|
9
|
+
from agent_core_multi_agent.types import AgentResult, AgentTask, CoordinationExecution, RoutingDecision
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from agent_core_multi_agent.registry import AgentRegistry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class AgentInvoker(Protocol):
|
|
17
|
+
"""任何可由 Supervisor 调用的 Agent 最小接口。"""
|
|
18
|
+
|
|
19
|
+
async def invoke(self, task: AgentTask) -> AgentResult: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class AgentRouter(Protocol):
|
|
24
|
+
"""根据任务和当前可访问注册表选择 Agent。"""
|
|
25
|
+
|
|
26
|
+
async def route(self, task: AgentTask, registry: AgentRegistry) -> RoutingDecision: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@runtime_checkable
|
|
30
|
+
class CoordinationStore(Protocol):
|
|
31
|
+
"""协调实例持久化协议。"""
|
|
32
|
+
|
|
33
|
+
async def save(self, execution: CoordinationExecution) -> None: ...
|
|
34
|
+
|
|
35
|
+
async def load(
|
|
36
|
+
self,
|
|
37
|
+
execution_id: str,
|
|
38
|
+
*,
|
|
39
|
+
access: AccessContext | None = None,
|
|
40
|
+
) -> CoordinationExecution | None: ...
|
|
41
|
+
|
|
42
|
+
async def list(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
access: AccessContext | None = None,
|
|
46
|
+
) -> list[CoordinationExecution]: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["AgentInvoker", "AgentRouter", "CoordinationStore"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Agent 注册、能力发现、权限过滤和并发限制。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from agent_core import AccessContext
|
|
9
|
+
|
|
10
|
+
from agent_core_multi_agent.errors import (
|
|
11
|
+
AgentAccessDeniedError,
|
|
12
|
+
AgentInvocationError,
|
|
13
|
+
AgentNotFoundError,
|
|
14
|
+
AgentRegistrationError,
|
|
15
|
+
)
|
|
16
|
+
from agent_core_multi_agent.protocols import AgentInvoker
|
|
17
|
+
from agent_core_multi_agent.types import AgentDescriptor, AgentResult, AgentTask
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class _RegisteredAgent:
|
|
22
|
+
descriptor: AgentDescriptor
|
|
23
|
+
invoker: AgentInvoker
|
|
24
|
+
semaphore: asyncio.Semaphore | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AgentRegistry:
|
|
28
|
+
"""保存 Agent 描述和调用器,并在调用前执行权限与并发检查。"""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._agents: dict[str, _RegisteredAgent] = {}
|
|
32
|
+
self._aliases: dict[str, str] = {}
|
|
33
|
+
|
|
34
|
+
def register(
|
|
35
|
+
self,
|
|
36
|
+
descriptor: AgentDescriptor,
|
|
37
|
+
invoker: AgentInvoker,
|
|
38
|
+
*,
|
|
39
|
+
replace: bool = False,
|
|
40
|
+
) -> None:
|
|
41
|
+
agent_id = descriptor.agent_id
|
|
42
|
+
collisions = {
|
|
43
|
+
name
|
|
44
|
+
for name in (agent_id, *descriptor.aliases)
|
|
45
|
+
if name in self._aliases and self._aliases[name] != agent_id
|
|
46
|
+
}
|
|
47
|
+
if collisions:
|
|
48
|
+
raise AgentRegistrationError(f"Agent 名称或别名冲突:{', '.join(sorted(collisions))}")
|
|
49
|
+
if agent_id in self._agents and not replace:
|
|
50
|
+
raise AgentRegistrationError(f"Agent 已注册:{agent_id}")
|
|
51
|
+
if replace:
|
|
52
|
+
self.unregister(agent_id)
|
|
53
|
+
semaphore = (
|
|
54
|
+
asyncio.Semaphore(descriptor.max_concurrency) if descriptor.max_concurrency is not None else None
|
|
55
|
+
)
|
|
56
|
+
self._agents[agent_id] = _RegisteredAgent(descriptor, invoker, semaphore)
|
|
57
|
+
self._aliases[agent_id] = agent_id
|
|
58
|
+
for alias in descriptor.aliases:
|
|
59
|
+
self._aliases[alias] = agent_id
|
|
60
|
+
|
|
61
|
+
def unregister(self, agent_id: str) -> bool:
|
|
62
|
+
normalized = self._aliases.get(agent_id, agent_id)
|
|
63
|
+
registered = self._agents.pop(normalized, None)
|
|
64
|
+
if registered is None:
|
|
65
|
+
return False
|
|
66
|
+
for name, owner in tuple(self._aliases.items()):
|
|
67
|
+
if owner == normalized:
|
|
68
|
+
del self._aliases[name]
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
def get_descriptor(
|
|
72
|
+
self,
|
|
73
|
+
agent_id: str,
|
|
74
|
+
*,
|
|
75
|
+
access: AccessContext | None = None,
|
|
76
|
+
) -> AgentDescriptor:
|
|
77
|
+
registered = self._resolve(agent_id)
|
|
78
|
+
self._check_access(registered.descriptor, access)
|
|
79
|
+
return registered.descriptor
|
|
80
|
+
|
|
81
|
+
def descriptors(self, *, access: AccessContext | None = None) -> tuple[AgentDescriptor, ...]:
|
|
82
|
+
values = [item.descriptor for item in self._agents.values() if item.descriptor.can_invoke(access)]
|
|
83
|
+
return tuple(sorted(values, key=lambda item: (-item.priority, item.agent_id)))
|
|
84
|
+
|
|
85
|
+
def candidates(
|
|
86
|
+
self,
|
|
87
|
+
capability: str,
|
|
88
|
+
*,
|
|
89
|
+
access: AccessContext | None = None,
|
|
90
|
+
) -> tuple[AgentDescriptor, ...]:
|
|
91
|
+
normalized = capability.strip()
|
|
92
|
+
return tuple(
|
|
93
|
+
descriptor
|
|
94
|
+
for descriptor in self.descriptors(access=access)
|
|
95
|
+
if normalized in descriptor.capabilities
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def invoke(self, agent_id: str, task: AgentTask) -> AgentResult:
|
|
99
|
+
registered = self._resolve(agent_id)
|
|
100
|
+
self._check_access(registered.descriptor, task.access)
|
|
101
|
+
|
|
102
|
+
async def execute() -> AgentResult:
|
|
103
|
+
result = await registered.invoker.invoke(task)
|
|
104
|
+
if result.task_id != task.task_id:
|
|
105
|
+
raise AgentInvocationError(
|
|
106
|
+
f"Agent {registered.descriptor.agent_id} 返回了错误的 task_id:{result.task_id}"
|
|
107
|
+
)
|
|
108
|
+
if result.agent_id != registered.descriptor.agent_id:
|
|
109
|
+
raise AgentInvocationError(
|
|
110
|
+
f"Agent 返回 ID {result.agent_id} 与注册 ID {registered.descriptor.agent_id} 不一致"
|
|
111
|
+
)
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
if registered.semaphore is None:
|
|
115
|
+
return await execute()
|
|
116
|
+
async with registered.semaphore:
|
|
117
|
+
return await execute()
|
|
118
|
+
|
|
119
|
+
def _resolve(self, name: str) -> _RegisteredAgent:
|
|
120
|
+
agent_id = self._aliases.get(name)
|
|
121
|
+
if agent_id is None:
|
|
122
|
+
available = ", ".join(sorted(self._agents)) or "无"
|
|
123
|
+
raise AgentNotFoundError(f"Agent 不存在:{name};已注册:{available}")
|
|
124
|
+
return self._agents[agent_id]
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def _check_access(descriptor: AgentDescriptor, access: AccessContext | None) -> None:
|
|
128
|
+
if not descriptor.can_invoke(access):
|
|
129
|
+
roles = ", ".join(sorted(descriptor.required_roles))
|
|
130
|
+
raise AgentAccessDeniedError(f"调用 Agent {descriptor.agent_id} 需要角色:{roles}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
__all__ = ["AgentRegistry"]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""确定性能力路由和可选模型路由。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_core import ModelAdapter, StructuredOutputSpec, chat_structured, system_message, user_message
|
|
9
|
+
|
|
10
|
+
from agent_core_multi_agent.errors import AgentRoutingError
|
|
11
|
+
from agent_core_multi_agent.registry import AgentRegistry
|
|
12
|
+
from agent_core_multi_agent.types import AgentTask, RoutingDecision
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CapabilityRouter:
|
|
16
|
+
"""优先显式目标,其次能力匹配,最后使用可选默认 Agent。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, default_agent_id: str | None = None) -> None:
|
|
19
|
+
self._default_agent_id = default_agent_id
|
|
20
|
+
|
|
21
|
+
async def route(self, task: AgentTask, registry: AgentRegistry) -> RoutingDecision:
|
|
22
|
+
if task.target_agent_id:
|
|
23
|
+
descriptor = registry.get_descriptor(task.target_agent_id, access=task.access)
|
|
24
|
+
if (
|
|
25
|
+
task.required_capability
|
|
26
|
+
and task.required_capability not in descriptor.capabilities
|
|
27
|
+
):
|
|
28
|
+
raise AgentRoutingError(
|
|
29
|
+
f"Agent {descriptor.agent_id} 不提供能力:{task.required_capability}"
|
|
30
|
+
)
|
|
31
|
+
return RoutingDecision(descriptor.agent_id, "任务显式指定目标 Agent")
|
|
32
|
+
|
|
33
|
+
if task.required_capability:
|
|
34
|
+
candidates = registry.candidates(task.required_capability, access=task.access)
|
|
35
|
+
if not candidates:
|
|
36
|
+
raise AgentRoutingError(f"没有可访问的 Agent 提供能力:{task.required_capability}")
|
|
37
|
+
selected = candidates[0]
|
|
38
|
+
return RoutingDecision(
|
|
39
|
+
selected.agent_id,
|
|
40
|
+
f"按能力 {task.required_capability} 和优先级选择",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if self._default_agent_id:
|
|
44
|
+
descriptor = registry.get_descriptor(self._default_agent_id, access=task.access)
|
|
45
|
+
return RoutingDecision(descriptor.agent_id, "使用默认 Agent")
|
|
46
|
+
|
|
47
|
+
candidates = registry.descriptors(access=task.access)
|
|
48
|
+
if len(candidates) == 1:
|
|
49
|
+
return RoutingDecision(candidates[0].agent_id, "只有一个可访问的 Agent")
|
|
50
|
+
raise AgentRoutingError("任务没有指定目标或能力,且无法唯一选择 Agent")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ModelAgentRouter:
|
|
54
|
+
"""让模型在权限过滤后的候选集合中选择 Agent。"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
model: ModelAdapter,
|
|
59
|
+
*,
|
|
60
|
+
fallback: CapabilityRouter | None = None,
|
|
61
|
+
max_retries: int = 1,
|
|
62
|
+
) -> None:
|
|
63
|
+
self._model = model
|
|
64
|
+
self._fallback = fallback or CapabilityRouter()
|
|
65
|
+
self._max_retries = max_retries
|
|
66
|
+
|
|
67
|
+
async def route(self, task: AgentTask, registry: AgentRegistry) -> RoutingDecision:
|
|
68
|
+
if task.target_agent_id or task.required_capability:
|
|
69
|
+
return await self._fallback.route(task, registry)
|
|
70
|
+
|
|
71
|
+
candidates = registry.descriptors(access=task.access)
|
|
72
|
+
if not candidates:
|
|
73
|
+
raise AgentRoutingError("没有可访问的 Agent")
|
|
74
|
+
if len(candidates) == 1:
|
|
75
|
+
return RoutingDecision(candidates[0].agent_id, "只有一个可访问的 Agent")
|
|
76
|
+
|
|
77
|
+
agent_ids = [item.agent_id for item in candidates]
|
|
78
|
+
candidate_payload = [
|
|
79
|
+
{
|
|
80
|
+
"agent_id": item.agent_id,
|
|
81
|
+
"description": item.description,
|
|
82
|
+
"capabilities": sorted(item.capabilities),
|
|
83
|
+
}
|
|
84
|
+
for item in candidates
|
|
85
|
+
]
|
|
86
|
+
spec = StructuredOutputSpec[RoutingDecision](
|
|
87
|
+
name="agent_routing_decision",
|
|
88
|
+
schema={
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {
|
|
91
|
+
"agent_id": {"type": "string", "enum": agent_ids},
|
|
92
|
+
"reason": {"type": "string", "minLength": 1},
|
|
93
|
+
},
|
|
94
|
+
"required": ["agent_id", "reason"],
|
|
95
|
+
"additionalProperties": False,
|
|
96
|
+
},
|
|
97
|
+
decoder=self._decode,
|
|
98
|
+
max_retries=self._max_retries,
|
|
99
|
+
)
|
|
100
|
+
result = await chat_structured(
|
|
101
|
+
self._model,
|
|
102
|
+
[
|
|
103
|
+
system_message(
|
|
104
|
+
"你是多 Agent 路由器。只能从候选 Agent 中选择一个最适合当前任务的 Agent。"
|
|
105
|
+
),
|
|
106
|
+
user_message(
|
|
107
|
+
"任务:"
|
|
108
|
+
f"{task.input}\n上下文:{json.dumps(task.context, ensure_ascii=False, default=str)}"
|
|
109
|
+
f"\n候选:{json.dumps(candidate_payload, ensure_ascii=False)}"
|
|
110
|
+
),
|
|
111
|
+
],
|
|
112
|
+
spec,
|
|
113
|
+
temperature=0,
|
|
114
|
+
)
|
|
115
|
+
return result.value
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _decode(value: Any) -> RoutingDecision:
|
|
119
|
+
if not isinstance(value, dict):
|
|
120
|
+
raise TypeError("路由结果必须是对象")
|
|
121
|
+
return RoutingDecision(agent_id=str(value["agent_id"]), reason=str(value["reason"]))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = ["CapabilityRouter", "ModelAgentRouter"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""复用 Core Workflow Store 的协调实例存储适配器。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from agent_core import (
|
|
8
|
+
AccessContext,
|
|
9
|
+
MemoryWorkflowExecutionStore,
|
|
10
|
+
WorkflowExecution,
|
|
11
|
+
WorkflowExecutionStore,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from agent_core_multi_agent.types import CoordinationExecution
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WorkflowCoordinationStore:
|
|
18
|
+
"""把协调快照保存到 Core 的 Memory/SQLite/PostgreSQL Workflow Store。"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
store: WorkflowExecutionStore | None = None,
|
|
23
|
+
*,
|
|
24
|
+
definition_id: str = "multi-agent-supervisor",
|
|
25
|
+
require_access: bool = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._store = store or MemoryWorkflowExecutionStore()
|
|
28
|
+
self._definition_id = definition_id
|
|
29
|
+
self._require_access = require_access
|
|
30
|
+
|
|
31
|
+
async def save(self, execution: CoordinationExecution) -> None:
|
|
32
|
+
access = execution.access
|
|
33
|
+
if self._require_access and access is None:
|
|
34
|
+
raise PermissionError("该 CoordinationStore 要求提供 AccessContext")
|
|
35
|
+
payload = execution.to_dict()
|
|
36
|
+
try:
|
|
37
|
+
json.dumps(payload, ensure_ascii=False)
|
|
38
|
+
except (TypeError, ValueError) as exc:
|
|
39
|
+
raise TypeError("协调实例包含不可 JSON 序列化的数据") from exc
|
|
40
|
+
record = WorkflowExecution(
|
|
41
|
+
execution_id=execution.execution_id,
|
|
42
|
+
definition_id=self._definition_id,
|
|
43
|
+
execution_type="multi_agent",
|
|
44
|
+
user_id=access.user_id if access else None,
|
|
45
|
+
tenant_id=access.tenant_id if access else None,
|
|
46
|
+
status=execution.status,
|
|
47
|
+
current_step=execution.current_agent_id,
|
|
48
|
+
steps_completed=len(execution.results),
|
|
49
|
+
total_steps=execution.agent_calls,
|
|
50
|
+
input_data=execution.root_task.to_dict(),
|
|
51
|
+
result_data=payload,
|
|
52
|
+
events=[event.to_dict() for event in execution.events],
|
|
53
|
+
version=execution.version,
|
|
54
|
+
created_at=execution.created_at,
|
|
55
|
+
updated_at=execution.updated_at,
|
|
56
|
+
)
|
|
57
|
+
await self._store.save(record)
|
|
58
|
+
execution.version = record.version
|
|
59
|
+
execution.updated_at = record.updated_at
|
|
60
|
+
|
|
61
|
+
async def load(
|
|
62
|
+
self,
|
|
63
|
+
execution_id: str,
|
|
64
|
+
*,
|
|
65
|
+
access: AccessContext | None = None,
|
|
66
|
+
) -> CoordinationExecution | None:
|
|
67
|
+
record = await self._store.load(execution_id)
|
|
68
|
+
if record is None or record.execution_type != "multi_agent":
|
|
69
|
+
return None
|
|
70
|
+
self._check_access(record, access)
|
|
71
|
+
execution = CoordinationExecution.from_dict(record.result_data)
|
|
72
|
+
execution.version = record.version
|
|
73
|
+
execution.updated_at = record.updated_at
|
|
74
|
+
return execution
|
|
75
|
+
|
|
76
|
+
async def list(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
access: AccessContext | None = None,
|
|
80
|
+
) -> list[CoordinationExecution]:
|
|
81
|
+
if self._require_access and access is None:
|
|
82
|
+
raise PermissionError("该 CoordinationStore 要求提供 AccessContext")
|
|
83
|
+
tenant_id = access.tenant_id if access else None
|
|
84
|
+
records = await self._store.list(self._definition_id, tenant_id)
|
|
85
|
+
executions: list[CoordinationExecution] = []
|
|
86
|
+
for record in records:
|
|
87
|
+
if record.execution_type != "multi_agent":
|
|
88
|
+
continue
|
|
89
|
+
if access is not None and not access.can_access(record.user_id, record.tenant_id):
|
|
90
|
+
continue
|
|
91
|
+
execution = CoordinationExecution.from_dict(record.result_data)
|
|
92
|
+
execution.version = record.version
|
|
93
|
+
execution.updated_at = record.updated_at
|
|
94
|
+
executions.append(execution)
|
|
95
|
+
return executions
|
|
96
|
+
|
|
97
|
+
def _check_access(
|
|
98
|
+
self,
|
|
99
|
+
record: WorkflowExecution,
|
|
100
|
+
access: AccessContext | None,
|
|
101
|
+
) -> None:
|
|
102
|
+
if self._require_access and access is None:
|
|
103
|
+
raise PermissionError("该 CoordinationStore 要求提供 AccessContext")
|
|
104
|
+
if self._require_access and (record.user_id is None or record.tenant_id is None):
|
|
105
|
+
raise PermissionError("该协调实例尚未绑定用户和租户")
|
|
106
|
+
if access is not None and not access.can_access(record.user_id, record.tenant_id):
|
|
107
|
+
raise PermissionError("无权访问该协调实例")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ["WorkflowCoordinationStore"]
|