Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Agent Runtime — stateless-сервис, который превращает т
- долговременное хранение данных;
- окончательное решение о безопасности действия.

Текущие `DemoModelRepository` и `DemoPolicyRepository` работают только локально. Их правила —
Текущие `DemoIntentModel` и `DemoPolicyRepository` работают только локально. Их правила —
техническая заглушка, а не согласованное поведение продукта.

## Текущий продуктовый срез
Expand Down
4 changes: 2 additions & 2 deletions src/portable_agent/config/services.py
Original file line number Diff line number Diff line change
@@ -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())
6 changes: 4 additions & 2 deletions src/portable_agent/repositories/model_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions src/portable_agent/services/proposal_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions tests/test_demo_intent_model.py
Original file line number Diff line number Diff line change
@@ -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
Loading