Text_Agent/docs/ARCHITECTURE.md

255 lines
11 KiB
Markdown
Raw Permalink Normal View History

# 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/<id>.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 <slug>
├─ Schritt 1: Claims extrahieren
│ → ~/.pi/agent/jobs/<datum>_<slug>/input.txt
│ → ~/.pi/agent/jobs/<datum>_<slug>/claims.json
│ → meta.json: status="verifying"
├─ Schritt 2: Perplexity (pro Claim)
│ → ~/.pi/agent/jobs/<datum>_<slug>/perplexity/c001.json
│ → ~/.pi/agent/jobs/<datum>_<slug>/perplexity/c002.json ...
├─ Schritt 3: Ollama-Batch-Verdict
│ → ~/.pi/agent/jobs/<datum>_<slug>/report.json
│ → meta.json: status="completed"
writer.ts --from-job <slug>
├─ 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 <slug>`, `--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/<timestamp>[_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<T>(claimText)` — normalisiert Text, liest JSON wenn nicht abgelaufen
- `setCached<T>(claimText, data)` — schreibt `~/.pi/agent/cache/perplexity/<sha256>.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<T>(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
```