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

13 KiB
Raw Blame History

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

Створити (або доповнити, якщо частково вже є):

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:

  1. зберегти в DB (city_room_messages)
  2. зберегти в city_feed_events
  3. розіслати WS подію room.message усім підписникам

5.2 Presence WS (/ws/city/presence)

  • Вхідні:
{ "event": "presence.heartbeat", "user_id": "u_123" }

Обробка:

  1. SETEX presence:user:u_123 "online" 40
  2. broadcast (опційно) presence.update:
{ "event": "presence.update", "user_id": "u_123", "status": "online" }
  1. Періодично (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).
  • при припиненні 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."