synops/docs/infra/jobbkø.md
vegard 63f022d739 AI-foreslåtte edges: LLM-analyse av innhold → topics og mentions (oppgave 10.2)
Ny jobbtype `suggest_edges` som automatisk trigges ved opprettelse av
content-noder med tilstrekkelig tekst (≥20 tegn). Sender innholdet til
LiteLLM (sidelinja/rutine) via AI Gateway, parser JSON-respons med
topics og mentions, og oppretter topic-noder + mentions-edges i grafen.

Flyten:
1. create_node oppdager content-node med nok tekst → enqueue suggest_edges
2. Worker henter node-innhold og eksisterende topics fra PG
3. LLM analyserer tekst og returnerer foreslåtte topics/mentions
4. Nye topic-noder opprettes (med ai_generated-flagg i metadata)
5. mentions-edges opprettes fra innholdsnode til topic/entitet-noder
6. Deduplisering: gjenbruker eksisterende topics ved case-insensitivt match

Filer:
- maskinrommet/src/ai_edges.rs: Ny modul med LLM-kall og edge-opprettelse
- maskinrommet/src/jobs.rs: suggest_edges registrert i dispatcher
- maskinrommet/src/intentions.rs: Trigger i create_node
- docs/: Oppdatert jobbkø og AI gateway-docs med ny jobbtype

NB: Krever gyldig API-nøkkel i LiteLLM (OpenRouter/Gemini/Anthropic).
Jobben feiler gracefully med retry+backoff ved manglende nøkkel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:24:29 +00:00

140 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Infrastruktur: Jobbkø (PostgreSQL-basert)
**Filsti:** `docs/infra/jobbkø.md`
## 1. Konsept
Et felles, sentralisert køsystem for alle asynkrone bakgrunnsjobber i Sidelinja. Bygget som en enkel tabell i PostgreSQL med Rust-workers som konsumerer jobber. Ingen ekstern message broker — PostgreSQL er køen.
## 2. Hvorfor PostgreSQL?
- Allerede i stacken, ingen ny infrastruktur å drifte
- Transaksjonell garanti: jobben og resultatet kan committes sammen med dataendringer
- Lavt volum (titalls jobber/time) gjør polling neglisjerbart
- Enkel feilsøking via SQL (`SELECT * FROM job_queue WHERE status = 'error'`)
- `SELECT ... FOR UPDATE SKIP LOCKED` gir trygg concurrent polling uten låsekonflikt
## 3. Datastruktur
```sql
CREATE TYPE job_status AS ENUM ('pending', 'running', 'completed', 'error', 'retry');
CREATE TABLE job_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
collection_node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, -- Samlings-node jobben tilhører
job_type TEXT NOT NULL, -- 'whisper_transcribe', 'openrouter_analyze', 'stats_parse', 'research_clip'
payload JSONB NOT NULL, -- Inputdata (filsti, tekst, tema_id, etc.)
status job_status NOT NULL DEFAULT 'pending',
priority SMALLINT NOT NULL DEFAULT 0, -- Høyere = viktigere
result JSONB, -- Resultatet ved fullført jobb
error_msg TEXT, -- Feilmelding ved error
attempts SMALLINT NOT NULL DEFAULT 0,
max_attempts SMALLINT NOT NULL DEFAULT 3,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
scheduled_for TIMESTAMPTZ NOT NULL DEFAULT now() -- For utsatte jobber / retry med backoff
);
CREATE INDEX idx_job_queue_pending ON job_queue (priority DESC, scheduled_for ASC)
WHERE status IN ('pending', 'retry');
```
## 4. Worker-arkitektur (Rust)
### 4.1 Designprinsipp: Orkestrator, ikke prosesseringsmotor
Workeren gjør lite tung prosessering selv. Den er en **orkestrator** som koordinerer eksterne tjenester:
| Jobbtype | Hva workeren gjør | Tung logikk i workeren? |
|---|---|---|
| `whisper_transcribe` | HTTP-kall til faster-whisper-server (SRT), parse → `transcription_segments` | Lett SRT-parsing |
| `openrouter_analyze` | HTTP-kall til AI Gateway | Nei — venter på svar |
| `stats_parse` | Parser Caddy-loggfiler, skriver til PG | Lett I/O |
| `research_clip` | HTTP-kall til AI Gateway | Nei — venter på svar |
| `generate_embeddings` | HTTP-kall til AI Gateway | Nei — venter på svar |
Ny jobbtype = ny handler-funksjon (bygg request, håndter respons, feilhåndtering). Tynt glue-code. Rekompilering er triviell og inkrementell.
### 4.2 Én worker, prioritetsstyrt
Én enkelt worker-prosess håndterer **alle jobbtyper**. Prioritering skjer via `priority`-kolonnen i køen — SQL-spørringen plukker alltid viktigste jobb først. Ingen behov for separate prosesser per jobbtype.
```
┌──────────────────────────────────────────────────┐
│ Rust Worker (sidelinja-worker) │
│ │
│ Konfigurasjon: │
│ --max-concurrent 3 (samtidige jobber) │
│ --poll-interval 1s │
│ │
│ Loop (per ledig slot): │
│ 1. SELECT ... FOR UPDATE SKIP LOCKED │
│ WHERE status IN ('pending','retry') │
│ AND scheduled_for <= now() │
│ ORDER BY priority DESC, scheduled_for │
│ LIMIT 1 │
│ │
│ 2. UPDATE status = 'running' │
│ 3. Dispatch til handler basert på job_type │
│ 4a. OK: UPDATE status = 'completed' │
│ 4b. Feil: attempts += 1 │
│ Hvis attempts < max_attempts: │
│ status = 'retry' │
│ scheduled_for = now() │
│ + backoff(attempts) │
│ Ellers: status = 'error' │
│ │
└──────────────────────────────────────────────────┘
```
### 4.3 Prioritetsmodell
| Prioritet | Kategori | Eksempler |
|---|---|---|
| 10 | Brukerrettet / sanntid | `dictation_cleanup`, `research_clip` |
| 5 | Normal | `whisper_transcribe`, `openrouter_analyze`, `srt_parse` |
| 1 | Bakgrunn | `stats_parse`, `generate_embeddings`, `prompt_eval` |
Verdiene er veiledende — SvelteKit setter prioritet ved opprettelse basert på kontekst. En manuelt trigget transkripsjon kan få høyere prioritet enn en automatisk nattjobb.
### 4.4 Ressursstyring
* **Concurrency:** `--max-concurrent` begrenser antall samtidige jobber. Default 3 — passer for 8 vCPU der noen slots er Whisper (CPU-tung) og resten er HTTP-kall (ventetid).
* **Resource Governor (Whisper):** Når et LiveKit-rom er aktivt, reduserer workeren Whisper-tråder (`--threads 2` i HTTP-kall til faster-whisper) for å beskytte lydkvaliteten. Sjekkes via LiveKit room-status før Whisper-kall.
* **Skalering senere:** To nivåer:
1. **Worker-splitting:** Workeren splittes til to binærer fra samme crate (`worker-heavy`, `worker-light`) via CLI-argument (`--types whisper_transcribe,openrouter_analyze`). Ingen kodeendring nødvendig — kun deploy-konfigurasjon.
2. **Compute-separasjon:** Flytt Rust-worker + faster-whisper til en separat Hetzner-node (evt. ARM/Ampere for pris/ytelse). LiveKit er ekstremt sensitivt for CPU-stotring — ved samtidig WebRTC og Whisper på samme maskin risikerer vi audio glitches uansett cgroups. Worker-noden poller jobbkøen i PostgreSQL over internt nettverk — arkitekturen støtter dette uten kodeendring.
**Backoff-strategi:** Eksponentiell: `30s × 2^(attempts-1)` (30s, 60s, 120s).
## 5. Jobbtyper
| `job_type` | Konsument | Beskrivelse |
|---|---|---|
| `whisper_transcribe` | Universell lyd-tjeneste | Transkriber lydfil via faster-whisper (SRT) → `transcription_segments` |
| `openrouter_analyze` | Podcastfabrikken | Metadata-uttrekk fra transkripsjon |
| `ai_text_process` | Editor (AI-knapp) | Rens, oppsummer, trekk ut fakta, skriv om (se `docs/proposals/editor.md`) |
| `stats_parse` | Podcast-Statistikk | Batch-prosesser Caddy-logger |
| `meeting_summarize` | Møterommet | Generer møtereferat og action points fra transkripsjon |
| `valgomat_generate_profile` | Valgomat | Generer syntetiske kandidatprofiler fra partiprogrammer |
| `valgomat_moderation` | Valgomat | Semantisk deduplisering og nøytralitetsvask av brukerspørsmål |
| `dictation_cleanup` | Lydmeldinger | AI-opprydding av diktert transkripsjon til strukturert notat |
| `generate_embeddings` | Kunnskaps-Bridge | Generer vector embeddings for noder (pgvector) |
| `prompt_eval` | Prompt-Laboratorium | Batch-evaluering av testsett mot valgte modeller |
| `suggest_edges` | Kunnskapsgraf (AI) | Analyser innhold via LLM, foreslå topics og mentions-edges. Trigges automatisk ved opprettelse av content-noder med tilstrekkelig tekst |
| `url_ingest` | Web Clipper (proposal) | Hent URL, oppsummer via AI, opprett research-klipp med graf-koblinger |
| `generate_waveform` | Waveforms (proposal) | Generer audio-peaks fra lydfil for visuell bølgeform |
## 6. Tilgangsisolasjon
Alle jobber merkes med `collection_node_id`. Rust-workers kjører som superuser (bypasser RLS) og sikrer isolasjon i applikasjonskode:
* Worker leser `collection_node_id` fra jobben og bruker det til å lagre resultater tilbake i riktig samlings-node
* Per samlings-node config (AI-prompts, navnelister) hentes fra samlings-nodens JSONB-metadata
* Feilede jobber vises kun for brukere med tilgang til samlings-noden (via node_access) i admin-visningen
## 7. Observabilitet
- Jobber med `status = 'error'` skal være synlige i admin-visningen (SvelteKit `/admin/jobs`)
- Valgfritt: SpacetimeDB-event ved statusendring slik at UI kan vise fremdrift i sanntid (f.eks. "Transkriberer... 2/3 forsøk")
## 8. Instruks for Claude Code
- Én binær: `sidelinja-worker`. Én Rust-crate med polling-loop + handler-dispatch
- Hver jobbtype implementeres som en handler-funksjon som registreres i en `HashMap<String, Handler>`
- Bruk `tokio` med semaphore for concurrency-kontroll (`--max-concurrent`)
- Aldri lagre lydfiler i `payload` — bruk filstier
- Opprett alltid jobber med riktig `collection_node_id` — hent fra konteksten (innlogget bruker, webhook, etc.)
- Ved `stats_parse`: denne erstatter den frittstående cronjobben beskrevet i podcast_statistikk.md — bruk jobbkøen med `scheduled_for` for periodisk kjøring
- Splitt til flere binærer kun hvis det blir eksplisitt bedt om — start med én