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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ ACTION_SERVICE_CLIENT_SECRET=local_action_service_change_me
ACTION_SERVICE_PORT=18081
ACTION_SERVICE_TENANT_ID=11111111-1111-4111-8111-111111111111

CONVERSATION_DB_USER=conversations
CONVERSATION_DB_PASSWORD=local_conversations_change_me
CONVERSATION_SERVICE_PORT=18085

AGENT_RUNTIME_PORT=18080
CHANNEL_GATEWAY_PORT=18084
MCP_GATEWAY_PORT=18083
Expand All @@ -23,6 +27,7 @@ CHANNEL_GATEWAY_CONTEXT=../../portable-agent-channel-gateway
ACTION_SERVICE_CONTEXT=../../portable-agent-action-service
MCP_GATEWAY_CONTEXT=../../portable-agent-mcp-gateway
CALENDAR_MCP_CONTEXT=../../portable-agent-calendar-mcp
CONVERSATION_SERVICE_CONTEXT=../../portable-agent-conversation-service
TEST_LAB_PATH=../portable-agent-test-lab

KEYCLOAK_DB_USER=keycloak
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/app-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
CHANNEL_GATEWAY_CONTEXT: ${{ github.workspace }}/repos/channel-gateway
AGENT_RUNTIME_CONTEXT: ${{ github.workspace }}/repos/agent-runtime
ACTION_SERVICE_CONTEXT: ${{ github.workspace }}/repos/action-service
CONVERSATION_SERVICE_CONTEXT: ${{ github.workspace }}/repos/conversation-service
MCP_GATEWAY_CONTEXT: ${{ github.workspace }}/repos/mcp-gateway
CALENDAR_MCP_CONTEXT: ${{ github.workspace }}/repos/calendar-mcp
TEST_LAB_PATH: ${{ github.workspace }}/repos/test-lab
Expand All @@ -44,6 +45,7 @@ jobs:
"channel=$($versions.CHANNEL_GATEWAY_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"agent=$($versions.AGENT_RUNTIME_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"action=$($versions.ACTION_SERVICE_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"conversation=$($versions.CONVERSATION_SERVICE_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"gateway=$($versions.MCP_GATEWAY_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"calendar=$($versions.CALENDAR_MCP_IMAGE.Split(':')[-1])" >> $env:GITHUB_OUTPUT
"test_lab=$($versions.TEST_LAB_REF)" >> $env:GITHUB_OUTPUT
Expand All @@ -69,6 +71,13 @@ jobs:
ref: ${{ steps.versions.outputs.action }}
path: repos/action-service

- name: Получить Conversation Service
uses: actions/checkout@v7
with:
repository: portable-agent/conversation-service
ref: ${{ steps.versions.outputs.conversation }}
path: repos/conversation-service

- name: Получить MCP Gateway
uses: actions/checkout@v7
with:
Expand Down
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Платформа разработки включает:

- Compose-профиль `core`: PostgreSQL, Redpanda, Keycloak, Temporal и OPA;
- Compose-профиль `apps`: Channel Gateway, Agent Runtime, Action Service, MCP Gateway и Calendar MCP;
- Compose-профиль `apps`: Channel Gateway, Conversation Service, Agent Runtime, Action Service, MCP Gateway и Calendar MCP;
- Compose-профиль `observe`: OpenTelemetry, Prometheus, Grafana, Tempo и Loki;
- безопасный общий chart `charts/service`;
- изолированный PR preview namespace с quota, limits и default-deny сетью;
Expand Down Expand Up @@ -52,7 +52,7 @@ task service:up SERVICE=action-service
task service:restart SERVICE=action-service
```

Чтобы поднять и проверить путь `Channel → Agent → Action → Gateway → Calendar`, используй:
Чтобы поднять и проверить путь `Channel → Conversation → Agent → Action → Gateway → Calendar`, используй:

```powershell
task services:up
Expand All @@ -74,12 +74,17 @@ Compose project и всегда удаляет только созданные

`-Apps` собирает образы из соседних локальных репозиториев через `compose/apps.local.yaml`. Пути можно
переопределить переменными `CHANNEL_GATEWAY_CONTEXT`, `AGENT_RUNTIME_CONTEXT`, `ACTION_SERVICE_CONTEXT`,
`MCP_GATEWAY_CONTEXT` и `CALENDAR_MCP_CONTEXT`; вход в GHCR для локального запуска не нужен.
`CONVERSATION_SERVICE_CONTEXT`, `MCP_GATEWAY_CONTEXT` и `CALENDAR_MCP_CONTEXT`; вход в GHCR для локального
запуска не нужен.
`TEST_LAB_PATH` по умолчанию указывает на соседний `../portable-agent-test-lab`; путь можно
переопределить в `.env`. Channel Gateway доступен на `http://localhost:18084`, Agent Runtime — на
`http://localhost:18080`, Action API — на
`http://localhost:18081`, MCP Gateway — на `http://localhost:18083`, а Calendar MCP — на
`http://localhost:18082`.
`http://localhost:18082`. Conversation Service доступен на `http://localhost:18085`. Новый маршрут
`/api/v1/conversations/messages` в Channel Gateway передаёт сообщения в него; старый маршрут
`/api/v1/messages` временно сохранён для обратной совместимости. Решение confirmation-виджета
отправляется в `/api/v1/actions/{actionId}/decisions` того же Gateway и только затем попадает в Action
Service.

Файл `.env` локальный и не коммитится. Значения `dev` и `stage` должны приходить из secret
manager, а не из Git.
Expand All @@ -89,15 +94,15 @@ manager, а не из Git.
в Compose fixture. Отдельный confidential client `action-service` выдаёт worker служебный токен с
`tenant_id`, audience `mcp-gateway` и `calendar-mcp`, scopes `mcp:call` и `calendar:write`. Значения
локального секрета и tenant приходят из `.env`, а не зашиты в image. PostgreSQL создаёт отдельную БД
`actions` для Action Service.
`actions` для Action Service и `conversations` для Conversation Service.

Проверочный API Calendar MCP включён только в локальном профиле `apps`, привязан к loopback-порту и
защищён `CALENDAR_TEST_API_KEY`. Хранилище fake-calendar пока находится в памяти: перезапуск контейнера
очищает созданные тестовые встречи.

После запуска скрипт получает настоящий JWT и сверяет пользователя, `tenant_id` и audience
`channel-gateway`, `agent-runtime`, `action-service` и `calendar-mcp` с realm fixture. Один
пользовательский токен проходит независимую проверку в Channel Gateway, Agent Runtime и Action API.
`channel-gateway`, `conversation-service`, `agent-runtime`, `action-service` и `calendar-mcp` с realm fixture.
Один пользовательский токен проходит независимую проверку во всех пользовательских API.
Если Keycloak volume создан старой версией fixture, запуск остановится с командой для явного
пересоздания локальных данных.

Expand Down
6 changes: 6 additions & 0 deletions compose/apps.local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ services:
context: ${ACTION_SERVICE_CONTEXT:-../../portable-agent-action-service}
pull_policy: build

conversation-service:
image: portable-agent/conversation-service:local
build:
context: ${CONVERSATION_SERVICE_CONTEXT:-../../portable-agent-conversation-service}
pull_policy: build

mcp-gateway:
image: portable-agent/mcp-gateway:local
build:
Expand Down
49 changes: 49 additions & 0 deletions compose/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ services:
TEMPORAL_DB_PASSWORD: ${TEMPORAL_DB_PASSWORD:?set TEMPORAL_DB_PASSWORD}
ACTION_DB_USER: ${ACTION_DB_USER:?set ACTION_DB_USER}
ACTION_DB_PASSWORD: ${ACTION_DB_PASSWORD:?set ACTION_DB_PASSWORD}
CONVERSATION_DB_USER: ${CONVERSATION_DB_USER:?set CONVERSATION_DB_USER}
CONVERSATION_DB_PASSWORD: ${CONVERSATION_DB_PASSWORD:?set CONVERSATION_DB_PASSWORD}
ports:
- "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
volumes:
Expand Down Expand Up @@ -105,6 +107,8 @@ services:
TEMPORAL_DB_PASSWORD: ${TEMPORAL_DB_PASSWORD:?set TEMPORAL_DB_PASSWORD}
ACTION_DB_USER: ${ACTION_DB_USER:?set ACTION_DB_USER}
ACTION_DB_PASSWORD: ${ACTION_DB_PASSWORD:?set ACTION_DB_PASSWORD}
CONVERSATION_DB_USER: ${CONVERSATION_DB_USER:?set CONVERSATION_DB_USER}
CONVERSATION_DB_PASSWORD: ${CONVERSATION_DB_PASSWORD:?set CONVERSATION_DB_PASSWORD}
volumes:
- ./postgres:/scripts:ro
depends_on:
Expand Down Expand Up @@ -206,6 +210,10 @@ services:
AGENT_URL: http://agent-runtime:8080
AGENT_TIMEOUT_MS: 5000
AGENT_AVAILABLE_CONNECTORS: '["fake-calendar"]'
CONVERSATION_URL: http://conversation-service:8080
CONVERSATION_TIMEOUT_MS: 10000
ACTION_URL: http://action-service:8080
ACTION_TIMEOUT_MS: 10000
OIDC_ISSUER_URL: http://localhost:${KEYCLOAK_PORT:-8081}/realms/portable-agent
OIDC_JWKS_URL: http://keycloak:8080/realms/portable-agent/protocol/openid-connect/certs
OIDC_AUDIENCE: channel-gateway
Expand All @@ -214,6 +222,10 @@ services:
condition: service_healthy
agent-runtime:
condition: service_healthy
conversation-service:
condition: service_healthy
action-service:
condition: service_healthy
ports:
- "127.0.0.1:${CHANNEL_GATEWAY_PORT:-18084}:8080"
healthcheck:
Expand Down Expand Up @@ -346,6 +358,43 @@ services:
start_period: 20s
networks: [platform]

conversation-service:
profiles: [apps]
image: ${CONVERSATION_SERVICE_IMAGE:?set CONVERSATION_SERVICE_IMAGE}
environment:
DB_URL: jdbc:postgresql://postgres:5432/conversations
DB_USER: ${CONVERSATION_DB_USER:?set CONVERSATION_DB_USER}
DB_PASSWORD: ${CONVERSATION_DB_PASSWORD:?set CONVERSATION_DB_PASSWORD}
OIDC_ISSUER: http://localhost:${KEYCLOAK_PORT:-8081}/realms/portable-agent
OIDC_JWKS_URL: http://keycloak:8080/realms/portable-agent/protocol/openid-connect/certs
OIDC_AUDIENCE: conversation-service
AGENT_URL: http://agent-runtime:8080
ACTION_URL: http://action-service:8080
REMOTE_CONNECT_TIMEOUT: PT2S
REMOTE_READ_TIMEOUT: PT10S
depends_on:
postgres:
condition: service_healthy
keycloak:
condition: service_healthy
agent-runtime:
condition: service_healthy
action-service:
condition: service_healthy
ports:
- "127.0.0.1:${CONVERSATION_SERVICE_PORT:-18085}:8080"
healthcheck:
test:
[
CMD-SHELL,
"bash -c 'exec 3<>/dev/tcp/127.0.0.1/8080 && printf \"GET /actuator/health HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n\" >&3 && grep -q \"HTTP/1.1 200 \" <&3'",
]
interval: 5s
timeout: 3s
retries: 30
start_period: 20s
networks: [platform]

tempo:
profiles: [observe]
image: ${TEMPO_IMAGE:?set TEMPO_IMAGE}
Expand Down
19 changes: 19 additions & 0 deletions compose/keycloak/portable-agent-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
"bearerOnly": true,
"protocol": "openid-connect"
},
{
"clientId": "conversation-service",
"name": "Conversation Service Resource Server",
"enabled": true,
"bearerOnly": true,
"protocol": "openid-connect"
},
{
"clientId": "action-service",
"name": "Action Service Worker",
Expand Down Expand Up @@ -185,6 +192,18 @@
"introspection.token.claim": "true"
}
},
{
"name": "conversation-service-audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "conversation-service",
"id.token.claim": "false",
"access.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "calendar-mcp-audience",
"protocol": "openid-connect",
Expand Down
13 changes: 12 additions & 1 deletion compose/postgres/init/01-users.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ set -eu
: "${TEMPORAL_DB_PASSWORD:?TEMPORAL_DB_PASSWORD is required}"
: "${ACTION_DB_USER:?ACTION_DB_USER is required}"
: "${ACTION_DB_PASSWORD:?ACTION_DB_PASSWORD is required}"
: "${CONVERSATION_DB_USER:?CONVERSATION_DB_USER is required}"
: "${CONVERSATION_DB_PASSWORD:?CONVERSATION_DB_PASSWORD is required}"

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
--set=keycloak_user="$KEYCLOAK_DB_USER" \
--set=keycloak_password="$KEYCLOAK_DB_PASSWORD" \
--set=temporal_user="$TEMPORAL_DB_USER" \
--set=temporal_password="$TEMPORAL_DB_PASSWORD" \
--set=action_user="$ACTION_DB_USER" \
--set=action_password="$ACTION_DB_PASSWORD" <<-'SQL'
--set=action_password="$ACTION_DB_PASSWORD" \
--set=conversation_user="$CONVERSATION_DB_USER" \
--set=conversation_password="$CONVERSATION_DB_PASSWORD" <<-'SQL'
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'keycloak_user', :'keycloak_password')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'keycloak_user') \gexec
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'keycloak_user', :'keycloak_password') \gexec
Expand All @@ -33,4 +37,11 @@ SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'action_user', :'action_p

SELECT format('CREATE DATABASE actions OWNER %I', :'action_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'actions') \gexec

SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'conversation_user', :'conversation_password')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'conversation_user') \gexec
SELECT format('ALTER ROLE %I WITH LOGIN PASSWORD %L', :'conversation_user', :'conversation_password') \gexec

SELECT format('CREATE DATABASE conversations OWNER %I', :'conversation_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'conversations') \gexec
SQL
5 changes: 3 additions & 2 deletions config/versions.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ GRAFANA_IMAGE=grafana/grafana:13.2.0
TEMPO_IMAGE=grafana/tempo:3.0.3
LOKI_IMAGE=grafana/loki:3.7.7
BUSYBOX_IMAGE=busybox:1.37.0
CHANNEL_GATEWAY_IMAGE=ghcr.io/portable-agent/channel-gateway:5084949e4aa1f0c4be4d2a4bce5e970197efa831
CHANNEL_GATEWAY_IMAGE=ghcr.io/portable-agent/channel-gateway:65e8406b017e81ac6993818290f211a9b2c845aa
AGENT_RUNTIME_IMAGE=ghcr.io/portable-agent/agent-runtime:4dd541e459d0bdbf889ddc1fbe68e1f7c3b47b89
ACTION_SERVICE_IMAGE=ghcr.io/portable-agent/action-service:8d449ab990001e31cd42a7655a16c5716c5d6f20
CONVERSATION_SERVICE_IMAGE=ghcr.io/portable-agent/conversation-service:fb069a35c6a8406f88f665e8197ebfbc3564008a
MCP_GATEWAY_IMAGE=ghcr.io/portable-agent/mcp-gateway:0a3884d52ad1156ed173a4ecf9182ea3a0d2e686
CALENDAR_MCP_IMAGE=ghcr.io/portable-agent/calendar-mcp:bbab64774eab482c9d3543c7b8220b4cb42df56e
TEST_LAB_REF=1ee8b67bb67b13507f32c8761ffa697ea6a7794b
TEST_LAB_REF=26a8842c1b403bf50e6c67fdb7eaa7ae83d36d48
24 changes: 16 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,27 @@ services/catalog.json -> environments/<env>/services/<name>/values.yaml
sequenceDiagram
participant User as Пользователь
participant Channel as Channel Gateway
participant Conversation as Conversation Service
participant Agent as Agent Runtime
participant Action as Action Service
participant Keycloak
participant Gateway as MCP Gateway
participant Calendar as Calendar MCP

User->>Keycloak: логин
Keycloak-->>User: tenant + audience channel, agent и action
Keycloak-->>User: tenant + audience channel, conversation, agent и action
User->>Channel: текст + безопасный context + JWT
Channel->>Channel: JWT + server connector list
Channel->>Agent: нормализованный текст + тот же JWT
Channel->>Channel: JWT + нормализация канала
Channel->>Conversation: сообщение + тот же JWT
Conversation->>Conversation: сохранить сообщение
Conversation->>Agent: текст + server connector list + тот же JWT
Agent->>Agent: JWT + подготовка ActionPlan
Agent-->>User: proposal требует подтверждения
User->>Action: создать и подтвердить действие
Agent-->>Conversation: готовое предложение
Conversation->>Action: создать действие
Action-->>Conversation: actionId + payloadHash
Conversation-->>User: переносимый виджет подтверждения
User->>Channel: подтвердить виджет
Channel->>Action: actionId + payloadHash + решение
Action->>Action: JWT issuer + audience + tenant
Action->>Keycloak: client_credentials
Keycloak-->>Action: tenant + две audience + два scope
Expand All @@ -58,6 +65,7 @@ sequenceDiagram
Canonical issuer локального realm доступен хосту через `localhost`. Контейнеры загружают JWKS по
внутреннему имени `keycloak`, поэтому проверка токена не зависит от DNS хоста. `start-local -Apps`
сначала поднимает зависимости и создаёт Temporal namespace, затем собирает и запускает приложения.
Channel Gateway не зависит от Telegram и принимает общий текстовый контракт `2.2.0`. Agent Runtime не
исполняет и не сохраняет действие. Он создаёт предложение по совместимому контракту `2.2.0`;
после подтверждения Action Service становится источником состояния, аудита и Temporal workflow.
Channel Gateway не зависит от Telegram и принимает общий контракт сообщения `2.4.0`. Conversation
Service хранит состояние диалога, получает предложение от Agent Runtime, создаёт Action и возвращает
каналу переносимый виджет. Agent Runtime не исполняет и не сохраняет действие. После подтверждения
Action Service остаётся источником состояния, аудита и Temporal workflow.
13 changes: 11 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@ Taskfile — единая точка входа. Внутренние PowerShell
4. Посмотри состояние через `task status`, логи — через `task service:logs SERVICE=<name>`.
5. Перед pull request выполни `task test:e2e`.

Допустимые имена: `channel-gateway`, `agent-runtime`, `action-service`, `mcp-gateway` и
`calendar-mcp`. Неизвестное имя отклоняется до вызова Docker.
Допустимые имена: `channel-gateway`, `conversation-service`, `agent-runtime`, `action-service`,
`mcp-gateway` и `calendar-mcp`. Неизвестное имя отклоняется до вызова Docker.

Например, для локальной проверки HTTP-потока диалога выполни:

```powershell
task service:up SERVICE=conversation-service
```

Compose соберёт сервис из соседнего репозитория, поднимет PostgreSQL, Keycloak, Agent Runtime и
Action Service. Отдельно запускать его зависимости не нужно.

`task test:e2e` поднимает полный локальный срез и ждёт healthchecks. Затем `deploy` вызывает
`task test:e2e` в соседнем репозитории `test-lab`, который владеет black-box сценарием от текста до
Expand Down
Loading
Loading