feat: add prototype local state store
This commit is contained in:
parent
de20ff638a
commit
2fad1aaa66
2 changed files with 154 additions and 0 deletions
79
sdk/prototype_state.py
Normal file
79
sdk/prototype_state.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sdk.interface import User, UserSettings
|
||||||
|
|
||||||
|
# Keep the prototype backend self-contained; do not import these from sdk.mock.
|
||||||
|
DEFAULT_SKILLS: dict[str, bool] = {
|
||||||
|
"web-search": True,
|
||||||
|
"fetch-url": True,
|
||||||
|
"email": False,
|
||||||
|
"browser": False,
|
||||||
|
"image-gen": False,
|
||||||
|
"files": True,
|
||||||
|
}
|
||||||
|
DEFAULT_SAFETY: dict[str, bool] = {
|
||||||
|
"email-send": True,
|
||||||
|
"file-delete": True,
|
||||||
|
"social-post": True,
|
||||||
|
}
|
||||||
|
DEFAULT_SOUL: dict[str, str] = {"name": "Лямбда", "instructions": ""}
|
||||||
|
DEFAULT_PLAN: dict[str, Any] = {
|
||||||
|
"name": "Beta",
|
||||||
|
"tokens_used": 0,
|
||||||
|
"tokens_limit": 1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PrototypeStateStore:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._users: dict[str, User] = {}
|
||||||
|
self._settings: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
async def get_or_create_user(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
external_id: str,
|
||||||
|
platform: str,
|
||||||
|
display_name: str | None = None,
|
||||||
|
) -> User:
|
||||||
|
key = f"{platform}:{external_id}"
|
||||||
|
existing = self._users.get(key)
|
||||||
|
if existing is not None:
|
||||||
|
return existing.model_copy(update={"is_new": False})
|
||||||
|
|
||||||
|
user = User(
|
||||||
|
user_id=f"usr-{platform}-{external_id}",
|
||||||
|
external_id=external_id,
|
||||||
|
platform=platform,
|
||||||
|
display_name=display_name,
|
||||||
|
created_at=datetime.now(UTC),
|
||||||
|
is_new=True,
|
||||||
|
)
|
||||||
|
self._users[key] = user
|
||||||
|
return user
|
||||||
|
|
||||||
|
async def get_settings(self, user_id: str) -> UserSettings:
|
||||||
|
stored = self._settings.get(user_id, {})
|
||||||
|
return UserSettings(
|
||||||
|
skills={**DEFAULT_SKILLS, **stored.get("skills", {})},
|
||||||
|
connectors=stored.get("connectors", {}),
|
||||||
|
soul={**DEFAULT_SOUL, **stored.get("soul", {})},
|
||||||
|
safety={**DEFAULT_SAFETY, **stored.get("safety", {})},
|
||||||
|
plan={**DEFAULT_PLAN, **stored.get("plan", {})},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_settings(self, user_id: str, action: Any) -> None:
|
||||||
|
settings = self._settings.setdefault(user_id, {})
|
||||||
|
|
||||||
|
if action.action == "toggle_skill":
|
||||||
|
skills = settings.setdefault("skills", DEFAULT_SKILLS.copy())
|
||||||
|
skills[action.payload["skill"]] = action.payload.get("enabled", True)
|
||||||
|
elif action.action == "set_soul":
|
||||||
|
soul = settings.setdefault("soul", DEFAULT_SOUL.copy())
|
||||||
|
soul[action.payload["field"]] = action.payload["value"]
|
||||||
|
elif action.action == "set_safety":
|
||||||
|
safety = settings.setdefault("safety", DEFAULT_SAFETY.copy())
|
||||||
|
safety[action.payload["trigger"]] = action.payload.get("enabled", True)
|
||||||
75
tests/platform/test_prototype_state.py
Normal file
75
tests/platform/test_prototype_state.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.protocol import SettingsAction
|
||||||
|
from sdk.interface import UserSettings
|
||||||
|
from sdk.prototype_state import PrototypeStateStore
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_or_create_user_is_stable_per_surface_identity():
|
||||||
|
store = PrototypeStateStore()
|
||||||
|
|
||||||
|
first = await store.get_or_create_user(
|
||||||
|
external_id="@alice:example.org",
|
||||||
|
platform="matrix",
|
||||||
|
display_name="Alice",
|
||||||
|
)
|
||||||
|
second = await store.get_or_create_user(
|
||||||
|
external_id="@alice:example.org",
|
||||||
|
platform="matrix",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert first.user_id == "usr-matrix-@alice:example.org"
|
||||||
|
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_settings_defaults_match_existing_mock_shape():
|
||||||
|
store = PrototypeStateStore()
|
||||||
|
|
||||||
|
settings = await store.get_settings("usr-matrix-@alice:example.org")
|
||||||
|
|
||||||
|
assert isinstance(settings, UserSettings)
|
||||||
|
assert settings.skills == {
|
||||||
|
"web-search": True,
|
||||||
|
"fetch-url": True,
|
||||||
|
"email": False,
|
||||||
|
"browser": False,
|
||||||
|
"image-gen": False,
|
||||||
|
"files": True,
|
||||||
|
}
|
||||||
|
assert settings.safety == {
|
||||||
|
"email-send": True,
|
||||||
|
"file-delete": True,
|
||||||
|
"social-post": True,
|
||||||
|
}
|
||||||
|
assert settings.soul == {"name": "Лямбда", "instructions": ""}
|
||||||
|
assert settings.plan == {"name": "Beta", "tokens_used": 0, "tokens_limit": 1000}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_settings_supports_toggle_skill_and_setters():
|
||||||
|
store = PrototypeStateStore()
|
||||||
|
|
||||||
|
await store.update_settings(
|
||||||
|
"usr-matrix-@alice:example.org",
|
||||||
|
SettingsAction(action="toggle_skill", payload={"skill": "browser", "enabled": True}),
|
||||||
|
)
|
||||||
|
await store.update_settings(
|
||||||
|
"usr-matrix-@alice:example.org",
|
||||||
|
SettingsAction(action="set_soul", payload={"field": "instructions", "value": "Be concise"}),
|
||||||
|
)
|
||||||
|
await store.update_settings(
|
||||||
|
"usr-matrix-@alice:example.org",
|
||||||
|
SettingsAction(action="set_safety", payload={"trigger": "social-post", "enabled": False}),
|
||||||
|
)
|
||||||
|
|
||||||
|
settings = await store.get_settings("usr-matrix-@alice:example.org")
|
||||||
|
|
||||||
|
assert settings.skills["browser"] is True
|
||||||
|
assert settings.skills["web-search"] is True
|
||||||
|
assert settings.soul["instructions"] == "Be concise"
|
||||||
|
assert settings.safety["social-post"] is False
|
||||||
Loading…
Add table
Add a link
Reference in a new issue