feat: add agent registry manifests and coverage
Introduce a manifest-backed agent registry surface and align graph tests with the new runtime prompt and tool indexing behavior.
This commit is contained in:
11
backend/app/agents/registry/__init__.py
Normal file
11
backend/app/agents/registry/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Registry manifest models and validation helpers."""
|
||||
|
||||
from app.agents.registry.indexes import RegistryIndexes, build_registry_indexes
|
||||
from app.agents.registry.loader import RegistryBundle, load_builtin_registry_bundle
|
||||
|
||||
__all__ = [
|
||||
"RegistryBundle",
|
||||
"RegistryIndexes",
|
||||
"build_registry_indexes",
|
||||
"load_builtin_registry_bundle",
|
||||
]
|
||||
114
backend/app/agents/registry/builtins.py
Normal file
114
backend/app/agents/registry/builtins.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from app.agents.prompts import SUB_COMMANDER_PROMPTS_BY_KEY
|
||||
from app.agents.registry.models import (
|
||||
AgentManifest,
|
||||
CapabilityManifest,
|
||||
SpecialistTemplateManifest,
|
||||
SubCommanderManifest,
|
||||
)
|
||||
from app.agents.state import AgentRole
|
||||
from app.agents.tools import SUB_COMMANDER_TOOLSETS
|
||||
|
||||
|
||||
TOP_LEVEL_AGENT_DEFAULT_SUB_COMMANDERS: dict[str, tuple[str, ...]] = {
|
||||
AgentRole.MASTER.value: (),
|
||||
AgentRole.SCHEDULE_PLANNER.value: (
|
||||
"schedule_analysis",
|
||||
"schedule_planning",
|
||||
),
|
||||
AgentRole.EXECUTOR.value: (
|
||||
"executor_tasks",
|
||||
"executor_forum",
|
||||
),
|
||||
AgentRole.LIBRARIAN.value: (
|
||||
"librarian_retrieval",
|
||||
"librarian_graph",
|
||||
),
|
||||
AgentRole.ANALYST.value: (
|
||||
"analyst_progress",
|
||||
"analyst_insights",
|
||||
),
|
||||
}
|
||||
|
||||
TOP_LEVEL_AGENT_DISPLAY_NAMES: dict[str, str] = {
|
||||
AgentRole.MASTER.value: "Master",
|
||||
AgentRole.SCHEDULE_PLANNER.value: "Schedule Planner",
|
||||
AgentRole.EXECUTOR.value: "Executor",
|
||||
AgentRole.LIBRARIAN.value: "Librarian",
|
||||
AgentRole.ANALYST.value: "Analyst",
|
||||
}
|
||||
|
||||
TOP_LEVEL_AGENT_ROUTING_HINTS: dict[str, tuple[str, ...]] = {
|
||||
AgentRole.MASTER.value: (
|
||||
"Route user requests to the most suitable top-level runtime agent or answer directly.",
|
||||
),
|
||||
AgentRole.SCHEDULE_PLANNER.value: (
|
||||
"Handle planning-oriented requests using schedule analysis and schedule planning sub-commanders.",
|
||||
),
|
||||
AgentRole.EXECUTOR.value: (
|
||||
"Handle execution-oriented requests using task and forum sub-commanders.",
|
||||
),
|
||||
AgentRole.LIBRARIAN.value: (
|
||||
"Handle knowledge retrieval and graph-context requests using librarian sub-commanders.",
|
||||
),
|
||||
AgentRole.ANALYST.value: (
|
||||
"Handle reporting and insight requests using analyst sub-commanders.",
|
||||
),
|
||||
}
|
||||
|
||||
SUB_COMMANDER_PARENT_AGENT_IDS: dict[str, str] = {
|
||||
"schedule_analysis": AgentRole.SCHEDULE_PLANNER.value,
|
||||
"schedule_planning": AgentRole.SCHEDULE_PLANNER.value,
|
||||
"executor_tasks": AgentRole.EXECUTOR.value,
|
||||
"executor_forum": AgentRole.EXECUTOR.value,
|
||||
"librarian_retrieval": AgentRole.LIBRARIAN.value,
|
||||
"librarian_graph": AgentRole.LIBRARIAN.value,
|
||||
"analyst_progress": AgentRole.ANALYST.value,
|
||||
"analyst_insights": AgentRole.ANALYST.value,
|
||||
}
|
||||
|
||||
|
||||
BUILTIN_AGENT_MANIFESTS: tuple[AgentManifest, ...] = tuple(
|
||||
AgentManifest(
|
||||
agent_id=role.value,
|
||||
display_name=TOP_LEVEL_AGENT_DISPLAY_NAMES[role.value],
|
||||
role_value=role.value,
|
||||
system_prompt_key=role.value,
|
||||
routing_hints=list(TOP_LEVEL_AGENT_ROUTING_HINTS[role.value]),
|
||||
default_sub_commanders=list(TOP_LEVEL_AGENT_DEFAULT_SUB_COMMANDERS[role.value]),
|
||||
skill_context_key=role.value.replace("agent_", ""),
|
||||
)
|
||||
for role in AgentRole
|
||||
)
|
||||
|
||||
|
||||
_capability_tool_names = tuple(
|
||||
dict.fromkeys(
|
||||
tool.name
|
||||
for tools in SUB_COMMANDER_TOOLSETS.values()
|
||||
for tool in tools
|
||||
)
|
||||
)
|
||||
|
||||
BUILTIN_CAPABILITY_MANIFESTS: tuple[CapabilityManifest, ...] = tuple(
|
||||
CapabilityManifest(
|
||||
capability_id=tool_name,
|
||||
tool_name=tool_name,
|
||||
)
|
||||
for tool_name in _capability_tool_names
|
||||
)
|
||||
|
||||
|
||||
BUILTIN_SUB_COMMANDER_MANIFESTS: tuple[SubCommanderManifest, ...] = tuple(
|
||||
SubCommanderManifest(
|
||||
sub_commander_id=sub_commander_id,
|
||||
parent_agent_id=SUB_COMMANDER_PARENT_AGENT_IDS[sub_commander_id],
|
||||
prompt_text=SUB_COMMANDER_PROMPTS_BY_KEY[sub_commander_id],
|
||||
capability_ids=list(
|
||||
dict.fromkeys(tool.name for tool in tools)
|
||||
),
|
||||
)
|
||||
for sub_commander_id, tools in SUB_COMMANDER_TOOLSETS.items()
|
||||
)
|
||||
|
||||
|
||||
BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS: tuple[SpecialistTemplateManifest, ...] = ()
|
||||
76
backend/app/agents/registry/indexes.py
Normal file
76
backend/app/agents/registry/indexes.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from types import MappingProxyType
|
||||
|
||||
from app.agents.registry.loader import RegistryBundle
|
||||
from app.agents.registry.models import (
|
||||
AgentManifest,
|
||||
CapabilityManifest,
|
||||
SpecialistTemplateManifest,
|
||||
SubCommanderManifest,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegistryIndexes:
|
||||
agent_by_id: Mapping[str, AgentManifest]
|
||||
sub_commander_by_id: Mapping[str, SubCommanderManifest]
|
||||
capability_by_id: Mapping[str, CapabilityManifest]
|
||||
specialist_template_by_id: Mapping[str, SpecialistTemplateManifest]
|
||||
agent_prompt_key_by_id: Mapping[str, str]
|
||||
sub_commander_prompt_key_by_id: Mapping[str, str]
|
||||
skill_context_key_by_agent_id: Mapping[str, str]
|
||||
capability_id_by_tool_name: Mapping[str, str]
|
||||
capability_ids_by_sub_commander_id: Mapping[str, tuple[str, ...]]
|
||||
|
||||
|
||||
def summarize_registry_indexes(indexes: RegistryIndexes) -> dict[str, int]:
|
||||
return {
|
||||
"agent_count": len(indexes.agent_by_id),
|
||||
"sub_commander_count": len(indexes.sub_commander_by_id),
|
||||
"capability_count": len(indexes.capability_by_id),
|
||||
"specialist_template_count": len(indexes.specialist_template_by_id),
|
||||
}
|
||||
|
||||
|
||||
def build_registry_indexes(bundle: RegistryBundle) -> RegistryIndexes:
|
||||
agent_by_id = {agent.agent_id: agent for agent in bundle.agents}
|
||||
sub_commander_by_id = {
|
||||
sub_commander.sub_commander_id: sub_commander
|
||||
for sub_commander in bundle.sub_commanders
|
||||
}
|
||||
capability_by_id = {
|
||||
capability.capability_id: capability for capability in bundle.capabilities
|
||||
}
|
||||
specialist_template_by_id = {
|
||||
template.template_id: template for template in bundle.specialist_templates
|
||||
}
|
||||
|
||||
return RegistryIndexes(
|
||||
agent_by_id=MappingProxyType(agent_by_id),
|
||||
sub_commander_by_id=MappingProxyType(sub_commander_by_id),
|
||||
capability_by_id=MappingProxyType(capability_by_id),
|
||||
specialist_template_by_id=MappingProxyType(specialist_template_by_id),
|
||||
agent_prompt_key_by_id=MappingProxyType({
|
||||
agent.agent_id: agent.system_prompt_key for agent in bundle.agents
|
||||
}),
|
||||
sub_commander_prompt_key_by_id=MappingProxyType({
|
||||
sub_commander.sub_commander_id: sub_commander.sub_commander_id
|
||||
for sub_commander in bundle.sub_commanders
|
||||
}),
|
||||
skill_context_key_by_agent_id=MappingProxyType({
|
||||
agent.agent_id: agent.skill_context_key
|
||||
for agent in bundle.agents
|
||||
if agent.skill_context_key is not None
|
||||
}),
|
||||
capability_id_by_tool_name=MappingProxyType({
|
||||
capability.tool_name: capability.capability_id
|
||||
for capability in bundle.capabilities
|
||||
}),
|
||||
capability_ids_by_sub_commander_id=MappingProxyType({
|
||||
sub_commander.sub_commander_id: tuple(sub_commander.capability_ids)
|
||||
for sub_commander in bundle.sub_commanders
|
||||
}),
|
||||
)
|
||||
33
backend/app/agents/registry/loader.py
Normal file
33
backend/app/agents/registry/loader.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.agents.registry.builtins import (
|
||||
BUILTIN_AGENT_MANIFESTS,
|
||||
BUILTIN_CAPABILITY_MANIFESTS,
|
||||
BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS,
|
||||
BUILTIN_SUB_COMMANDER_MANIFESTS,
|
||||
)
|
||||
from app.agents.registry.models import (
|
||||
AgentManifest,
|
||||
CapabilityManifest,
|
||||
SpecialistTemplateManifest,
|
||||
SubCommanderManifest,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegistryBundle:
|
||||
agents: tuple[AgentManifest, ...]
|
||||
sub_commanders: tuple[SubCommanderManifest, ...]
|
||||
capabilities: tuple[CapabilityManifest, ...]
|
||||
specialist_templates: tuple[SpecialistTemplateManifest, ...]
|
||||
|
||||
|
||||
def load_builtin_registry_bundle() -> RegistryBundle:
|
||||
return RegistryBundle(
|
||||
agents=BUILTIN_AGENT_MANIFESTS,
|
||||
sub_commanders=BUILTIN_SUB_COMMANDER_MANIFESTS,
|
||||
capabilities=BUILTIN_CAPABILITY_MANIFESTS,
|
||||
specialist_templates=BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS,
|
||||
)
|
||||
32
backend/app/agents/registry/models.py
Normal file
32
backend/app/agents/registry/models.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AgentManifest(BaseModel):
|
||||
agent_id: str
|
||||
display_name: str
|
||||
role_value: str
|
||||
system_prompt_key: str
|
||||
routing_hints: list[str]
|
||||
default_sub_commanders: list[str]
|
||||
skill_context_key: str | None = None
|
||||
continuity_policy: str | None = None
|
||||
clarification_policy: str | None = None
|
||||
|
||||
|
||||
class SubCommanderManifest(BaseModel):
|
||||
sub_commander_id: str
|
||||
parent_agent_id: str
|
||||
prompt_text: str
|
||||
capability_ids: list[str]
|
||||
|
||||
|
||||
class CapabilityManifest(BaseModel):
|
||||
capability_id: str
|
||||
tool_name: str
|
||||
|
||||
|
||||
class SpecialistTemplateManifest(BaseModel):
|
||||
template_id: str
|
||||
display_name: str
|
||||
description: str
|
||||
allowed_capability_ids: list[str] | None = None
|
||||
55
backend/app/agents/registry/validator.py
Normal file
55
backend/app/agents/registry/validator.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from collections.abc import Iterable
|
||||
|
||||
from app.agents.registry.models import (
|
||||
AgentManifest,
|
||||
CapabilityManifest,
|
||||
SpecialistTemplateManifest,
|
||||
SubCommanderManifest,
|
||||
)
|
||||
|
||||
|
||||
def _validate_unique_ids(values: Iterable[str], label: str) -> set[str]:
|
||||
unique_values: set[str] = set()
|
||||
for value in values:
|
||||
if value in unique_values:
|
||||
raise ValueError(f"duplicate {label}: {value}")
|
||||
unique_values.add(value)
|
||||
return unique_values
|
||||
|
||||
|
||||
def validate_registry_bundle(
|
||||
*,
|
||||
agents: list[AgentManifest],
|
||||
sub_commanders: list[SubCommanderManifest],
|
||||
capabilities: list[CapabilityManifest],
|
||||
specialist_templates: list[SpecialistTemplateManifest],
|
||||
) -> None:
|
||||
agent_ids = _validate_unique_ids((agent.agent_id for agent in agents), "agent id")
|
||||
_validate_unique_ids(
|
||||
(sub_commander.sub_commander_id for sub_commander in sub_commanders),
|
||||
"sub commander id",
|
||||
)
|
||||
capability_ids = _validate_unique_ids(
|
||||
(capability.capability_id for capability in capabilities),
|
||||
"capability id",
|
||||
)
|
||||
_validate_unique_ids(
|
||||
(specialist_template.template_id for specialist_template in specialist_templates),
|
||||
"template id",
|
||||
)
|
||||
|
||||
for sub_commander in sub_commanders:
|
||||
if sub_commander.parent_agent_id not in agent_ids:
|
||||
raise ValueError(f"unknown parent agent id: {sub_commander.parent_agent_id}")
|
||||
|
||||
for capability_id in sub_commander.capability_ids:
|
||||
if capability_id not in capability_ids:
|
||||
raise ValueError(f"unknown capability id: {capability_id}")
|
||||
|
||||
for specialist_template in specialist_templates:
|
||||
if specialist_template.allowed_capability_ids is None:
|
||||
continue
|
||||
|
||||
for capability_id in specialist_template.allowed_capability_ids:
|
||||
if capability_id not in capability_ids:
|
||||
raise ValueError(f"unknown capability id: {capability_id}")
|
||||
Reference in New Issue
Block a user