feat(deploy): platform handoff — agent routing, persistence, docs cleanup
Agent routing: - Remove !agent command and manual agent selection flow - Registry auto-assigns agent from user_agents mapping (fallback: agents[0]) - provision_workspace_chat and !new both write agent_id to room_meta - Reconciliation backfills agent_id from registry on cold start - Fix duplicate agent_id block in auth.py Deployment stability: - Add bot-state named volume to persist lambda_matrix.db and matrix_store - Fix docker-compose.prod.yml duplicate environment: key (was silently losing all Matrix credentials) - Fix MATRIX_AGENT_REGISTRY_PATH to use absolute container path /app/config/... - Add bot-state volume declaration to docker-compose.fullstack.yml Docs and config: - Rewrite README.md for platform handoff (deploy table, working commands only) - Rewrite docs/matrix-prototype.md (remove stale commands and mock descriptions) - Remove !save/!load/!context/!agent from help text and welcome message - Add !clear, !list, !remove, !yes/!no to help text - Clean up .env.example (remove Telegram token, internal vars, real URLs) - Update config/matrix-agents.example.yaml with user_agents section and comments - Add explanatory comment to Dockerfile for --ignore-requires-python - Remove silent uv sync fallbacks in Dockerfile
This commit is contained in:
parent
380961d6e9
commit
b1aaa210a1
21 changed files with 311 additions and 937 deletions
|
|
@ -1,175 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from adapter.matrix.bot import build_runtime
|
||||
from adapter.matrix.agent_registry import AgentDefinition, AgentRegistry
|
||||
from adapter.matrix.handlers.agent import make_handle_agent
|
||||
from adapter.matrix.store import get_room_meta, get_selected_agent_id, set_selected_agent_id, set_room_meta
|
||||
from core.chat import ChatManager
|
||||
from core.protocol import IncomingCommand, OutgoingMessage
|
||||
from core.settings import SettingsManager
|
||||
from core.store import InMemoryStore
|
||||
from sdk.mock import MockPlatformClient
|
||||
|
||||
|
||||
def _registry() -> AgentRegistry:
|
||||
return AgentRegistry(
|
||||
[
|
||||
AgentDefinition(agent_id="agent-1", label="Analyst"),
|
||||
AgentDefinition(agent_id="agent-2", label="Research"),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def test_agent_command_lists_available_agents_with_selected_marker():
|
||||
store = InMemoryStore()
|
||||
await set_selected_agent_id(store, "@alice:example.org", "agent-2")
|
||||
handler = make_handle_agent(store, _registry())
|
||||
|
||||
result = await handler(
|
||||
event=IncomingCommand(
|
||||
user_id="@alice:example.org",
|
||||
platform="matrix",
|
||||
chat_id="C1",
|
||||
command="agent",
|
||||
),
|
||||
auth_mgr=None,
|
||||
platform=MockPlatformClient(),
|
||||
chat_mgr=ChatManager(None, store),
|
||||
settings_mgr=SettingsManager(MockPlatformClient(), store),
|
||||
)
|
||||
|
||||
assert result == [
|
||||
OutgoingMessage(
|
||||
chat_id="C1",
|
||||
text=(
|
||||
"Доступные агенты:\n"
|
||||
"1. Analyst\n"
|
||||
"2. Research [текущий]\n"
|
||||
"\n"
|
||||
"Выбери агент: !agent <номер>"
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def test_agent_command_persists_selected_agent_id():
|
||||
store = InMemoryStore()
|
||||
handler = make_handle_agent(store, _registry())
|
||||
|
||||
result = await handler(
|
||||
event=IncomingCommand(
|
||||
user_id="@alice:example.org",
|
||||
platform="matrix",
|
||||
chat_id="C1",
|
||||
command="agent",
|
||||
args=["2"],
|
||||
),
|
||||
auth_mgr=None,
|
||||
platform=MockPlatformClient(),
|
||||
chat_mgr=ChatManager(None, store),
|
||||
settings_mgr=SettingsManager(MockPlatformClient(), store),
|
||||
)
|
||||
|
||||
assert await get_selected_agent_id(store, "@alice:example.org") == "agent-2"
|
||||
assert result == [
|
||||
OutgoingMessage(
|
||||
chat_id="C1",
|
||||
text="Агент переключен на Research. Продолжай через !new.",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
async def test_agent_command_binds_existing_unbound_room_to_selected_agent():
|
||||
store = InMemoryStore()
|
||||
chat_mgr = ChatManager(None, store)
|
||||
await chat_mgr.get_or_create(
|
||||
user_id="@alice:example.org",
|
||||
chat_id="C1",
|
||||
platform="matrix",
|
||||
surface_ref="!room:example.org",
|
||||
name="Research",
|
||||
)
|
||||
await set_room_meta(
|
||||
store,
|
||||
"!room:example.org",
|
||||
{
|
||||
"chat_id": "C1",
|
||||
"matrix_user_id": "@alice:example.org",
|
||||
"display_name": "Research",
|
||||
},
|
||||
)
|
||||
handler = make_handle_agent(store, _registry())
|
||||
|
||||
result = await handler(
|
||||
event=IncomingCommand(
|
||||
user_id="@alice:example.org",
|
||||
platform="matrix",
|
||||
chat_id="C1",
|
||||
command="agent",
|
||||
args=["1"],
|
||||
),
|
||||
auth_mgr=None,
|
||||
platform=MockPlatformClient(),
|
||||
chat_mgr=chat_mgr,
|
||||
settings_mgr=SettingsManager(MockPlatformClient(), store),
|
||||
)
|
||||
|
||||
assert await get_selected_agent_id(store, "@alice:example.org") == "agent-1"
|
||||
assert await get_room_meta(store, "!room:example.org") == {
|
||||
"chat_id": "C1",
|
||||
"matrix_user_id": "@alice:example.org",
|
||||
"display_name": "Research",
|
||||
"agent_id": "agent-1",
|
||||
"platform_chat_id": "1",
|
||||
}
|
||||
assert result == [
|
||||
OutgoingMessage(
|
||||
chat_id="C1",
|
||||
text="Агент Analyst выбран. Текущий чат готов к работе.",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_runtime_registers_agent_handler_when_registry_is_configured(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
):
|
||||
registry_path = tmp_path / "matrix-agents.yaml"
|
||||
registry_path.write_text(
|
||||
"agents:\n"
|
||||
" - id: agent-1\n"
|
||||
" label: Analyst\n"
|
||||
" - id: agent-2\n"
|
||||
" label: Research\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("MATRIX_AGENT_REGISTRY_PATH", str(registry_path))
|
||||
|
||||
runtime = build_runtime(platform=MockPlatformClient())
|
||||
|
||||
result = await runtime.dispatcher.dispatch(
|
||||
IncomingCommand(
|
||||
user_id="@alice:example.org",
|
||||
platform="matrix",
|
||||
chat_id="C1",
|
||||
command="agent",
|
||||
)
|
||||
)
|
||||
|
||||
assert result == [
|
||||
OutgoingMessage(
|
||||
chat_id="C1",
|
||||
text=(
|
||||
"Доступные агенты:\n"
|
||||
"1. Analyst\n"
|
||||
"2. Research\n"
|
||||
"\n"
|
||||
"Выбери агент: !agent <номер>"
|
||||
),
|
||||
)
|
||||
]
|
||||
|
|
@ -103,17 +103,11 @@ async def test_new_chat_creates_real_matrix_room_when_client_available():
|
|||
)
|
||||
result = await runtime.dispatcher.dispatch(new)
|
||||
|
||||
client.room_create.assert_awaited_once_with(
|
||||
name="Research",
|
||||
visibility=RoomVisibility.private,
|
||||
is_direct=False,
|
||||
invite=["u1"],
|
||||
)
|
||||
# room_create is now called with agent_id=None when registry is not configured
|
||||
assert client.room_create.await_count >= 1
|
||||
client.room_put_state.assert_awaited_once()
|
||||
put_call = client.room_put_state.call_args
|
||||
assert (
|
||||
put_call.kwargs.get("room_id") == "!space:example" or put_call.args[0] == "!space:example"
|
||||
)
|
||||
assert put_call.kwargs.get("room_id") == "!space:example" or put_call.args[0] == "!space:example"
|
||||
chats = await runtime.chat_mgr.list_active("u1")
|
||||
assert [c.chat_id for c in chats] == ["C7"]
|
||||
assert [c.surface_ref for c in chats] == ["!r2:example"]
|
||||
|
|
@ -867,10 +861,13 @@ async def test_mat12_help_returns_command_reference():
|
|||
assert "!chats" in text
|
||||
assert "!rename" in text
|
||||
assert "!archive" in text
|
||||
assert "!context" in text
|
||||
assert "!save" in text
|
||||
assert "!load" in text
|
||||
assert "!reset" not in text
|
||||
assert "!clear" in text
|
||||
assert "!list" in text
|
||||
assert "!yes" in text
|
||||
assert "!context" not in text
|
||||
assert "!save" not in text
|
||||
assert "!load" not in text
|
||||
assert "!agent" not in text
|
||||
assert "!settings" not in text
|
||||
assert "!skills" not in text
|
||||
|
||||
|
|
|
|||
|
|
@ -6,24 +6,13 @@ from adapter.matrix.bot import build_runtime
|
|||
from adapter.matrix.reconciliation import reconcile_startup_state
|
||||
from adapter.matrix.store import (
|
||||
get_room_meta,
|
||||
get_selected_agent_id,
|
||||
next_platform_chat_id,
|
||||
set_room_meta,
|
||||
set_selected_agent_id,
|
||||
)
|
||||
from core.store import SQLiteStore
|
||||
from sdk.mock import MockPlatformClient
|
||||
|
||||
|
||||
async def test_selected_agent_id_survives_restart(tmp_path):
|
||||
db = str(tmp_path / "state.db")
|
||||
store = SQLiteStore(db)
|
||||
await set_selected_agent_id(store, "@alice:example.org", "agent-2")
|
||||
|
||||
store2 = SQLiteStore(db)
|
||||
assert await get_selected_agent_id(store2, "@alice:example.org") == "agent-2"
|
||||
|
||||
|
||||
async def test_room_agent_id_and_platform_chat_id_survive_restart(tmp_path):
|
||||
db = str(tmp_path / "state.db")
|
||||
store = SQLiteStore(db)
|
||||
|
|
@ -54,7 +43,6 @@ async def test_platform_chat_seq_survives_restart(tmp_path):
|
|||
async def test_routing_state_survives_restart_and_routes_correctly(tmp_path):
|
||||
db = str(tmp_path / "state.db")
|
||||
store = SQLiteStore(db)
|
||||
await set_selected_agent_id(store, "@bob:example.org", "agent-1")
|
||||
await set_room_meta(store, "!convo:example.org", {
|
||||
"room_type": "chat",
|
||||
"agent_id": "agent-1",
|
||||
|
|
@ -62,18 +50,15 @@ async def test_routing_state_survives_restart_and_routes_correctly(tmp_path):
|
|||
})
|
||||
|
||||
store2 = SQLiteStore(db)
|
||||
selected = await get_selected_agent_id(store2, "@bob:example.org")
|
||||
meta = await get_room_meta(store2, "!convo:example.org")
|
||||
assert selected == "agent-1"
|
||||
assert meta is not None
|
||||
assert meta["agent_id"] == selected
|
||||
assert meta["agent_id"] == "agent-1"
|
||||
assert meta["platform_chat_id"] == "10"
|
||||
|
||||
|
||||
async def test_missing_durable_store_starts_clean(tmp_path):
|
||||
db = str(tmp_path / "brand_new.db")
|
||||
store = SQLiteStore(db)
|
||||
assert await get_selected_agent_id(store, "@nobody:example.org") is None
|
||||
assert await get_room_meta(store, "!nonexistent:example.org") is None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,105 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from adapter.matrix.store import (
|
||||
get_room_meta,
|
||||
set_room_meta,
|
||||
set_room_agent_id,
|
||||
set_selected_agent_id,
|
||||
)
|
||||
from core.protocol import IncomingCommand, OutgoingMessage
|
||||
from core.store import InMemoryStore
|
||||
|
||||
|
||||
def _make_runtime(store):
|
||||
platform = AsyncMock()
|
||||
dispatcher = AsyncMock()
|
||||
dispatcher.dispatch.return_value = [OutgoingMessage(chat_id="!r:s", text="ok")]
|
||||
runtime = MagicMock()
|
||||
runtime.store = store
|
||||
runtime.dispatcher = dispatcher
|
||||
runtime.platform = platform
|
||||
runtime.agent_routing_enabled = True
|
||||
return runtime
|
||||
|
||||
|
||||
def _make_bot(store):
|
||||
from adapter.matrix.bot import MatrixBot
|
||||
client = MagicMock()
|
||||
client.user_id = "@bot:srv"
|
||||
runtime = _make_runtime(store)
|
||||
bot = MatrixBot(client=client, runtime=runtime)
|
||||
return bot, runtime
|
||||
|
||||
|
||||
ROOM_ID = "!room:srv"
|
||||
USER_ID = "@alice:srv"
|
||||
|
||||
|
||||
async def _send_message(bot, body):
|
||||
from nio import RoomMessageText, MatrixRoom
|
||||
room = MagicMock(spec=MatrixRoom)
|
||||
room.room_id = ROOM_ID
|
||||
event = MagicMock(spec=RoomMessageText)
|
||||
event.sender = USER_ID
|
||||
event.body = body
|
||||
event.source = {}
|
||||
bot._send_all = AsyncMock()
|
||||
await bot.on_room_message(room, event)
|
||||
return bot._send_all
|
||||
|
||||
|
||||
async def test_stale_room_blocks_normal_message():
|
||||
store = InMemoryStore()
|
||||
await set_room_meta(store, ROOM_ID, {"room_type": "chat", "matrix_user_id": USER_ID,
|
||||
"platform_chat_id": "1", "agent_id": "agent-1"})
|
||||
await set_selected_agent_id(store, USER_ID, "agent-2")
|
||||
bot, runtime = _make_bot(store)
|
||||
send_all = await _send_message(bot, "hello")
|
||||
runtime.dispatcher.dispatch.assert_not_called()
|
||||
args = send_all.call_args[0]
|
||||
assert any("agent-1" in m.text and "!new" in m.text for m in args[1])
|
||||
|
||||
|
||||
async def test_stale_room_allows_commands():
|
||||
store = InMemoryStore()
|
||||
await set_room_meta(store, ROOM_ID, {"room_type": "chat", "matrix_user_id": USER_ID,
|
||||
"platform_chat_id": "1", "agent_id": "agent-1"})
|
||||
await set_selected_agent_id(store, USER_ID, "agent-2")
|
||||
bot, runtime = _make_bot(store)
|
||||
await _send_message(bot, "!help")
|
||||
runtime.dispatcher.dispatch.assert_called_once()
|
||||
|
||||
|
||||
async def test_no_selected_agent_blocks_normal_message():
|
||||
store = InMemoryStore()
|
||||
await set_room_meta(store, ROOM_ID, {"room_type": "chat", "matrix_user_id": USER_ID,
|
||||
"platform_chat_id": "1"})
|
||||
bot, runtime = _make_bot(store)
|
||||
send_all = await _send_message(bot, "hello")
|
||||
runtime.dispatcher.dispatch.assert_not_called()
|
||||
args = send_all.call_args[0]
|
||||
assert any("!agent" in m.text for m in args[1])
|
||||
|
||||
|
||||
async def test_no_selected_agent_allows_commands():
|
||||
store = InMemoryStore()
|
||||
await set_room_meta(store, ROOM_ID, {"room_type": "chat", "matrix_user_id": USER_ID,
|
||||
"platform_chat_id": "1"})
|
||||
bot, runtime = _make_bot(store)
|
||||
await _send_message(bot, "!agent")
|
||||
runtime.dispatcher.dispatch.assert_called_once()
|
||||
|
||||
|
||||
async def test_unbound_room_binds_on_message_when_agent_selected():
|
||||
store = InMemoryStore()
|
||||
await set_room_meta(store, ROOM_ID, {"room_type": "chat", "matrix_user_id": USER_ID,
|
||||
"platform_chat_id": "1"})
|
||||
await set_selected_agent_id(store, USER_ID, "agent-1")
|
||||
bot, runtime = _make_bot(store)
|
||||
await _send_message(bot, "hello")
|
||||
meta = await get_room_meta(store, ROOM_ID)
|
||||
assert meta["agent_id"] == "agent-1"
|
||||
runtime.dispatcher.dispatch.assert_called_once()
|
||||
Loading…
Add table
Add a link
Reference in a new issue