feat(04-02): add matrix context management commands
- add save/load/reset/context handlers and matrix interception flows - persist current session and last token usage in prototype state
This commit is contained in:
parent
da0b76882e
commit
b52fdc4670
7 changed files with 638 additions and 21 deletions
|
|
@ -7,6 +7,12 @@ from adapter.matrix.handlers.chat import (
|
|||
make_handle_rename,
|
||||
)
|
||||
from adapter.matrix.handlers.confirm import make_handle_cancel, make_handle_confirm
|
||||
from adapter.matrix.handlers.context_commands import (
|
||||
make_handle_context,
|
||||
make_handle_load,
|
||||
make_handle_reset,
|
||||
make_handle_save,
|
||||
)
|
||||
from adapter.matrix.handlers.settings import (
|
||||
handle_help,
|
||||
handle_settings,
|
||||
|
|
@ -23,7 +29,14 @@ from core.handler import EventDispatcher
|
|||
from core.protocol import IncomingCallback, IncomingCommand
|
||||
|
||||
|
||||
def register_matrix_handlers(dispatcher: EventDispatcher, client=None, store=None) -> None:
|
||||
def register_matrix_handlers(
|
||||
dispatcher: EventDispatcher,
|
||||
client=None,
|
||||
store=None,
|
||||
agent_api=None,
|
||||
prototype_state=None,
|
||||
agent_base_url: str = "http://127.0.0.1:8000",
|
||||
) -> None:
|
||||
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))
|
||||
|
|
@ -41,3 +54,9 @@ def register_matrix_handlers(dispatcher: EventDispatcher, client=None, store=Non
|
|||
dispatcher.register(IncomingCallback, "confirm", make_handle_confirm(store))
|
||||
dispatcher.register(IncomingCallback, "cancel", make_handle_cancel(store))
|
||||
dispatcher.register(IncomingCallback, "toggle_skill", handle_toggle_skill)
|
||||
|
||||
if agent_api is not None and prototype_state is not None:
|
||||
dispatcher.register(IncomingCommand, "save", make_handle_save(agent_api, store, prototype_state))
|
||||
dispatcher.register(IncomingCommand, "load", make_handle_load(store, prototype_state))
|
||||
dispatcher.register(IncomingCommand, "reset", make_handle_reset(store, agent_base_url))
|
||||
dispatcher.register(IncomingCommand, "context", make_handle_context(store, prototype_state))
|
||||
|
|
|
|||
172
adapter/matrix/handlers/context_commands.py
Normal file
172
adapter/matrix/handlers/context_commands.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
import structlog
|
||||
|
||||
from adapter.matrix.store import set_load_pending, set_reset_pending
|
||||
from core.protocol import IncomingCommand, OutgoingEvent, OutgoingMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.store import StateStore
|
||||
from sdk.prototype_state import PrototypeStateStore
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
SAVE_PROMPT = (
|
||||
"Summarize our conversation and save to /workspace/contexts/{name}.md. "
|
||||
"Reply only with: Saved: {name}"
|
||||
)
|
||||
LOAD_PROMPT = (
|
||||
"Load context from /workspace/contexts/{name}.md and use it as background "
|
||||
"for our conversation. Reply: Loaded: {name}"
|
||||
)
|
||||
_VALID_NAME = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
|
||||
|
||||
def _sanitize_session_name(raw_name: str) -> str | None:
|
||||
name = raw_name.strip()
|
||||
if not name or not _VALID_NAME.fullmatch(name):
|
||||
return None
|
||||
return name
|
||||
|
||||
|
||||
async def _resolve_room_id(event: IncomingCommand, chat_mgr) -> str:
|
||||
if chat_mgr is None:
|
||||
return event.chat_id
|
||||
ctx = await chat_mgr.get(event.chat_id, user_id=event.user_id)
|
||||
if ctx is not None and ctx.surface_ref:
|
||||
return ctx.surface_ref
|
||||
return event.chat_id
|
||||
|
||||
|
||||
def make_handle_save(agent_api, store: "StateStore", prototype_state: "PrototypeStateStore"):
|
||||
async def handle_save(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
if event.args:
|
||||
name = _sanitize_session_name(event.args[0])
|
||||
if name is None:
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=event.chat_id,
|
||||
text="Имя сохранения может содержать только буквы, цифры, _ и -.",
|
||||
)
|
||||
]
|
||||
else:
|
||||
name = f"context-{datetime.now(UTC).strftime('%Y%m%d-%H%M%S')}"
|
||||
|
||||
try:
|
||||
await platform.send_message(
|
||||
event.user_id,
|
||||
event.chat_id,
|
||||
SAVE_PROMPT.format(name=name),
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("save_agent_call_failed", error=str(exc))
|
||||
return [OutgoingMessage(chat_id=event.chat_id, text=f"Ошибка при сохранении: {exc}")]
|
||||
|
||||
await prototype_state.add_saved_session(event.user_id, name)
|
||||
return [OutgoingMessage(chat_id=event.chat_id, text=f"Сохранение запущено: {name}")]
|
||||
|
||||
return handle_save
|
||||
|
||||
|
||||
def make_handle_load(store: "StateStore", prototype_state: "PrototypeStateStore"):
|
||||
async def handle_load(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
sessions = await prototype_state.list_saved_sessions(event.user_id)
|
||||
if not sessions:
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=event.chat_id,
|
||||
text="Нет сохранённых сессий. Используй !save [имя].",
|
||||
)
|
||||
]
|
||||
|
||||
room_id = await _resolve_room_id(event, chat_mgr)
|
||||
lines = ["Сохранённые сессии:"]
|
||||
for index, session in enumerate(sessions, start=1):
|
||||
created = session.get("created_at", "")[:10]
|
||||
lines.append(f" {index}. {session['name']} ({created})")
|
||||
lines.append("")
|
||||
lines.append("Введи номер или 0 / !cancel для отмены.")
|
||||
|
||||
await set_load_pending(store, event.user_id, room_id, {"saves": sessions})
|
||||
return [OutgoingMessage(chat_id=event.chat_id, text="\n".join(lines))]
|
||||
|
||||
return handle_load
|
||||
|
||||
|
||||
def make_handle_reset(store: "StateStore", agent_base_url: str):
|
||||
async def handle_reset(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
room_id = await _resolve_room_id(event, chat_mgr)
|
||||
await set_reset_pending(store, event.user_id, room_id, {"active": True})
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=event.chat_id,
|
||||
text=(
|
||||
"Сбросить контекст агента? Выбери:\n"
|
||||
" !yes - сбросить\n"
|
||||
" !save [имя] - сохранить и сбросить\n"
|
||||
" !no - отмена"
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
return handle_reset
|
||||
|
||||
|
||||
async def _call_reset_endpoint(agent_base_url: str, chat_id: str) -> list[OutgoingEvent]:
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(f"{agent_base_url}/reset", timeout=5.0)
|
||||
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
||||
logger.warning("reset_endpoint_unreachable", error=str(exc))
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=chat_id,
|
||||
text="Reset endpoint недоступен. Обратитесь к администратору.",
|
||||
)
|
||||
]
|
||||
|
||||
if response.status_code == 404:
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=chat_id,
|
||||
text="Reset endpoint недоступен. Обратитесь к администратору.",
|
||||
)
|
||||
]
|
||||
return [OutgoingMessage(chat_id=chat_id, text="Контекст сброшен.")]
|
||||
|
||||
|
||||
def make_handle_context(store: "StateStore", prototype_state: "PrototypeStateStore"):
|
||||
async def handle_context(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
current_session = await prototype_state.get_current_session(event.user_id)
|
||||
tokens_used = await prototype_state.get_last_tokens_used(event.user_id)
|
||||
sessions = await prototype_state.list_saved_sessions(event.user_id)
|
||||
|
||||
lines = [
|
||||
"Контекст:",
|
||||
f" Сессия: {current_session or 'не загружена'}",
|
||||
f" Токены (последний ответ): {tokens_used}",
|
||||
f" Сохранения ({len(sessions)}):",
|
||||
]
|
||||
if sessions:
|
||||
for session in sessions:
|
||||
created = session.get("created_at", "")[:10]
|
||||
lines.append(f" - {session['name']} ({created})")
|
||||
else:
|
||||
lines.append(" (нет)")
|
||||
|
||||
return [OutgoingMessage(chat_id=event.chat_id, text="\n".join(lines))]
|
||||
|
||||
return handle_context
|
||||
Loading…
Add table
Add a link
Reference in a new issue