- 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
13 KiB
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. База / припущення
- Primary DB: PostgreSQL (той самий, що й для microdao).
- Cache / presence: Redis (ok додати новий контейнер або використовувати існуючий).
- Message bus: NATS JetStream (вже є для Agents Core).
- HTTP API gateway: уже налаштований (
/api/...,/ws/...), ти додаєш нові маршрути. - Існує Agents Core з endpoints
/agents/{id}/invokeі NATS-темамиagents.invoke/agents.reply.
1. Структура Backend-модулів City
Створити (або доповнити, якщо частково вже є):
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
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
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
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)
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:
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:
[
{
"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:
{
"name": "Science",
"slug": "science",
"description": "Наукова кімната"
}
- Генерує
id = room_city_{slug}. - Створює запис у
city_rooms. - Віддає створену кімнату.
GET /city/rooms/{room_id}
Returns:
- room meta
- останні 50 повідомлень
- приблизний
members_online
{
"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:
{
"body": "Текст повідомлення"
}
- Запис у
city_room_messages. - Запис у
city_feed_eventsз kind ="room_message". - Публікація WS event (див. WS нижче).
- Повертає створене повідомлення.
GET /city/feed
- Повертає останні N (наприклад, 20) подій:
[
{
"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):
- Вхідні від клієнта:
{ "event": "room.join", "room_id": "room_city_general" }
{ "event": "room.leave", "room_id": "room_city_general" }
{ "event": "room.message.send", "room_id": "...", "body": "..." }
- Вихідні до клієнтів:
{ "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:
- зберегти в DB (
city_room_messages) - зберегти в
city_feed_events - розіслати WS подію
room.messageусім підписникам
5.2 Presence WS (/ws/city/presence)
- Вхідні:
{ "event": "presence.heartbeat", "user_id": "u_123" }
Обробка:
SETEX presence:user:u_123 "online" 40- broadcast (опційно)
presence.update:
{ "event": "presence.update", "user_id": "u_123", "status": "online" }
-
Періодично (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)
Функції:
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:
{ "prompt": "..." }
З HTTP-контексту брати user_id (із JWT).
Викликати invoke_second_me, повернути:
{
"reply": "Текст відповіді",
"history": [
{"role": "user", "content": "..."},
{"role": "assistant", "content": "..."}
]
}
GET /secondme/history
Query: (optional) limit, default 5.
Повернути:
[
{"role": "user", "content": "...", "created_at": "..."},
{"role": "assistant", "content": "...", "created_at": "..."}
]
7. Інтеграція з Agents Core
Для Second Me використати стандартний шлях:
-
або HTTP-level:
POST /agents/{second_me_agent_id}/invoke { "input": "...prompt+context...", "context": { "user_id": "...", "kind": "secondme" } } -
або NATS (якщо вже зручно):
{ "event": "agents.invoke", "agent_id": "ag_secondme", "payload": { "input": "...", "user_id": "u_123", "source": "secondme" } }
Для MVP допустимо використати HTTP-виклик до Agents Core service.
8. Конфігурація (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).
- Redis має ключ
-
при припиненні 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."