feat: add real platform compatibility layer
This commit is contained in:
parent
fabedb105b
commit
9784ca6783
4 changed files with 243 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
||||||
|
from sdk.real import RealPlatformClient
|
||||||
|
|
||||||
|
__all__ = ["RealPlatformClient"]
|
||||||
58
sdk/real.py
Normal file
58
sdk/real.py
Normal 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)
|
||||||
|
|
@ -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
118
tests/platform/test_real.py
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue