- 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
498 lines
13 KiB
Markdown
498 lines
13 KiB
Markdown
# TASK_PHASE_CITY_BACKEND_FINISHER.md
|
||
|
||
DAARION CITY — Backend Completion for Phase 3 (MVP)
|
||
|
||
Цей таск **закриває City Backend** до рівня, коли MVP можна деплоїти на сервер (daarion.space) і реально користуватись:
|
||
|
||
- Public Rooms (міські кімнати)
|
||
- Presence System (онлайн-статуси)
|
||
- Second Me (персональний агент MVP)
|
||
- City Home інтеграція (дані для дашборду міста)
|
||
|
||
Фронтенд уже реалізований (CityRoomsPage, SecondMePage, PresenceBar тощо),
|
||
цей таск — про **backend-реалізацію API + WS + Redis + DB + інтеграцію з Agents Core**.
|
||
|
||
---
|
||
|
||
## 0. База / припущення
|
||
|
||
1. Primary DB: **PostgreSQL** (той самий, що й для microdao).
|
||
2. Cache / presence: **Redis** (ok додати новий контейнер або використовувати існуючий).
|
||
3. Message bus: **NATS JetStream** (вже є для Agents Core).
|
||
4. HTTP API gateway: уже налаштований (`/api/...`, `/ws/...`), ти додаєш нові маршрути.
|
||
5. Існує **Agents Core** з endpoints `/agents/{id}/invoke` і NATS-темами `agents.invoke` / `agents.reply`.
|
||
|
||
---
|
||
|
||
## 1. Структура Backend-модулів City
|
||
|
||
Створити (або доповнити, якщо частково вже є):
|
||
|
||
```text
|
||
services/
|
||
city-service/
|
||
__init__.py
|
||
models.py
|
||
schemas.py
|
||
routes_city.py
|
||
ws_city.py
|
||
presence.py
|
||
feed.py
|
||
rooms.py
|
||
repo.py
|
||
secondme-service/
|
||
__init__.py
|
||
models.py
|
||
schemas.py
|
||
routes_secondme.py
|
||
service_secondme.py
|
||
common/
|
||
redis_client.py # якщо ще немає
|
||
```
|
||
|
||
І підключити:
|
||
|
||
* `routes_city.py` і `routes_secondme.py` до основного `main.py` (або відповідного API-aggregator service).
|
||
* `ws_city.py` — до WebSocket router'а (`/ws/...`).
|
||
|
||
---
|
||
|
||
## 2. PostgreSQL: нові таблиці
|
||
|
||
### 2.1 Таблиця `city_rooms`
|
||
|
||
```sql
|
||
create table city_rooms (
|
||
id text primary key, -- room_id, напр. "room_city_general"
|
||
slug text not null unique, -- "general", "science"
|
||
name text not null, -- "General", "Science"
|
||
description text null,
|
||
is_default boolean not null default false,
|
||
created_at timestamptz not null default now(),
|
||
created_by text null -- user_id (u_*)
|
||
);
|
||
create index ix_city_rooms_slug on city_rooms(slug);
|
||
```
|
||
|
||
### 2.2 Таблиця `city_room_messages`
|
||
|
||
```sql
|
||
create table city_room_messages (
|
||
id text primary key, -- ksuid/ulid, префікс m_city_
|
||
room_id text not null references city_rooms(id) on delete cascade,
|
||
author_user_id text null, -- u_*
|
||
author_agent_id text null, -- ag_*
|
||
body text not null,
|
||
created_at timestamptz not null default now()
|
||
);
|
||
create index ix_city_room_messages_room_time on city_room_messages(room_id, created_at desc);
|
||
```
|
||
|
||
### 2.3 Таблиця `city_feed_events`
|
||
|
||
```sql
|
||
create table city_feed_events (
|
||
id text primary key, -- evt_city_*
|
||
kind text not null, -- 'room_message','agent_reply','system'
|
||
room_id text null references city_rooms(id) on delete set null,
|
||
user_id text null,
|
||
agent_id text null,
|
||
payload jsonb not null,
|
||
created_at timestamptz not null default now()
|
||
);
|
||
create index ix_city_feed_time on city_feed_events(created_at desc);
|
||
```
|
||
|
||
### 2.4 Таблиця `secondme_sessions` (історія Second Me)
|
||
|
||
```sql
|
||
create table secondme_sessions (
|
||
id text primary key, -- smsess_*
|
||
user_id text not null, -- u_*
|
||
created_at timestamptz not null default now()
|
||
);
|
||
|
||
create table secondme_messages (
|
||
id text primary key, -- smmsg_*
|
||
session_id text not null references secondme_sessions(id) on delete cascade,
|
||
user_id text not null,
|
||
role text not null check (role in ('user','assistant')),
|
||
content text not null,
|
||
created_at timestamptz not null default now()
|
||
);
|
||
create index ix_secondme_messages_session_time on secondme_messages(session_id, created_at desc);
|
||
```
|
||
|
||
Для MVP: можна використовувати **одну активну session per user** (останню).
|
||
|
||
---
|
||
|
||
## 3. Redis: Presence System
|
||
|
||
Використати Redis як KV-store для онлайн-присутності:
|
||
|
||
* key: `presence:user:{user_id}` → value: `"online"`
|
||
* TTL: 40 секунд
|
||
* WS heartbeat кожні 20 секунд оновлює TTL
|
||
|
||
### Redis-клієнт
|
||
|
||
`common/redis_client.py`:
|
||
|
||
```python
|
||
import aioredis
|
||
from typing import Optional
|
||
|
||
_redis = None
|
||
|
||
async def get_redis() -> aioredis.Redis:
|
||
global _redis
|
||
if _redis is None:
|
||
_redis = await aioredis.from_url(
|
||
os.getenv("REDIS_URL", "redis://redis:6379/0"),
|
||
encoding="utf-8",
|
||
decode_responses=True,
|
||
)
|
||
return _redis
|
||
```
|
||
|
||
---
|
||
|
||
## 4. HTTP API — City Rooms / Feed
|
||
|
||
### 4.1 Маршрути (routes_city.py)
|
||
|
||
Base prefix: **`/city`**.
|
||
|
||
#### GET `/city/rooms`
|
||
|
||
* Повертає список всіх кімнат.
|
||
* Query params: (optional) `limit`, `offset`.
|
||
* Response:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "room_city_general",
|
||
"slug": "general",
|
||
"name": "General",
|
||
"description": "Головна кімната міста",
|
||
"members_online": 42,
|
||
"last_event": "2025-11-23T10:15:00Z"
|
||
}
|
||
]
|
||
```
|
||
|
||
`members_online` рахувати через Redis:
|
||
|
||
* keys: `presence:user:*` → map users → rooms (див. нижче в Presence).
|
||
|
||
Для MVP можна:
|
||
|
||
* рахувати `members_online` приблизно: число унікальних `presence:user:*` (спрощено),
|
||
* або додати key `presence:room:{room_id}` (більш точно).
|
||
|
||
#### POST `/city/rooms`
|
||
|
||
Body:
|
||
|
||
```json
|
||
{
|
||
"name": "Science",
|
||
"slug": "science",
|
||
"description": "Наукова кімната"
|
||
}
|
||
```
|
||
|
||
* Генерує `id = room_city_{slug}`.
|
||
* Створює запис у `city_rooms`.
|
||
* Віддає створену кімнату.
|
||
|
||
#### GET `/city/rooms/{room_id}`
|
||
|
||
Returns:
|
||
|
||
* room meta
|
||
* останні 50 повідомлень
|
||
* приблизний `members_online`
|
||
|
||
```json
|
||
{
|
||
"room": {
|
||
"id": "room_city_general",
|
||
"name": "General",
|
||
"description": "Головна кімната міста"
|
||
},
|
||
"messages": [
|
||
{
|
||
"id": "m_city_...",
|
||
"author_user_id": "u_123",
|
||
"author_agent_id": null,
|
||
"body": "Привіт місто!",
|
||
"created_at": "..."
|
||
}
|
||
],
|
||
"members_online": 12
|
||
}
|
||
```
|
||
|
||
#### POST `/city/rooms/{room_id}/messages`
|
||
|
||
* Body:
|
||
|
||
```json
|
||
{
|
||
"body": "Текст повідомлення"
|
||
}
|
||
```
|
||
|
||
* Запис у `city_room_messages`.
|
||
* Запис у `city_feed_events` з kind = `"room_message"`.
|
||
* Публікація WS event (див. WS нижче).
|
||
* Повертає створене повідомлення.
|
||
|
||
#### GET `/city/feed`
|
||
|
||
* Повертає останні N (наприклад, 20) подій:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"id": "evt_city_...",
|
||
"kind": "room_message",
|
||
"room_id": "room_city_general",
|
||
"user_id": "u_123",
|
||
"payload": {"body": "Текст...", "snippet": "..."},
|
||
"created_at": "..."
|
||
}
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## 5. WebSocket — City Rooms + Presence
|
||
|
||
### 5.1 City Rooms WS (`ws_city.py`)
|
||
|
||
Шлях (через already existing WS server):
|
||
|
||
```
|
||
/ws/city/rooms/{room_id}
|
||
```
|
||
|
||
Події (JSON):
|
||
|
||
* Вхідні від клієнта:
|
||
|
||
```json
|
||
{ "event": "room.join", "room_id": "room_city_general" }
|
||
{ "event": "room.leave", "room_id": "room_city_general" }
|
||
{ "event": "room.message.send", "room_id": "...", "body": "..." }
|
||
```
|
||
|
||
* Вихідні до клієнтів:
|
||
|
||
```json
|
||
{ "event": "room.message", "room_id": "...", "message": { ... } }
|
||
{ "event": "room.join", "room_id": "...", "user_id": "u_123" }
|
||
{ "event": "room.leave", "room_id": "...", "user_id": "u_123" }
|
||
{ "event": "room.presence", "room_id": "...", "user_id": "u_123", "status": "online" }
|
||
```
|
||
|
||
При `room.message.send`:
|
||
|
||
1. зберегти в DB (`city_room_messages`)
|
||
2. зберегти в `city_feed_events`
|
||
3. розіслати WS подію `room.message` усім підписникам
|
||
|
||
### 5.2 Presence WS (`/ws/city/presence`)
|
||
|
||
* Вхідні:
|
||
|
||
```json
|
||
{ "event": "presence.heartbeat", "user_id": "u_123" }
|
||
```
|
||
|
||
Обробка:
|
||
|
||
1. `SETEX presence:user:u_123 "online" 40`
|
||
2. broadcast (опційно) `presence.update`:
|
||
|
||
```json
|
||
{ "event": "presence.update", "user_id": "u_123", "status": "online" }
|
||
```
|
||
|
||
3. Періодично (background task, наприклад кожні 30 сек):
|
||
|
||
* сканувати `presence:user:*`,
|
||
* якщо TTL минув (Redis сам видаляє keys), ws-клієнтам можна розіслати `presence.update` зі статусом `"offline"` (або робити lazy-оновлення при наступних запитах).
|
||
|
||
---
|
||
|
||
## 6. Backend для Second Me
|
||
|
||
### 6.1 Service logic (`secondme-service/service_secondme.py`)
|
||
|
||
Функції:
|
||
|
||
```python
|
||
async def get_or_create_session(user_id: str) -> SecondMeSession:
|
||
# бере останню сесію або створює нову
|
||
|
||
async def get_last_messages(user_id: str, limit: int = 5) -> list[SecondMeMessage]:
|
||
# повертає останні 5 повідомлень з secondme_messages
|
||
|
||
async def invoke_second_me(user_id: str, prompt: str) -> SecondMeMessage:
|
||
# 1. get_or_create_session
|
||
# 2. зберегти user-повідомлення
|
||
# 3. зібрати короткий контекст (останні N повідомлень)
|
||
# 4. викликати Agents Core:
|
||
# POST /agents/{second_me_agent_id}/invoke
|
||
# або NATS publish agents.invoke
|
||
# 5. зберегти assistant-відповідь
|
||
# 6. повернути відповідь
|
||
```
|
||
|
||
`second_me_agent_id` поки можна:
|
||
|
||
* або hardcode (один глобальний Second Me agent),
|
||
* або зберігати у таблиці `users` / `agents` як поле.
|
||
|
||
Для MVP — допустимо hardcode у конфігу.
|
||
|
||
### 6.2 API (`routes_secondme.py`)
|
||
|
||
#### POST `/secondme/invoke`
|
||
|
||
Body:
|
||
|
||
```json
|
||
{ "prompt": "..." }
|
||
```
|
||
|
||
З HTTP-контексту брати `user_id` (із JWT).
|
||
Викликати `invoke_second_me`, повернути:
|
||
|
||
```json
|
||
{
|
||
"reply": "Текст відповіді",
|
||
"history": [
|
||
{"role": "user", "content": "..."},
|
||
{"role": "assistant", "content": "..."}
|
||
]
|
||
}
|
||
```
|
||
|
||
#### GET `/secondme/history`
|
||
|
||
Query: (optional) `limit`, default 5.
|
||
Повернути:
|
||
|
||
```json
|
||
[
|
||
{"role": "user", "content": "...", "created_at": "..."},
|
||
{"role": "assistant", "content": "...", "created_at": "..."}
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Інтеграція з Agents Core
|
||
|
||
Для Second Me використати стандартний шлях:
|
||
|
||
* або **HTTP-level**:
|
||
|
||
```text
|
||
POST /agents/{second_me_agent_id}/invoke
|
||
{
|
||
"input": "...prompt+context...",
|
||
"context": { "user_id": "...", "kind": "secondme" }
|
||
}
|
||
```
|
||
|
||
* або **NATS** (якщо вже зручно):
|
||
|
||
```json
|
||
{
|
||
"event": "agents.invoke",
|
||
"agent_id": "ag_secondme",
|
||
"payload": {
|
||
"input": "...",
|
||
"user_id": "u_123",
|
||
"source": "secondme"
|
||
}
|
||
}
|
||
```
|
||
|
||
Для MVP допустимо використати HTTP-виклик до Agents Core service.
|
||
|
||
---
|
||
|
||
## 8. Конфігурація (ENV)
|
||
|
||
Додати змінні:
|
||
|
||
```env
|
||
REDIS_URL=redis://redis:6379/0
|
||
SECONDME_AGENT_ID=ag_secondme_global
|
||
CITY_DEFAULT_ROOMS=general,welcome,builders
|
||
```
|
||
|
||
При старті `city-service`:
|
||
|
||
* якщо `CITY_DEFAULT_ROOMS` порожній → створити дефолтні кімнати:
|
||
|
||
* `room_city_general` ("General")
|
||
* `room_city_welcome` ("Welcome")
|
||
* `room_city_builders` ("Builders")
|
||
|
||
---
|
||
|
||
## 9. Acceptance Criteria
|
||
|
||
### 9.1 Public Rooms
|
||
|
||
* `GET /city/rooms` повертає список кімнат.
|
||
* `POST /city/rooms` створює кімнату, видно в фронтенді.
|
||
* `GET /city/rooms/{room_id}` повертає кімнату та останні повідомлення.
|
||
* `POST /city/rooms/{room_id}/messages`:
|
||
|
||
* зберігає повідомлення в DB,
|
||
* відправляє WS-івент `room.message`,
|
||
* додає запис у `city_feed_events`.
|
||
|
||
### 9.2 Presence
|
||
|
||
* при підключенні фронтенду до `/ws/city/presence` і надсиланні `presence.heartbeat`:
|
||
|
||
* Redis має ключ `presence:user:<user_id>` із TTL ~40с;
|
||
* інші клієнти отримують `presence.update` (online).
|
||
|
||
* при припиненні heartbeat ключ зникає → статус offline (або lazily оновлюється).
|
||
|
||
### 9.3 Second Me
|
||
|
||
* `POST /secondme/invoke`:
|
||
|
||
* робить виклик до Agents Core,
|
||
* повертає текст відповіді,
|
||
* історія зберігається у `secondme_messages`.
|
||
|
||
* `GET /secondme/history` повертає останні N записів.
|
||
|
||
### 9.4 City Home
|
||
|
||
* `GET /city/feed` повертає події (room messages мінімум).
|
||
* Frontend City Home (вже реалізований) отримує всі необхідні дані через API.
|
||
|
||
---
|
||
|
||
## 10. Команда до Cursor
|
||
|
||
**"Реалізувати backend для City MVP згідно TASK_PHASE_CITY_BACKEND_FINISHER.md.
|
||
Створити city-service (rooms, feed, presence) та secondme-service.
|
||
Інтегрувати з Redis (presence) та Agents Core (Second Me).
|
||
Не змінювати існуючий фронтенд, тільки підключити API."**
|
||
|