feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint - usePresenceHeartbeat hook with activity tracking - Auto away after 5 min inactivity - Offline on page close/visibility change - Integrated in MatrixChatRoom component
This commit is contained in:
421
docs/tasks/TASK_PHASE2_AGENT_INTEGRATION.md
Normal file
421
docs/tasks/TASK_PHASE2_AGENT_INTEGRATION.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# TASK: PHASE 2 — Agent Integration (agent_filter + DAGI Router + Agent Runtime)
|
||||
|
||||
**Goal:**
|
||||
Зробити Messenger повноцінно агентним:
|
||||
- новий сервіс agent_filter, який вирішує, коли й який агент відповідає;
|
||||
- розширити DAGI Router, щоб маршрутизувати події з Messenger до агентів;
|
||||
- реалізувати agent-runtime-service, який читає історію каналів, викликає LLM і постить відповіді назад у Messenger.
|
||||
|
||||
**Existing:**
|
||||
- Messenger Module (Matrix-aware, Full Stack) вже реалізований:
|
||||
- messaging-service (FastAPI, 9 endpoints + WS)
|
||||
- matrix-gateway API spec (services/matrix-gateway/API_SPEC.md)
|
||||
- DB schema (channels, messages, channel_members, message_reactions, channel_events)
|
||||
- frontend /messenger (ChannelList, MessageList, MessageComposer, WS)
|
||||
- NATS, Synapse, matrix-gateway, messaging-service у docker-compose.messenger.yml
|
||||
- Документація:
|
||||
- docs/MESSENGER_MODULE_COMPLETE.md
|
||||
- docs/MESSAGING_ARCHITECTURE.md
|
||||
- docs/messaging-erd.dbml
|
||||
- docs/MESSENGER_TESTING_GUIDE.md
|
||||
|
||||
**PHASE 2 складається з 3 підзадач:**
|
||||
|
||||
---
|
||||
|
||||
## 1) Сервіс agent_filter
|
||||
|
||||
**Create new service:** `services/agent-filter/`
|
||||
|
||||
**Files:**
|
||||
- `services/agent-filter/main.py`
|
||||
- `services/agent-filter/models.py`
|
||||
- `services/agent-filter/rules.py`
|
||||
- `services/agent-filter/config.yaml`
|
||||
- `services/agent-filter/requirements.txt`
|
||||
- `services/agent-filter/Dockerfile`
|
||||
- `services/agent-filter/README.md`
|
||||
|
||||
**Tech:**
|
||||
- Python + FastAPI
|
||||
- NATS JetStream client (python-nats)
|
||||
- Config з YAML
|
||||
|
||||
### 1.1 Models (models.py)
|
||||
|
||||
Define Pydantic models:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, Literal
|
||||
from datetime import datetime
|
||||
|
||||
class MessageCreatedEvent(BaseModel):
|
||||
channel_id: str
|
||||
message_id: Optional[str] = None
|
||||
matrix_event_id: str
|
||||
sender_id: str
|
||||
sender_type: Literal["human", "agent"]
|
||||
microdao_id: str
|
||||
created_at: datetime
|
||||
|
||||
class FilterDecision(BaseModel):
|
||||
channel_id: str
|
||||
message_id: Optional[str] = None
|
||||
matrix_event_id: str
|
||||
microdao_id: str
|
||||
decision: Literal["allow", "deny", "modify"]
|
||||
target_agent_id: Optional[str] = None
|
||||
rewrite_prompt: Optional[str] = None
|
||||
|
||||
class ChannelContext(BaseModel):
|
||||
microdao_id: str
|
||||
visibility: Literal["public", "private", "microdao"]
|
||||
allowed_agents: list[str] = []
|
||||
disabled_agents: list[str] = []
|
||||
|
||||
class FilterContext(BaseModel):
|
||||
channel: ChannelContext
|
||||
sender_is_owner: bool = False
|
||||
sender_is_admin: bool = False
|
||||
sender_is_member: bool = True
|
||||
local_time: Optional[datetime] = None
|
||||
```
|
||||
|
||||
### 1.2 Rules (rules.py)
|
||||
|
||||
Implement:
|
||||
|
||||
```python
|
||||
def decide(event: MessageCreatedEvent, ctx: FilterContext) -> FilterDecision:
|
||||
"""
|
||||
Baseline rules v1:
|
||||
- Якщо event.sender_type == "agent" → decision = "deny" (щоб не було loop).
|
||||
- Якщо channel.visibility == "microdao" і є default assistant агента для microDAO:
|
||||
- target_agent_id = цей агент (поки можна жорстко прописати в config або заглушка).
|
||||
- Якщо час у quiet_hours (23:00–07:00 з config.yaml):
|
||||
- decision = "modify"
|
||||
- rewrite_prompt = "Відповідай стисло і тільки якщо запит важливий. Не ініціюй розмову сам."
|
||||
- Якщо агент заборонений у цьому каналі (agent_id у disabled_agents) → decision = "deny".
|
||||
- Якщо немає жодного кандидата → decision = "deny".
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
**config.yaml:**
|
||||
```yaml
|
||||
nats:
|
||||
servers: ["nats://nats:4222"]
|
||||
messaging_subject: "messaging.message.created"
|
||||
decision_subject: "agent.filter.decision"
|
||||
rules:
|
||||
quiet_hours:
|
||||
start: "23:00"
|
||||
end: "07:00"
|
||||
default_agents:
|
||||
"microdao:daarion": "agent:sofia"
|
||||
```
|
||||
|
||||
### 1.3 main.py
|
||||
|
||||
- Підняти FastAPI:
|
||||
- `GET /health` → `{ "status": "ok" }`
|
||||
- `POST /internal/agent-filter/test` → приймає MessageCreatedEvent, викликає rules.decide(...), повертає FilterDecision.
|
||||
|
||||
- На startup:
|
||||
- підʼєднатися до NATS
|
||||
- підписатися на subject `messaging.message.created`
|
||||
|
||||
**Алгоритм обробки:**
|
||||
1. Deserialize payload у MessageCreatedEvent.
|
||||
2. Підібрати ChannelContext:
|
||||
- `GET /internal/messaging/channels/{channel_id}/context` (потрібно додати цей endpoint у messaging-service, якщо ще нема).
|
||||
- Очікуваний response:
|
||||
```json
|
||||
{
|
||||
"microdao_id": "...",
|
||||
"visibility": "microdao",
|
||||
"allowed_agents": ["agent:sofia"],
|
||||
"disabled_agents": []
|
||||
}
|
||||
```
|
||||
3. Побудувати FilterContext.
|
||||
4. Викликати rules.decide(event, ctx).
|
||||
5. Опублікувати FilterDecision у NATS:
|
||||
- subject: `agent.filter.decision`
|
||||
- payload: `decision.json()`
|
||||
|
||||
### 1.4 Dockerfile + README
|
||||
|
||||
- Dockerfile подібний до messaging-service.
|
||||
- README.md:
|
||||
- як запускати локально,
|
||||
- як тестувати `/internal/agent-filter/test`,
|
||||
- приклад NATS payload.
|
||||
|
||||
---
|
||||
|
||||
## 2) Розширення DAGI Router під messaging.inbound
|
||||
|
||||
**Goal:**
|
||||
- DAGI Router має слухати `agent.filter.decision` і на основі allow-рішень створювати AgentInvocation і штовхати в `router.invoke.agent`.
|
||||
|
||||
### 2.1 NATS subscription
|
||||
|
||||
У `services/router/` (або де реалізований DAGI Router):
|
||||
|
||||
- Підписка на subject: `agent.filter.decision`.
|
||||
|
||||
**Очікуваний payload:** FilterDecision (див. вище).
|
||||
|
||||
**Алгоритм:**
|
||||
- Якщо `decision != "allow"` → ігноруємо.
|
||||
- Якщо `decision == "allow"` і `target_agent_id` не заданий → логування + ігнор.
|
||||
- Інакше: побудувати AgentInvocation:
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_id": "<target_agent_id>",
|
||||
"entrypoint": "channel_message",
|
||||
"payload": {
|
||||
"channel_id": "<channel_id>",
|
||||
"message_id": "<message_id>",
|
||||
"matrix_event_id": "<matrix_event_id>",
|
||||
"microdao_id": "<microdao_id>",
|
||||
"rewrite_prompt": "<rewrite_prompt>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Опублікувати у NATS:
|
||||
- subject: `router.invoke.agent`
|
||||
- payload: AgentInvocation JSON.
|
||||
|
||||
### 2.2 Моделі
|
||||
|
||||
Додати в Router:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import Literal
|
||||
|
||||
class AgentInvocation(BaseModel):
|
||||
agent_id: str
|
||||
entrypoint: Literal["channel_message", "direct", "cron"] = "channel_message"
|
||||
payload: dict
|
||||
```
|
||||
|
||||
### 2.3 Конфіг
|
||||
|
||||
Файл `router_config.yaml` (або аналог):
|
||||
|
||||
```yaml
|
||||
messaging_inbound:
|
||||
enabled: true
|
||||
source_subject: "agent.filter.decision"
|
||||
target_subject: "router.invoke.agent"
|
||||
```
|
||||
|
||||
### 2.4 HTTP debug endpoint
|
||||
|
||||
Додати в Router:
|
||||
|
||||
**POST /internal/router/test-messaging**
|
||||
|
||||
Body: FilterDecision
|
||||
|
||||
Behavior:
|
||||
- прогнати той самий код, який обробляє NATS event,
|
||||
- повернути AgentInvocation JSON без публікації у NATS.
|
||||
|
||||
---
|
||||
|
||||
## 3) Agent Runtime integration з Messenger
|
||||
|
||||
**Goal:**
|
||||
- Реалізувати agent-runtime-service, який:
|
||||
- читає контекст каналу (останні повідомлення),
|
||||
- читає памʼять агента,
|
||||
- викликає LLM через LLM Proxy,
|
||||
- постить відповідь у канал через messaging-service.
|
||||
|
||||
**Create service:** `services/agent-runtime/`
|
||||
|
||||
**Files:**
|
||||
- `services/agent-runtime/main.py`
|
||||
- `services/agent-runtime/models.py`
|
||||
- `services/agent-runtime/llm_client.py`
|
||||
- `services/agent-runtime/messaging_client.py`
|
||||
- `services/agent-runtime/memory_client.py`
|
||||
- `services/agent-runtime/config.yaml`
|
||||
- `services/agent-runtime/requirements.txt`
|
||||
- `services/agent-runtime/Dockerfile`
|
||||
- `services/agent-runtime/README.md`
|
||||
|
||||
### 3.1 Models (models.py)
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from typing import Literal
|
||||
from datetime import datetime
|
||||
|
||||
class AgentInvocation(BaseModel):
|
||||
agent_id: str
|
||||
entrypoint: Literal["channel_message", "direct", "cron"] = "channel_message"
|
||||
payload: dict
|
||||
|
||||
class ChannelContextMessage(BaseModel):
|
||||
sender_id: str
|
||||
sender_type: Literal["human", "agent"]
|
||||
content: str
|
||||
created_at: datetime
|
||||
```
|
||||
|
||||
### 3.2 NATS subscription
|
||||
|
||||
**main.py:**
|
||||
- Підʼєднатись до NATS.
|
||||
- Підписатися на `router.invoke.agent`.
|
||||
|
||||
**Алгоритм:**
|
||||
1. Deserialize AgentInvocation.
|
||||
2. Якщо `entrypoint != "channel_message"` → поки що ігноруємо (або лог).
|
||||
3. Витягти:
|
||||
- `agent_id`
|
||||
- `channel_id`, `message_id`, `matrix_event_id`, `microdao_id`, `rewrite_prompt` з payload.
|
||||
4. Завантажити blueprint агента:
|
||||
```http
|
||||
GET /internal/agents/{agent_id}/blueprint
|
||||
```
|
||||
Очікуваний response:
|
||||
```json
|
||||
{
|
||||
"id": "...",
|
||||
"name": "Sofia-Prime",
|
||||
"model": "gpt-4.1",
|
||||
"instructions": "System prompt...",
|
||||
"capabilities": {...}
|
||||
}
|
||||
```
|
||||
5. Завантажити історію каналу:
|
||||
```http
|
||||
GET /internal/messaging/channels/{channel_id}/messages?limit=50
|
||||
```
|
||||
→ вернути список повідомлень у форматі ChannelContextMessage.
|
||||
|
||||
6. Витягти останнє human-повідомлення як user input.
|
||||
|
||||
7. Запитати памʼять:
|
||||
```http
|
||||
POST /internal/agent-memory/query
|
||||
{
|
||||
"agent_id": "<agent_id>",
|
||||
"microdao_id": "<microdao_id>",
|
||||
"channel_id": "<channel_id>",
|
||||
"query": "<останній текст користувача>"
|
||||
}
|
||||
```
|
||||
Очікуваний response: список релевантних фрагментів knowledge base.
|
||||
|
||||
8. Побудувати промпт для LLM (llm_client.py):
|
||||
- system: інструкції з blueprint +, якщо є, rewrite_prompt
|
||||
- context: останні N повідомлень (з імʼям, роллю, часом)
|
||||
- memory: релевантні фрагменти
|
||||
- user: останній текст користувача
|
||||
|
||||
9. Викликати LLM через LLM Proxy:
|
||||
```http
|
||||
POST /internal/llm/proxy
|
||||
{
|
||||
"model": "<з blueprint>",
|
||||
"messages": [ {"role": "...", "content": "..."}, ... ]
|
||||
}
|
||||
```
|
||||
Очікуваний response:
|
||||
```json
|
||||
{ "content": "<текст відповіді>" }
|
||||
```
|
||||
|
||||
10. Надіслати відповідь у канал:
|
||||
```http
|
||||
POST /internal/agents/{agentId}/post-to-channel
|
||||
{
|
||||
"channel_id": "<channel_id>",
|
||||
"text": "<llm response>"
|
||||
}
|
||||
```
|
||||
Цей endpoint вже повинен існувати у messaging-service (як внутрішній).
|
||||
|
||||
11. (optional v1) Записати в памʼять:
|
||||
```http
|
||||
POST /internal/agent-memory/store
|
||||
{
|
||||
"agent_id": "<agent_id>",
|
||||
"microdao_id": "<microdao_id>",
|
||||
"channel_id": "<channel_id>",
|
||||
"content": {
|
||||
"user_message": "...",
|
||||
"agent_reply": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 HTTP debug endpoint
|
||||
|
||||
**main.py:**
|
||||
|
||||
**POST /internal/agent-runtime/test-channel**
|
||||
|
||||
Body: AgentInvocation
|
||||
|
||||
Behavior:
|
||||
- викликає ту саму логіку, що NATS handler,
|
||||
- але замість реального POST до `/internal/agents/{agentId}/post-to-channel` просто повертає згенерований текст і зібраний prompt (обережно, без секретів у логах).
|
||||
|
||||
### 3.4 Docker + README
|
||||
|
||||
- Dockerfile за шаблоном інших сервісів.
|
||||
- README.md:
|
||||
- як запускати локально,
|
||||
- як тестувати через `/internal/agent-runtime/test-channel`,
|
||||
- як дивитись NATS events.
|
||||
|
||||
---
|
||||
|
||||
## 4) Інтеграція в docker-compose та документацію
|
||||
|
||||
### 4.1 docker-compose
|
||||
|
||||
- Додати `agent-filter`, `router` (якщо ще не доданий), `agent-runtime` у загальний docker-compose (або створити окремий `docker-compose.agents.yml`).
|
||||
- Забезпечити доступ до:
|
||||
- NATS
|
||||
- messaging-service
|
||||
- agent-memory-service (якщо вже існує) / stub
|
||||
- agents-service (blueprints) / stub
|
||||
- llm-proxy-service / stub
|
||||
|
||||
### 4.2 Документація
|
||||
|
||||
Оновити/додати:
|
||||
- `docs/MESSAGING_ARCHITECTURE.md` — помітити, що PHASE 2 реалізовано.
|
||||
- `docs/MESSENGER_COMPLETE_SPECIFICATION.md` — додати розділ "Agent Integration (PHASE 2)" з посиланнями на нові сервіси.
|
||||
- При потребі: окремий `docs/AGENT_INTEGRATION_PHASE2.md` з коротким описом flow.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- ✅ Human → пише в канал → agent_filter приймає event → DAGI Router відправляє AgentInvocation → Agent Runtime читає історію й памʼять → агент відповідає в той самий канал → повідомлення відображається у /messenger і в Element.
|
||||
- ✅ Є мінімум один робочий агент (наприклад, Sofia-Prime), який стабільно відповідає в одному каналі microDAO.
|
||||
- ✅ Всі сервіси стартують через docker-compose, health-checkи зелені.
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Date:** 2025-11-24
|
||||
**Priority:** High
|
||||
**Estimated Time:** 4 weeks
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user