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:
2026-04-02 14:34:26 +08:00
parent e9ba8597e9
commit 4251a79062
12 changed files with 1111 additions and 423 deletions

View File

@@ -0,0 +1 @@
"""Agent package."""

View File

@@ -62,17 +62,40 @@ def _filter_user_messages(messages: list[BaseMessage]) -> list[BaseMessage]:
return [m for m in messages if m.type in ("human", "user")] return [m for m in messages if m.type in ("human", "user")]
def _dedupe_tools_by_name(tools: list) -> list:
deduped_tools = []
seen_tool_names: set[str] = set()
for tool in tools:
if tool.name in seen_tool_names:
continue
deduped_tools.append(tool)
seen_tool_names.add(tool.name)
return deduped_tools
def _get_role_tools(role: AgentRole) -> list: def _get_role_tools(role: AgentRole) -> list:
"""获取角色对应的所有可用工具集""" """获取角色对应的所有可用工具集"""
if role == AgentRole.SCHEDULE_PLANNER: if role == AgentRole.SCHEDULE_PLANNER:
# 合并分析和规划工具 # 合并分析和规划工具
return list(set(SUB_COMMANDER_TOOLSETS["schedule_analysis"] + SUB_COMMANDER_TOOLSETS["schedule_planning"])) return _dedupe_tools_by_name(
SUB_COMMANDER_TOOLSETS["schedule_analysis"]
+ SUB_COMMANDER_TOOLSETS["schedule_planning"]
)
if role == AgentRole.EXECUTOR: if role == AgentRole.EXECUTOR:
return list(set(SUB_COMMANDER_TOOLSETS["executor_tasks"] + SUB_COMMANDER_TOOLSETS["executor_forum"])) return _dedupe_tools_by_name(
SUB_COMMANDER_TOOLSETS["executor_tasks"]
+ SUB_COMMANDER_TOOLSETS["executor_forum"]
)
if role == AgentRole.LIBRARIAN: if role == AgentRole.LIBRARIAN:
return list(set(SUB_COMMANDER_TOOLSETS["librarian_retrieval"] + SUB_COMMANDER_TOOLSETS["librarian_graph"])) return _dedupe_tools_by_name(
SUB_COMMANDER_TOOLSETS["librarian_retrieval"]
+ SUB_COMMANDER_TOOLSETS["librarian_graph"]
)
if role == AgentRole.ANALYST: if role == AgentRole.ANALYST:
return list(set(SUB_COMMANDER_TOOLSETS["analyst_progress"] + SUB_COMMANDER_TOOLSETS["analyst_insights"])) return _dedupe_tools_by_name(
SUB_COMMANDER_TOOLSETS["analyst_progress"]
+ SUB_COMMANDER_TOOLSETS["analyst_insights"]
)
return [] return []

View File

@@ -342,3 +342,24 @@ JSON_ACTION_FALLBACK_PROMPT = """你当前运行在 JSON action fallback 模式
6. 只能使用系统消息里明确列出的工具名。 6. 只能使用系统消息里明确列出的工具名。
7. `arguments` 必须是 JSON 对象。 7. `arguments` 必须是 JSON 对象。
""" """
TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY = {
"master": MASTER_SYSTEM_PROMPT,
"schedule_planner": SCHEDULE_PLANNER_SYSTEM_PROMPT,
"executor": EXECUTOR_SYSTEM_PROMPT,
"librarian": LIBRARIAN_SYSTEM_PROMPT,
"analyst": ANALYST_SYSTEM_PROMPT,
}
SUB_COMMANDER_PROMPTS_BY_KEY = {
"schedule_analysis": SCHEDULE_ANALYSIS_PROMPT,
"schedule_planning": SCHEDULE_PLANNING_PROMPT,
"executor_tasks": EXECUTOR_TASKS_PROMPT,
"executor_forum": EXECUTOR_FORUM_PROMPT,
"librarian_retrieval": LIBRARIAN_RETRIEVAL_PROMPT,
"librarian_graph": LIBRARIAN_GRAPH_PROMPT,
"analyst_progress": ANALYST_PROGRESS_PROMPT,
"analyst_insights": ANALYST_INSIGHTS_PROMPT,
}

View 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",
]

View 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, ...] = ()

View 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
}),
)

View 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,
)

View 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

View 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}")

View File

@@ -1,470 +1,291 @@
from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import sys
from langchain_core.messages import AIMessage, HumanMessage WORKTREE_ROOT = Path(__file__).resolve().parents[4]
if str(WORKTREE_ROOT) not in sys.path:
sys.path.insert(0, str(WORKTREE_ROOT))
for module_name in list(sys.modules):
if module_name == "app" or module_name.startswith("app."):
del sys.modules[module_name]
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
from langgraph.graph import END
from app.agents.graph import ( from app.agents.graph import (
_choose_sub_commander, JSON_ACTION_FALLBACK_PROMPT,
_parse_json_action, _get_role_tools,
_route_agent_from_user_query, call_agent_llm,
_run_sub_commander, execute_tools_node,
master_node, master_node,
route_after_agent,
route_master,
) )
from app.agents.tools.time_reasoning import resolve_time_expression
from app.agents.state import AgentRole from app.agents.state import AgentRole
from app.agents.tools import SUB_COMMANDER_TOOLSETS
from app.agents.prompts import MASTER_SYSTEM_PROMPT
def _base_state(message: str = "帮我安排今天的重点") -> dict:
def _base_state(message: str, user_llm_config: dict | None = None) -> dict:
return { return {
'messages': [HumanMessage(content=message)], "messages": [HumanMessage(content=message)],
'user_id': 'u1', "user_id": "u1",
'conversation_id': 'c1', "conversation_id": "c1",
'current_agent': AgentRole.MASTER, "current_agent": AgentRole.MASTER.value,
'active_agents': [AgentRole.MASTER], "next_step": None,
'current_sub_commander': None, "agent_trace": [AgentRole.MASTER.value],
'active_sub_commanders': [], "pending_tasks": [],
'sub_commander_trace': [], "completed_tasks": [],
'pending_tasks': [], "created_entities": [],
'completed_tasks': [], "knowledge_context": None,
'tool_calls': [], "schedule_context_summary": None,
'last_tool_result': None, "analysis_report": None,
'action_results': [], "final_response": None,
'created_entities': [], "memory_context": None,
'tool_strategy_used': None, "current_datetime_context": None,
'provider_capabilities': None, "user_llm_config": None,
'fallback_parse_error': None, "provider_capabilities": None,
'knowledge_context': None,
'graph_context': None,
'schedule_context_summary': None,
'plan': None,
'plan_steps': [],
'analysis_report': None,
'final_response': None,
'should_respond': True,
'memory_context': None,
'current_datetime_context': 'CURRENT_TIME: 2026-03-28T12:00:00+08:00',
'current_datetime_reference': {'current_time_iso': '2026-03-28T12:00:00+08:00', 'current_date_iso': '2026-03-28', 'timezone': 'UTC'},
'user_llm_config': user_llm_config,
} }
class FakeFallbackLLM:
def __init__(self, first_content: str, followup_content: str = '已创建提醒:开会,时间为 2026-03-29 09:00按当前时间理解为“明天早上9点”'):
self.first_content = first_content
self.followup_content = followup_content
self.calls = 0
async def ainvoke(self, messages):
self.calls += 1
if self.calls == 1:
return AIMessage(content=self.first_content)
return AIMessage(content=self.followup_content)
def bind_tools(self, tools):
raise AssertionError('bind_tools should not be called in JSON fallback mode')
class FakeNativeBoundLLM:
async def ainvoke(self, messages):
return AIMessage(
content='',
tool_calls=[
{
'id': 'call_1',
'name': 'create_reminder',
'args': {'title': '开会', 'reminder_at': '明天 09:00'},
}
],
)
class FakeNativeLLM:
def __init__(self):
self.bound = FakeNativeBoundLLM()
self.tool_binding_count = 0
self.calls = 0
self._jarvis_provider_capabilities = SimpleNamespace(provider='openai', supports_native_tools=True, preferred_tool_strategy='native')
def bind_tools(self, tools):
self.tool_binding_count += 1
return self.bound
async def ainvoke(self, messages):
self.calls += 1
return AIMessage(content='已创建提醒:开会,时间为 2026-03-29 09:00按当前时间理解为“明天早上9点”')
class FakeTool:
def __init__(self, name: str, result: str):
self.name = name
self.result = result
self.invocations: list[dict] = []
def invoke(self, args: dict):
self.invocations.append(args)
return self.result
class CapturingLLM:
def __init__(self, content: str = '{"mode":"final","final_response":"好的。"}'):
self.content = content
self.messages = None
self._jarvis_provider_capabilities = SimpleNamespace(provider='ollama', supports_native_tools=False, preferred_tool_strategy='json_fallback')
async def ainvoke(self, messages):
self.messages = messages
return AIMessage(content=self.content)
class FailIfCalledLLM: class FailIfCalledLLM:
async def ainvoke(self, messages): async def ainvoke(self, messages):
raise AssertionError('LLM should not be called for simple greetings') raise AssertionError("LLM should not be called for greeting fast-path")
async def test_master_node_returns_stable_reply_for_simple_greeting(monkeypatch): class StaticResponseLLM:
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: FailIfCalledLLM()) def __init__(self, response: AIMessage):
self.response = response
self.messages = None
state = { async def ainvoke(self, messages):
'messages': [HumanMessage(content='你好')], self.messages = messages
'user_id': 'u1', return self.response
'conversation_id': 'c1',
'current_agent': AgentRole.MASTER,
'active_agents': [AgentRole.MASTER],
'pending_tasks': [],
'completed_tasks': [],
'tool_calls': [],
'last_tool_result': None,
'knowledge_context': None,
'graph_context': None,
'plan': None,
'plan_steps': [],
'analysis_report': None,
'final_response': None,
'should_respond': True,
'memory_context': None,
'user_llm_config': None,
}
result = await master_node(state)
assert result['final_response'] == '您好。我在。\n\n您把问题给我,我先帮您收束重点,再往下推。'
assert result['current_agent'] == AgentRole.MASTER
assert result['active_agents'] == [AgentRole.MASTER]
async def test_master_node_returns_stable_reply_for_identity_question(monkeypatch): class CaptureFallbackLLM:
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: FailIfCalledLLM()) def __init__(self, response: AIMessage):
self.response = response
self.messages = None
self.bind_tools_called = False
state = { async def ainvoke(self, messages):
'messages': [HumanMessage(content='你是谁')], self.messages = messages
'user_id': 'u1', return self.response
'conversation_id': 'c1',
'current_agent': AgentRole.MASTER,
'active_agents': [AgentRole.MASTER],
'pending_tasks': [],
'completed_tasks': [],
'tool_calls': [],
'last_tool_result': None,
'knowledge_context': None,
'graph_context': None,
'plan': None,
'plan_steps': [],
'analysis_report': None,
'final_response': None,
'should_respond': True,
'memory_context': None,
'user_llm_config': None,
}
result = await master_node(state) def bind_tools(self, tools):
self.bind_tools_called = True
assert result['final_response'] == '我是 Jarvis。\n\n比起做一个泛泛的助手,我更像您的判断型协作伙伴:帮您看清问题、压缩路径、把事情往前推进。' raise AssertionError("bind_tools should not be used when native tools are unsupported")
assert result['current_agent'] == AgentRole.MASTER
assert result['active_agents'] == [AgentRole.MASTER]
async def test_master_node_returns_stable_reply_for_identity_question_with_punctuation(monkeypatch): class AsyncFakeTool:
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: FailIfCalledLLM()) def __init__(self, name: str, result: str):
self.name = name
self.result = result
self.calls: list[dict] = []
state = { async def ainvoke(self, args: dict):
'messages': [HumanMessage(content='你是谁?')], self.calls.append(args)
'user_id': 'u1', return self.result
'conversation_id': 'c1',
'current_agent': AgentRole.MASTER,
'active_agents': [AgentRole.MASTER],
'pending_tasks': [],
'completed_tasks': [],
'tool_calls': [],
'last_tool_result': None,
'knowledge_context': None,
'graph_context': None,
'plan': None,
'plan_steps': [],
'analysis_report': None,
'final_response': None,
'should_respond': True,
'memory_context': None,
'user_llm_config': None,
}
result = await master_node(state)
assert result['final_response'] == '我是 Jarvis。\n\n比起做一个泛泛的助手,我更像您的判断型协作伙伴:帮您看清问题、压缩路径、把事情往前推进。'
assert result['current_agent'] == AgentRole.MASTER
assert result['active_agents'] == [AgentRole.MASTER]
async def test_master_node_returns_stable_reply_for_identity_question_with_particle(monkeypatch): class SyncFakeTool:
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: FailIfCalledLLM()) def __init__(self, name: str, result: str):
self.name = name
self.result = result
self.calls: list[dict] = []
state = { def invoke(self, args: dict):
'messages': [HumanMessage(content='你是谁啊')], self.calls.append(args)
'user_id': 'u1', return self.result
'conversation_id': 'c1',
'current_agent': AgentRole.MASTER,
'active_agents': [AgentRole.MASTER],
'pending_tasks': [],
'completed_tasks': [],
'tool_calls': [],
'last_tool_result': None,
'knowledge_context': None,
'graph_context': None,
'plan': None,
'plan_steps': [],
'analysis_report': None,
'final_response': None,
'should_respond': True,
'memory_context': None,
'user_llm_config': None,
}
result = await master_node(state)
assert result['final_response'] == '我是 Jarvis。\n\n比起做一个泛泛的助手,我更像您的判断型协作伙伴:帮您看清问题、压缩路径、把事情往前推进。'
assert result['current_agent'] == AgentRole.MASTER
assert result['active_agents'] == [AgentRole.MASTER]
async def test_master_node_returns_stable_reply_for_capability_question(monkeypatch): async def test_master_node_greeting_fast_path_returns_stable_reply_without_llm(monkeypatch):
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: FailIfCalledLLM()) monkeypatch.setattr("app.agents.graph._get_llm_for_state", lambda state: (FailIfCalledLLM(), SimpleNamespace()))
state = { result = await master_node(_base_state("你好"))
'messages': [HumanMessage(content='你能做什么')],
'user_id': 'u1',
'conversation_id': 'c1',
'current_agent': AgentRole.MASTER,
'active_agents': [AgentRole.MASTER],
'pending_tasks': [],
'completed_tasks': [],
'tool_calls': [],
'last_tool_result': None,
'knowledge_context': None,
'graph_context': None,
'plan': None,
'plan_steps': [],
'analysis_report': None,
'final_response': None,
'should_respond': True,
'memory_context': None,
'user_llm_config': None,
}
result = await master_node(state) assert result["final_response"] == "您好。我在。\n\n您把问题给我,我先帮您收束重点,再往下推。"
assert result["messages"][0].content == "您好。我在。"
assert result['final_response'] == '主要做三件事。\n- 帮您判断:看问题本质、梳理取舍、给出方向\n- 帮您收束:把复杂内容理顺,把重点拎出来\n- 帮您推进:拆任务、定步骤、把下一步变清楚\n\n如果您现在有具体目标,我可以直接进入处理。'
assert result['current_agent'] == AgentRole.MASTER
assert result['active_agents'] == [AgentRole.MASTER]
def test_choose_sub_commander_routes_schedule_requests_to_schedule_planning(): async def test_master_node_routes_to_agent_when_llm_returns_role_name(monkeypatch):
assert _choose_sub_commander(AgentRole.SCHEDULE_PLANNER, '帮我安排一下这周计划') == 'schedule_planning' llm = StaticResponseLLM(AIMessage(content="schedule_planner"))
monkeypatch.setattr(
"app.agents.graph._get_llm_for_state",
def test_choose_sub_commander_routes_focus_requests_to_schedule_analysis(): lambda state: (llm, SimpleNamespace(provider="test", supports_native_tools=True)),
assert _choose_sub_commander(AgentRole.SCHEDULE_PLANNER, '基于最近对话帮我判断该聚焦什么') == 'schedule_analysis'
def test_route_agent_from_user_query_routes_knowledge_requests_to_librarian():
assert _route_agent_from_user_query('帮我搜索知识库里的项目资料') == AgentRole.LIBRARIAN
def test_route_agent_from_user_query_routes_schedule_requests_to_schedule_planner():
assert _route_agent_from_user_query('明天提醒我开会') == AgentRole.SCHEDULE_PLANNER
def test_route_agent_from_user_query_routes_explicit_month_day_milestone_to_schedule_planner():
assert _route_agent_from_user_query('3月29日对话系统交付节点') == AgentRole.SCHEDULE_PLANNER
def test_choose_sub_commander_routes_explicit_month_day_milestone_to_schedule_planning():
assert _choose_sub_commander(AgentRole.SCHEDULE_PLANNER, '3月29日对话系统交付节点') == 'schedule_planning'
def test_parse_json_action_extracts_tool_calls_from_fenced_json():
parsed = _parse_json_action(
'```json\n{"mode":"tool_call","tool_calls":[{"name":"create_reminder","arguments":{"title":"开会","reminder_at":"明天 09:00"}}]}\n```',
['create_reminder'],
) )
assert parsed == { state = _base_state("帮我安排这周重点")
'mode': 'tool_call', result = await master_node(state)
'tool_calls': [
{ assert result["current_agent"] == AgentRole.SCHEDULE_PLANNER.value
'name': 'create_reminder', assert result["agent_trace"] == [AgentRole.MASTER.value, AgentRole.SCHEDULE_PLANNER.value]
'args': {'title': '开会', 'reminder_at': '明天 09:00'}, assert result["messages"][0].content == f"已分发至 {AgentRole.SCHEDULE_PLANNER.value} 处理。"
'reason': None, assert isinstance(llm.messages[0], SystemMessage)
} assert MASTER_SYSTEM_PROMPT in llm.messages[0].content
async def test_master_node_returns_final_response_when_llm_answers_directly(monkeypatch):
response = AIMessage(content="我建议先收束需求,再拆执行步骤。")
llm = StaticResponseLLM(response)
monkeypatch.setattr(
"app.agents.graph._get_llm_for_state",
lambda state: (llm, SimpleNamespace(provider="test", supports_native_tools=True)),
)
result = await master_node(_base_state("现在应该怎么推进这个项目?"))
assert result["final_response"] == response.content
assert result["messages"] == [response]
def test_route_after_agent_sends_tool_calls_to_tools_node():
state = _base_state()
state["messages"] = [AIMessage(content="", tool_calls=[{"id": "1", "name": "create_task", "args": {}}])]
assert route_after_agent(state) == "tools"
def test_route_after_agent_ends_when_no_tool_calls_exist():
state = _base_state()
state["messages"] = [AIMessage(content="done")]
assert route_after_agent(state) == END
def test_route_master_ends_when_final_response_exists():
state = _base_state()
state["final_response"] = "done"
state["current_agent"] = AgentRole.EXECUTOR.value
assert route_master(state) == END
def test_route_master_returns_current_agent_when_more_work_remains():
state = _base_state()
state["current_agent"] = AgentRole.LIBRARIAN.value
assert route_master(state) == AgentRole.LIBRARIAN.value
def test_get_role_tools_returns_expected_semantic_tool_sets():
expected_by_role = {
AgentRole.SCHEDULE_PLANNER: [
"get_schedule_day",
"get_tasks",
"resolve_time_expression",
"create_todo",
"create_schedule_task",
"create_reminder",
"create_goal",
],
AgentRole.EXECUTOR: [
"get_tasks",
"create_task",
"update_task_status",
"resolve_time_expression",
"create_todo",
"create_schedule_task",
"create_reminder",
"create_goal",
"get_forum_posts",
"create_forum_post",
"scan_forum_for_instructions",
],
AgentRole.LIBRARIAN: [
"search_knowledge",
"hybrid_search",
"web_search",
"get_knowledge_graph_context",
"build_knowledge_graph",
],
AgentRole.ANALYST: [
"get_tasks",
"get_forum_posts",
"scan_forum_for_instructions",
"search_knowledge",
"hybrid_search",
"web_search",
], ],
} }
for role, expected_tool_names in expected_by_role.items():
def test_parse_json_action_returns_none_for_invalid_or_unknown_payload(): actual_tools = _get_role_tools(role)
assert _parse_json_action('not json', ['create_reminder']) is None actual_tool_names = [tool.name for tool in actual_tools]
assert _parse_json_action('{"mode":"tool_call","tool_calls":[{"name":"unknown","arguments":{}}]}', ['create_reminder']) is None assert actual_tool_names == expected_tool_names
assert len(actual_tool_names) == len(set(actual_tool_names))
def test_parse_json_action_tolerates_prefix_and_suffix_text(): async def test_execute_tools_node_executes_tool_calls_and_tracks_created_entities(monkeypatch):
parsed = _parse_json_action( create_tool = AsyncFakeTool("create_task", "created task 123")
'好的,下面是 JSON\n```json\n{"mode":"tool_call","tool_calls":[{"name":"create_reminder","arguments":{"title":"开会","reminder_at":"明天 09:00"}}]}\n```\n谢谢', read_tool = SyncFakeTool("get_tasks", "[]")
['create_reminder'],
)
assert parsed is not None
assert parsed['mode'] == 'tool_call'
assert parsed['tool_calls'][0]['name'] == 'create_reminder'
monkeypatch.setattr("app.agents.graph.ALL_TOOLS", [create_tool, read_tool])
def test_parse_json_action_accepts_parameters_alias_for_tool_calls(): monkeypatch.setattr(
parsed = _parse_json_action( "app.agents.graph.normalize_tool_time_arguments",
'{"mode":"tool_call","tool_calls":[{"name":"create_reminder","parameters":{"title":"收被子","reminder_at":"2026-03-29T09:00:00+08:00"}}]}', lambda tool_name, tool_args, current_datetime_context: {**tool_args, "normalized": True},
['create_reminder'],
) )
assert parsed == { state = _base_state()
'mode': 'tool_call', state["created_entities"] = [{"tool": "existing", "result": "already there"}]
'tool_calls': [ state["current_datetime_context"] = "2026-04-02T09:00:00+08:00"
{ state["messages"] = [
'name': 'create_reminder', AIMessage(
'args': {'title': '收被子', 'reminder_at': '2026-03-29T09:00:00+08:00'}, content="",
'reason': None, tool_calls=[
} {"id": "tool-1", "name": "create_task", "args": {"title": "Write tests"}},
], {"id": "tool-2", "name": "get_tasks", "args": {"status": "open"}},
} ],
)
]
result = await execute_tools_node(state)
assert create_tool.calls == [{"title": "Write tests", "normalized": True}]
assert read_tool.calls == [{"status": "open", "normalized": True}]
assert [type(message) for message in result["messages"]] == [ToolMessage, ToolMessage]
assert result["messages"][0].tool_call_id == "tool-1"
assert result["messages"][0].name == "create_task"
assert result["messages"][0].content == "created task 123"
assert result["messages"][1].tool_call_id == "tool-2"
assert result["messages"][1].name == "get_tasks"
assert result["messages"][1].content == "[]"
assert result["created_entities"] == [
{"tool": "existing", "result": "already there"},
{"tool": "create_task", "result": "created task 123"},
]
async def test_run_sub_commander_uses_json_fallback_for_non_native_provider(monkeypatch): async def test_call_agent_llm_includes_context_messages_and_uses_json_fallback(monkeypatch):
fake_llm = FakeFallbackLLM( llm = CaptureFallbackLLM(AIMessage(content='{"mode":"final","final_response":"好的。"}'))
'{"mode":"tool_call","tool_calls":[{"name":"create_reminder","arguments":{"title":"开会","reminder_at":"明天 09:00"}}]}' capabilities = SimpleNamespace(
provider="ollama",
supports_native_tools=False,
preferred_tool_strategy="json_fallback",
) )
fake_tool = FakeTool('create_reminder', '成功创建 reminder: 开会 @ 明天 09:00') fake_tools = [SimpleNamespace(name="create_reminder"), SimpleNamespace(name="get_tasks")]
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: fake_llm) monkeypatch.setattr("app.agents.graph._get_llm_for_state", lambda state: (llm, capabilities))
monkeypatch.setitem( monkeypatch.setattr("app.agents.graph._get_role_tools", lambda role: fake_tools)
__import__('app.agents.graph', fromlist=['SUB_COMMANDER_TOOLSETS']).SUB_COMMANDER_TOOLSETS, monkeypatch.setattr("app.agents.graph.build_skill_context", lambda role_key: "技能上下文: 先判断,再执行")
'schedule_planning',
[fake_tool],
)
state = _base_state('明天 9 点提醒我开会', {'provider': 'ollama', 'model': 'qwen2.5'}) state = _base_state("明天提醒我开会")
state['current_agent'] = AgentRole.SCHEDULE_PLANNER state["messages"] = [HumanMessage(content="明天提醒我开会")]
state["current_datetime_context"] = "CURRENT_TIME: 2026-04-02T09:00:00+08:00"
state["memory_context"] = "用户偏好早上处理深度工作。"
result = await _run_sub_commander( result = await call_agent_llm(state, AgentRole.EXECUTOR, "executor system prompt")
state,
AgentRole.SCHEDULE_PLANNER,
'manager prompt',
'明天 9 点提醒我开会',
use_tools=True,
)
assert result['tool_strategy_used'] == 'json_fallback' assert result["messages"][0].content == '{"mode":"final","final_response":"好的。"}'
assert fake_tool.invocations == [{'title': '开会', 'reminder_at': '2026-03-29T09:00:00'}] assert llm.bind_tools_called is False
assert result['tool_calls'][0]['name'] == 'create_reminder' assert llm.messages is not None
assert result['created_entities'][0]['type'] == 'reminder'
assert result['fallback_parse_error'] is None
assert result['final_response'] == '已创建提醒:开会,时间为 2026-03-29 09:00按当前时间理解为“明天早上9点”'
system_contents = [message.content for message in llm.messages if isinstance(message, SystemMessage)]
async def test_run_sub_commander_includes_current_datetime_context_in_system_messages(monkeypatch): assert "executor system prompt" in system_contents[0]
fake_llm = CapturingLLM('{"mode":"final","final_response":"好的。"}') assert any("当前时间上下文: CURRENT_TIME: 2026-04-02T09:00:00+08:00" == content for content in system_contents)
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: fake_llm) assert any("长期记忆上下文: 用户偏好早上处理深度工作。" == content for content in system_contents)
assert any("技能上下文: 先判断,再执行" == content for content in system_contents)
state = _base_state('明天 9 点提醒我开会', {'provider': 'ollama', 'model': 'qwen2.5'}) assert any(content == JSON_ACTION_FALLBACK_PROMPT for content in system_contents)
state['current_agent'] = AgentRole.SCHEDULE_PLANNER assert any(content == "本次可用工具列表: create_reminder, get_tasks" for content in system_contents)
state['current_datetime_context'] = 'CURRENT_TIME: 2026-03-28T12:00:00+08:00' assert any(isinstance(message, HumanMessage) and message.content == "明天提醒我开会" for message in llm.messages)
await _run_sub_commander(
state,
AgentRole.SCHEDULE_PLANNER,
'manager prompt',
'明天 9 点提醒我开会',
use_tools=True,
)
assert fake_llm.messages is not None
assert any(
getattr(m, 'type', None) == 'system' and 'CURRENT_TIME:' in str(getattr(m, 'content', ''))
for m in fake_llm.messages
)
async def test_run_sub_commander_uses_web_search_in_json_fallback(monkeypatch):
fake_llm = FakeFallbackLLM(
'{"mode":"tool_call","tool_calls":[{"name":"web_search","arguments":{"query":"Jarvis 最新模型更新","top_k":2}}]}',
'我查了外部网页,下面是最新结果摘要。',
)
fake_tool = FakeTool('web_search', '成功搜索到 2 条网页结果')
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: fake_llm)
monkeypatch.setitem(
__import__('app.agents.graph', fromlist=['SUB_COMMANDER_TOOLSETS']).SUB_COMMANDER_TOOLSETS,
'librarian_retrieval',
[fake_tool],
)
state = _base_state('帮我上网查一下 Jarvis 最新模型更新', {'provider': 'ollama', 'model': 'qwen2.5'})
state['current_agent'] = AgentRole.LIBRARIAN
result = await _run_sub_commander(
state,
AgentRole.LIBRARIAN,
'manager prompt',
'帮我上网查一下 Jarvis 最新模型更新',
use_tools=True,
summary_target='knowledge_context',
)
assert result['tool_strategy_used'] == 'json_fallback'
assert fake_tool.invocations == [{'query': 'Jarvis 最新模型更新', 'top_k': 2}]
assert result['tool_calls'][0]['name'] == 'web_search'
assert result['last_tool_result'] == '[web_search] 成功搜索到 2 条网页结果'
assert result['final_response'] == '我查了外部网页,下面是最新结果摘要。'
fake_llm = FakeNativeLLM()
fake_tool = FakeTool('create_reminder', '成功创建 reminder: 开会 @ 明天 09:00')
monkeypatch.setattr('app.agents.graph._get_llm_for_state', lambda state: fake_llm)
monkeypatch.setitem(
__import__('app.agents.graph', fromlist=['SUB_COMMANDER_TOOLSETS']).SUB_COMMANDER_TOOLSETS,
'schedule_planning',
[fake_tool],
)
state = _base_state('明天 9 点提醒我开会', {'provider': 'openai', 'model': 'gpt-4o'})
state['current_agent'] = AgentRole.SCHEDULE_PLANNER
result = await _run_sub_commander(
state,
AgentRole.SCHEDULE_PLANNER,
'manager prompt',
'明天 9 点提醒我开会',
use_tools=True,
)
assert result['tool_strategy_used'] == 'native'
assert fake_llm.tool_binding_count == 1
assert fake_tool.invocations == [{'title': '开会', 'reminder_at': '2026-03-29T09:00:00'}]
assert result['created_entities'][0]['type'] == 'reminder'
assert result['final_response'] == '已创建提醒:开会,时间为 2026-03-29 09:00按当前时间理解为“明天早上9点”'

View File

@@ -0,0 +1,360 @@
import pytest
from collections.abc import Mapping
from app.agents.prompts import (
SUB_COMMANDER_PROMPTS_BY_KEY,
TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY,
)
from app.agents.registry import build_registry_indexes, load_builtin_registry_bundle
from app.agents.registry.indexes import summarize_registry_indexes
from app.agents.registry.models import (
AgentManifest,
CapabilityManifest,
SpecialistTemplateManifest,
SubCommanderManifest,
)
from app.agents.registry.validator import validate_registry_bundle
from app.agents.registry.builtins import (
BUILTIN_AGENT_MANIFESTS,
BUILTIN_CAPABILITY_MANIFESTS,
BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS,
BUILTIN_SUB_COMMANDER_MANIFESTS,
)
from app.agents.state import AgentRole
from app.agents.tools import SUB_COMMANDER_TOOLSETS
def make_agent(
agent_id: str = "master",
*,
display_name: str = "Master",
role_value: str = "master",
system_prompt_key: str = "master",
default_sub_commanders: list[str] | None = None,
) -> AgentManifest:
return AgentManifest(
agent_id=agent_id,
display_name=display_name,
role_value=role_value,
system_prompt_key=system_prompt_key,
routing_hints=["route"],
default_sub_commanders=default_sub_commanders or [],
)
def make_sub_commander(
sub_commander_id: str = "planner",
*,
parent_agent_id: str = "master",
capability_ids: list[str] | None = None,
) -> SubCommanderManifest:
return SubCommanderManifest(
sub_commander_id=sub_commander_id,
parent_agent_id=parent_agent_id,
prompt_text="Plan the work.",
capability_ids=capability_ids or [],
)
def make_capability(capability_id: str = "calendar") -> CapabilityManifest:
return CapabilityManifest(capability_id=capability_id, tool_name=f"{capability_id}_tool")
def make_specialist_template(
template_id: str = "researcher",
*,
allowed_capability_ids: list[str] | None = None,
) -> SpecialistTemplateManifest:
return SpecialistTemplateManifest(
template_id=template_id,
display_name="Researcher",
description="Research specialist",
allowed_capability_ids=allowed_capability_ids,
)
def test_validate_registry_bundle_accepts_valid_bundle() -> None:
validate_registry_bundle(
agents=[make_agent(default_sub_commanders=["planner"])],
sub_commanders=[make_sub_commander(capability_ids=["calendar"])],
capabilities=[make_capability()],
specialist_templates=[make_specialist_template(allowed_capability_ids=["calendar"])],
)
def test_validate_registry_bundle_rejects_duplicate_agent_ids() -> None:
agents = [
make_agent(default_sub_commanders=["planner"]),
make_agent(
display_name="Duplicate Master",
role_value="master_duplicate",
system_prompt_key="master_duplicate",
),
]
with pytest.raises(ValueError, match="duplicate agent id: master"):
validate_registry_bundle(
agents=agents,
sub_commanders=[],
capabilities=[],
specialist_templates=[],
)
def test_validate_registry_bundle_rejects_duplicate_sub_commander_ids() -> None:
with pytest.raises(ValueError, match="duplicate sub commander id: planner"):
validate_registry_bundle(
agents=[make_agent()],
sub_commanders=[make_sub_commander(), make_sub_commander()],
capabilities=[],
specialist_templates=[],
)
def test_validate_registry_bundle_rejects_duplicate_capability_ids() -> None:
with pytest.raises(ValueError, match="duplicate capability id: calendar"):
validate_registry_bundle(
agents=[],
sub_commanders=[],
capabilities=[make_capability(), make_capability()],
specialist_templates=[],
)
def test_validate_registry_bundle_rejects_duplicate_template_ids() -> None:
with pytest.raises(ValueError, match="duplicate template id: researcher"):
validate_registry_bundle(
agents=[],
sub_commanders=[],
capabilities=[],
specialist_templates=[make_specialist_template(), make_specialist_template()],
)
def test_validate_registry_bundle_rejects_unknown_sub_commander_parent_agent_ids() -> None:
sub_commanders = [make_sub_commander(parent_agent_id="missing-agent")]
with pytest.raises(ValueError, match="unknown parent agent id: missing-agent"):
validate_registry_bundle(
agents=[],
sub_commanders=sub_commanders,
capabilities=[],
specialist_templates=[],
)
def test_validate_registry_bundle_rejects_unknown_sub_commander_capability_references() -> None:
with pytest.raises(ValueError, match="unknown capability id: search"):
validate_registry_bundle(
agents=[make_agent(default_sub_commanders=["planner"])],
sub_commanders=[make_sub_commander(capability_ids=["search"])],
capabilities=[make_capability()],
specialist_templates=[],
)
def test_validate_registry_bundle_rejects_unknown_specialist_template_capability_references() -> None:
with pytest.raises(ValueError, match="unknown capability id: missing-capability"):
validate_registry_bundle(
agents=[],
sub_commanders=[],
capabilities=[make_capability()],
specialist_templates=[
make_specialist_template(allowed_capability_ids=["missing-capability"])
],
)
def test_registry_bundle_agent_roles_match_runtime_agent_role_enum_values() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
assert set(indexes.agent_by_id) == {role.value for role in AgentRole}
assert {agent.role_value for agent in bundle.agents} == {role.value for role in AgentRole}
def test_registry_bundle_agent_system_prompt_keys_match_runtime_top_level_prompt_surface() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
expected_prompt_keys_by_agent_id = {
role.value: role.value for role in AgentRole if role.value in TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY
}
assert set(TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY) == {role.value for role in AgentRole}
assert indexes.agent_prompt_key_by_id == expected_prompt_keys_by_agent_id
assert {
agent.agent_id: TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY[agent.system_prompt_key]
for agent in bundle.agents
} == {
role.value: TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY[role.value]
for role in AgentRole
}
def test_registry_bundle_skill_context_keys_match_graph_role_derivation_rule() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
expected_skill_context_keys = {
role.value: role.value.replace("agent_", "")
for role in AgentRole
}
assert indexes.skill_context_key_by_agent_id == expected_skill_context_keys
assert {
agent.agent_id: agent.skill_context_key for agent in bundle.agents
} == expected_skill_context_keys
def test_registry_bundle_sub_commander_prompt_texts_match_runtime_prompt_map() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
assert set(indexes.sub_commander_by_id) == set(SUB_COMMANDER_PROMPTS_BY_KEY)
assert indexes.sub_commander_prompt_key_by_id == {
sub_commander_id: sub_commander_id
for sub_commander_id in SUB_COMMANDER_PROMPTS_BY_KEY
}
assert {
sub_commander.sub_commander_id: sub_commander.prompt_text
for sub_commander in bundle.sub_commanders
} == SUB_COMMANDER_PROMPTS_BY_KEY
def test_registry_bundle_sub_commander_tool_membership_and_order_match_runtime_toolsets() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
assert set(indexes.sub_commander_by_id) == set(SUB_COMMANDER_TOOLSETS)
assert indexes.capability_ids_by_sub_commander_id == {
sub_commander_id: tuple(tool.name for tool in tools)
for sub_commander_id, tools in SUB_COMMANDER_TOOLSETS.items()
}
assert {
sub_commander.sub_commander_id: tuple(sub_commander.capability_ids)
for sub_commander in bundle.sub_commanders
} == {
sub_commander_id: tuple(tool.name for tool in tools)
for sub_commander_id, tools in SUB_COMMANDER_TOOLSETS.items()
}
def test_builtin_capabilities_reference_actual_runtime_tool_names() -> None:
expected_tool_names = {
tool.name
for tools in SUB_COMMANDER_TOOLSETS.values()
for tool in tools
}
manifest_tool_names = {manifest.tool_name for manifest in BUILTIN_CAPABILITY_MANIFESTS}
assert manifest_tool_names == expected_tool_names
def test_builtin_sub_commander_capabilities_match_runtime_toolsets() -> None:
capabilities_by_tool_name = {
manifest.tool_name: manifest.capability_id for manifest in BUILTIN_CAPABILITY_MANIFESTS
}
for sub_commander in BUILTIN_SUB_COMMANDER_MANIFESTS:
expected_capability_ids = {
capabilities_by_tool_name[tool.name]
for tool in SUB_COMMANDER_TOOLSETS[sub_commander.sub_commander_id]
}
assert set(sub_commander.capability_ids) == expected_capability_ids
def test_builtin_manifests_form_a_valid_registry_bundle() -> None:
validate_registry_bundle(
agents=list(BUILTIN_AGENT_MANIFESTS),
sub_commanders=list(BUILTIN_SUB_COMMANDER_MANIFESTS),
capabilities=list(BUILTIN_CAPABILITY_MANIFESTS),
specialist_templates=list(BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS),
)
def test_load_builtin_registry_bundle_returns_non_empty_manifest_sets() -> None:
bundle = load_builtin_registry_bundle()
assert bundle.agents
assert bundle.sub_commanders
assert bundle.capabilities
assert isinstance(bundle.specialist_templates, tuple)
def test_build_registry_indexes_exposes_manifest_lookups_by_id() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
assert indexes.agent_by_id
assert indexes.sub_commander_by_id
assert indexes.capability_by_id
assert isinstance(indexes.specialist_template_by_id, Mapping)
assert set(indexes.agent_by_id) == {agent.agent_id for agent in bundle.agents}
assert set(indexes.sub_commander_by_id) == {
sub_commander.sub_commander_id for sub_commander in bundle.sub_commanders
}
assert set(indexes.capability_by_id) == {
capability.capability_id for capability in bundle.capabilities
}
assert set(indexes.specialist_template_by_id) == {
template.template_id for template in bundle.specialist_templates
}
def test_summarize_registry_indexes_returns_read_only_debug_counts() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
assert summarize_registry_indexes(indexes) == {
"agent_count": len(bundle.agents),
"sub_commander_count": len(bundle.sub_commanders),
"capability_count": len(bundle.capabilities),
"specialist_template_count": len(bundle.specialist_templates),
}
def test_build_registry_indexes_exposes_prompt_keys_skill_context_keys_and_capability_mappings() -> None:
bundle = load_builtin_registry_bundle()
indexes = build_registry_indexes(bundle)
assert indexes.agent_prompt_key_by_id == {
agent.agent_id: agent.system_prompt_key for agent in bundle.agents
}
assert indexes.agent_prompt_key_by_id == {
agent.agent_id: agent.system_prompt_key for agent in BUILTIN_AGENT_MANIFESTS
}
assert set(indexes.agent_prompt_key_by_id.values()) == set(TOP_LEVEL_SYSTEM_PROMPTS_BY_KEY)
assert indexes.sub_commander_prompt_key_by_id == {
sub_commander.sub_commander_id: sub_commander.sub_commander_id
for sub_commander in bundle.sub_commanders
}
assert set(indexes.sub_commander_prompt_key_by_id.values()) == {
sub_commander.sub_commander_id for sub_commander in bundle.sub_commanders
}
assert indexes.skill_context_key_by_agent_id == {
agent.agent_id: agent.skill_context_key
for agent in bundle.agents
if agent.skill_context_key is not None
}
assert indexes.capability_ids_by_sub_commander_id == {
sub_commander.sub_commander_id: tuple(sub_commander.capability_ids)
for sub_commander in bundle.sub_commanders
}
def test_validate_registry_bundle_accepts_loaded_builtin_registry_bundle() -> None:
bundle = load_builtin_registry_bundle()
validate_registry_bundle(
agents=list(bundle.agents),
sub_commanders=list(bundle.sub_commanders),
capabilities=list(bundle.capabilities),
specialist_templates=list(bundle.specialist_templates),
)
def test_phase_one_still_declares_specialist_template_surface_even_if_runtime_is_deferred() -> None:
assert isinstance(BUILTIN_SPECIALIST_TEMPLATE_MANIFESTS, tuple)

143
backend/uv.lock generated
View File

@@ -4,7 +4,8 @@ requires-python = ">=3.11"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.14'", "python_full_version >= '3.14'",
"python_full_version == '3.13.*'", "python_full_version == '3.13.*'",
"python_full_version < '3.13'", "python_full_version == '3.12.*'",
"python_full_version < '3.12'",
] ]
[[package]] [[package]]
@@ -258,6 +259,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/0a/0896b829a39b5669a2d811e1a79598de661693685cd62b31f11d0c18e65b/av-17.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dba98603fc4665b4f750de86fbaf6c0cfaece970671a9b529e0e3d1711e8367e", size = 22071058, upload-time = "2026-03-14T14:38:43.663Z" }, { url = "https://files.pythonhosted.org/packages/41/0a/0896b829a39b5669a2d811e1a79598de661693685cd62b31f11d0c18e65b/av-17.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dba98603fc4665b4f750de86fbaf6c0cfaece970671a9b529e0e3d1711e8367e", size = 22071058, upload-time = "2026-03-14T14:38:43.663Z" },
] ]
[[package]]
name = "backoff"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
]
[[package]] [[package]]
name = "banks" name = "banks"
version = "2.4.1" version = "2.4.1"
@@ -1297,6 +1307,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
] ]
[[package]]
name = "h2"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
]
[[package]] [[package]]
name = "hf-xet" name = "hf-xet"
version = "1.4.2" version = "1.4.2"
@@ -1329,6 +1352,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" }, { url = "https://files.pythonhosted.org/packages/b4/7e/ccf239da366b37ba7f0b36095450efae4a64980bdc7ec2f51354205fdf39/hf_xet-1.4.2-cp37-abi3-win_arm64.whl", hash = "sha256:32c012286b581f783653e718c1862aea5b9eb140631685bb0c5e7012c8719a87", size = 3533426, upload-time = "2026-03-13T06:58:55.46Z" },
] ]
[[package]]
name = "hpack"
version = "4.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
]
[[package]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.9" version = "1.0.9"
@@ -1393,6 +1425,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
] ]
[package.optional-dependencies]
http2 = [
{ name = "h2" },
]
[[package]] [[package]]
name = "httpx-retries" name = "httpx-retries"
version = "0.4.6" version = "0.4.6"
@@ -1425,6 +1462,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/de/3ad061a05f74728927ded48c90b73521b9a9328c85d841bdefb30e01fb85/huggingface_hub-1.7.2-py3-none-any.whl", hash = "sha256:288f33a0a17b2a73a1359e2a5fd28d1becb2c121748c6173ab8643fb342c850e", size = 618036, upload-time = "2026-03-20T10:36:06.824Z" }, { url = "https://files.pythonhosted.org/packages/08/de/3ad061a05f74728927ded48c90b73521b9a9328c85d841bdefb30e01fb85/huggingface_hub-1.7.2-py3-none-any.whl", hash = "sha256:288f33a0a17b2a73a1359e2a5fd28d1becb2c121748c6173ab8643fb342c850e", size = 618036, upload-time = "2026-03-20T10:36:06.824Z" },
] ]
[[package]]
name = "hyperframe"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
[[package]] [[package]]
name = "identify" name = "identify"
version = "2.6.18" version = "2.6.18"
@@ -1508,6 +1554,7 @@ dependencies = [
{ name = "langsmith" }, { name = "langsmith" },
{ name = "llama-index" }, { name = "llama-index" },
{ name = "llama-index-vector-stores-chroma" }, { name = "llama-index-vector-stores-chroma" },
{ name = "mem0ai" },
{ name = "mineru" }, { name = "mineru" },
{ name = "openpyxl" }, { name = "openpyxl" },
{ name = "passlib", extra = ["bcrypt"] }, { name = "passlib", extra = ["bcrypt"] },
@@ -1552,6 +1599,7 @@ requires-dist = [
{ name = "langsmith", specifier = ">=0.1.0" }, { name = "langsmith", specifier = ">=0.1.0" },
{ name = "llama-index", specifier = ">=0.12.0" }, { name = "llama-index", specifier = ">=0.12.0" },
{ name = "llama-index-vector-stores-chroma", specifier = ">=0.3.0" }, { name = "llama-index-vector-stores-chroma", specifier = ">=0.3.0" },
{ name = "mem0ai", specifier = ">=1.0.0" },
{ name = "mineru", specifier = ">=2.0.3" }, { name = "mineru", specifier = ">=2.0.3" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" },
{ name = "openpyxl", specifier = ">=3.1.0" }, { name = "openpyxl", specifier = ">=3.1.0" },
@@ -2431,6 +2479,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "mem0ai"
version = "1.0.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "openai" },
{ name = "posthog" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "pytz" },
{ name = "qdrant-client" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/d9/1fbf24b055f9f14ec61d593bc95b75b033fd1a69bfddee33d6453b21e5d0/mem0ai-1.0.10.tar.gz", hash = "sha256:f3e22c9aff695ca6c66631c4e79ceef92457c8d3355c56359ab4257fa031c046", size = 200416, upload-time = "2026-04-01T18:23:27.018Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/e6/2c6ea68c404757e683da23b942dfff6987fe283ccbf2fa1fb0c128ddbdc6/mem0ai-1.0.10-py3-none-any.whl", hash = "sha256:9ff586c3a39a834042ce6755fc9da2315e284fb622ee773cd344ecca756ccad5", size = 295374, upload-time = "2026-04-01T18:23:25.022Z" },
]
[[package]] [[package]]
name = "mineru" name = "mineru"
version = "2.7.6" version = "2.7.6"
@@ -3372,6 +3438,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "portalocker"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pywin32", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" },
]
[[package]]
name = "posthog"
version = "7.9.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff" },
{ name = "distro" },
{ name = "python-dateutil" },
{ name = "requests" },
{ name = "six" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/a7/2865487853061fbd62383492237b546d2d8f7c1846272350d2b9e14138cd/posthog-7.9.12.tar.gz", hash = "sha256:ebabf2eb2e1c1fbf22b0759df4644623fa43cc6c9dcbe9fd429b7937d14251ec", size = 176828, upload-time = "2026-03-12T09:01:15.184Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/a9/7a803aed5a5649cf78ea7b31e90d0080181ba21f739243e1741a1e607f1f/posthog-7.9.12-py3-none-any.whl", hash = "sha256:7175bd1698a566bfea98a016c64e3456399f8046aeeca8f1d04ae5bf6c5a38d0", size = 202469, upload-time = "2026-03-12T09:01:13.38Z" },
]
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "4.5.1" version = "4.5.1"
@@ -3996,6 +4091,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
] ]
[[package]]
name = "pytz"
version = "2026.1.post1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" },
]
[[package]]
name = "pywin32"
version = "311"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
{ url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
{ url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
{ url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.3" version = "6.0.3"
@@ -4051,6 +4174,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
] ]
[[package]]
name = "qdrant-client"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "grpcio" },
{ name = "httpx", extra = ["http2"] },
{ name = "numpy" },
{ name = "portalocker" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/30/dd/f8a8261b83946af3cd65943c93c4f83e044f01184e8525404989d22a81a5/qdrant_client-1.17.1.tar.gz", hash = "sha256:22f990bbd63485ed97ba551a4c498181fcb723f71dcab5d6e4e43fe1050a2bc0", size = 344979, upload-time = "2026-03-13T17:13:44.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" },
]
[[package]] [[package]]
name = "qwen-vl-utils" name = "qwen-vl-utils"
version = "0.0.14" version = "0.0.14"