# ARCHITECTURE.md — System-Architektur **Projekt:** Pi Text-Agent **Stand:** 2026-04-16 (nach P3-Features: Claim-Cache, Testkorpus, Test-Runner) --- ## Überblick Pipeline aus spezialisierten Agenten für: 1. **Webrecherche** — Perplexity Sonar API 2. **Claim-Extraktion** — Fakten aus Freitext (mit automatischem Chunking für lange Texte) 3. **Fact-Checking** — Verifikation einzelner Claims via Perplexity + Ollama 4. **Artikelschreiben** — Nur aus verifizierten Facts 5. **Argumentationsanalyse** — Fehlschlüsse + Qualitätsbewertung Jeder Agent: Pi-Extension + CLI-Tool. Persistenz via Job-Speicher (`lib/jobs.ts`). --- ## Datenfluß ``` Freitext / Artikel │ ┌──────▼──────┐ │ollama-claim-extractor│ Ollama: qwen3.5:27b │ (Chunking) │ Texte >4000 Zeichen → Chunks └──────┬──────┘ → claims.json (Job-Cache) │ ClaimSet (JSON) ┌──────▼──────┐ │verify-article│ Perplexity (parallel, max 5) │ (Orchestrator)│ → perplexity/.json (Job-Cache) │ │ Ollama-Batch-Verdict (1 Call) └──────┬──────┘ → report.json (Job-Cache) │ VerificationReport (JSON) ┌────────────┼────────────┐ │ │ ┌──────▼──────┐ ┌───────▼──────┐ │ writer │ │ logic-editor │ │ (--from-job)│ │ │ └──────┬──────┘ └───────┬──────┘ │ article.md (Job-Cache) │ ArgumentMap ▼ ▼ Fertiggestellter Argumentkarte + Artikel mit Quellen Fehlschluss-Liste ``` --- ## Job-Speicher-Datenfluß ``` verify-article.ts --job-id │ ├─ Schritt 1: Claims extrahieren │ → ~/.pi/agent/jobs/_/input.txt │ → ~/.pi/agent/jobs/_/claims.json │ → meta.json: status="verifying" │ ├─ Schritt 2: Perplexity (pro Claim) │ → ~/.pi/agent/jobs/_/perplexity/c001.json │ → ~/.pi/agent/jobs/_/perplexity/c002.json ... │ ├─ Schritt 3: Ollama-Batch-Verdict │ → ~/.pi/agent/jobs/_/report.json │ → meta.json: status="completed" │ writer.ts --from-job │ ├─ Liest report.json aus Job ├─ Schreibt article.md in Job └─ meta.json: steps.write ergänzt ``` Bei Unterbrechung: erneuter Aufruf mit gleichem `--job-id` überspringt vorhandene Schritte. --- ## Komponenten ### Agenten (`agenten/`) #### `ollama-claim-extractor.ts` - **Input:** Freitext (beliebige Länge) - **Output:** `ClaimSet` (Array von `Claim`-Objekten) - **Modell:** `qwen3.5:27b` (Ollama structured output, `num_ctx=8192`) - **Chunking:** Texte > 4000 Zeichen → Chunks ≤ 3000 Zeichen (Absatzgrenzen), sequenziell verarbeitet, dann dedupliziert + zusammengeführt - **Retry:** 3 Versuche mit 15s Pause bei `fetch failed` - **Flags:** `--only-checkable`, `--max-claims`, `--verbose`, `--json` Claim-Felder: `claim_id`, `text`, `claim_type`, `checkability`, `needs_citation`, `entities`, `time_scope`, `source_sentence` Checkability-Werte: `checkable`, `partly_checkable`, `not_checkable` #### `ollama-verifier.ts` - **Input:** Claim-Text + optionaler Kontext - **Output:** `VerificationResult` - **Pipeline:** `searchPerplexity()` → Ollama-Verdict-Synthesis - **Flags:** `--mode fast|deep`, `--verbose`, `--json` Status-Werte: `supported`, `contradicted`, `mixed`, `insufficient_evidence`, `needs_human_review` #### `verify-article.ts` (Orchestrator) - **Input:** Artikel-Text - **Output:** `VerificationReport` - **Ablauf:** 1. `callOllamaClaimExtract()` — alle Claims (mit Chunking bei langen Texten) 2. Für jeden `checkable` Claim: prüfe globalen Cache (`lib/cache.ts`) → prüfe Job-Cache → `searchPerplexity()` parallel (max. 5) 3. Ergebnisse in globalem Cache + Job-Cache speichern 4. Ein Batch-Ollama-Call für alle Verdicts - **Claim-Cache-Priorität:** globaler Cache (SHA256, 7 Tage) → Job-Cache → live Perplexity - **Flags:** `--job-id`, `--mode`, `--max-claims`, `--no-cache`, `--verbose`, `--json` #### `writer.ts` - **Input:** `VerificationReport` (stdin oder Job-Speicher) - **Output:** `ArticleDraft` - **Regel:** Nur `supported` Claims → Artikel; Rest → `excluded_claims` - **Routing:** lokal (Standard) oder OpenRouter mit `--cloud` - **Flags:** `--from-report` (stdin), `--from-job `, `--style`, `--words`, `--lang`, `--cloud`, `--json` Stile: `journalistic`, `blog`, `academic`, `editorial`, `explanatory` #### `logic-editor.ts` - **Input:** Argumentativer Text - **Output:** `ArgumentMap` - **Modell:** `deepseek-r1:32b` (lokal) oder `--cloud` (OpenRouter) - **Flags:** `--only-fallacies`, `--verbose`, `--json` 12 Fehlschluss-Typen: `ad_hominem`, `straw_man`, `false_dichotomy`, `slippery_slope`, `appeal_to_authority`, `hasty_generalization`, `circular_reasoning`, `red_herring`, `appeal_to_emotion`, `false_cause`, `bandwagon`, `anecdotal` #### `research-web.ts` - Standalone (keine relativen Imports), direkt in Pi-Extensions-Root - Perplexity `sonar` / `sonar-pro` --- ### Shared Libraries (`lib/`) #### `lib/perplexity.ts` - `searchPerplexity(query, opts)` → `PerplexityResult` - `formatSourcesForPrompt(sources, maxLen)` → String - Exponentielles Retry (3×), Quell-Deduplizierung, Kostenberechnung - Kostenmodell: `sonar` $1/Mio Tokens, `sonar-pro` $3/$15 (In/Out) #### `lib/router.ts` - `routeModel(task, complexity)` → `{ provider, model }` - `callOpenRouter(model, messages, opts)` → `{ text, promptTokens, completionTokens, latencyMs }` - `estimateOpenRouterCost(model, in, out)` → USD - ENV-Overrides: `ROUTER_FORCE_LOCAL=1`, `ROUTER_FORCE_CLOUD=1` - Standard: alle Tasks → lokal (Ollama) wenn `complexity: "low"` oder kein `OPENROUTER_API_KEY` #### `lib/logger.ts` - `createLogger({ verbose?, jobId? })` → `Logger` - `Logger.info/warn/error/debug(msg, data?)` — strukturierte Einträge mit ISO-Timestamp - Schreibt in `~/.pi/agent/logs/[_jobId].log` - `verbose=true` → alle Einträge auf stderr; `warn`/`error` immer auf stderr - `nullLogger` — Null-Objekt für Pi-Extension-Kontext #### `lib/cache.ts` - SHA256-basierter File-Cache für Perplexity-Ergebnisse - `getCached(claimText)` — normalisiert Text, liest JSON wenn nicht abgelaufen - `setCached(claimText, data)` — schreibt `~/.pi/agent/cache/perplexity/.json` - `claimHash(text)` — exportierter SHA256-Helper - `pruneCache()` — löscht Einträge älter als 7 Tage - `cacheStats()` — `{ total, expired, sizeBytes }` - TTL: 7 Tage (basiert auf Datei-mtime) - Normalisierung: lowercase + collapse whitespace → konsistentes Hashing auch bei minimalen Unterschieden #### `lib/jobs.ts` - `createJob(slug, model)` → jobDir - `findJobDir(slug)` → neuestes Verzeichnis mit diesem Slug oder null - `getOrCreateJob(slug, model)` → `{ jobDir, isNew }` - `saveJobFile(jobDir, filename, data)` / `loadJobFile(jobDir, filename)` → T | null - `jobFileExists(jobDir, filename)` → boolean - `updateJobMeta(jobDir, updates)` — shallow merge in `meta.json` - `listJobs()` → `JobMeta[]` (neueste zuerst) - `formatJobList(jobs)` → Tabelle für CLI Job-Status: `created`, `extracting`, `verifying`, `writing`, `completed`, `failed` --- ## VRAM-Constraints (RTX 3090, 24 GB) | Modell | Gewichte | KV@8192 | Gesamt | Passt? | |--------|----------|---------|--------|--------| | qwen3.5:27b Q4_K_M | 15.5 GB | 4.2 GB | ~21 GB | ✓ | | qwen3.5:27b Q4_K_M @ 16384 | 15.5 GB | 8.4 GB | ~25 GB | ✗ OOM | | deepseek-r1:32b Q4_K_M | 19 GB | 4.2 GB | ~23 GB | ✓ (knapp) | **Deshalb `num_ctx=8192` hardcoded** in `ollama-claim-extractor.ts` und `ollama-verifier.ts`. **Deshalb Chunking** statt größerer Kontext: Texte werden in ≤3000-Zeichen-Stücke zerlegt. Mit `CUDA_VISIBLE_DEVICES=1,2` (beide RTX 3090): 48 GB VRAM → 70B-Modelle möglich. --- ## Ollama-Integrationsdetails ```typescript // Structured Output — IMMER so, nie format: "json" await fetch("http://localhost:11434/api/chat", { method: "POST", body: JSON.stringify({ model: "qwen3.5:27b", messages: [...], format: { type: "object", additionalProperties: false, properties: {...}, required: [...] }, stream: false, options: { temperature: 0.1, num_ctx: 8192 } }) }); ``` Retry-Pattern in `ollama-claim-extractor.ts` (Vorlage für andere Agenten): ```typescript for (let attempt = 1; attempt <= 3; attempt++) { try { resp = await fetch(...); break; } catch (err) { if (attempt === 3) throw new Error(`fetch failed nach 3 Versuchen: ${err}`); await new Promise(r => setTimeout(r, 15_000)); // 15s warten } } ``` --- ## Deployment-Architektur ``` ~/Pi_Agent_Projekts/text_agent/ ← git-Repository ├── agenten/ ◄──── Symlinks ────────────────────────────────┐ └── lib/ ◄────── Symlink ─────────────────────────────────┐ │ │ │ ~/.pi/agent/extensions/ │ │ ├── lib ────────────────────────────────────────────────────┘ │ └── fact-checker/ │ ├── package.json │ ├── ollama-claim-extractor.ts ──────────────────────────────────┘ ├── ollama-verifier.ts ──────────────────────────────────────────┘ ├── verify-article.ts ────────────────────────────────────┘ ├── logic-editor.ts ──────────────────────────────────────┘ └── writer.ts ────────────────────────────────────────────┘ ~/.pi/agent/jobs/ ← Job-Verzeichnisse (Runtime-Daten, nicht im git) ~/.pi/agent/logs/ ← Log-Dateien (Runtime-Daten, nicht im git) ~/.pi/agent/cache/ ← Perplexity-Claim-Cache (Runtime-Daten, nicht im git) text_agent/tests/ ├── corpus/ ← 10 Testfälle (input.txt, expected.json, notes.md) ├── results/ ← Test-Runner-Outputs (nicht im git, .gitignore) └── run_corpus.sh ← Precision/Recall-Test-Runner ```