feat(deploy): finalize MVP deployment and file transfer approach
This commit is contained in:
parent
6369721876
commit
0f79494fbe
43 changed files with 3078 additions and 645 deletions
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import yaml
|
||||
|
||||
|
|
@ -19,6 +20,16 @@ class AgentDefinition:
|
|||
workspace_path: str = field(default="")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentAssignment:
|
||||
agent_id: str | None
|
||||
source: Literal["configured", "default", "none"]
|
||||
|
||||
@property
|
||||
def is_default(self) -> bool:
|
||||
return self.source == "default"
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -38,6 +49,14 @@ class AgentRegistry:
|
|||
def get_agent_id_for_user(self, matrix_user_id: str) -> str | None:
|
||||
return self._user_agents.get(matrix_user_id)
|
||||
|
||||
def resolve_agent_for_user(self, matrix_user_id: str) -> AgentAssignment:
|
||||
agent_id = self.get_agent_id_for_user(matrix_user_id)
|
||||
if agent_id is not None:
|
||||
return AgentAssignment(agent_id=agent_id, source="configured")
|
||||
if self.agents:
|
||||
return AgentAssignment(agent_id=self.agents[0].agent_id, source="default")
|
||||
return AgentAssignment(agent_id=None, source="none")
|
||||
|
||||
|
||||
def _required_text(entry: Mapping[str, object], key: str) -> str:
|
||||
value = entry.get(key)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -24,21 +25,26 @@ from nio import (
|
|||
)
|
||||
from nio.responses import SyncResponse
|
||||
|
||||
from adapter.matrix.agent_registry import AgentRegistry, AgentRegistryError, load_agent_registry
|
||||
from adapter.matrix.converter import from_room_event
|
||||
from adapter.matrix.files import (
|
||||
download_matrix_attachment,
|
||||
matrix_msgtype_for_attachment,
|
||||
resolve_workspace_attachment_path,
|
||||
)
|
||||
from adapter.matrix.agent_registry import AgentRegistry, AgentRegistryError, load_agent_registry
|
||||
from adapter.matrix.handlers import register_matrix_handlers
|
||||
from adapter.matrix.handlers.auth import handle_invite, provision_workspace_chat
|
||||
from adapter.matrix.handlers.auth import (
|
||||
default_agent_notice,
|
||||
handle_invite,
|
||||
provision_workspace_chat,
|
||||
restore_workspace_access,
|
||||
)
|
||||
from adapter.matrix.handlers.context_commands import (
|
||||
LOAD_PROMPT,
|
||||
)
|
||||
from adapter.matrix.routed_platform import RoutedPlatformClient
|
||||
from adapter.matrix.reconciliation import reconcile_startup_state
|
||||
from adapter.matrix.room_router import resolve_chat_id
|
||||
from adapter.matrix.routed_platform import RoutedPlatformClient
|
||||
from adapter.matrix.store import (
|
||||
add_staged_attachment,
|
||||
clear_load_pending,
|
||||
|
|
@ -50,7 +56,6 @@ from adapter.matrix.store import (
|
|||
remove_staged_attachment_at,
|
||||
set_pending_confirm,
|
||||
set_platform_chat_id,
|
||||
set_room_agent_id,
|
||||
set_room_meta,
|
||||
)
|
||||
from core.auth import AuthManager
|
||||
|
|
@ -118,6 +123,26 @@ def _normalize_agent_base_url(url: str) -> str:
|
|||
return urlunsplit((parsed.scheme, parsed.netloc, path, "", ""))
|
||||
|
||||
|
||||
def _ws_debug_enabled() -> bool:
|
||||
value = os.environ.get("SURFACES_DEBUG_WS", "")
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _configure_debug_logging() -> None:
|
||||
if not _ws_debug_enabled():
|
||||
return
|
||||
root_logger = logging.getLogger()
|
||||
if not root_logger.handlers:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)-8s] %(name)s %(message)s",
|
||||
)
|
||||
elif root_logger.level > logging.INFO:
|
||||
root_logger.setLevel(logging.INFO)
|
||||
logging.getLogger("lambda_agent_api").setLevel(logging.INFO)
|
||||
logging.getLogger("lambda_agent_api.agent_api").setLevel(logging.INFO)
|
||||
|
||||
|
||||
def _agent_base_url_from_env() -> str:
|
||||
if base_url := os.environ.get("AGENT_BASE_URL"):
|
||||
return base_url
|
||||
|
|
@ -135,13 +160,39 @@ def _load_agent_registry_from_env(required: bool = False) -> AgentRegistry | Non
|
|||
)
|
||||
return None
|
||||
try:
|
||||
return load_agent_registry(registry_path)
|
||||
registry = load_agent_registry(registry_path)
|
||||
except (AgentRegistryError, OSError) as exc:
|
||||
raise RuntimeError(f"failed to load matrix agent registry: {registry_path}") from exc
|
||||
if _ws_debug_enabled():
|
||||
logger.warning(
|
||||
"matrix_agent_registry_loaded",
|
||||
registry_path=registry_path,
|
||||
agent_count=len(registry.agents),
|
||||
)
|
||||
for agent in registry.agents:
|
||||
logger.warning(
|
||||
"matrix_agent_registry_entry",
|
||||
registry_path=registry_path,
|
||||
agent_id=agent.agent_id,
|
||||
label=agent.label,
|
||||
configured_base_url=agent.base_url,
|
||||
normalized_base_url=_normalize_agent_base_url(agent.base_url)
|
||||
if agent.base_url
|
||||
else "",
|
||||
workspace_path=agent.workspace_path,
|
||||
)
|
||||
return registry
|
||||
|
||||
|
||||
def _build_platform_from_env(*, store: StateStore, chat_mgr: ChatManager) -> PlatformClient:
|
||||
backend = os.environ.get("MATRIX_PLATFORM_BACKEND", "mock").strip().lower()
|
||||
if _ws_debug_enabled():
|
||||
logger.warning(
|
||||
"matrix_platform_backend_selected",
|
||||
backend=backend,
|
||||
global_agent_base_url=_agent_base_url_from_env(),
|
||||
registry_path=os.environ.get("MATRIX_AGENT_REGISTRY_PATH", "").strip(),
|
||||
)
|
||||
if backend == "real":
|
||||
prototype_state = PrototypeStateStore()
|
||||
registry = _load_agent_registry_from_env(required=True)
|
||||
|
|
@ -220,6 +271,36 @@ class MatrixBot:
|
|||
await next_platform_chat_id(self.runtime.store),
|
||||
)
|
||||
|
||||
async def _refresh_room_agent_assignment(
|
||||
self, room_id: str, matrix_user_id: str, room_meta: dict | None
|
||||
) -> tuple[dict | None, bool]:
|
||||
if not room_meta or room_meta.get("redirect_room_id") or self.runtime.registry is None:
|
||||
return room_meta, False
|
||||
|
||||
assignment = self.runtime.registry.resolve_agent_for_user(matrix_user_id)
|
||||
updated = dict(room_meta)
|
||||
should_warn_default = False
|
||||
|
||||
if assignment.source == "configured" and (
|
||||
updated.get("agent_id") != assignment.agent_id
|
||||
or updated.get("agent_assignment") != "configured"
|
||||
):
|
||||
updated["agent_id"] = assignment.agent_id
|
||||
updated["agent_assignment"] = "configured"
|
||||
updated.pop("default_agent_notice_sent", None)
|
||||
elif assignment.source == "default":
|
||||
if not updated.get("agent_id"):
|
||||
updated["agent_id"] = assignment.agent_id
|
||||
if updated.get("agent_id") == assignment.agent_id:
|
||||
updated["agent_assignment"] = "default"
|
||||
should_warn_default = not updated.get("default_agent_notice_sent")
|
||||
updated["default_agent_notice_sent"] = True
|
||||
|
||||
if updated != room_meta:
|
||||
await set_room_meta(self.runtime.store, room_id, updated)
|
||||
return updated, should_warn_default
|
||||
return room_meta, should_warn_default
|
||||
|
||||
async def on_room_message(self, room: MatrixRoom, event: RoomMessageText) -> None:
|
||||
if getattr(event, "sender", None) == self.client.user_id:
|
||||
return
|
||||
|
|
@ -228,6 +309,14 @@ class MatrixBot:
|
|||
room_meta = await get_room_meta(self.runtime.store, room.room_id)
|
||||
if room_meta is not None and not room_meta.get("redirect_room_id"):
|
||||
await self._ensure_platform_chat_id(room.room_id, room_meta)
|
||||
room_meta, warn_default_agent = await self._refresh_room_agent_assignment(
|
||||
room.room_id, sender, room_meta
|
||||
)
|
||||
if warn_default_agent and not body.startswith("!"):
|
||||
await self._send_all(
|
||||
room.room_id,
|
||||
[OutgoingMessage(chat_id=room.room_id, text=default_agent_notice())],
|
||||
)
|
||||
|
||||
load_pending = await get_load_pending(self.runtime.store, sender, room.room_id)
|
||||
if load_pending is not None and (body.isdigit() or body == "!cancel"):
|
||||
|
|
@ -241,17 +330,97 @@ class MatrixBot:
|
|||
await self._send_all(room.room_id, outgoing)
|
||||
return
|
||||
elif room_meta.get("redirect_room_id"):
|
||||
display_name = getattr(room, "display_name", None) or sender
|
||||
if body == "!new":
|
||||
try:
|
||||
created = await provision_workspace_chat(
|
||||
self.client,
|
||||
sender,
|
||||
display_name,
|
||||
self.runtime.platform,
|
||||
self.runtime.store,
|
||||
self.runtime.auth_mgr,
|
||||
self.runtime.chat_mgr,
|
||||
registry=self.runtime.registry,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"matrix_entry_room_new_chat_failed",
|
||||
room_id=room.room_id,
|
||||
sender=sender,
|
||||
error=str(exc),
|
||||
)
|
||||
await self._send_all(
|
||||
room.room_id,
|
||||
[
|
||||
OutgoingMessage(
|
||||
chat_id=room.room_id,
|
||||
text="Не удалось создать новый рабочий чат.",
|
||||
)
|
||||
],
|
||||
)
|
||||
return
|
||||
|
||||
welcome = f"Создал новый рабочий чат {created['room_name']}."
|
||||
if created.get("agent_assignment") == "default":
|
||||
welcome = f"{welcome}\n\n{default_agent_notice()}"
|
||||
await self.client.room_send(
|
||||
created["chat_room_id"],
|
||||
"m.room.message",
|
||||
{"msgtype": "m.text", "body": welcome},
|
||||
)
|
||||
await set_room_meta(
|
||||
self.runtime.store,
|
||||
room.room_id,
|
||||
{
|
||||
**room_meta,
|
||||
"redirect_room_id": created["chat_room_id"],
|
||||
"redirect_chat_id": created["chat_id"],
|
||||
},
|
||||
)
|
||||
await self._send_all(
|
||||
room.room_id,
|
||||
[
|
||||
OutgoingMessage(
|
||||
chat_id=room.room_id,
|
||||
text=(
|
||||
f"Создал рабочий чат {created['room_name']} "
|
||||
f"({created['chat_id']}) и отправил приглашение."
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
return
|
||||
|
||||
restored = await restore_workspace_access(
|
||||
self.client,
|
||||
sender,
|
||||
display_name,
|
||||
self.runtime.platform,
|
||||
self.runtime.store,
|
||||
self.runtime.auth_mgr,
|
||||
self.runtime.chat_mgr,
|
||||
registry=self.runtime.registry,
|
||||
)
|
||||
redirect_room_id = room_meta["redirect_room_id"]
|
||||
redirect_chat_id = room_meta.get("redirect_chat_id", "рабочий чат")
|
||||
if restored.get("created_new_chat"):
|
||||
text = (
|
||||
f"Создал новый рабочий чат {restored['room_name']} "
|
||||
f"({restored['chat_id']}) и отправил приглашение."
|
||||
)
|
||||
else:
|
||||
text = (
|
||||
f"Рабочий чат уже создан: {redirect_chat_id}. "
|
||||
"Я повторно отправил приглашения в пространство Lambda и рабочие чаты. "
|
||||
"Чтобы создать новый чат, напишите !new здесь."
|
||||
)
|
||||
await self._send_all(
|
||||
room.room_id,
|
||||
[
|
||||
OutgoingMessage(
|
||||
chat_id=room.room_id,
|
||||
text=(
|
||||
f"Рабочий чат уже создан: {redirect_chat_id}. "
|
||||
"Открой приглашённую комнату для продолжения."
|
||||
),
|
||||
text=text,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -302,6 +471,15 @@ class MatrixBot:
|
|||
incoming,
|
||||
)
|
||||
agent_id = (room_meta or {}).get("agent_id")
|
||||
if _ws_debug_enabled() and not body.startswith("!"):
|
||||
logger.warning(
|
||||
"matrix_incoming_message_route",
|
||||
room_id=room.room_id,
|
||||
sender=sender,
|
||||
local_chat_id=local_chat_id,
|
||||
agent_id=agent_id,
|
||||
platform_chat_id=(room_meta or {}).get("platform_chat_id"),
|
||||
)
|
||||
workspace_root = self._agent_workspace_root(agent_id)
|
||||
try:
|
||||
outgoing = await self.runtime.dispatcher.dispatch(incoming)
|
||||
|
|
@ -520,6 +698,8 @@ class MatrixBot:
|
|||
f"Привет, {created['user'].display_name or sender}! Пиши — я здесь.\n\n"
|
||||
"Команды: !new · !chats · !rename · !archive · !context · !save · !load · !help"
|
||||
)
|
||||
if created.get("agent_assignment") == "default":
|
||||
welcome = f"{welcome}\n\n{default_agent_notice()}"
|
||||
await set_room_meta(
|
||||
self.runtime.store,
|
||||
room.room_id,
|
||||
|
|
@ -715,6 +895,7 @@ async def send_outgoing(
|
|||
|
||||
|
||||
async def main() -> None:
|
||||
_configure_debug_logging()
|
||||
homeserver = os.environ.get("MATRIX_HOMESERVER")
|
||||
user_id = os.environ.get("MATRIX_USER_ID")
|
||||
device_id = os.environ.get("MATRIX_DEVICE_ID", "")
|
||||
|
|
@ -768,6 +949,15 @@ async def main() -> None:
|
|||
store_path=store_path,
|
||||
request_timeout=client_config.request_timeout,
|
||||
)
|
||||
if _ws_debug_enabled():
|
||||
logger.warning(
|
||||
"matrix_ws_debug_enabled",
|
||||
homeserver=homeserver,
|
||||
user_id=user_id,
|
||||
backend=os.environ.get("MATRIX_PLATFORM_BACKEND", "mock").strip().lower(),
|
||||
global_agent_base_url=_agent_base_url_from_env(),
|
||||
registry_path=os.environ.get("MATRIX_AGENT_REGISTRY_PATH", "").strip(),
|
||||
)
|
||||
try:
|
||||
await client.sync_forever(timeout=30000, since=since_token)
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ from __future__ import annotations
|
|||
|
||||
import mimetypes
|
||||
import re
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
from core.protocol import Attachment
|
||||
|
||||
|
||||
def _sanitize_component(value: str) -> str:
|
||||
cleaned = re.sub(r"[^A-Za-z0-9._-]+", "_", value)
|
||||
cleaned = cleaned.strip("._-")
|
||||
return cleaned or "unknown"
|
||||
def _sanitize_filename(value: str) -> str:
|
||||
filename = PurePosixPath(str(value).replace("\\", "/")).name.strip()
|
||||
cleaned = re.sub(r"[\x00-\x1f\x7f<>:\"/\\|?*]+", "_", filename)
|
||||
cleaned = cleaned.strip(" .")
|
||||
return cleaned or "attachment.bin"
|
||||
|
||||
|
||||
def _default_filename(attachment: Attachment) -> str:
|
||||
|
|
@ -28,38 +28,38 @@ def _default_filename(attachment: Attachment) -> str:
|
|||
return f"{base}{extension}"
|
||||
|
||||
|
||||
def build_workspace_attachment_path(
|
||||
*,
|
||||
workspace_root: Path,
|
||||
matrix_user_id: str,
|
||||
room_id: str,
|
||||
filename: str,
|
||||
timestamp: str | None = None,
|
||||
) -> tuple[str, Path]:
|
||||
"""Legacy path builder used when no per-agent workspace_path is configured."""
|
||||
stamp = timestamp or datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||
safe_user = _sanitize_component(matrix_user_id.lstrip("@"))
|
||||
safe_room = _sanitize_component(room_id.lstrip("!"))
|
||||
safe_name = _sanitize_component(filename) or "attachment.bin"
|
||||
relative_path = (
|
||||
Path("surfaces") / "matrix" / safe_user / safe_room / "inbox" / f"{stamp}-{safe_name}"
|
||||
)
|
||||
return relative_path.as_posix(), workspace_root / relative_path
|
||||
def _with_copy_index(filename: str, index: int) -> str:
|
||||
path = Path(filename)
|
||||
suffix = path.suffix
|
||||
stem = path.stem if suffix else filename
|
||||
return f"{stem} ({index}){suffix}"
|
||||
|
||||
|
||||
def build_agent_incoming_path(
|
||||
def _unique_workspace_relative_path(workspace_root: Path, filename: str) -> tuple[str, Path]:
|
||||
safe_name = _sanitize_filename(filename)
|
||||
candidate = workspace_root / safe_name
|
||||
if not candidate.exists():
|
||||
return safe_name, candidate
|
||||
|
||||
index = 1
|
||||
while True:
|
||||
indexed_name = _with_copy_index(safe_name, index)
|
||||
candidate = workspace_root / indexed_name
|
||||
if not candidate.exists():
|
||||
return indexed_name, candidate
|
||||
index += 1
|
||||
|
||||
|
||||
def build_agent_workspace_path(
|
||||
*,
|
||||
workspace_root: Path,
|
||||
filename: str,
|
||||
timestamp: str | None = None,
|
||||
) -> tuple[str, Path]:
|
||||
"""Per-agent path builder: saves to {workspace_root}/incoming/{stamp}-{filename}.
|
||||
"""Saves user files directly to {workspace_root}/{filename}.
|
||||
|
||||
The returned relative path is what gets passed to agent.send_message(attachments=[...]).
|
||||
"""
|
||||
stamp = timestamp or datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
||||
safe_name = _sanitize_component(filename) or "attachment.bin"
|
||||
relative_path = Path("incoming") / f"{stamp}-{safe_name}"
|
||||
return relative_path.as_posix(), workspace_root / relative_path
|
||||
return _unique_workspace_relative_path(workspace_root, filename)
|
||||
|
||||
|
||||
async def download_matrix_attachment(
|
||||
|
|
@ -76,21 +76,11 @@ async def download_matrix_attachment(
|
|||
|
||||
filename = _default_filename(attachment)
|
||||
|
||||
if workspace_root.name and str(workspace_root) not in (".", "/workspace", "/agents"):
|
||||
# Per-agent workspace configured — use simple incoming/ layout
|
||||
relative_path, absolute_path = build_agent_incoming_path(
|
||||
workspace_root=workspace_root,
|
||||
filename=filename,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
else:
|
||||
relative_path, absolute_path = build_workspace_attachment_path(
|
||||
workspace_root=workspace_root,
|
||||
matrix_user_id=matrix_user_id,
|
||||
room_id=room_id,
|
||||
filename=filename,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
del matrix_user_id, room_id, timestamp
|
||||
relative_path, absolute_path = build_agent_workspace_path(
|
||||
workspace_root=workspace_root,
|
||||
filename=filename,
|
||||
)
|
||||
|
||||
absolute_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,31 @@ def _default_room_name(chat_id: str) -> str:
|
|||
return f"Чат {suffix}"
|
||||
|
||||
|
||||
def default_agent_notice() -> str:
|
||||
return (
|
||||
"Внимание: ваш Matrix ID не найден в конфиге агентов. "
|
||||
"Пока используется агент по умолчанию. После добавления вас в конфиг "
|
||||
"бот переключит существующие комнаты на назначенного агента."
|
||||
)
|
||||
|
||||
|
||||
async def _invite_if_possible(client: Any, room_id: str, matrix_user_id: str) -> bool:
|
||||
room_invite = getattr(client, "room_invite", None)
|
||||
if not callable(room_invite):
|
||||
return False
|
||||
try:
|
||||
await room_invite(room_id, matrix_user_id)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"matrix_workspace_reinvite_failed",
|
||||
room_id=room_id,
|
||||
user=matrix_user_id,
|
||||
error=str(exc),
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
async def provision_workspace_chat(
|
||||
client: Any,
|
||||
matrix_user_id: str,
|
||||
|
|
@ -68,10 +93,11 @@ async def provision_workspace_chat(
|
|||
room_name = room_name_override or _default_room_name(chat_id)
|
||||
|
||||
agent_id = None
|
||||
agent_assignment = "none"
|
||||
if registry is not None:
|
||||
agent_id = registry.get_agent_id_for_user(matrix_user_id)
|
||||
if agent_id is None and registry.agents:
|
||||
agent_id = registry.agents[0].agent_id
|
||||
assignment = registry.resolve_agent_for_user(matrix_user_id)
|
||||
agent_id = assignment.agent_id
|
||||
agent_assignment = assignment.source
|
||||
|
||||
chat_resp = await client.room_create(
|
||||
name=room_name,
|
||||
|
|
@ -110,6 +136,7 @@ async def provision_workspace_chat(
|
|||
"space_id": space_id,
|
||||
"platform_chat_id": platform_chat_id,
|
||||
"agent_id": agent_id,
|
||||
"agent_assignment": agent_assignment,
|
||||
},
|
||||
)
|
||||
await chat_mgr.get_or_create(
|
||||
|
|
@ -126,6 +153,64 @@ async def provision_workspace_chat(
|
|||
"chat_room_id": chat_room_id,
|
||||
"chat_id": chat_id,
|
||||
"room_name": room_name,
|
||||
"agent_assignment": agent_assignment,
|
||||
"agent_id": agent_id,
|
||||
}
|
||||
|
||||
|
||||
async def restore_workspace_access(
|
||||
client: Any,
|
||||
matrix_user_id: str,
|
||||
display_name: str,
|
||||
platform,
|
||||
store,
|
||||
auth_mgr,
|
||||
chat_mgr,
|
||||
registry: AgentRegistry | None = None,
|
||||
) -> dict:
|
||||
user_meta = await get_user_meta(store, matrix_user_id) or {}
|
||||
space_id = user_meta.get("space_id")
|
||||
if not space_id:
|
||||
created = await provision_workspace_chat(
|
||||
client,
|
||||
matrix_user_id,
|
||||
display_name,
|
||||
platform,
|
||||
store,
|
||||
auth_mgr,
|
||||
chat_mgr,
|
||||
room_name_override="Чат 1",
|
||||
registry=registry,
|
||||
)
|
||||
return {**created, "reinvited_rooms": [], "created_new_chat": True}
|
||||
|
||||
await auth_mgr.confirm(matrix_user_id)
|
||||
await _invite_if_possible(client, space_id, matrix_user_id)
|
||||
|
||||
chats = await chat_mgr.list_active(matrix_user_id)
|
||||
if not chats:
|
||||
created = await provision_workspace_chat(
|
||||
client,
|
||||
matrix_user_id,
|
||||
display_name,
|
||||
platform,
|
||||
store,
|
||||
auth_mgr,
|
||||
chat_mgr,
|
||||
registry=registry,
|
||||
)
|
||||
return {**created, "reinvited_rooms": [], "created_new_chat": True}
|
||||
|
||||
reinvited_rooms = []
|
||||
for chat in chats:
|
||||
if chat.surface_ref:
|
||||
if await _invite_if_possible(client, chat.surface_ref, matrix_user_id):
|
||||
reinvited_rooms.append(chat.surface_ref)
|
||||
|
||||
return {
|
||||
"space_id": space_id,
|
||||
"reinvited_rooms": reinvited_rooms,
|
||||
"created_new_chat": False,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -146,6 +231,29 @@ async def handle_invite(
|
|||
|
||||
existing = await get_user_meta(store, matrix_user_id)
|
||||
if existing and existing.get("space_id"):
|
||||
restored = await restore_workspace_access(
|
||||
client,
|
||||
matrix_user_id,
|
||||
display_name,
|
||||
platform,
|
||||
store,
|
||||
auth_mgr,
|
||||
chat_mgr,
|
||||
registry=registry,
|
||||
)
|
||||
body = "Я отправил повторные приглашения в пространство Lambda и рабочие чаты."
|
||||
if restored.get("created_new_chat"):
|
||||
body = (
|
||||
f"Создал новый рабочий чат {restored['room_name']} "
|
||||
f"({restored['chat_id']}) и отправил приглашение."
|
||||
)
|
||||
if restored.get("agent_assignment") == "default":
|
||||
body = f"{body}\n\n{default_agent_notice()}"
|
||||
await client.room_send(
|
||||
room.room_id,
|
||||
"m.room.message",
|
||||
{"msgtype": "m.text", "body": body},
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
@ -168,6 +276,8 @@ async def handle_invite(
|
|||
f"Привет, {created['user'].display_name or matrix_user_id}! Пиши — я здесь.\n\n"
|
||||
"Команды: !new · !chats · !rename · !archive · !clear · !help"
|
||||
)
|
||||
if created.get("agent_assignment") == "default":
|
||||
welcome = f"{welcome}\n\n{default_agent_notice()}"
|
||||
await client.room_send(
|
||||
created["chat_room_id"],
|
||||
"m.room.message",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from nio.api import RoomVisibility
|
|||
from nio.responses import RoomCreateError
|
||||
|
||||
from adapter.matrix.agent_registry import AgentRegistry
|
||||
from adapter.matrix.handlers.auth import default_agent_notice
|
||||
from adapter.matrix.store import (
|
||||
get_user_meta,
|
||||
next_chat_id,
|
||||
|
|
@ -107,10 +108,11 @@ def make_handle_new_chat(
|
|||
)
|
||||
|
||||
agent_id = None
|
||||
agent_assignment = "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
|
||||
assignment = registry.resolve_agent_for_user(event.user_id)
|
||||
agent_id = assignment.agent_id
|
||||
agent_assignment = assignment.source
|
||||
|
||||
room_meta: dict = {
|
||||
"room_type": "chat",
|
||||
|
|
@ -120,6 +122,7 @@ def make_handle_new_chat(
|
|||
"space_id": space_id,
|
||||
"platform_chat_id": platform_chat_id,
|
||||
"agent_id": agent_id,
|
||||
"agent_assignment": agent_assignment,
|
||||
}
|
||||
await set_room_meta(store, room_id, room_meta)
|
||||
ctx = await chat_mgr.get_or_create(
|
||||
|
|
@ -129,10 +132,13 @@ def make_handle_new_chat(
|
|||
surface_ref=room_id,
|
||||
name=room_name,
|
||||
)
|
||||
text = f"Создан чат: {ctx.display_name} ({ctx.chat_id})"
|
||||
if agent_assignment == "default":
|
||||
text = f"{text}\n\n{default_agent_notice()}"
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=event.chat_id,
|
||||
text=f"Создан чат: {ctx.display_name} ({ctx.chat_id})",
|
||||
text=text,
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ def _chat_id_from_room(room: object, existing_meta: dict | None) -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def _space_id_for_room(room: object, rooms_by_id: dict[str, object], existing_meta: dict | None) -> str | None:
|
||||
def _space_id_for_room(
|
||||
room: object, rooms_by_id: dict[str, object], existing_meta: dict | None
|
||||
) -> str | None:
|
||||
existing_space_id = (existing_meta or {}).get("space_id")
|
||||
if isinstance(existing_space_id, str) and existing_space_id:
|
||||
return existing_space_id
|
||||
|
|
@ -69,7 +71,9 @@ def _space_id_for_room(room: object, rooms_by_id: dict[str, object], existing_me
|
|||
return None
|
||||
|
||||
|
||||
def _matrix_user_id_for_room(room: object, bot_user_id: str | None, existing_meta: dict | None) -> str | None:
|
||||
def _matrix_user_id_for_room(
|
||||
room: object, bot_user_id: str | None, existing_meta: dict | None
|
||||
) -> str | None:
|
||||
existing_user_id = (existing_meta or {}).get("matrix_user_id")
|
||||
if isinstance(existing_user_id, str) and existing_user_id:
|
||||
return existing_user_id
|
||||
|
|
@ -128,11 +132,26 @@ async def reconcile_startup_state(client: object, runtime: object) -> Reconcilia
|
|||
if not room_meta.get("agent_id"):
|
||||
registry = getattr(runtime, "registry", None)
|
||||
if registry is not None:
|
||||
agent_id = registry.get_agent_id_for_user(matrix_user_id)
|
||||
if agent_id is None and registry.agents:
|
||||
agent_id = registry.agents[0].agent_id
|
||||
if agent_id:
|
||||
room_meta["agent_id"] = agent_id
|
||||
assignment = registry.resolve_agent_for_user(matrix_user_id)
|
||||
if assignment.agent_id:
|
||||
room_meta["agent_id"] = assignment.agent_id
|
||||
room_meta["agent_assignment"] = assignment.source
|
||||
else:
|
||||
registry = getattr(runtime, "registry", None)
|
||||
if registry is not None:
|
||||
assignment = registry.resolve_agent_for_user(matrix_user_id)
|
||||
if assignment.source == "configured" and (
|
||||
room_meta.get("agent_id") != assignment.agent_id
|
||||
or room_meta.get("agent_assignment") != "configured"
|
||||
):
|
||||
room_meta["agent_id"] = assignment.agent_id
|
||||
room_meta["agent_assignment"] = "configured"
|
||||
elif (
|
||||
assignment.source == "default"
|
||||
and room_meta.get("agent_id") == assignment.agent_id
|
||||
and not room_meta.get("agent_assignment")
|
||||
):
|
||||
room_meta["agent_assignment"] = "default"
|
||||
|
||||
if existing_meta is None:
|
||||
result.recovered_rooms += 1
|
||||
|
|
@ -153,7 +172,9 @@ async def reconcile_startup_state(client: object, runtime: object) -> Reconcilia
|
|||
user_meta = dict(await get_user_meta(runtime.store, matrix_user_id) or {})
|
||||
user_meta["space_id"] = user_meta.get("space_id") or recovered_space_id
|
||||
next_chat_index = max_chat_index_by_user[matrix_user_id] + 1
|
||||
user_meta["next_chat_index"] = max(int(user_meta.get("next_chat_index", 1)), next_chat_index)
|
||||
user_meta["next_chat_index"] = max(
|
||||
int(user_meta.get("next_chat_index", 1)), next_chat_index
|
||||
)
|
||||
await set_user_meta(runtime.store, matrix_user_id, user_meta)
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import AsyncIterator, Mapping
|
||||
|
||||
import structlog
|
||||
|
||||
from adapter.matrix.store import get_room_meta
|
||||
from core.chat import ChatManager
|
||||
from core.store import StateStore
|
||||
|
|
@ -15,6 +18,13 @@ from sdk.interface import (
|
|||
UserSettings,
|
||||
)
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def _ws_debug_enabled() -> bool:
|
||||
value = os.environ.get("SURFACES_DEBUG_WS", "")
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
class RoutedPlatformClient(PlatformClient):
|
||||
def __init__(
|
||||
|
|
@ -77,7 +87,9 @@ class RoutedPlatformClient(PlatformClient):
|
|||
if callable(close):
|
||||
await close()
|
||||
|
||||
async def _resolve_delegate(self, user_id: str, local_chat_id: str) -> tuple[PlatformClient, str]:
|
||||
async def _resolve_delegate(
|
||||
self, user_id: str, local_chat_id: str
|
||||
) -> tuple[PlatformClient, str]:
|
||||
chat = await self._chat_mgr.get(local_chat_id, user_id)
|
||||
if chat is None:
|
||||
raise PlatformError(
|
||||
|
|
@ -107,4 +119,15 @@ class RoutedPlatformClient(PlatformClient):
|
|||
code="MATRIX_AGENT_NOT_FOUND",
|
||||
)
|
||||
|
||||
if _ws_debug_enabled():
|
||||
logger.warning(
|
||||
"matrix_route_resolved",
|
||||
user_id=user_id,
|
||||
local_chat_id=local_chat_id,
|
||||
surface_ref=chat.surface_ref,
|
||||
agent_id=str(agent_id),
|
||||
platform_chat_id=str(platform_chat_id),
|
||||
delegate_type=type(delegate).__name__,
|
||||
)
|
||||
|
||||
return delegate, str(platform_chat_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue