Files
microdao-daarion/docs/tasks/TASK_PHASE_CITY_BACKEND_FINISHER.md
Apple 3de3c8cb36 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
2025-11-27 00:19:40 -08:00

498 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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."**