Přeskočit obsah

ai-chat — specifikace (LM Studio integrace + chat + per-app AI)

Status: shipped (MVP V1.0) Verze spec: 0.2 (M2M API key flow) Aktualizováno: 2026-05-14 Související: per-app-container.md, apps-gateway.md, auth-organization.md, chat.md (user-to-user, NEZAMĚŇOVAT)

1. Cíl

Integrace lokálních LLM modelů (LM Studio v LAN, případně i jiných OpenAI-compatible serverů) do AVAX platformy. Dvě úrovně použití:

  1. Platforma-wide asistent — uživatelé AVAX chatují s AI přímo z launcheru / webu (nápověda v platformě, obecné dotazy). Konverzace per user, persist v DB.
  2. Per-app speciální AI — každá AVAX aplikace si může deklarovat vlastního AI asistenta (system prompt + default model). App backend ho volá na behalf uživatele přes per-asistent API klíč (Bearer), dostává odpovědi.

Co MVP dělá: - CRUD AI serverů v admin webu (LM Studio instance, model defaults). - Per-user konverzace s SSE token streamingem. - Per-app asistenti (system prompt, default model, parametry) — apps backend je volá přes M2M. - Document upload do S3 (bez retrieval — schema pripravené pro V2 RAG).

Co MVP NEDĚLÁ (záměrně): - Retrieval (RAG) — embeddings/vector search řeší samostatná verze 2 (viz §9). - Token quotas / billing — žádný enforcement, jen tokens_in/out log (viz §8). - Multi-server load balancing — admin si vybere který server volá kdo, žádné automatické routing. - Function calling / tools — V1 jen text in, text out. - TLS na LM Studio — server běží v LAN, backend volá HTTP. Bezpečnost je síťová (LAN+JWT z core-api).

Disclaimer: Tento spec popisuje /ai/* endpointy v core-api. NEZAMĚŇOVAT s /chat/* (user-to-user chat mezi uživateli — viz chat.md).

2. Architektura

                ┌────────────────────────────────────────────┐
   Klient ───►  │   Launcher2 (chat panel) / Next.js web     │
                │   - SSE EventSource                        │
                │   - JWT Authorization                      │
                └────────────────┬───────────────────────────┘
                                 │ HTTPS /ai/*
                ┌────────────────────────────────────────────┐
                │   vm-gateway Nginx (TLS, proxy_buffering off) │
                └────────────────┬───────────────────────────┘
                ┌────────────────────────────────────────────┐
                │   core-api (FastAPI :8000)                 │
                │   - /ai/servers     (admin CRUD)           │
                │   - /ai/assistants  (per-app definice)     │
                │   - /ai/conversations + messages           │
                │   - /ai/documents   (upload, V2 retrieval) │
                │   - /ai/chat        (M2M pro apps)         │
                └─────────────┬──────────────────┬───────────┘
                              │ DB               │ HTTP /v1/*
                              ▼                  ▼
                ┌────────────────────┐   ┌─────────────────────┐
                │  PostgreSQL        │   │  LM Studio          │
                │  ai_* tabulky      │   │  192.168.1.167:1234 │
                └────────────────────┘   │  (LAN, OpenAI API)  │
                                         └─────────────────────┘
                                            ▲          ▲
                                            │          │ (další serverry —
                                            │          │  192.168.1.x:1234,
                                            │          │  registr v ai_servers)

Klíčové vlastnosti: - Stateless vůči LM Studio — žádný proxy state. Konverzační historie je v PG (ai_messages), backend ji při každém request-u skládá do messages pole OpenAI requestu. - SSE streaming — backend čte z LM Studio streaming responu a forwarduje tokeny klientovi přes SSE. Při dokončení response uloží assistant message do DB. - Per-app AI — apps mají per-asistent API klíč v .env. Volání POST /ai/chat s Authorization: Bearer <key> na behalf uživatele (on_behalf_of_user_id v body). - Žádné LLM v core-api — backend je jen proxy + persistence + auth gate. Veškeré inference děje LM Studio.

3. Datový model

3.1 ai_servers — registr LLM endpointů

Sloupec Typ Účel
id UUID PK
name TEXT NOT NULL „LM Studio doma", „LM Studio kancl GPU"
base_url TEXT NOT NULL http://192.168.1.167:1234/v1 (bez trailing slash)
api_key TEXT NULL Pokud server vyžaduje (LM Studio default neřeší)
default_model TEXT NULL Model identifier který server preferuje (llama-3.1-8b-instruct apod.)
capabilities JSONB DEFAULT '{}' {"general": true, "code": true, "embeddings": false} — pro V2 routing
is_active BOOL DEFAULT true False = server skryt z UI, ale data zůstávají
created_at TIMESTAMP DEFAULT now()
updated_at TIMESTAMP Set v backend update endpointu

Konvence: max 1 server s is_active=true a default_model setnutý je „platform default" pro nové konverzace pokud klient nepošle server_id.

3.2 ai_assistants — predefined asistenti (per-app + platform-wide)

Sloupec Typ Účel
id UUID PK
slug TEXT UNIQUE NOT NULL platform-helper, legal-asistent, mzdy-asistent
name TEXT NOT NULL UI label
description TEXT NULL Co asistent umí (zobrazí se v UI dropdown)
app_id UUID FK apps(id) NULL NULL = platform-wide, jinak per-app
system_prompt TEXT NOT NULL Vždy injektován jako první message s rolí system
default_server_id UUID FK ai_servers(id) NULL Preference; pokud NULL fallback na platform default
default_model TEXT NULL Override pro tento asistent (jinak server default)
default_temperature REAL DEFAULT 0.7 Klient může overridenout per konverzace
default_max_tokens INT DEFAULT 2048
is_active BOOL DEFAULT true
api_key_hash TEXT NULL SHA-256 hex M2M klíče (migrace 022). Plain text NIKDY v DB.
api_key_prefix VARCHAR(12) NULL Prvních 12 znaků klíče (avx_xxxxxxxx) — UI preview
api_key_created_at TIMESTAMPTZ NULL Kdy byl klíč vystaven
created_at TIMESTAMP DEFAULT now()

Index: (app_id), (slug) UNIQUE.

3.3 ai_conversations — uživatelovy konverzace

Sloupec Typ Účel
id UUID PK
user_id UUID FK users(id) NOT NULL
assistant_id UUID FK ai_assistants(id) NULL NULL = ad-hoc konverzace bez assistanta
server_id UUID FK ai_servers(id) NOT NULL Pin na konkrétní server (immutable po create)
model TEXT NOT NULL Model name (kopie z assistant/server v čase create)
system_prompt TEXT NULL Kopie z assistant.system_prompt nebo custom
temperature REAL DEFAULT 0.7
max_tokens INT DEFAULT 2048
title TEXT NULL Auto-fill z první user message (prvních 60 chars) nebo manual
app_id UUID FK apps(id) NULL Pokud konverzace iniciovaná z appky
created_at TIMESTAMP DEFAULT now()
updated_at TIMESTAMP Set při každém append message

Indexy: (user_id, updated_at DESC) pro list, (app_id) pro per-app filter.

Cascade: delete user → delete conversations → delete messages (CASCADE).

3.4 ai_messages — single message v konverzaci

Sloupec Typ Účel
id UUID PK
conversation_id UUID FK ai_conversations(id) NOT NULL ON DELETE CASCADE
role TEXT NOT NULL CHECK (role IN ('system','user','assistant','tool'))
content TEXT NOT NULL
tokens_in INT NULL LM Studio usage.prompt_tokens (jen u assistant msg)
tokens_out INT NULL LM Studio usage.completion_tokens
latency_ms INT NULL Celková doba LLM call-u (jen u assistant msg)
error TEXT NULL Pokud LLM call selhal — zachytí důvod, message zůstane (idempotence retry)
created_at TIMESTAMP DEFAULT now()

Index: (conversation_id, created_at ASC) pro chronologické čtení.

3.5 ai_documents — uploadované docs (V1 storage, V2 retrieval)

Sloupec Typ Účel
id UUID PK
owner_user_id UUID FK users(id) NOT NULL
company_id UUID FK companies(id) NULL Pokud sdíleno v rámci firmy
filename TEXT NOT NULL Original název (sanitizovaný)
mime_type TEXT NOT NULL application/pdf, text/markdown, text/plain
size_bytes BIGINT NOT NULL
s3_key TEXT NOT NULL Cesta v S3 (viz §3.6)
sha256 TEXT NOT NULL Pro dedup
status TEXT DEFAULT 'uploaded' CHECK (status IN ('uploaded','processing','indexed','failed')) V2 přechody: uploaded→processing→indexed
error TEXT NULL
created_at TIMESTAMP DEFAULT now()

Index: (owner_user_id), (company_id), (sha256) pro dedup.

V2 budoucí tabulky (zatím nemigrujeme, jen referenční): - ai_chunks — chunked text + embedding vector (vector(1536) přes pgvector) - ai_conversation_documents — N:M napojení document → conversation (které docs jsou v kontextu konverzace)

3.6 S3 prefix struktura

backup-<ico>/ai-docs/<user_id>/<sha256>/<filename>     ← per-firma + per-user, content-addressed
backup-system/ai-shared/<doc_id>/<filename>            ← platform-wide knowledge (super_admin)

S3 klíče (admin / per-firma) přes existující s3_app_keys pool s key_type='ai_docs'.

4. Backend API (/ai/* v core-api)

Všechny endpointy vyžadují JWT (kromě interních M2M flow popsaných v §6).

4.1 Admin — servery (/ai/servers/*)

Vyžaduje super_admin.

Endpoint Účel
GET /ai/servers List všech serverů
POST /ai/servers Body: {name, base_url, api_key?, default_model?, capabilities?}
GET /ai/servers/{id} Detail
PUT /ai/servers/{id} Update (partial)
DELETE /ai/servers/{id} Soft-delete: is_active=false (hard delete jen pokud žádná konverzace nereferencuje)
GET /ai/servers/{id}/models Proxy na <base_url>/models — vrací list dostupných modelů (cache 60s)
POST /ai/servers/{id}/ping Test connectivity — GET <base_url>/models s timeout 5s. Vrací {ok, latency_ms, error?}

4.2 Admin — asistenti (/ai/assistants/*)

Vyžaduje super_admin (V1; V2 možno povolit company_admin pro per-firma asistenty).

Endpoint Účel
GET /ai/assistants List; query ?app_id=... filtr
POST /ai/assistants Body: {slug, name, description?, app_id?, system_prompt, default_server_id?, default_model?, default_temperature?, default_max_tokens?}
GET /ai/assistants/{id} Detail
PUT /ai/assistants/{id} Update
DELETE /ai/assistants/{id} Soft-delete

4.3 User — konverzace (/ai/conversations/*)

Vyžaduje JWT (jakýkoli přihlášený user).

Endpoint Účel
GET /ai/conversations List uživatelovy konverzace, sort updated_at DESC, paginace ?limit=20&offset=0
POST /ai/conversations Body: {assistant_id?, server_id?, model?, system_prompt?, temperature?, max_tokens?, title?, app_id?}. Pokud assistant_id set, ostatní fields se vyplní z asistenta + možno overridenout. Pokud nic → platform default server + default model.
GET /ai/conversations/{id} Detail s metadaty (bez messages)
GET /ai/conversations/{id}/messages Plná historie, ?limit=50&before_id=... pagace
POST /ai/conversations/{id}/messages SSE endpoint! Body: {content} (user message). Server: 1) uloží user message do DB, 2) skládá messages do OpenAI requestu, 3) volá LM Studio streaming, 4) forward-uje tokeny přes SSE klientovi, 5) na konci uloží assistant message s tokens_in/out a latency_ms. SSE events: event: token, event: done, event: error.
PUT /ai/conversations/{id} Update title (jen)
DELETE /ai/conversations/{id} CASCADE smaže messages

SSE message flow:

POST /ai/conversations/<id>/messages
Body: {"content": "Ahoj"}

Response: text/event-stream
event: message_id
data: {"user_message_id": "uuid1", "assistant_message_id": "uuid2"}

event: token
data: {"delta": "Ahoj"}

event: token
data: {"delta": "! Jak"}

event: token
data: {"delta": " ti mohu pomoct?"}

event: done
data: {"tokens_in": 12, "tokens_out": 8, "latency_ms": 1340}

Při error mid-stream:

event: error
data: {"detail": "LM Studio timeout", "code": "upstream_timeout"}

4.4 Documents (/ai/documents/*)

V1.5: storage (S3 + DB) hotové. V2: indexace + retrieval.

Bucket: legacy claudeai (settings.S3_BUCKET) s prefixem ai-docs/<user_id>/<sha256>/<filename>. Per-firma isolation (backup-<ico>/ai-docs/...) je V2.

Mime whitelist V1.5: application/pdf, text/plain, text/markdown, MS Word (doc + docx), text/csv. Max velikost 100 MB.

Endpoint Účel
GET /ai/documents List uživatelovy dokumenty + sdílené firmou
POST /ai/documents/upload-url Body: {filename, mime_type, size_bytes}. Vrátí pre-signed PUT URL pro přímý S3 upload. Po uploadu klient volá /finalize.
POST /ai/documents/finalize Body: {filename, mime_type, size_bytes, sha256, s3_key}. Backend ověří v S3 (HEAD) a vytvoří ai_documents row se statusem uploaded. V2: trigger indexace.
GET /ai/documents/{id} Detail metadat
GET /ai/documents/{id}/download-url Pre-signed GET URL pro stažení
DELETE /ai/documents/{id} Smaže DB row + S3 object (GC asynchronně, viz garbage-collector.md)

4.5 M2M pro per-app AI (/ai/chat)

Apps backend volá s per-asistent API klíčem v Authorization: Bearer headeru. Klíč je vystavený super_adminem v admin UI (záložka AI → Asistenti → konkrétní asistent → API klíč → Vygenerovat) a klient ho dostane v plain textu jen jednou (pak je v DB jen SHA-256 hash).

API key management (super_admin endpointy)

Endpoint Účel
GET /ai/assistants/{id}/api-key Status: {has_key, prefix, created_at} — bez plain textu
POST /ai/assistants/{id}/api-key/rotate Vygeneruje nový klíč → vrátí {api_key, prefix, created_at}. Existující klíč se přepíše. Plain text je v response JEN tady.
DELETE /ai/assistants/{id}/api-key Zneplatní klíč (asistent přestane být přístupný přes /ai/chat)

Formát klíče: avx_<token_urlsafe_32> (~47 znaků). Prefix v UI = prvních 12 chars. Storage: SHA-256 hex v ai_assistants.api_key_hash, indexovaný (unique partial WHERE NOT NULL) pro O(1) lookup při každém M2M requestu.

M2M endpoint

Endpoint Účel
POST /ai/chat Bearer API key + body s on_behalf_of_user_id, messages, volitelně conversation_id a stream.

Body schema:

{
  "on_behalf_of_user_id": "uuid-uzivatele",  // povinný — backend ověří že existuje + is_active
  "messages": [
    {"role": "user", "content": "Otázka uživatele"}
  ],
  "conversation_id": "uuid-konverzace",  // volitelný — pokud chybí, vytvoří se nová
  "stream": false,                        // true = SSE response, false = JSON
  "temperature": 0.5,                     // volitelný override default asistenta
  "max_tokens": 1024                      // volitelný override
}

messages mohou obsahovat jen role user — assistant výstupy si backend řídí sám (single source of truth). System prompt se bere automaticky z asistenta, klient ho neposílá.

Flow při requestu: 1. Backend ověří Bearer key → SHA-256 hash lookup → najde AiAssistant (musí být is_active=true) 2. Ověří on_behalf_of_user_id (existuje + aktivní) 3. Pokud conversation_id chybí, vytvoří novou: - user_id = on_behalf_of_user_id - assistant_id = nalezený asistent - server_id = assistant.default_server_id nebo první aktivní server - model = assistant.default_model nebo server.default_model - system_prompt = assistant.system_prompt - app_id = assistant.app_id (může být null pro platform-wide) - temperature, max_tokens z asistenta (nebo override z body) 4. Pokud conversation_id je daný, ověří že patří on_behalf_of_user_id (anti-spoofing) 5. Persistuje user messages → načte history → volá LM Studio 6. Streaming: SSE event: conversation (s conversation_id + assistant_message_id), event: token (delta), event: done (usage), event: error (detail) 7. Non-streaming: JSON {conversation_id, message: {role, content}, usage: {tokens_in, tokens_out, latency_ms}}

SSE flow pro stream=true:

event: conversation
data: {"conversation_id":"<uuid>","assistant_message_id":"<uuid>"}

event: token
data: {"delta":"Ahoj"}

event: token
data: {"delta":"!"}

event: done
data: {"tokens_in":42,"tokens_out":8,"latency_ms":850}

Bezpečnost: - Klíč v plain textu existuje jen v paměti klienta + jeho .env. Nikdy v DB / logu / git. - Klient (apps backend) ho NESMÍ předávat dál (do prohlížeče, do logu, …). Žije v server-side .env v kontejneru aplikace. - app_id cross-check se NEDĚLÁ — klíč sám o sobě vymezuje co lze. Pokud admin nechce aby asistent legal-asistent byl volaný z appky mzdy, prostě každé appce dá vlastního asistenta s vlastním klíčem. - Konverzace je vždy přiřazená konkrétnímu user. Uživatel ji vidí ve své history v launcheru (filtruje ?app_id=<X> pro per-app pohled).

5. Klient

5.1 Launcher2 (Python customtkinter)

Nový panel AI chat v levním rail nebo jako odkaz z Aplikace → ai assistant.

Komponenty: - Seznam konverzací (levá kolona) — GET /ai/conversations - Aktivní konverzace (středová kolona) — messages + input - Asistent selector (nahoře nové konverzace) — dropdown GET /ai/assistants (filtr platform-wide + apps na které user má assignment) - Server/model selector (gear icon) — GET /ai/servers + GET /ai/servers/{id}/models - SSE consumer — používá httpx async + parsuje text/event-stream (nebo sseclient-py package)

Stack: - Existující desktop/launcher2/ Python codebase - Streaming přes httpx.AsyncClient.stream("POST", ..., headers={"Accept": "text/event-stream"}) - Render token-by-token do customtkinter.CTkTextbox (append na konec)

5.2 Web (Next.js admin)

Top-level tab AI v /admin/ai: - /admin/ai/servers — CRUD tabulka (name, base_url, default_model, status, akce: edit/delete/ping/test) - /admin/ai/assistants — CRUD tabulka (slug, name, app, server, model, system prompt preview) - /admin/ai/chat — testovací chat UI (super_admin) pro ověření funkčnosti serveru/asistenta - /admin/ai/usage (V2) — token statistics

Komponenty: - EventSource API pro SSE (browser native) - Server registry: form + table, ping button → toast (latency) - Chat UI: jednoduchá messages list + input, stream renderer

User-facing chat (mimo admin) — fáze 1.5: stránka /chat v Next.js public app (vyžaduje user login). Reuse komponenta z admin.

6. Per-app AI flow (V1)

Příklad: vendor staví avax-legal (právní app). Chce, aby uživatel uvnitř appky mohl chatovat s asistentem znalým českého práva.

6.1 Setup (super_admin přes admin web)

  1. Vytvořit asistenta v admin UI (launcher2 → AVAX Admin → 🤖 AI → Asistenti → + Nový asistent):
  2. slug: legal-asistent (unikátní, immutable)
  3. name: „Právní asistent"
  4. app_id: <uuid avax-legal> (pro per-app; nebo prázdné pro platform-wide)
  5. system_prompt: „Jsi expert na české právo…"
  6. default_server: dropdown — vybere LM Studio (LAN)
  7. default_model: dropdown — vybere llama-3.1-70b-instruct (nebo qwen3.6-27b)
  8. temperature: 0.3 (právní fakta → nízký creative)
  9. max_tokens: 4096

  10. Vygenerovat API klíč (v editaci asistenta → sekce 🔑 API klíč → Vygenerovat):

  11. Klíč ve formátu avx_<random> se zobrazí v žluté boxu jen jednou
  12. Kliknout 📋 Kopírovat a uložit do .env appky:

    AVAX_AI_KEY=avx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    AVAX_CORE_API_URL=https://api.avaxis.cz
    

  13. Při ztrátě klíče → znovu Vygenerovat nový (přepíše předchozí — .env appky musí být aktualizován). Zneplatnit odstraní klíč úplně (asistent přestane být přístupný přes /ai/chat).

6.2 Klient — runtime (uvnitř app kontejneru)

App backend implementuje /legal/ask (nebo cokoli):

# avax-legal backend/app/services/ai_proxy.py
import httpx
from app.core.config import settings

async def ask_legal(question: str, user_id: str,
                    conversation_id: str | None = None) -> tuple[str, str]:
    """Vrátí (assistant_content, conversation_id)."""
    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.post(
            f"{settings.AVAX_CORE_API_URL}/ai/chat",
            headers={"Authorization": f"Bearer {settings.AVAX_AI_KEY}"},
            json={
                "on_behalf_of_user_id": user_id,
                "messages": [{"role": "user", "content": question}],
                "conversation_id": conversation_id,  # None = nová
                "stream": False,
            },
        )
        resp.raise_for_status()
        data = resp.json()
        return data["message"]["content"], data["conversation_id"]


# avax-legal backend/app/routers/legal.py
@router.post("/ask")
async def ask(body: AskRequest, user = Depends(get_user)):
    content, conv_id = await ask_legal(
        body.question, str(user.id), body.conversation_id,
    )
    return {"answer": content, "conversation_id": conv_id}

Streaming variantstream=True, response je text/event-stream. App backend buď proxy-uje SSE klientovi přímo (přes apps-gateway s proxy_buffering off), nebo si stream sám consumuje a vrací výsledek až po event: done.

6.3 Conversation continuity

Klient appky (frontend nebo backend session) drží conversation_id v lokálním stavu (localStorage / DB row). Při dalších volání pošle stejný conversation_id — backend appendne novou user message do ai_messages, načte celou history (incl. system prompt z asistenta) a zavolá LLM. Uživatel pak v launcheru → Podpora → AI → 📚 Historie uvidí tu samou konverzaci (filtruje se per-app dropdown podle app_id).

7. Bezpečnost a autorizace

7.1 Auth gates

Endpoint Auth
/ai/servers/* super_admin
/ai/assistants/* (CRUD) super_admin
/ai/assistants (list) Jakýkoli přihlášený user (read-only)
/ai/conversations/* Jakýkoli přihlášený user, scoped na user_id
/ai/documents/* Jakýkoli přihlášený user, scoped na owner_user_id nebo company sharing
/ai/chat (M2M) Per-asistent API key (Bearer) — SHA-256 hash lookup
/ai/assistants/{id}/api-key/* super_admin

7.2 LM Studio přístup

  • LM Studio běží bez auth v LAN.
  • core-api volá přes HTTP (TLS není nutné v LAN, neukládají se tam tajné údaje).
  • Firewall: LM Studio port 1234 dostupný jen z avaxdev (iptables -A INPUT -p tcp --dport 1234 -s 192.168.1.55 -j ACCEPT; iptables -A INPUT -p tcp --dport 1234 -j DROP) — TODO operations.

7.3 Prompt injection a content safety

  • V1 bez ochrany proti prompt injection (model dostane vše).
  • System prompt je vždy první message, pak history + new user message.
  • Klient může poslat libovolný text v content — backend nevaliduje obsah (jen délku).
  • Doporučení do V2: content filter (regex / mini-classifier) + rate limit per user.

7.4 PII a logging

  • Plné texty zpráv jsou v DB (ai_messages.content) — citlivé. Backup/GC pravidla viz garbage-collector.md.
  • Strukturovaný log loguje jen metadata (conversation_id, user_id, model, tokens_in/out, latency, error). NIKDY content.

8. Telemetrie a logging

Backend app/services/ai.py loguje (JSON):

{
  "ts": "2026-05-15T10:23:45Z",
  "level": "INFO",
  "logger": "app.services.ai",
  "msg": "llm_call",
  "user_id": "uuid",
  "conversation_id": "uuid",
  "server_id": "uuid",
  "model": "llama-3.1-8b-instruct",
  "tokens_in": 245,
  "tokens_out": 89,
  "latency_ms": 1340,
  "status": "ok"
}

Metriky (Prometheus, expose v core-api /metrics): - ai_requests_total{server, model, status} counter - ai_tokens_total{server, direction=in|out} counter - ai_latency_seconds{server, model} histogram

9. RAG roadmap (V2)

Po MVP doplnit:

  1. pgvector extension v PG + migrace s ai_chunks tabulkou (vector(1536) column).
  2. Embedding pipeline — Celery task triggernutý při ai_documents.status='uploaded'. Volá embedding model (LM Studio má embedding endpoint nebo separátní server s nomic-embed-text).
  3. Chunking strategy — markdown/text splitter (1000 chars overlap 200), per chunk embedding.
  4. Retrieval — při user message volat SELECT ... ORDER BY embedding <-> :query_embedding LIMIT 5 + prepend top chunks jako system context.
  5. ai_conversation_documents napojení — uživatel přidá doc do konverzace, retrieval scopuje jen na ně.
  6. Citations — assistant message má sources JSONB s [{document_id, chunk_id, score}, ...].

Separátní spec: docs/spec/ai-rag.md (TBD).

10. Acceptance criteria

MVP (V1.0) — SHIPPED 2026-05-14

Backend: - [x] Alembic migrace 021_ai_chat.py — 5 tabulek - [x] Alembic migrace 022_ai_assistant_api_key.py — api_key_hash + prefix + created_at - [x] app/routers/ai.py — všechny endpointy z §4 - [x] app/services/ai.py — LM Studio client (httpx async), SSE forwarder - [x] Per-asistent API key (rotate/revoke) + /ai/chat M2M (stream + non-stream) - [x] Smoke test: tests/test_ai_chat.py — 6/6 pass

Admin UI (launcher2 AvaxAdminScreen): - [x] Tab 🤖 AI → sub-tab Servery: list + CRUD + Ping + Modely - [x] Tab 🤖 AI → sub-tab Asistenti: list + CRUD + API key rotate/revoke - [x] Model dropdown v asistent dialogu (z /ai/servers/{id}/models)

Launcher2 — uživatel: - [x] AI chat v Podpora → AI sekce - [x] SSE streaming render - [x] 📎 Document attach (txt/md/csv/pdf/docx → inline LLM context) - [x] 📚 Historie konverzací (per-app filter, klik = pokračovat, ✕ smazat)

Deploy: - [x] LM Studio 192.168.1.167:1234 dostupný z avaxdev - [x] Server zaregistrovaný v ai_servers - [x] Default platform-helper asistent vytvořený

V1.5 — open

  • avax-legal pilot — první vendor app integruje /ai/chat přes API key
  • Title auto-generation — z první user message (prefix 60 chars)
  • Conversation context overflow — když historie > model context window, truncate

V2 — RAG

  • Viz §9 + samostatný docs/spec/ai-rag.md

11. Otevřené otázky

  • Title auto-generation — generovat title z první user message přes LLM (separátní krátký call) nebo jen prefix? Návrh: prefix (60 chars), edit-uje uživatel.
  • Konverzace dlouhé než context window — strategie: truncate od začátku, summarize, refuse? Návrh V1: truncate (oldest first) + warning v response header.
  • API key TTL / expirace — V1: klíče bez expirace, jen ruční rotate. V2 zvážit auto-expire (90d?) + warning v admin UI.
  • Per-firma vs global asistent — V1 jen platform-wide + per-app. Per-firma (company_admin si dělá vlastní system prompt) až ve V2?
  • Multi-model konverzace — uživatel mění model uprostřed konverzace — povolit? Návrh V1: ne (model je pin pri create), V2 možno změnit (vede k „nezavešení" historie).
  • LM Studio offline handling — circuit breaker per server (5 fail v řadě → mark is_active=false na 5min, frontend dostane 503 s retry-after)? Nebo jen pass-through error?
  • Streaming chunking — SSE event per token nebo per N tokenů (latency vs throughput)? Návrh: per token (LM Studio sám buffruje).
  • Konverzace export — JSON / Markdown export endpoint pro user? Návrh V2.
  • Konverzace search — full-text search v messages (PG tsvector)? Návrh V2.

12. Timeline a status

Fáze Status Datum
Spec review ✓ done 2026-05-14
Migrace + backend skelet ✓ done 2026-05-14
LM Studio client + SSE ✓ done 2026-05-14
Admin UI (launcher2 AI tab) ✓ done 2026-05-14
Launcher2 user AI panel + documents + history ✓ done 2026-05-14
Per-app M2M API key + /ai/chat ✓ done 2026-05-14
MVP shipped ✓ done 2026-05-14
avax-legal pilot (vendor coordination) ⏳ open
RAG V2 spec start ⏳ open

13. Související

  • per-app-container.md — per-app architektura, container isolation, JWT validace (M2M tokens v per-app-container.md se týkají widget core-api volání; pro AI je tu vlastní API key flow popsaný v §4.5)
  • apps-gateway.mdproxy_buffering off pro SSE
  • auth-organization.md — JWT scopes, role gates
  • chat.mduser-to-user chat (NEZAMĚŇOVAT s tímto)
  • s3-architecture.md — ukládání ai_documents
  • garbage-collector.md — cleanup orphaned ai_documents S3 objektů
  • docs/spec/ai-rag.mdTBD (RAG retrieval V2)