diff --git a/.env.example b/.env.example index 5c1cb66..e251708 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,7 @@ MATRIX_HOMESERVER=https://matrix.org MATRIX_USER_ID=@bot:matrix.org MATRIX_PASSWORD=your_password_here MATRIX_PLATFORM_BACKEND=real +MATRIX_AGENT_REGISTRY_PATH=config/matrix-agents.yaml # Shared workspace contract SURFACES_WORKSPACE_DIR=/workspace diff --git a/README.md b/README.md index b4b4f16..94b54db 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ MATRIX_PASSWORD=... # или MATRIX_ACCESS_TOKEN=... # Выбор backend: mock (по умолчанию) или real (подключение к platform-agent) MATRIX_PLATFORM_BACKEND=real +MATRIX_AGENT_REGISTRY_PATH=config/matrix-agents.yaml # compose runtime: platform-agent service name + shared /workspace AGENT_BASE_URL=http://platform-agent:8000 @@ -131,7 +132,13 @@ PROVIDER_URL=https://openrouter.ai/api/v1 PROVIDER_API_KEY=... ``` -### 3. Compose runtime +### 3. Registry агентов + +1. Скопируй `config/matrix-agents.example.yaml` в `config/matrix-agents.yaml` +2. Укажи `MATRIX_AGENT_REGISTRY_PATH=config/matrix-agents.yaml` +3. Используй `!agent` в Matrix, чтобы выбрать активного upstream-агента + +### 4. Compose runtime Root `docker-compose.yml` теперь является основным локальным runtime для Matrix и platform-agent. Он поднимает `matrix-bot`, `platform-agent` и общий volume `/workspace`. diff --git a/adapter/matrix/agent_registry.py b/adapter/matrix/agent_registry.py new file mode 100644 index 0000000..2955daf --- /dev/null +++ b/adapter/matrix/agent_registry.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +import yaml + + +class AgentRegistryError(ValueError): + pass + + +@dataclass(frozen=True) +class AgentDefinition: + agent_id: str + label: str + + +class AgentRegistry: + def __init__(self, agents: list[AgentDefinition]) -> None: + self.agents = agents + self._by_id = {agent.agent_id: agent for agent in agents} + + def get(self, agent_id: str) -> AgentDefinition: + try: + return self._by_id[agent_id] + except KeyError as exc: + raise AgentRegistryError(f"unknown agent id: {agent_id}") from exc + + +def load_agent_registry(path: str | Path) -> AgentRegistry: + raw = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {} + entries = raw.get("agents") + if not isinstance(entries, list) or not entries: + raise AgentRegistryError("agents registry must contain a non-empty agents list") + + agents: list[AgentDefinition] = [] + seen: set[str] = set() + for entry in entries: + agent_id = str(entry.get("id", "")).strip() + label = str(entry.get("label", "")).strip() + if not agent_id or not label: + raise AgentRegistryError("each agent entry requires id and label") + if agent_id in seen: + raise AgentRegistryError(f"duplicate agent id: {agent_id}") + seen.add(agent_id) + agents.append(AgentDefinition(agent_id=agent_id, label=label)) + return AgentRegistry(agents) diff --git a/config/matrix-agents.example.yaml b/config/matrix-agents.example.yaml new file mode 100644 index 0000000..23d4b37 --- /dev/null +++ b/config/matrix-agents.example.yaml @@ -0,0 +1,5 @@ +agents: + - id: agent-1 + label: Analyst + - id: agent-2 + label: Research diff --git a/pyproject.toml b/pyproject.toml index ccc6309..f2fc338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "python-dotenv>=1.0", "httpx>=0.27", "aiohttp>=3.9", + "PyYAML>=6.0", ] [project.optional-dependencies] diff --git a/tests/adapter/matrix/test_agent_registry.py b/tests/adapter/matrix/test_agent_registry.py new file mode 100644 index 0000000..dfa9050 --- /dev/null +++ b/tests/adapter/matrix/test_agent_registry.py @@ -0,0 +1,37 @@ +from pathlib import Path + +import pytest + +from adapter.matrix.agent_registry import AgentRegistryError, load_agent_registry + + +def test_load_agent_registry_reads_yaml_entries(tmp_path: Path): + path = tmp_path / "agents.yaml" + path.write_text( + "agents:\n" + " - id: agent-1\n" + " label: Analyst\n" + " - id: agent-2\n" + " label: Research\n", + encoding="utf-8", + ) + + registry = load_agent_registry(path) + + assert [agent.agent_id for agent in registry.agents] == ["agent-1", "agent-2"] + assert registry.get("agent-1").label == "Analyst" + + +def test_load_agent_registry_rejects_duplicate_ids(tmp_path: Path): + path = tmp_path / "agents.yaml" + path.write_text( + "agents:\n" + " - id: agent-1\n" + " label: Analyst\n" + " - id: agent-1\n" + " label: Duplicate\n", + encoding="utf-8", + ) + + with pytest.raises(AgentRegistryError, match="duplicate agent id"): + load_agent_registry(path)