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:
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)¶
Додати змінні:
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."