Instead of calling a /reset endpoint on platform-agent, !reset now generates a new thread_id (platform_chat_id) for the room. The old WebSocket connection is closed and the next message creates a fresh context automatically. No platform changes required.
204 lines
7.7 KiB
Python
204 lines
7.7 KiB
Python
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 get_room_meta, set_load_pending, set_platform_chat_id
|
|
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
|
|
|
|
|
|
async def _resolve_context_scope(
|
|
event: IncomingCommand,
|
|
store: "StateStore",
|
|
chat_mgr,
|
|
) -> tuple[str, str | None]:
|
|
room_id = await _resolve_room_id(event, chat_mgr)
|
|
room_meta = await get_room_meta(store, room_id)
|
|
platform_chat_id = room_meta.get("platform_chat_id") if room_meta else None
|
|
return room_id, platform_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}")]
|
|
|
|
_, platform_chat_id = await _resolve_context_scope(event, store, chat_mgr)
|
|
await prototype_state.add_saved_session(
|
|
event.user_id,
|
|
name,
|
|
source_context_id=platform_chat_id or event.chat_id,
|
|
)
|
|
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_context_scope(event, store, 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", prototype_state: "PrototypeStateStore"):
|
|
async def handle_reset(
|
|
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
|
) -> list[OutgoingEvent]:
|
|
import time
|
|
|
|
room_id = await _resolve_room_id(event, chat_mgr)
|
|
room_meta = await get_room_meta(store, room_id)
|
|
old_chat_id = (room_meta or {}).get("platform_chat_id") or room_id
|
|
|
|
new_chat_id = f"matrix:{room_id}#{int(time.time())}"
|
|
await set_platform_chat_id(store, room_id, new_chat_id)
|
|
|
|
disconnect = getattr(platform, "disconnect_chat", None)
|
|
if callable(disconnect):
|
|
await disconnect(old_chat_id)
|
|
|
|
await prototype_state.clear_current_session(new_chat_id)
|
|
|
|
return [OutgoingMessage(chat_id=event.chat_id, text="Контекст сброшен. Агент не помнит предыдущий разговор.")]
|
|
|
|
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]:
|
|
_, platform_chat_id = await _resolve_context_scope(event, store, chat_mgr)
|
|
context_key = platform_chat_id or event.chat_id
|
|
current_session = await prototype_state.get_current_session(context_key)
|
|
tokens_used = await prototype_state.get_last_tokens_used(context_key)
|
|
if platform_chat_id is not None and event.chat_id != platform_chat_id:
|
|
if current_session is None:
|
|
current_session = await prototype_state.get_current_session(event.chat_id)
|
|
if tokens_used == 0:
|
|
tokens_used = await prototype_state.get_last_tokens_used(event.chat_id)
|
|
sessions = await prototype_state.list_saved_sessions(event.user_id)
|
|
|
|
lines = [
|
|
"Контекст:",
|
|
f" Контекст чата: {platform_chat_id or event.chat_id}",
|
|
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
|