feat: Pi Text-Agent — initialer Commit (sauberes Repo)
Vollständiges Multi-Agenten-System für Fact-Checking, Artikelschreiben und Argumentationsanalyse. Zwei Backends: llama.cpp (★ bevorzugt) und Ollama. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
5146b7fa30
62 changed files with 11279 additions and 0 deletions
162
lib/cache.ts
Normal file
162
lib/cache.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* lib/cache.ts
|
||||
* Hash-basierter File-Cache für Perplexity-Ergebnisse.
|
||||
*
|
||||
* Vermeidet doppelte Perplexity-Kosten wenn derselbe Claim in mehreren Artikeln
|
||||
* oder in Wiederholungsläufen geprüft wird.
|
||||
*
|
||||
* Ablageort: ~/.pi/agent/cache/perplexity/<sha256>.json
|
||||
* TTL: 7 Tage (ältere Einträge werden beim Lesen ignoriert)
|
||||
* Schlüssel: SHA256 des normalisierten Claim-Textes
|
||||
*
|
||||
* Verwendung in verify-article.ts:
|
||||
* import { getCached, setCached } from "../lib/cache.js";
|
||||
* const cached = getCached(claimText);
|
||||
* if (cached) return cached;
|
||||
* const result = await searchPerplexity(claimText, opts);
|
||||
* setCached(claimText, result);
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, writeFileSync, readFileSync, statSync, readdirSync, unlinkSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Konstanten
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const CACHE_DIR = join(homedir(), ".pi", "agent", "cache", "perplexity");
|
||||
const TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 Tage
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interner Typ (Cache-Datei)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type CacheEntry<T> = {
|
||||
cachedAt: string; // ISO-Timestamp
|
||||
data: T;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hilfsfunktionen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ensureCacheDir(): void {
|
||||
mkdirSync(CACHE_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisiert einen Claim-Text für konsistentes Hashing:
|
||||
* - Whitespace kollabieren
|
||||
* - Kleinschreibung
|
||||
* - Führende/nachfolgende Leerzeichen entfernen
|
||||
*/
|
||||
function normalizeText(text: string): string {
|
||||
return text.toLowerCase().replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA256-Hash des normalisierten Claim-Textes als Hex-String (64 Zeichen).
|
||||
*/
|
||||
export function claimHash(claimText: string): string {
|
||||
return createHash("sha256").update(normalizeText(claimText)).digest("hex");
|
||||
}
|
||||
|
||||
function cachePath(hash: string): string {
|
||||
return join(CACHE_DIR, `${hash}.json`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Öffentliche API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Liest einen gecachten Perplexity-Wert für den gegebenen Claim-Text.
|
||||
* Gibt null zurück wenn:
|
||||
* - kein Cache-Eintrag vorhanden
|
||||
* - der Eintrag älter als TTL_MS ist
|
||||
* - der Eintrag korrupt ist
|
||||
*/
|
||||
export function getCached<T>(claimText: string): T | null {
|
||||
try {
|
||||
const path = cachePath(claimHash(claimText));
|
||||
const stat = statSync(path);
|
||||
const ageMs = Date.now() - stat.mtimeMs;
|
||||
if (ageMs > TTL_MS) return null; // abgelaufen
|
||||
|
||||
const entry = JSON.parse(readFileSync(path, "utf8")) as CacheEntry<T>;
|
||||
return entry.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert ein Perplexity-Ergebnis im Cache.
|
||||
* Fehler beim Schreiben werden ignoriert (Cache ist optional).
|
||||
*/
|
||||
export function setCached<T>(claimText: string, data: T): void {
|
||||
try {
|
||||
ensureCacheDir();
|
||||
const entry: CacheEntry<T> = {
|
||||
cachedAt: new Date().toISOString(),
|
||||
data,
|
||||
};
|
||||
writeFileSync(cachePath(claimHash(claimText)), JSON.stringify(entry, null, 2), "utf8");
|
||||
} catch {
|
||||
// Cache-Fehler dürfen den Programmablauf nicht unterbrechen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht abgelaufene Cache-Einträge (älter als TTL_MS).
|
||||
* Gibt die Anzahl gelöschter Einträge zurück.
|
||||
*/
|
||||
export function pruneCache(): number {
|
||||
try {
|
||||
ensureCacheDir();
|
||||
const files = readdirSync(CACHE_DIR).filter((f) => f.endsWith(".json"));
|
||||
let deleted = 0;
|
||||
for (const file of files) {
|
||||
try {
|
||||
const path = join(CACHE_DIR, file);
|
||||
const ageMs = Date.now() - statSync(path).mtimeMs;
|
||||
if (ageMs > TTL_MS) {
|
||||
unlinkSync(path);
|
||||
deleted++;
|
||||
}
|
||||
} catch {
|
||||
// Einzelne Fehler ignorieren
|
||||
}
|
||||
}
|
||||
return deleted;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Statistiken über den Cache zurück.
|
||||
*/
|
||||
export function cacheStats(): { total: number; expired: number; sizeBytes: number } {
|
||||
try {
|
||||
ensureCacheDir();
|
||||
const files = readdirSync(CACHE_DIR).filter((f) => f.endsWith(".json"));
|
||||
let expired = 0;
|
||||
let sizeBytes = 0;
|
||||
for (const file of files) {
|
||||
try {
|
||||
const path = join(CACHE_DIR, file);
|
||||
const stat = statSync(path);
|
||||
sizeBytes += stat.size;
|
||||
if (Date.now() - stat.mtimeMs > TTL_MS) expired++;
|
||||
} catch {
|
||||
// ignorieren
|
||||
}
|
||||
}
|
||||
return { total: files.length, expired, sizeBytes };
|
||||
} catch {
|
||||
return { total: 0, expired: 0, sizeBytes: 0 };
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue