diff --git a/sdk/__init__.py b/sdk/__init__.py index e69de29..36f81c1 100644 --- a/sdk/__init__.py +++ b/sdk/__init__.py @@ -0,0 +1,3 @@ +from sdk.real import RealPlatformClient + +__all__ = ["RealPlatformClient"] diff --git a/sdk/real.py b/sdk/real.py new file mode 100644 index 0000000..cd38cc2 --- /dev/null +++ b/sdk/real.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import AsyncIterator + +from sdk.agent_session import AgentSessionClient, build_thread_key +from sdk.interface import Attachment, MessageChunk, MessageResponse, PlatformClient, User, UserSettings +from sdk.prototype_state import PrototypeStateStore + + +class RealPlatformClient(PlatformClient): + def __init__( + self, + agent_sessions: AgentSessionClient, + prototype_state: PrototypeStateStore, + platform: str, + ) -> None: + self._agent_sessions = agent_sessions + self._prototype_state = prototype_state + self._platform = platform + + async def get_or_create_user( + self, + external_id: str, + platform: str, + display_name: str | None = None, + ) -> User: + return await self._prototype_state.get_or_create_user( + external_id=external_id, + platform=platform, + display_name=display_name, + ) + + async def send_message( + self, + user_id: str, + chat_id: str, + text: str, + attachments: list[Attachment] | None = None, + ) -> MessageResponse: + thread_key = build_thread_key(self._platform, user_id, chat_id) + return await self._agent_sessions.send_message(thread_key=thread_key, text=text) + + async def stream_message( + self, + user_id: str, + chat_id: str, + text: str, + attachments: list[Attachment] | None = None, + ) -> AsyncIterator[MessageChunk]: + thread_key = build_thread_key(self._platform, user_id, chat_id) + async for chunk in self._agent_sessions.stream_message(thread_key=thread_key, text=text): + yield chunk + + async def get_settings(self, user_id: str) -> UserSettings: + return await self._prototype_state.get_settings(user_id) + + async def update_settings(self, user_id: str, action) -> None: + await self._prototype_state.update_settings(user_id, action) diff --git a/tests/core/test_integration.py b/tests/core/test_integration.py index 207a0ba..db2cf8f 100644 --- a/tests/core/test_integration.py +++ b/tests/core/test_integration.py @@ -5,6 +5,10 @@ Smoke test: полный цикл через dispatcher + реальные manag """ import pytest from sdk.mock import MockPlatformClient +from sdk.agent_session import build_thread_key +from sdk.interface import MessageChunk, MessageResponse +from sdk.prototype_state import PrototypeStateStore +from sdk.real import RealPlatformClient from core.store import InMemoryStore from core.chat import ChatManager from core.auth import AuthManager @@ -18,6 +22,30 @@ from core.protocol import ( ) +class FakeAgentSessionClient: + def __init__(self) -> None: + self.send_calls: list[tuple[str, str]] = [] + + async def send_message(self, *, thread_key: str, text: str) -> MessageResponse: + self.send_calls.append((thread_key, text)) + return MessageResponse( + message_id=thread_key, + response=f"[REAL] {text}", + tokens_used=5, + finished=True, + ) + + async def stream_message(self, *, thread_key: str, text: str): + self.send_calls.append((thread_key, text)) + if False: + yield MessageChunk( + message_id=thread_key, + delta=text, + tokens_used=0, + finished=True, + ) + + @pytest.fixture def dispatcher(): platform = MockPlatformClient() @@ -32,6 +60,25 @@ def dispatcher(): return d +@pytest.fixture +def real_dispatcher(): + agent_sessions = FakeAgentSessionClient() + platform = RealPlatformClient( + agent_sessions=agent_sessions, + prototype_state=PrototypeStateStore(), + platform="matrix", + ) + store = InMemoryStore() + d = EventDispatcher( + platform=platform, + chat_mgr=ChatManager(platform, store), + auth_mgr=AuthManager(platform, store), + settings_mgr=SettingsManager(platform, store), + ) + register_all(d) + return d, agent_sessions + + async def test_full_flow_start_then_message(dispatcher): start = IncomingCommand(user_id="tg_123", platform="telegram", chat_id="C1", command="start") result = await dispatcher.dispatch(start) @@ -83,3 +130,20 @@ async def test_toggle_skill_callback(dispatcher): ) result = await dispatcher.dispatch(cb) assert any("browser" in r.text for r in result if isinstance(r, OutgoingMessage)) + + +async def test_full_flow_with_real_platform_uses_thread_key(real_dispatcher): + dispatcher, agent_sessions = real_dispatcher + + start = IncomingCommand(user_id="u1", platform="matrix", chat_id="C1", command="start") + result = await dispatcher.dispatch(start) + assert any(isinstance(r, OutgoingMessage) for r in result) + + msg = IncomingMessage(user_id="u1", platform="matrix", chat_id="C1", text="Привет!") + result = await dispatcher.dispatch(msg) + texts = [r.text for r in result if isinstance(r, OutgoingMessage)] + + assert texts == ["[REAL] Привет!"] + assert agent_sessions.send_calls == [ + (build_thread_key("matrix", "u1", "C1"), "Привет!") + ] diff --git a/tests/platform/test_real.py b/tests/platform/test_real.py new file mode 100644 index 0000000..f10e2c0 --- /dev/null +++ b/tests/platform/test_real.py @@ -0,0 +1,118 @@ +import pytest + +from core.protocol import SettingsAction +from sdk.interface import MessageChunk, MessageResponse, UserSettings +from sdk.prototype_state import PrototypeStateStore +from sdk.real import RealPlatformClient + + +class FakeAgentSessionClient: + def __init__(self) -> None: + self.send_calls: list[tuple[str, str]] = [] + self.stream_calls: list[tuple[str, str]] = [] + + async def send_message(self, *, thread_key: str, text: str) -> MessageResponse: + self.send_calls.append((thread_key, text)) + return MessageResponse( + message_id=thread_key, + response=f"echo:{text}", + tokens_used=3, + finished=True, + ) + + async def stream_message(self, *, thread_key: str, text: str): + self.stream_calls.append((thread_key, text)) + yield MessageChunk(message_id=thread_key, delta=text[:2], finished=False) + yield MessageChunk(message_id=thread_key, delta=text[2:], finished=True, tokens_used=3) + + +@pytest.mark.asyncio +async def test_real_platform_client_get_or_create_user_uses_local_state(): + client = RealPlatformClient( + agent_sessions=FakeAgentSessionClient(), + prototype_state=PrototypeStateStore(), + platform="telegram", + ) + + first = await client.get_or_create_user("u1", "telegram", "Alice") + second = await client.get_or_create_user("u1", "telegram") + + assert first.user_id == "usr-telegram-u1" + assert first.is_new is True + assert second.user_id == first.user_id + assert second.is_new is False + assert second.display_name == "Alice" + + +@pytest.mark.asyncio +async def test_real_platform_client_send_message_uses_configured_platform(): + agent_sessions = FakeAgentSessionClient() + client = RealPlatformClient( + agent_sessions=agent_sessions, + prototype_state=PrototypeStateStore(), + platform="telegram", + ) + + result = await client.send_message("usr-telegram-u1", "C1", "hello") + + assert result == MessageResponse( + message_id="8:telegram15:usr-telegram-u12:C1", + response="echo:hello", + tokens_used=3, + finished=True, + ) + assert agent_sessions.send_calls == [ + ("8:telegram15:usr-telegram-u12:C1", "hello") + ] + + +@pytest.mark.asyncio +async def test_real_platform_client_stream_message_uses_configured_platform(): + agent_sessions = FakeAgentSessionClient() + client = RealPlatformClient( + agent_sessions=agent_sessions, + prototype_state=PrototypeStateStore(), + platform="telegram", + ) + + chunks = [] + async for chunk in client.stream_message("usr-telegram-u1", "C1", "hello"): + chunks.append(chunk) + + assert chunks == [ + MessageChunk( + message_id="8:telegram15:usr-telegram-u12:C1", + delta="he", + finished=False, + tokens_used=0, + ), + MessageChunk( + message_id="8:telegram15:usr-telegram-u12:C1", + delta="llo", + finished=True, + tokens_used=3, + ), + ] + assert agent_sessions.stream_calls == [ + ("8:telegram15:usr-telegram-u12:C1", "hello") + ] + + +@pytest.mark.asyncio +async def test_real_platform_client_settings_are_local(): + client = RealPlatformClient( + agent_sessions=FakeAgentSessionClient(), + prototype_state=PrototypeStateStore(), + platform="matrix", + ) + + await client.update_settings( + "usr-matrix-u1", + SettingsAction(action="toggle_skill", payload={"skill": "browser", "enabled": True}), + ) + + settings = await client.get_settings("usr-matrix-u1") + + assert isinstance(settings, UserSettings) + assert settings.skills["browser"] is True + assert settings.skills["web-search"] is True