2026-05-12 04:21:48 +02:00
/ * *
* llama - verifier . ts
* Pi - Extension + CLI : Eine einzelne Behauptung via Perplexity + llama . cpp verifizieren .
*
* Als Pi - Extension : ~ / . p i / a g e n t / e x t e n s i o n s / f a c t - c h e c k e r / l l a m a - v e r i f i e r . t s
* Nach Ä nderungen in Pi : / r e l o a d
*
* Als CLI :
* npx tsx agenten / llama - verifier . ts "Die Inflationsrate betrug 2024 in Deutschland 3,2%."
* npx tsx agenten / llama - verifier . ts -- mode deep "Die Erde ist 4,6 Milliarden Jahre alt."
* npx tsx agenten / llama - verifier . ts -- user - language en "Trump called Iran's response 'totally unacceptable'."
* npx tsx agenten / llama - verifier . ts -- json "..." ( gibt VerificationResult als JSON aus )
*
* Ablauf : Perplexity - Suche ( Originalsprache ) → llama . cpp - Urteil → formatierte Ausgabe
* /
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent" ;
import { Type } from "@sinclair/typebox" ;
import { fileURLToPath } from "node:url" ;
import { searchPerplexity , formatSourcesForPrompt , type PerplexitySource } from "../lib/perplexity.js" ;
import { createLogger , nullLogger , type Logger } from "../lib/logger.js" ;
// ---------------------------------------------------------------------------
// Typen
// ---------------------------------------------------------------------------
type VerificationStatus =
| "supported"
| "contradicted"
| "mixed"
| "insufficient_evidence"
| "needs_human_review" ;
type Confidence = "high" | "medium" | "low" ;
type VerdictRaw = {
status : VerificationStatus ;
confidence : Confidence ;
summary : string ;
counter_evidence : string | null ;
notes : string | null ;
supporting_urls : string [ ] ;
} ;
export type VerificationResult = {
claim : string ;
status : VerificationStatus ;
confidence : Confidence ;
summary : string ;
counter_evidence : string | null ;
notes : string | null ;
sources : PerplexitySource [ ] ;
supporting_urls : string [ ] ;
perplexityCostUSD : number ;
latencyMs : number ;
model : string ;
} ;
// llama.cpp OpenAI-kompatibles API-Format
type LlamaResponse = {
choices : Array < {
message ? : { content? : string ; reasoning_content? : string } ;
finish_reason? : string ;
} > ;
usage ? : {
prompt_tokens? : number ;
completion_tokens? : number ;
total_tokens? : number ;
} ;
} ;
// ---------------------------------------------------------------------------
// Konfiguration
// ---------------------------------------------------------------------------
const DEFAULT_MODEL = "Qwopus3.6-35B-A3B-v1-Q4_K_M.gguf" ;
const LLAMA_HOST = process . env . LLAMA_HOST ? ? "http://localhost:8000" ;
const DEFAULT_USER_LANGUAGE = "de" ;
const MAX_TOKENS = 16384 ;
const TEMPERATURE = 0.1 ;
const MAX_RETRIES = 3 ;
const RETRY_DELAY_MS = 15 _000 ;
// ---------------------------------------------------------------------------
// Verdict-Synthese via llama.cpp
// ---------------------------------------------------------------------------
function langLabel ( userLanguage : string ) : string {
if ( userLanguage === "de" ) return "Deutsch" ;
if ( userLanguage === "en" ) return "Englisch" ;
if ( userLanguage === "fr" ) return "Französisch" ;
if ( userLanguage === "es" ) return "Spanisch" ;
return userLanguage ;
}
function buildVerdictSystemPrompt ( userLanguage : string ) : string {
return ` Du bist ein erfahrener Fact-Checker. Bewerte eine Behauptung anhand bereitgestellter Webquellen.
Bewertungsskala :
- supported : Quellen bestätigen die Behauptung klar und konsistent
- contradicted : Quellen widersprechen der Behauptung klar und substanziell
- mixed : Quellen liefern widersprüchliche Belege ODER die Behauptung ist technisch ungenau aber im Kern korrekt
- insufficient_evidence : Zu wenig oder qualitativ unzureichende Quellen für ein Urteil
- needs_human_review : Komplex , politisch heikel , veraltete Quellen , oder stark kontextabhängig
Confidence :
- high : Quellenlage ist eindeutig und aus Primärquellen
- medium : Quellen vorhanden aber begrenzt oder sekundär
- low : Quellen sehr rar , veraltet oder widersprüchlich
WICHTIGE REGELN für "contradicted" :
2026-05-12 04:52:12 +02:00
- Nur bei klaren , substanziellen Fehlern verwenden : falsche Person , falsch zugeordnetes Ereignis , Zahl um mehr als 5 % abweichend , grundlegend falsche Kausalität
2026-05-12 04:21:48 +02:00
- Gerundete oder allgemein akzeptierte Näherungswerte sind "supported" ( z . B . "21 Millionen Bitcoin" ist korrekte Rundung für 20.999 . 999 , 97 BTC )
- Zeitzonendifferenzen bei historischen Ereignissen : "supported" wenn die Angabe im ü blichen regionalen / kulturellen Kontext korrekt ist
- Technische Präzisierungen zu im Wesentlichen korrekten Aussagen → "mixed" , nicht "contradicted"
- Im Zweifel : "mixed" statt "contradicted"
AUSGABESPRACHE : Schreibe summary , counter_evidence und notes auf $ { langLabel ( userLanguage ) } .
Die Enum - Werte status und confidence bleiben englisch ( wie im Schema definiert ) .
summary : 1 - 3 präzise Sätze basierend auf den Quellen . Nicht spekulieren .
counter_evidence : Gegenbelege als Satz beschreiben , falls vorhanden . Sonst null .
notes : Zeitabhängigkeit , regionale Einschränkungen , Vorbehalt . Sonst null .
supporting_urls : URLs aus den Quellen die den Claim stützen ( leeres Array wenn keine ) .
Antworte NUR mit diesem JSON - Objekt — kein Freitext davor oder danach :
{
"status" : "supported|contradicted|mixed|insufficient_evidence|needs_human_review" ,
"confidence" : "high|medium|low" ,
"summary" : "..." ,
"counter_evidence" : "..." | null ,
"notes" : "..." | null ,
"supporting_urls" : [ "url1" , "url2" ]
} ` ;
}
function buildVerdictUserPrompt ( claim : string , perplexitySummary : string , sources : PerplexitySource [ ] , context? : string ) : string {
const contextBlock = context ? ` \ nARTIKEL-KONTEXT: " ${ context . slice ( 0 , 300 ) } " \ n ` : "" ;
return ` /no_think
ZU PRÜFENDE BEHAUPTUNG :
"${claim}"
$ { contextBlock }
RECHERCHE - ERGEBNIS ( Perplexity ) :
$ { perplexitySummary }
QUELLEN :
$ { formatSourcesForPrompt ( sources , 300 ) }
Bewerte die Behauptung anhand der Recherche . ` ;
}
async function synthesizeVerdict (
claim : string ,
perplexitySummary : string ,
sources : PerplexitySource [ ] ,
model : string ,
context? : string ,
userLanguage? : string ,
signal? : AbortSignal ,
logger? : Logger
) : Promise < VerdictRaw > {
const log = logger ? ? nullLogger ;
const lang = userLanguage ? ? DEFAULT_USER_LANGUAGE ;
const body = {
model ,
messages : [
{ role : "system" , content : buildVerdictSystemPrompt ( lang ) } ,
{ role : "user" , content : buildVerdictUserPrompt ( claim , perplexitySummary , sources , context ) } ,
] ,
stream : false ,
temperature : TEMPERATURE ,
max_tokens : MAX_TOKENS ,
} ;
let resp : Response | null = null ;
for ( let attempt = 1 ; attempt <= MAX_RETRIES ; attempt ++ ) {
try {
resp = await fetch ( ` ${ LLAMA_HOST } /v1/chat/completions ` , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON.stringify ( body ) ,
signal ,
} ) ;
break ;
} catch ( err ) {
const isLast = attempt === MAX_RETRIES ;
log . warn ( ` llama.cpp fetch fehlgeschlagen (Versuch ${ attempt } / ${ MAX_RETRIES } ) ` , {
error : err instanceof Error ? err.message : String ( err ) ,
retryInMs : isLast ? 0 : RETRY_DELAY_MS ,
} ) ;
if ( isLast ) throw new Error ( ` fetch failed nach ${ MAX_RETRIES } Versuchen: ${ err instanceof Error ? err.message : err } ` ) ;
await new Promise ( ( r ) = > setTimeout ( r , RETRY_DELAY_MS ) ) ;
}
}
if ( ! resp ! . ok ) {
const errText = await resp ! . text ( ) . catch ( ( ) = > "" ) ;
throw new Error ( ` llama.cpp Fehler ${ resp ! . status } : ${ errText } ` ) ;
}
const data = ( await resp ! . json ( ) ) as LlamaResponse ;
const choice = data . choices ? . [ 0 ] ;
let raw = choice ? . message ? . content ? ? "" ;
// Reasoning-Modelle (Qwen3, DeepSeek-R1) schreiben Denkkette in reasoning_content.
// Wenn content leer ist aber reasoning_content JSON enthält: als Fallback verwenden.
if ( ! raw . trim ( ) && choice ? . message ? . reasoning_content ) {
const rc = choice . message . reasoning_content ;
const allMatches = [ . . . rc . matchAll ( /\{[^{}]*"status"\s*:/g ) ] ;
const lastIdx = allMatches . length > 0
? rc . lastIndexOf ( allMatches [ allMatches . length - 1 ] [ 0 ] )
: - 1 ;
const extracted = lastIdx >= 0
? rc . slice ( lastIdx ) . match ( /\{[\s\S]*\}/ ) ? . [ 0 ]
: rc . match ( /\{[\s\S]*"status"[\s\S]*\}/ ) ? . [ 0 ] ;
if ( extracted ) {
raw = extracted ;
log . warn ( "content leer — JSON aus reasoning_content extrahiert (Thinking-Modus aktiv trotz /no_think)" , {
finishReason : choice.finish_reason ,
rawLength : raw.length ,
} ) ;
}
}
const cleanedRaw = raw
. replace ( /^```(?:json)?\s*/i , "" )
. replace ( /\s*```$/i , "" )
. trim ( ) ;
log . debug ( "llama.cpp-Antwort empfangen" , {
promptTokens : data.usage?.prompt_tokens ,
outputTokens : data.usage?.completion_tokens ,
finishReason : choice?.finish_reason ,
rawLength : raw.length ,
} ) ;
if ( ! cleanedRaw ) {
throw new Error ( "Leere llama.cpp-Antwort für Verdict" ) ;
}
let parsed : unknown ;
try {
parsed = JSON . parse ( cleanedRaw ) ;
} catch {
throw new Error ( ` Kein gültiges JSON von llama.cpp: ${ cleanedRaw . slice ( 0 , 200 ) } ` ) ;
}
return parsed as VerdictRaw ;
}
// ---------------------------------------------------------------------------
// Hauptfunktion
// ---------------------------------------------------------------------------
export async function verifyClaim (
claim : string ,
options ? : {
context? : string ;
mode ? : "fast" | "deep" ;
model? : string ;
userLanguage? : string ;
signal? : AbortSignal ;
logger? : Logger ;
}
) : Promise < VerificationResult > {
const t0 = Date . now ( ) ;
const model = options ? . model ? ? DEFAULT_MODEL ;
const log = options ? . logger ? ? nullLogger ;
log . info ( "Perplexity-Suche gestartet" , { claim : claim.slice ( 0 , 80 ) , mode : options?.mode ? ? "fast" } ) ;
const perplexityResult = await searchPerplexity ( claim , {
mode : options?.mode ? ? "fast" ,
signal : options?.signal ,
} ) ;
log . info ( "Perplexity abgeschlossen" , {
sources : perplexityResult.sources.length ,
costUSD : perplexityResult.estimatedCostUSD.toFixed ( 4 ) ,
} ) ;
log . info ( "llama.cpp-Urteil generieren..." , { model , userLanguage : options?.userLanguage ? ? DEFAULT_USER_LANGUAGE } ) ;
const verdict = await synthesizeVerdict (
claim ,
perplexityResult . summary ,
perplexityResult . sources ,
model ,
options ? . context ,
options ? . userLanguage ,
options ? . signal ,
log
) ;
log . info ( "Urteil erhalten" , { status : verdict.status , confidence : verdict.confidence } ) ;
return {
claim ,
status : verdict.status ,
confidence : verdict.confidence ,
summary : verdict.summary ,
counter_evidence : verdict.counter_evidence ,
notes : verdict.notes ,
sources : perplexityResult.sources ,
supporting_urls : verdict.supporting_urls ,
perplexityCostUSD : perplexityResult.estimatedCostUSD ,
latencyMs : Date.now ( ) - t0 ,
model ,
} ;
}
// ---------------------------------------------------------------------------
// Formatierung
// ---------------------------------------------------------------------------
const STATUS_ICON : Record < VerificationStatus , string > = {
supported : "✓ BESTÄTIGT" ,
contradicted : "✗ WIDERLEGT" ,
mixed : "~ GEMISCHT" ,
insufficient_evidence : "? BELEGE UNZUREICHEND" ,
needs_human_review : "⚠ MENSCHLICHE PRÜFUNG NÖTIG" ,
} ;
const CONF_LABEL : Record < Confidence , string > = {
high : "hoch" ,
medium : "mittel" ,
low : "niedrig" ,
} ;
export function formatVerificationResult ( result : VerificationResult ) : string {
const lines : string [ ] = [ ] ;
lines . push ( ` ## Verifikation ` ) ;
lines . push ( ` **Behauptung:** " ${ result . claim } " ` ) ;
lines . push ( "" ) ;
lines . push ( ` ** ${ STATUS_ICON [ result . status ] } ** (Konfidenz: ${ CONF_LABEL [ result . confidence ] } ) ` ) ;
lines . push ( "" ) ;
lines . push ( ` **Begründung:** ${ result . summary } ` ) ;
if ( result . counter_evidence ) {
lines . push ( ` \ n**Gegenbelege:** ${ result . counter_evidence } ` ) ;
}
if ( result . notes ) {
lines . push ( ` \ n**Hinweise:** ${ result . notes } ` ) ;
}
if ( result . sources . length > 0 ) {
lines . push ( "\n**Quellen:**" ) ;
result . sources . forEach ( ( s , i ) = > {
const supporting = result . supporting_urls . includes ( s . url ) ? " ✓" : "" ;
const title = s . title ? ? s . url ;
lines . push ( ` [ ${ i + 1 } ] ${ supporting } [ ${ title } ]( ${ s . url } ) ` ) ;
} ) ;
} else {
lines . push ( "\n_(Keine Quellen gefunden)_" ) ;
}
const latSec = ( result . latencyMs / 1000 ) . toFixed ( 1 ) ;
lines . push ( ` \ n_[Perplexity: ~ $ ${ result . perplexityCostUSD . toFixed ( 4 ) } | llama.cpp: ${ result . model } | Gesamt: ${ latSec } s]_ ` ) ;
return lines . join ( "\n" ) ;
}
// ---------------------------------------------------------------------------
// Pi-Extension: Default Export
// ---------------------------------------------------------------------------
const PARAMS = Type . Object ( {
claim : Type.String ( {
description :
"Die zu verifizierende Behauptung als vollständiger, selbstständiger Satz. " +
"Idealerweise das Ergebnis von extract_claims_llama (claim_id + text). " +
"Übergib den Claim immer in seiner Originalsprache." ,
} ) ,
context : Type.Optional (
Type . String ( {
description :
"Optionaler Kontext: kurzer Auszug aus dem Artikel, in dem die Behauptung steht. " +
"Hilft dem Fact-Checker bei mehrdeutigen Claims. Max. 300 Zeichen." ,
} )
) ,
mode : Type.Optional (
Type . Union ( [ Type . Literal ( "fast" ) , Type . Literal ( "deep" ) ] , {
description :
"fast (Standard): sonar, für die meisten Behauptungen ausreichend. " +
"deep: sonar-pro, für komplexe, strittige oder heikle Behauptungen." ,
} )
) ,
model : Type.Optional (
Type . String ( {
description : ` llama.cpp-Modell für die Urteilssynthese. Standard: ${ DEFAULT_MODEL } . ` ,
} )
) ,
userLanguage : Type.Optional (
Type . String ( {
description :
"Sprache für summary, counter_evidence und notes im Urteil (z.B. \"de\", \"en\", \"fr\"). " +
` Standard: ${ DEFAULT_USER_LANGUAGE } . Die Enum-Felder status/confidence bleiben englisch. ` ,
} )
) ,
} ) ;
export default function llamaVerifierExtension ( pi : ExtensionAPI ) {
pi . registerTool ( {
name : "verify_claim_llama" ,
label : "Claim-Verifikation (llama.cpp)" ,
description :
"Verifiziert eine einzelne Behauptung: Perplexity-Recherche (Originalsprache) → llama.cpp-Urteil. " +
"Gibt Status (supported/contradicted/mixed/insufficient_evidence/needs_human_review), " +
"Konfidenz, Begründung und Quellen zurück. " +
"Nutze dieses Tool nach extract_claims_llama um spezifische Claims zu prüfen. " +
"Kosten: ~$0.005-0.015 pro Claim (Perplexity) + lokal (llama.cpp)." ,
promptGuidelines : [
"This is the PREFERRED claim verification tool. Use verify_claim_llama by default whenever the user wants a claim checked." ,
"Use verify_claim_llama after extract_claims or extract_claims_llama to check specific claims the user wants verified." ,
"Pass the full claim text as the 'claim' parameter — always in the original language of the article." ,
"Use mode=deep for complex, politically sensitive, or scientifically contested claims." ,
"The 'context' parameter helps when the claim is ambiguous without its original article context." ,
"Set userLanguage to match the user's preferred language (e.g. 'de' for German, 'en' for English). Default is 'de'." ,
"Show the full formatted output including the cost/latency line." ,
"If status is 'needs_human_review' or 'insufficient_evidence', clearly communicate this and suggest manual checking." ,
"If status is 'contradicted', always show the counter_evidence to the user." ,
"IMPORTANT: Never call verify_claim_llama for multiple claims simultaneously — llama.cpp processes one request at a time. Always verify claims sequentially." ,
] ,
parameters : PARAMS ,
async execute ( _toolCallId , params , signal ) {
try {
const result = await verifyClaim ( params . claim , {
context : params.context ,
mode : params.mode ,
model : params.model ,
userLanguage : params.userLanguage ,
signal ,
} ) ;
return {
content : [ { type : "text" , text : formatVerificationResult ( result ) } ] ,
details : {
status : result.status ,
confidence : result.confidence ,
model : result.model ,
sourceCount : result.sources.length ,
perplexityCostUSD : result.perplexityCostUSD ,
latencyMs : result.latencyMs ,
} ,
} ;
} catch ( err ) {
const msg = err instanceof Error ? err . message : "Unbekannter Fehler" ;
return { content : [ { type : "text" , text : ` Verifikationsfehler: ${ msg } ` } ] } ;
}
} ,
} ) ;
}
// ---------------------------------------------------------------------------
// CLI-Modus
// ---------------------------------------------------------------------------
function parseCliArgs ( args : string [ ] ) : {
claim : string ;
mode : "fast" | "deep" ;
model : string ;
userLanguage : string ;
jsonOutput : boolean ;
verbose : boolean ;
} {
let mode : "fast" | "deep" = "fast" ;
let model = DEFAULT_MODEL ;
let userLanguage = DEFAULT_USER_LANGUAGE ;
let jsonOutput = false ;
let verbose = false ;
const claimParts : string [ ] = [ ] ;
for ( let i = 0 ; i < args . length ; i ++ ) {
const arg = args [ i ] ;
if ( arg === "--mode" && args [ i + 1 ] ) {
const m = args [ ++ i ] ;
if ( m === "fast" || m === "deep" ) mode = m ;
} else if ( arg === "--model" && args [ i + 1 ] ) {
model = args [ ++ i ] ;
} else if ( arg === "--user-language" && args [ i + 1 ] ) {
userLanguage = args [ ++ i ] ;
} else if ( arg === "--json" ) {
jsonOutput = true ;
} else if ( arg === "--verbose" || arg === "-v" ) {
verbose = true ;
} else if ( ! arg . startsWith ( "--" ) ) {
claimParts . push ( arg ) ;
}
}
return { claim : claimParts.join ( " " ) . trim ( ) , mode , model , userLanguage , jsonOutput , verbose } ;
}
async function runCli() {
const args = process . argv . slice ( 2 ) ;
if ( args . length === 0 || args [ 0 ] === "--help" || args [ 0 ] === "-h" ) {
console . log ( `
Claim - Verifikator ( llama . cpp ) — Eine Behauptung mit Perplexity + llama . cpp prüfen
Verwendung :
npx tsx agenten / llama - verifier . ts [ Optionen ] "Behauptung..."
Optionen :
-- mode fast | deep Perplexity - Modus ( Standard : fast )
-- model < name > llama . cpp - Modell ( Standard : $ { DEFAULT_MODEL } )
-- user - language < lang > Sprache für Urteilstext , z . B . "de" , "en" ( Standard : $ { DEFAULT_USER_LANGUAGE } )
-- json Ausgabe als JSON ( VerificationResult )
-- verbose , - v Ausführliches Logging in ~ / . p i / a g e n t / l o g s /
-- help Diese Hilfe
Umgebungsvariablen :
LLAMA_HOST llama . cpp - Server - URL ( Standard : http : //localhost:8000)
PERPLEXITY_API_KEY Perplexity API - Key ( erforderlich )
Beispiele :
npx tsx agenten / llama - verifier . ts "Die EZB hat den Leitzins im Juni 2024 gesenkt."
npx tsx agenten / llama - verifier . ts -- mode deep "Die Erde ist 4,6 Milliarden Jahre alt."
npx tsx agenten / llama - verifier . ts -- user - language en "Trump called Iran's response 'totally unacceptable'."
npx tsx agenten / llama - verifier . ts -- json "Behauptung..." | python3 - m json . tool
` );
process . exit ( 0 ) ;
}
const { claim , mode , model , userLanguage , jsonOutput , verbose } = parseCliArgs ( args ) ;
if ( ! claim ) {
console . error ( "Fehler: Kein Claim übergeben. Nutze --help für Informationen." ) ;
process . exit ( 1 ) ;
}
if ( ! jsonOutput ) {
console . error ( ` \ nVerifiziere: " ${ claim } " \ nModus: ${ mode } | Modell: ${ model } | Urteils-Sprache: ${ userLanguage } \ n ` ) ;
}
const log = createLogger ( { verbose } ) ;
try {
const result = await verifyClaim ( claim , { mode , model , userLanguage , logger : log } ) ;
if ( jsonOutput ) {
console . log ( JSON . stringify ( result , null , 2 ) ) ;
} else {
console . log ( formatVerificationResult ( result ) ) ;
}
} catch ( err ) {
console . error ( "Fehler:" , err instanceof Error ? err.message : err ) ;
process . exit ( 1 ) ;
}
}
const __filename = fileURLToPath ( import . meta . url ) ;
if ( process . argv [ 1 ] === __filename ) {
runCli ( ) ;
}