diff --git a/README.md b/README.md index 58c8447..f3436af 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ - Bearer JWT с проверкой подписи, issuer, audience и срока жизни; - контракт `portable-agent/contracts` версии `2.1.0`; - простой MVC-подобный каркас; -- локальные demo-адаптеры модели и policy для разработки без внешних сервисов; +- внешний порт `IntentModel` и локальная `DemoIntentModel` для разработки без AI-сервиса; - результат с `proposal` или `clarification` для первого действия `calendar.create_event`; - Ruff, strict mypy, pytest и проверка покрытия; - русская документация MkDocs/Backstage TechDocs. diff --git a/docs/architecture.md b/docs/architecture.md index 6c47e41..770c42c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,7 +8,7 @@ sequenceDiagram participant OIDC as OIDC/JWKS participant Controller as Controller participant Service as ProposalService - participant Model as ModelRepository + participant Model as IntentModel participant Policy as PolicyRepository Client->>Controller: POST /api/v1/proposals + Bearer JWT diff --git a/docs/index.md b/docs/index.md index 8ad185e..8809a30 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,7 +22,7 @@ Agent Runtime — stateless-сервис, который превращает т - долговременное хранение данных; - окончательное решение о безопасности действия. -Текущие `DemoModelRepository` и `DemoPolicyRepository` работают только локально. Их правила — +Текущие `DemoIntentModel` и `DemoPolicyRepository` работают только локально. Их правила — техническая заглушка, а не согласованное поведение продукта. ## Текущий продуктовый срез diff --git a/src/portable_agent/config/services.py b/src/portable_agent/config/services.py index 2826f97..b6930af 100644 --- a/src/portable_agent/config/services.py +++ b/src/portable_agent/config/services.py @@ -1,7 +1,7 @@ -from portable_agent.repositories.model_repository import DemoModelRepository +from portable_agent.repositories.model_repository import DemoIntentModel from portable_agent.repositories.policy_repository import DemoPolicyRepository from portable_agent.services.proposal_service import ProposalService def get_proposal_service() -> ProposalService: - return ProposalService(DemoModelRepository(), DemoPolicyRepository()) + return ProposalService(DemoIntentModel(), DemoPolicyRepository()) diff --git a/src/portable_agent/repositories/model_repository.py b/src/portable_agent/repositories/model_repository.py index 5c49e94..20bfb48 100644 --- a/src/portable_agent/repositories/model_repository.py +++ b/src/portable_agent/repositories/model_repository.py @@ -4,11 +4,13 @@ from portable_agent.models.proposal import ModelReply, UserContext -class ModelRepository(Protocol): +class IntentModel(Protocol): + """Внешняя модель, которая превращает текст в типизированный ответ.""" + async def propose(self, text: str, context: UserContext) -> ModelReply | None: ... -class DemoModelRepository: +class DemoIntentModel: """Локальная заглушка для разработки без внешней AI-модели.""" async def propose(self, text: str, context: UserContext) -> ModelReply | None: diff --git a/src/portable_agent/services/proposal_service.py b/src/portable_agent/services/proposal_service.py index c07b2ec..aeceea1 100644 --- a/src/portable_agent/services/proposal_service.py +++ b/src/portable_agent/services/proposal_service.py @@ -7,12 +7,12 @@ ProposalResult, UserContext, ) -from portable_agent.repositories.model_repository import ModelRepository +from portable_agent.repositories.model_repository import IntentModel from portable_agent.repositories.policy_repository import PolicyRepository class ProposalService: - def __init__(self, model: ModelRepository, policy: PolicyRepository) -> None: + def __init__(self, model: IntentModel, policy: PolicyRepository) -> None: self._model = model self._policy = policy diff --git a/tests/test_demo_intent_model.py b/tests/test_demo_intent_model.py new file mode 100644 index 0000000..855f37f --- /dev/null +++ b/tests/test_demo_intent_model.py @@ -0,0 +1,45 @@ +from uuid import uuid4 + +import pytest + +from portable_agent.models.proposal import UserContext +from portable_agent.repositories.model_repository import DemoIntentModel + + +def context() -> UserContext: + return UserContext( + tenant_id=uuid4(), + user_id=uuid4(), + timezone="Europe/Moscow", + available_tools={"fake-calendar"}, + ) + + +@pytest.mark.asyncio +async def test_demo_model_when_command_is_complete_should_return_calendar_reply() -> None: + model = DemoIntentModel() + + reply = await model.propose( + 'Создай встречу "Обсуждение проекта" с 2026-09-01T12:00:00+03:00 ' + "до 2026-09-01T12:30:00+03:00", + context(), + ) + + assert reply is not None + assert reply.kind == "calendar.create_event" + assert reply.connector == "fake-calendar" + assert reply.payload == { + "title": "Обсуждение проекта", + "startAt": "2026-09-01T12:00:00+03:00", + "endAt": "2026-09-01T12:30:00+03:00", + "timeZone": "Europe/Moscow", + } + + +@pytest.mark.asyncio +async def test_demo_model_when_text_is_not_calendar_command_should_return_none() -> None: + model = DemoIntentModel() + + reply = await model.propose("Привет", context()) + + assert reply is None