255 lines
11 KiB
Markdown
255 lines
11 KiB
Markdown
|
|
# 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
|
|||
|
|
```
|