Skip to content

Project B, Generative Dashboard (AI Assistant): System Design & Administrator Manual

Scope. This document covers Project B only, the Generative Dashboard capability, surfaced in the product as the Chat assistant. That means the /chat subpage, the AI assistant it drives (running on the shared ai-service), the chart and dashboard artifacts it produces (/dashboard, /dashboard/{id}), the public share flow (/share/{token}), the back-end proxy endpoints that reach the assistant, the live Peec data it grounds its charts in, and the database tables that store conversations and dashboards.

Project B is one of two AI features that are cleanly separated by their own subpages. The other, Prompt Suggestion (the /prompt-suggestions tab, backed by the separate prompt_suggestion_service), is Project A and is documented in prompt-suggestion/system-design-and-admin-manual.md. Static browse/analytics pages that ship with mock data (/mydashboard, /overview, /prompts) are not part of Project B and are intentionally out of scope.

It is written to be understood by both non-technical readers (start with the "In plain language" boxes and the diagrams) and technical readers (tables, env vars, and file references throughout).


Part 1: System Design

1.1 Overview

In plain language. The Generative Dashboard is the feature behind the Chat tab. You ask the assistant a question about how your brand shows up in AI answers (e.g. "how did my visibility trend last month vs my competitors?"), and instead of only writing a paragraph, it builds charts on the spot from your real Peec data and shows them inside the conversation. You can then pin any chart to a dashboard you keep and revisit, rearrange the tiles, and share a read-only view by link, with an optional chat next to it.

Technically, Project B is an agentic, tool-calling pipeline exposed as server-sent-event (SSE) streaming endpoints on the shared ai-service:

  1. Converse: a LangGraph "deep agent" (built with deepagents) receives the user turn plus a system prompt, the conversation history, and a set of tools. It streams tokens back as it thinks and acts.
  2. Retrieve: the agent pulls live brand data through the Peec MCP server (mcp-server-peecai), not from a local copy, so numbers are always current.
  3. Render: render_* tools do not take data; they take a data recipe (ordered MCP calls loaded into DuckDB tables plus a SQL SELECT). The recipe is resolved against live data into a typed ComponentResponse chart spec. Charts forbid hard-coded dates (:start_date / :end_date placeholders) so a pinned chart re-resolves fresh data every time it is viewed.
  4. Persist: the transcript is written to the Message table and the agent's working memory to the LangGraph checkpointer tables; charts the user keeps become PinnedChart rows on a Dashboard.

The assistant is internal-only: the browser never talks to ai-service directly. It sits behind the Node/Express back-end (apps/server), which authenticates the user, validates the request, and proxies the SSE stream.

1.2 Components & responsibilities

Component Tech Port Role in Project B
Web app (apps/web) React 19 + Vite, served by nginx 80 The Chat subpage (/chat), the dashboard grid (/dashboard, /dashboard/{id}), and the public share view (/share/{token}). Uses assistant-ui with an ExternalStoreRuntime; streams over SSE; renders render_* charts with the same primitives as the rest of the app.
Back-end / BFF (apps/server) Node 20, Express 5, TypeScript 4000 The only public entry point. Authenticates (Firebase), validates (Zod), rate-limits, and proxies the SSE stream to the assistant, re-validating every frame. Routers: /api/threads, /api/dashboard, /api/dashboards, /api/dashboard-templates, /api/charts, /api/share.
AI service (apps/ai-service, Project B brain) Python 3.12, FastAPI, LangGraph, deepagents 8000 Runs the deep agent: tool selection, chart synthesis, dashboard mutations, conversation persistence, title generation. Streaming endpoints: /api/threads/message/stream, /api/threads/{id}/message/stream, /api/share/chat/stream; plus /api/threads/{id}/message, /api/charts/resolve, /api/completions, /api/embeddings.
PostgreSQL + pgvector pgvector/pgvector:pg16 5432 Stores Thread / Message (transcript), Dashboard / PinnedChart / DashboardShare (artifacts), and the LangGraph checkpointer tables (checkpoints, checkpoint_blobs, checkpoint_writes) that hold the agent's per-thread memory.
Peec MCP server (external) mcp-server-peecai via https://api.peec.ai/mcp 443 The live source of brand/visibility data every chart is grounded in. Reached only by ai-service.
LLM providers (external) OpenAI / Google 443 The models that power the agent. Provider is inferred from the model id (gpt-* → OpenAI, gemini* → Google).

In plain language: the Chat tab is the storefront, the Express server is the security desk that checks your badge and relays the live feed, ai-service is the analyst who actually reads your data and draws the charts, the Peec MCP server is the filing cabinet of real numbers, and Postgres is the notebook where your conversations and saved dashboards are kept.

1.3 Hardware / software mapping

Everything runs as Docker containers orchestrated by Docker Compose, on a single Coolify-managed Linux host (VPS). Containers share the host's CPU/RAM and are isolated at the container level. The only outbound "hardware" dependencies are third-party SaaS (OpenAI/Google, the Peec MCP/API, Langfuse, Firebase).

Software unit Runs as Host resource External hardware/SaaS reached
ai-service (Project B brain) Docker container (python:3.12-slim, uvicorn) CPU during chart resolution (DuckDB); mostly I/O-wait on the LLM and MCP OpenAI / Google LLMs; Peec MCP (api.peec.ai/mcp); Langfuse (traces)
server Docker container low; holds the SSE proxy open per active stream Firebase (token verification)
postgres (pgvector) Docker container Disk (named volume postgres-data) + RAM none
web Docker container (nginx static) low none
Traefik (provided by Coolify) Host reverse proxy network / TLS Let's Encrypt (certificates)

Project B is CPU-light and I/O-heavy: most wall-clock time is spent waiting on the LLM and on Peec MCP calls. Turns are long-lived SSE streams rather than single request/response round-trips, so the server holds a proxy connection open for the duration of each turn.

1.4 Deployment diagram

flowchart TB
    internet(["Internet (HTTPS)"])

    subgraph public["Published via Traefik (public subdomains)"]
        direction TB
        traefik["Traefik reverse proxy (Coolify)<br/>TLS via Let's Encrypt"]
        web["web · nginx :80<br/>React SPA · Chat + Dashboards + Share"]
        server["server · Express :4000<br/>Firebase auth · Zod · rate-limit · SSE proxy"]
    end

    subgraph internal["Internal Docker network (never exposed to the internet)"]
        direction TB
        ai["ai-service · FastAPI :8000 (PROJECT B)<br/>LangGraph deep agent · render_* tools<br/>/api/threads/message/stream · /api/threads/{id}/message/stream<br/>/api/share/chat/stream · /api/charts/resolve · /health"]
        pg[("postgres / pgvector :5432<br/>Thread · Message · Dashboard · PinnedChart<br/>DashboardShare · LangGraph checkpointer")]
    end

    subgraph external["External SaaS / APIs"]
        direction TB
        mcp["Peec MCP server (api.peec.ai/mcp)<br/>live brand / visibility data"]
        llm["OpenAI / Google"]
        langfuse["Langfuse (LLM tracing)"]
        firebase["Firebase (auth)"]
    end

    internet --> traefik
    traefik -->|"pr-N.preview… (dev.*)"| web
    traefik -->|"api-pr-N.preview… (dev-api.*)"| server
    web -.->|"browser → VITE_API_URL (SSE)"| server
    server -->|"internal only · SSE proxy"| ai
    server -->|"token verify"| firebase
    ai -->|"chat completions"| llm
    ai -->|"chart data (recipes)"| mcp
    ai -->|"transcript + checkpointer + dashboards"| pg
    ai --> langfuse

Only web and server are published through Traefik (they get public subdomains). ai-service and postgres are never exposed to the internet. They are reachable only on the internal Docker network (expose: not ports:).

⚠️ Local caveat: docker-compose.yml (development only) publishes ai-service on host port 8000 for convenience. The production (docker-compose.coolify.yml) and preview compose files use expose: only. See §1.7 and §2.4.

1.5 Persistent data

In plain language: unlike Prompt Suggestion, Project B does keep state: your conversations and your saved dashboards. Most of it lives in the shared Postgres database; the assistant's data itself (the brand numbers) is always fetched live and never copied.

Data Where it lives Owned by Notes
Conversation transcript (Thread, Message) Postgres (postgres-data volume) schema owned by apps/server (Prisma); written by both apps/server (Prisma) and ai-service (asyncpg) Thread links to a Firebase userId and optionally a dashboardId; Message stores role/content/tool-call metadata. Soft-deleted (deletedAt), restorable.
Agent working memory (checkpoints, checkpoint_blobs, checkpoint_writes) Postgres schema owned by Prisma; written by LangGraph (AsyncPostgresSaver) Keyed by thread_id == Thread.id. This is how a follow-up turn "remembers" the conversation. ai-service verifies this schema's version at readiness (/health/ready).
Dashboards & charts (Dashboard, PinnedChart) Postgres schema owned by apps/server; written by both PinnedChart holds a spec (the data recipe, re-resolved on view), a snapshot (last resolved result, used as fallback), plus layout (grid geometry) and presentation (per-chart visual edits). Identity is (dashboardId, toolCallId).
Share links (DashboardShare) Postgres apps/server token + accessLevel (public or members, default members) + a frozen viewState (time range).
Rich chat UI state Browser localStorage (peec.chat.sessions.v1) the browser only Tool-call pills, multi-chart ordering, and "interrupted" turns are reconstructed from this local blob, not the server. A cache clear or device switch loses that fidelity (see §2.4).
Brand / visibility data External Peec MCP server Peec platform Fetched live per chart resolution; never stored by Project B.
LLM traces Langfuse (external SaaS) ai-service Observability only.

ai-service owns no Prisma schema and runs no migrations (repo convention: only apps/server owns the schema). It connects to the same database to read and write Thread/Message/Dashboard/PinnedChart and the checkpointer tables, but never runs DDL.

Statefulness: Project B is stateful (conversations and dashboards persist). Containers themselves are stateless and can be restarted or scaled horizontally, but the Postgres volume is the source of truth and must be backed up.

1.6 Access control

In plain language: you must be logged in to chat and to see your dashboards. Login is checked at the front door (the Express server) and again by the assistant for chat actions. Shared dashboards can be opened by link, with an optional "members only" restriction.

Layer Mechanism Notes
Browser → server Firebase Authentication (ID token as Authorization: Bearer …) Verified by requireAuth (apps/server/src/middleware/auth.middleware.ts). Missing token → 401, invalid/expired → 403. Plus a global per-user rate limit (100) and per-route limiters (create-conversation, send-prompt, shared-chat).
Share links optionalAuth GET /api/share/:token and POST /api/share/:token/chat verify a token if present but never require one. accessLevel: "members" returns 401 without a logged-in user; "public" is open to anyone with the link.
Server → ai-service Internal Docker network; Authorization + x-request-id forwarded Chat/thread/persona routes on ai-service do verify Firebase (Depends(get_current_user)). POST /api/charts/resolve, GET /api/brands/metadata, and POST /api/share/chat/stream are intentionally unauthenticated (internal-only; share-chat authZ is enforced by the server).
ai-service → Peec MCP PEECAI_API_KEY + PEECAI_PROJECT_ID (service credentials) Server-to-server; scopes MCP data to the project.
ai-service → LLM providers Provider API keys (OPENAI_API_KEY, …) Server-to-server.
Ownership userOwnsThread, userOwnsDashboard, fetch_owned_dashboard Threads and dashboards are ownership-checked; foreign/unknown ids return 404 with no data leak. A follow-up turn ignores a client-supplied dashboardId and uses the stored Thread.dashboardId.

1.7 Security

Network isolation (strength). ai-service and Postgres are not internet-reachable in production. Only web and server are published via Traefik. All inter-service traffic stays on the private Docker network. TLS is terminated at Traefik with automatically renewed Let's Encrypt certificates.

Authentication & ownership (strength). Unlike Prompt Suggestion, Project B verifies Firebase auth end-to-end for chat, and enforces per-user ownership on threads and dashboards (userOwnsThread / userOwnsDashboard / fetch_owned_dashboard), returning 404 for anything the caller does not own, with no data leak.

Grounded, date-safe charts (strength). Every chart is resolved from live Peec MCP data through a SQL recipe; assert_no_literal_dates forbids hard-coded dates so pinned charts always re-resolve current data rather than freezing stale numbers. SSE frames are validated against SseEventSchema in the proxy and malformed frames are dropped.

Input validation (strength). The server validates every request with Zod and tool payloads are validated against ComponentResponseSchema before rendering. Unauthenticated calls get 401; bad input gets 400.

Secrets. LLM keys (OPENAI_API_KEY, GOOGLE_API_KEY), the Peec key (PEECAI_API_KEY), and the Firebase service account (FIREBASE_SERVICE_ACCOUNT_BASE64) are injected as environment variables (GitHub Actions secrets → Coolify env), never committed.

⚠️ Known security gaps (see also §2.4). Honest limitations of the current design, called out so administrators can compensate:

  1. ai-service is published to the host in local Compose. docker-compose.yml maps ports: 8000:8000 "for dev convenience." Because a few ai-service endpoints are unauthenticated by design (/api/charts/resolve, /api/brands/metadata, /api/share/chat/stream), exposing that port in a real environment would make them directly reachable. Mitigation: production/preview compose already use expose: only; keep port 8000 firewalled and never copy the dev port mapping to prod.
  2. Some ai-service endpoints have no auth of their own. They rely on the service being internal-only and on the BFF enforcing share authorization. Mitigation: keep the service internal-only; treat the BFF as the sole trust boundary; consider a service token for defense-in-depth.
  3. No SSE heartbeat or idle-timeout. The proxy sets Connection: keep-alive but emits no server ping and has no configurable stream timeout; a stalled LLM or MCP call can hang a turn, and the client does not reconnect. Mitigation: add a heartbeat/idle-timeout to sseProxy.ts and a bounded upstream timeout; surface a retry in the UI.
  4. Rich chat history is browser-only. The Message table stores flat text + one component per message; tool-call pills and multi-chart ordering live only in localStorage. A device switch or cache clear loses fidelity. Mitigation: persist structured turn parts server-side if durability is required.
  5. Auto-pin failures are swallowed. auto_pin_chart logs and returns on any error ("auto-pin must never fail the render"), so a chart can appear in chat but silently fail to pin to the dashboard. Mitigation: surface a non-blocking warning when a pin fails.
  6. Rate limits are per-instance, in-memory. They reset on restart and are not shared across replicas. Mitigation: move to a shared store if the service is scaled horizontally.

Part 2: Administrator Manual

2.1 Infrastructure setup

In plain language: one Linux server runs Coolify (a self-hosted deployment platform, similar to a private Heroku). Coolify runs Traefik for routing/TLS and builds/starts the containers from the repo's Compose files.

Prerequisites on the host

  • A Linux VPS with Docker Engine + Docker Compose v2.
  • Coolify installed (provides the Traefik reverse proxy and the coolify Docker network that the Compose files attach to).
  • DNS: a wildcard *.preview.peecai-csee.com (and the dev.* / dev-api.* records) pointing at the host, so Traefik can route per-environment subdomains.
  • Firewall: expose only 80/443 publicly. Ports 8000 (ai-service) and 5432 (Postgres) must not be publicly reachable.

Build toolchain (for local/CI builds)

  • Node.js 20+, pnpm 9.12.0 (via Corepack), uv (Python), Docker Compose v2.

Compose files (what runs where) | File | Used for | Key differences | |---|---|---| | docker-compose.yml | Local development | Publishes host ports (including ai-service 8000); Grafana anonymous-admin; loads each service's local .env. | | docker-compose.coolify.yml | Production (Coolify) | Services expose only (no host ports); secrets required. | | docker-compose.preview.yml | Per-PR preview envs | Traefik labels for pr-N.* / api-pr-N.*; attaches to the external coolify network. |

2.2 Deployment steps

A. Production (Coolify)

  1. In Coolify, the application points at this repo and uses docker-compose.coolify.yml.
  2. Set all required environment variables in the Coolify UI (see §2.3): the LLM provider key(s), PEECAI_API_KEY + PEECAI_PROJECT_ID, the Firebase service account, DATABASE_URL, and CORS_ALLOWED_ORIGINS.
  3. Coolify builds the images and starts the stack. server depends on a healthy ai-service, which depends on a healthy postgres.
  4. Migrations gate this feature. On boot the server container runs prisma migrate deploy, which creates/updates Thread, Message, Dashboard, PinnedChart, DashboardShare, and the checkpointer tables. ai-service refuses to be ready (/health/ready) until the checkpointer schema matches its expected version, so the agent cannot run against an un-migrated database.
  5. Traefik publishes web and server with Let's Encrypt TLS. ai-service comes up on the internal network at http://ai-service:8000.
  6. Verify: GET /health (and /health/ready) on ai-service from inside the network, then the Chat tab end-to-end from the browser (send a message, confirm a chart renders and can be pinned).

B. Preview deployments (per pull request)

Driven by .github/workflows/preview-deploy.yml. Every PR labelled deploy-preview gets its own throwaway copy of the whole stack at its own URL, and it is deleted when the PR closes.

  1. Trigger: add the deploy-preview label to a PR (or push to an already-labelled PR).
  2. The workflow SSHes to the host, rsyncs the repo, and writes a .env from the PREVIEW_ENV secret plus VITE_API_URL=https://api-pr-<N>.preview.peecai-csee.com and VITE_APP_ENV=dev.
  3. It brings the stack up with a dedicated project name (docker compose -f docker-compose.preview.yml -p preview-pr-<N>).
  4. Clean slate each deploy: the previous stack is torn down with its DB volume (down --volumes), so every preview starts with empty chat history and no dashboards.
  5. Migration self-heal: if Prisma is wedged (P3009), the workflow rolls back the named failed migrations and retries once.
  6. A bot comments the preview URL and sets a preview: live / preview: failed label from a health check.
  7. Teardown: closing the PR (or removing the label) runs cleanup-preview, which down --volumes --rmi all and deletes the preview directory.

Preview data caveat: because the DB volume is wiped on every preview deploy, conversations and dashboards do not carry over between deploys. Charts still resolve live from the Peec MCP server, so generation works from a clean database.

2.3 Environment variables

AI service (ai-service) (app/settings.py; UPPER_CASE as env vars):

Variable Default Purpose
AI_DEFAULT_MODEL gpt-5.4-mini Default agent model. Provider is inferred from the id (gpt-* → OpenAI, gemini* → Google).
OPENAI_API_KEY / GOOGLE_API_KEY (unset) Provider credentials. At least the one matching AI_DEFAULT_MODEL (and any model the UI offers) must be set.
EMBEDDING_MODEL text-embedding-3-small Embedding model (OpenAI).
LLM_MAX_RETRIES 6 Retries on a failing LLM call (rate-limit/transient).
AGENT_RECURSION_LIMIT 80 LangGraph step budget per turn (guards runaway tool loops).
PEECAI_API_KEY (unset) Credential for the Peec MCP server (the chart data source). Required for real charts.
PEECAI_PROJECT_ID (unset) Scopes MCP data to a project; required for project-scoped keys.
PEEC_API_URL https://api.peec.ai Base URL for the Peec MCP/API.
DATABASE_URL (compose-provided) Postgres connection (transcript, dashboards, checkpointer).
FIREBASE_SERVICE_ACCOUNT_BASE64 / FIREBASE_WEB_API_KEY (unset) Verifies user tokens on authenticated chat routes.
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL (unset) LLM tracing (optional).
PEEC_KNOWLEDGE_DIR / _SITEMAP_URL / _PATH_PREFIXES / _CACHE_TTL_SECONDS (unset) Read-only knowledge corpus mounted for the agent.
ENVIRONMENT / LOG_LEVEL local / info Environment label and log verbosity.

Back-end (server) (src/lib/env.ts; validated at boot):

Variable Purpose
AI_SERVICE_URL Base URL of ai-service (Compose sets http://ai-service:8000). The generative-dashboard upstream.
DATABASE_URL Postgres connection (Prisma).
FIREBASE_SERVICE_ACCOUNT_BASE64 Verifies user tokens (base64 service-account JSON).
CORS_ALLOWED_ORIGINS Allowed browser origins (required in prod).
SHUTDOWN_TIMEOUT_MS / SLOW_REQUEST_MS / LOG_LEVEL / PORT Graceful shutdown, slow-request logging, verbosity, port.

Web (build-time, baked into the bundle):

Variable Purpose
VITE_API_URL BFF base URL the SPA (and the SSE client) call.
VITE_APP_ENV Environment label (local / dev / prod).

2.4 Known issues & workarounds

# Issue Impact Workaround / fix
1 ai-service port 8000 published in local Compose. If copied to a real env, unauthenticated endpoints (/api/charts/resolve, /api/brands/metadata, /api/share/chat/stream) become directly reachable. Production/preview compose use expose: only; keep 8000 firewalled. Never carry the dev port mapping into prod.
2 No SSE heartbeat / idle-timeout; no client reconnect. A stalled LLM or MCP call can hang a turn indefinitely; a dropped socket ends the turn with no auto-retry. Add a heartbeat + bounded idle-timeout in sseProxy.ts; add a client retry affordance.
3 Rich chat history lives only in the browser (peec.chat.sessions.v1). Cache clear / device switch loses tool-call pills, multi-chart ordering, and "interrupted" turns; only flat text + one component per message is server-side. Persist structured turn parts server-side if durability is needed.
4 Auto-pin failures are swallowed. A chart renders in chat but silently fails to pin to the dashboard. Surface a non-blocking warning; log-and-alert on repeated failures.
5 Rate limits are per-instance, in-memory. Limits reset on restart and are not shared across replicas. Move to a shared/distributed store before scaling out.
6 PEECAI_API_KEY / PEECAI_PROJECT_ID unset disables live data. Charts cannot resolve real numbers; the assistant loses its grounding. Set both in every real environment.
7 Migrations gate readiness. If the checkpointer schema is not migrated, ai-service /health/ready fails and the agent will not run. Ensure prisma migrate deploy ran on server boot before expecting the assistant to work.
8 Static mock pages coexist with the real feature (/mydashboard, /overview, /prompts). Operators/users may mistake mock pages for live data. Treat only /chat + /dashboard + /share as the live generative-dashboard surface.
9 Preview deploy wipes the DB volume each run. Conversations and dashboards do not persist across preview deploys. Expected for throwaway previews; charts still resolve live.
10 Long turns hold a server proxy connection open. Many concurrent long turns tie up server connections. Monitor concurrent stream count; combine with the timeout fix (#2).

2.5 Third-party components

Component Type Used for Where configured
OpenAI External API LLM completions + embeddings OPENAI_API_KEY, AI_DEFAULT_MODEL, EMBEDDING_MODEL
Google (Gemini) External API Optional LLM provider (gemini* models) GOOGLE_API_KEY
Peec MCP server (mcp-server-peecai, api.peec.ai/mcp) External API (MCP) The live brand/visibility data every chart is grounded in PEECAI_API_KEY, PEECAI_PROJECT_ID, PEEC_API_URL
LangGraph / deepagents Python libraries The agent runtime, tool orchestration, and Postgres checkpointer apps/ai-service/app/llm
DuckDB Embedded engine Resolving chart data recipes (SQL over MCP results) app/llm/resolver.py
assistant-ui Frontend library The chat runtime/UI on the web app apps/web/src/features/assistant-chat
Langfuse External SaaS LLM trace/observability LANGFUSE_*
Firebase Authentication External SaaS (Google) User identity / token verification FIREBASE_SERVICE_ACCOUNT_BASE64
PostgreSQL 16 + pgvector Container image Transcript, dashboards, and agent checkpointer pgvector/pgvector:pg16, DATABASE_URL
FastAPI / Pydantic v2 / uv Python libraries Service framework + dependency manager apps/ai-service
Express 5 / Prisma Node libraries BFF + database access / migrations apps/server
Coolify Self-hosted PaaS Build & deploy orchestration on the host host
Traefik Reverse proxy Routing + TLS termination for public services provided by Coolify; labels in docker-compose.preview.yml
Let's Encrypt CA Automatic TLS certificates Traefik certresolver=letsencrypt
Docker / Docker Compose Runtime Containerization & orchestration all compose files
GitHub Actions CI/CD Preview deploys, migrations, checks .github/workflows/
Grafana + Loki + Promtail Observability stack Log aggregation/dashboards (platform-wide; Project B logs flow here) infra/ + compose

2.6 Quick operational reference

  • Health: GET http://ai-service:8000/health and /health/ready (internal; readiness also checks the checkpointer schema version).
  • Primary endpoints (reached by the browser through the server as POST /api/threads and POST /api/threads/{id}/message): POST /api/threads/message/stream (new thread) and POST /api/threads/{id}/message/stream (follow-up) on ai-service.
  • Chart refresh: POST /api/charts/resolve re-resolves a pinned chart's recipe against current data; the server falls back to the stored snapshot (returns 502) if the upstream fails.
  • OpenAPI/Swagger: FastAPI serves /openapi.json, /docs, /redoc on ai-service (internal); the server serves Swagger UI at /api-docs in non-production.
  • Logs: docker compose -p <project> logs -f ai-service (and server); aggregated in Grafana/Loki.
  • Restart safely: containers are stateless, but conversations and dashboards persist in Postgres, so back up the postgres-data volume. After a schema change, ensure migrations are applied before serving.
  • Full reference: the agent architecture guide is apps/ai-service/app/llm/AGENTS.md.