feat: add real platform compatibility layer

This commit is contained in:
Mikhail Putilovskij 2026-04-08 01:38:28 +03:00
parent fabedb105b
commit 9784ca6783
4 changed files with 243 additions and 0 deletions

View file

@ -0,0 +1,3 @@
from sdk.real import RealPlatformClient
__all__ = ["RealPlatformClient"]

58
sdk/real.py Normal file
View file

@ -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)

View file

@ -5,6 +5,10 @@ Smoke test: полный цикл через dispatcher + реальные manag
""" """
import pytest import pytest
from sdk.mock import MockPlatformClient 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.store import InMemoryStore
from core.chat import ChatManager from core.chat import ChatManager
from core.auth import AuthManager 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 @pytest.fixture
def dispatcher(): def dispatcher():
platform = MockPlatformClient() platform = MockPlatformClient()
@ -32,6 +60,25 @@ def dispatcher():
return d 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): async def test_full_flow_start_then_message(dispatcher):
start = IncomingCommand(user_id="tg_123", platform="telegram", chat_id="C1", command="start") start = IncomingCommand(user_id="tg_123", platform="telegram", chat_id="C1", command="start")
result = await dispatcher.dispatch(start) result = await dispatcher.dispatch(start)
@ -83,3 +130,20 @@ async def test_toggle_skill_callback(dispatcher):
) )
result = await dispatcher.dispatch(cb) result = await dispatcher.dispatch(cb)
assert any("browser" in r.text for r in result if isinstance(r, OutgoingMessage)) 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"), "Привет!")
]

118
tests/platform/test_real.py Normal file
View file

@ -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