surfaces/adapter/matrix/handlers/chat.py
Mikhail Putilovskij b1aaa210a1 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
2026-04-28 03:05:11 +03:00

214 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Any
import structlog
from nio.api import RoomVisibility
from nio.responses import RoomCreateError
from adapter.matrix.agent_registry import AgentRegistry
from adapter.matrix.store import (
get_user_meta,
next_chat_id,
next_platform_chat_id,
set_room_meta,
)
from core.protocol import IncomingCommand, OutgoingMessage
logger = structlog.get_logger(__name__)
def _is_unregistered_chat_id(chat_id: str) -> bool:
return chat_id.startswith("unregistered:")
async def _fallback_new_chat(
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
) -> list:
if not await auth_mgr.is_authenticated(event.user_id):
return [OutgoingMessage(chat_id=event.chat_id, text="Введите !start чтобы начать.")]
name = " ".join(event.args).strip() if event.args else ""
chats = await chat_mgr.list_active(event.user_id)
chat_id = f"C{len(chats) + 1}"
ctx = await chat_mgr.get_or_create(
user_id=event.user_id,
chat_id=chat_id,
platform=event.platform,
surface_ref=event.chat_id,
name=name or None,
)
return [
OutgoingMessage(
chat_id=event.chat_id, text=f"Создан чат: {ctx.display_name} ({ctx.chat_id})"
)
]
def make_handle_new_chat(
client: Any | None,
store: Any | None,
registry: AgentRegistry | None = None,
) -> Callable[..., Awaitable[list]]:
async def handle_new_chat(
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
) -> list:
if client is None or store is None:
return await _fallback_new_chat(event, auth_mgr, platform, chat_mgr, settings_mgr)
if not await auth_mgr.is_authenticated(event.user_id):
return [
OutgoingMessage(
chat_id=event.chat_id,
text="Сначала примите приглашение бота.",
)
]
user_meta = await get_user_meta(store, event.user_id)
space_id = (user_meta or {}).get("space_id")
if not space_id:
return [
OutgoingMessage(
chat_id=event.chat_id,
text="Ошибка: Space не найден. Примите приглашение бота заново.",
)
]
name = " ".join(event.args).strip() if event.args else ""
chat_id = await next_chat_id(store, event.user_id)
platform_chat_id = await next_platform_chat_id(store)
room_name = name or f"Чат {chat_id}"
response = await client.room_create(
name=room_name,
visibility=RoomVisibility.private,
is_direct=False,
invite=[event.user_id],
)
if isinstance(response, RoomCreateError):
logger.error(
"room_create failed",
user_id=event.user_id,
status_code=getattr(response, "status_code", None),
)
return [OutgoingMessage(chat_id=event.chat_id, text="Не удалось создать комнату.")]
room_id = getattr(response, "room_id", None)
if not room_id:
return [OutgoingMessage(chat_id=event.chat_id, text="Не удалось создать комнату.")]
homeserver = event.user_id.split(":")[-1]
await client.room_put_state(
room_id=space_id,
event_type="m.space.child",
content={"via": [homeserver]},
state_key=room_id,
)
agent_id = None
if registry is not None:
agent_id = registry.get_agent_id_for_user(event.user_id)
if agent_id is None and registry.agents:
agent_id = registry.agents[0].agent_id
room_meta: dict = {
"room_type": "chat",
"chat_id": chat_id,
"display_name": room_name,
"matrix_user_id": event.user_id,
"space_id": space_id,
"platform_chat_id": platform_chat_id,
"agent_id": agent_id,
}
await set_room_meta(store, room_id, room_meta)
ctx = await chat_mgr.get_or_create(
user_id=event.user_id,
chat_id=chat_id,
platform=event.platform,
surface_ref=room_id,
name=room_name,
)
return [
OutgoingMessage(
chat_id=event.chat_id,
text=f"Создан чат: {ctx.display_name} ({ctx.chat_id})",
)
]
return handle_new_chat
async def handle_list_chats(
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
) -> list:
chats = await chat_mgr.list_active(event.user_id)
if not chats:
return [OutgoingMessage(chat_id=event.chat_id, text="Нет активных чатов.")]
lines = [f"{c.display_name} ({c.chat_id})" for c in chats]
return [OutgoingMessage(chat_id=event.chat_id, text="\n".join(lines))]
def make_handle_rename(
client: Any | None,
store: Any | None,
) -> Callable[..., Awaitable[list]]:
async def handle_rename(
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
) -> list:
if not event.args:
return [
OutgoingMessage(chat_id=event.chat_id, text="Укажите название: !rename Название")
]
if _is_unregistered_chat_id(event.chat_id):
return [
OutgoingMessage(
chat_id=event.chat_id,
text=(
"Этот чат не найден в локальном состоянии бота. "
"Открой зарегистрированную комнату или создай новый чат через !new."
),
)
]
new_name = " ".join(event.args)
ctx = await chat_mgr.rename(event.chat_id, new_name, user_id=event.user_id)
if client is not None and ctx.surface_ref:
await client.room_put_state(
room_id=ctx.surface_ref,
event_type="m.room.name",
content={"name": new_name},
state_key="",
)
return [OutgoingMessage(chat_id=event.chat_id, text=f"Переименован в: {ctx.display_name}")]
return handle_rename
def make_handle_archive(
client: Any | None,
store: Any | None,
) -> Callable[..., Awaitable[list]]:
async def handle_archive(
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
) -> list:
if _is_unregistered_chat_id(event.chat_id):
return [
OutgoingMessage(
chat_id=event.chat_id,
text=(
"Этот чат не найден в локальном состоянии бота. "
"Создай новый чат через !new."
),
)
]
ctx = await chat_mgr.get(event.chat_id, user_id=event.user_id)
if ctx is None:
return [OutgoingMessage(chat_id=event.chat_id, text="Этот чат не найден.")]
await chat_mgr.archive(event.chat_id, user_id=event.user_id)
if client is not None and ctx.surface_ref:
await client.room_leave(ctx.surface_ref)
return [OutgoingMessage(chat_id=event.chat_id, text="Чат архивирован.")]
return handle_archive