feat: add !agent command and durable user agent selection

Users can now list available agents with !agent and select one by
number. Selection persists in user metadata (selected_agent_id). If the
current room has no agent binding yet, selecting an agent binds it
immediately so the user can start messaging without !new.

Also updates the dispatcher test to reflect that real-mode platform is
now RoutedPlatformClient, not a bare RealPlatformClient.
This commit is contained in:
Mikhail Putilovskij 2026-04-24 13:54:25 +03:00
parent a65227e490
commit 74cf028e8f
7 changed files with 292 additions and 6 deletions

View file

@ -1,5 +1,6 @@
from __future__ import annotations
from adapter.matrix.handlers.agent import make_handle_agent
from adapter.matrix.handlers.chat import (
handle_list_chats,
make_handle_archive,
@ -34,9 +35,12 @@ def register_matrix_handlers(
dispatcher: EventDispatcher,
client=None,
store=None,
registry=None,
prototype_state=None,
agent_base_url: str = "http://127.0.0.1:8000",
) -> None:
if store is not None and registry is not None:
dispatcher.register(IncomingCommand, "agent", make_handle_agent(store, registry))
dispatcher.register(IncomingCommand, "new", make_handle_new_chat(client, store))
dispatcher.register(IncomingCommand, "chats", handle_list_chats)
dispatcher.register(IncomingCommand, "rename", make_handle_rename(client, store))

View file

@ -0,0 +1,78 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from adapter.matrix.agent_registry import AgentRegistry
from adapter.matrix.store import (
get_platform_chat_id,
get_selected_agent_id,
get_room_meta,
next_platform_chat_id,
set_platform_chat_id,
set_room_agent_id,
set_selected_agent_id,
)
from core.protocol import IncomingCommand, OutgoingMessage
def make_handle_agent(store, registry: AgentRegistry) -> Callable[..., Awaitable[list]]:
async def handle_agent(
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
) -> list:
if not event.args:
selected_agent_id = await get_selected_agent_id(store, event.user_id)
lines = ["Доступные агенты:"]
for index, agent in enumerate(registry.agents, start=1):
suffix = " [текущий]" if agent.agent_id == selected_agent_id else ""
lines.append(f"{index}. {agent.label}{suffix}")
lines.extend(["", "Выбери агент: !agent <номер>"])
return [OutgoingMessage(chat_id=event.chat_id, text="\n".join(lines))]
try:
selected_index = int(event.args[0])
except ValueError:
return [
OutgoingMessage(
chat_id=event.chat_id,
text="Укажи номер агента из списка: !agent <номер>.",
)
]
if selected_index < 1 or selected_index > len(registry.agents):
return [
OutgoingMessage(
chat_id=event.chat_id,
text="Такого агента нет. Открой список через !agent.",
)
]
agent = registry.agents[selected_index - 1]
await set_selected_agent_id(store, event.user_id, agent.agent_id)
current_chat = await chat_mgr.get(event.chat_id, user_id=event.user_id)
if current_chat is not None and current_chat.surface_ref:
room_id = current_chat.surface_ref
room_meta = await get_room_meta(store, room_id)
if room_meta is not None and not room_meta.get("agent_id"):
await set_room_agent_id(store, room_id, agent.agent_id)
if await get_platform_chat_id(store, room_id) is None:
await set_platform_chat_id(
store,
room_id,
await next_platform_chat_id(store),
)
return [
OutgoingMessage(
chat_id=event.chat_id,
text=f"Агент {agent.label} выбран. Текущий чат готов к работе.",
)
]
return [
OutgoingMessage(
chat_id=event.chat_id,
text=f"Агент переключен на {agent.label}. Продолжай через !new.",
)
]
return handle_agent

View file

@ -14,6 +14,9 @@ HELP_TEXT = "\n".join(
"!save [имя] сохранить текущий контекст",
"!load показать сохранённые контексты",
"",
"!agent показать доступных агентов",
"!agent <номер> выбрать агента для следующих чатов",
"",
"Остальные команды и настройки скрыты в MVP, чтобы не вводить в заблуждение.",
]
)

View file

@ -45,6 +45,27 @@ async def set_user_meta(store: StateStore, matrix_user_id: str, meta: dict) -> N
await store.set(f"{USER_META_PREFIX}{matrix_user_id}", meta)
async def get_selected_agent_id(store: StateStore, matrix_user_id: str) -> str | None:
meta = await get_user_meta(store, matrix_user_id)
return meta.get("selected_agent_id") if meta else None
async def set_selected_agent_id(
store: StateStore,
matrix_user_id: str,
agent_id: str,
) -> None:
meta = dict(await get_user_meta(store, matrix_user_id) or {})
meta["selected_agent_id"] = agent_id
await set_user_meta(store, matrix_user_id, meta)
async def set_room_agent_id(store: StateStore, room_id: str, agent_id: str) -> None:
meta = dict(await get_room_meta(store, room_id) or {})
meta["agent_id"] = agent_id
await set_room_meta(store, room_id, meta)
async def get_room_state(store: StateStore, room_id: str) -> str:
data = await store.get(f"{ROOM_STATE_PREFIX}{room_id}")
return data["state"] if data else "idle"