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í:
- 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.
- 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:
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)¶
- Vytvořit asistenta v admin UI (launcher2 → AVAX Admin → 🤖 AI → Asistenti → + Nový asistent):
slug:legal-asistent(unikátní, immutable)name: „Právní asistent"app_id:<uuid avax-legal>(pro per-app; nebo prázdné pro platform-wide)system_prompt: „Jsi expert na české právo…"default_server: dropdown — vybere LM Studio (LAN)default_model: dropdown — vyberellama-3.1-70b-instruct(neboqwen3.6-27b)temperature: 0.3 (právní fakta → nízký creative)-
max_tokens: 4096 -
Vygenerovat API klíč (v editaci asistenta → sekce 🔑 API klíč → Vygenerovat):
- Klíč ve formátu
avx_<random>se zobrazí v žluté boxu jen jednou -
Kliknout 📋 Kopírovat a uložit do
.envappky: -
Při ztrátě klíče → znovu Vygenerovat nový (přepíše předchozí —
.envappky 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 variant — stream=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 vizgarbage-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:
- pgvector extension v PG + migrace s
ai_chunkstabulkou (vector(1536)column). - Embedding pipeline — Celery task triggernutý při
ai_documents.status='uploaded'. Volá embedding model (LM Studio má embedding endpoint nebo separátní server snomic-embed-text). - Chunking strategy — markdown/text splitter (1000 chars overlap 200), per chunk embedding.
- Retrieval — při user message volat
SELECT ... ORDER BY embedding <-> :query_embedding LIMIT 5+ prepend top chunks jako system context. ai_conversation_documentsnapojení — uživatel přidá doc do konverzace, retrieval scopuje jen na ně.- Citations — assistant message má
sourcesJSONB 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/chatpř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=falsena 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 vper-app-container.mdse týkají widget core-api volání; pro AI je tu vlastní API key flow popsaný v §4.5)apps-gateway.md—proxy_buffering offpro SSEauth-organization.md— JWT scopes, role gateschat.md— user-to-user chat (NEZAMĚŇOVAT s tímto)s3-architecture.md— ukládání ai_documentsgarbage-collector.md— cleanup orphaned ai_documents S3 objektůdocs/spec/ai-rag.md— TBD (RAG retrieval V2)