feat(max-bot): implement mock-based message flow with WIP file queue

- MAX API integration (long polling)
- MockPlatformClient for agent simulation
- File download & workspace storage
- Basic commands: /help, /start
- Attachment queue: add works, list/remove need testing

[WIP: attachment queue commands]
[MOCK-ONLY: requires real agent for production]
This commit is contained in:
Александра Пронина 2026-05-25 16:51:48 +03:00
parent 961ee7bb0b
commit b74277a189
6 changed files with 120 additions and 28 deletions

View file

@ -33,13 +33,20 @@ from core.auth import AuthManager
from core.chat import ChatManager
from core.handler import EventDispatcher
from core.handlers import register_all
from core.protocol import Attachment, IncomingCommand, OutgoingEvent, OutgoingMessage
from core.protocol import OutgoingNotification, OutgoingTyping, OutgoingUI
from core.protocol import (
Attachment,
IncomingCommand,
IncomingMessage,
OutgoingEvent,
OutgoingMessage,
OutgoingNotification,
OutgoingTyping,
OutgoingUI
)
from core.settings import SettingsManager
from core.store import InMemoryStore, StateStore
from sdk.interface import PlatformClient, PlatformError
from sdk.prototype_state import PrototypeStateStore
from sdk.real import RealPlatformClient
logger = structlog.get_logger(__name__)
@ -130,6 +137,7 @@ class MaxBotApp:
self.registry: AgentRegistry = load_from_env()
except (AgentRegistryError, OSError) as exc:
raise RuntimeError("failed to load MAX agent registry") from exc
self.chat_store = ChatStore()
self.max_chat_handler = MaxChatHandler(self.chat_store)
@ -138,22 +146,35 @@ class MaxBotApp:
self.core_store: StateStore = InMemoryStore()
self.prototype_state = PrototypeStateStore()
delegates: dict[str, RealPlatformClient] = {}
for agent in self.registry.agents:
base_raw = agent.base_url.strip() if agent.base_url else agent_base_url
delegates[agent.agent_id] = RealPlatformClient(
agent_id=agent.agent_id,
agent_base_url=base_raw,
prototype_state=self.prototype_state,
platform="max",
)
backend_mode = os.environ.get("MAX_PLATFORM_BACKEND", "mock").strip().lower()
logger.info("max_platform_backend_selected", backend=backend_mode)
default_client = next(iter(delegates.values()))
self.platform: RoutedMaxPlatformClient = RoutedMaxPlatformClient(
chat_store=self.chat_store,
delegates=delegates,
default_client=default_client,
)
if backend_mode == "real":
from sdk.real import RealPlatformClient
delegates: dict[str, RealPlatformClient] = {}
for agent in self.registry.agents:
base_raw = agent.base_url.strip() if agent.base_url else agent_base_url
delegates[agent.agent_id] = RealPlatformClient(
agent_id=agent.agent_id,
agent_base_url=base_raw,
prototype_state=self.prototype_state,
platform="max",
)
if not delegates:
raise RuntimeError("No agents configured for real backend")
default_client = next(iter(delegates.values()))
self.platform: RoutedMaxPlatformClient = RoutedMaxPlatformClient(
chat_store=self.chat_store,
delegates=delegates,
default_client=default_client,
)
else:
# Mock backend for local development/testing
logger.warning("max_using_mock_backend", note="No real agent connection")
from sdk.mock import MockPlatformClient
self.platform = MockPlatformClient() # type: ignore[assignment]
self.chat_mgr = ChatManager(self.platform, self.core_store)
self.auth_mgr = AuthManager(self.platform, self.core_store)
@ -244,6 +265,10 @@ class MaxBotApp:
return refreshed
async def process_message_created(self, payload: dict) -> None:
logger.info(
"DEBUG_PAYLOAD",
body=payload.get("message", {}).get("body"),
)
message = payload.get("message")
if not isinstance(message, dict):
return
@ -289,7 +314,9 @@ class MaxBotApp:
attachments_core = await self._materialize_attachments(room, attachments_core, raw_meta)
if attachments_core and not text:
logger.info("QUEUE_ADD", chat_key=chat_key, count=len(attachments_core))
for att in attachments_core:
logger.info("QUEUE_ITEM", filename=att.filename)
self.chat_store.stage_attachment(chat_key, (att.workspace_path or "", att.filename or "file"))
return
@ -345,6 +372,7 @@ class MaxBotApp:
async def _handle_local_attachment_command(self, incoming: IncomingCommand, chat_key: str) -> str:
if incoming.command == "list":
logger.info("QUEUE_LIST_REQUEST", chat_key=chat_key)
return self.attach_handler.handle_list(chat_key)
return self.attach_handler.handle_remove(chat_key, incoming.args[0] if incoming.args else "")
@ -463,7 +491,7 @@ class MaxBotApp:
deeplink_note = f" (payload: {dl})"
welcome = (
"Здравствуйте, я помогу с задачами Lambda. "
"Здравствуйте, я бот Lambda, который готов помочь с задачами."
f"Отправьте текст или файл.{deeplink_note}"
)