feat(stage5): implement claim extraction — atomic verifiable claims from sources
- Claim model with provenance, evidence_span, attribution, claim_type
- Stage5Extractor: LLM-based atomic claim extraction from source content
- Never summarizes — always extracts atomic, verifiable claims
- Claims require evidence span (exact quote from source)
- Attribution per claim (who says what)
- Claim types: fact, opinion, prediction, recommendation, claim
- Confidence score 0.0–1.0 per claim
- Bounded concurrency, SSRF-safe, max content truncation
- REST API: GET/POST /research/{run_id}/claims
- 36 tests: parsing, edge cases, integration, validation
This commit is contained in:
107
src/nsct/models/claim.py
Normal file
107
src/nsct/models/claim.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Pydantic v2 schema — Claim für Stage 5: Claim Extraction.
|
||||
|
||||
Jeder Claim ist eine atomare, überprüfbare Behauptung mit Provenance.
|
||||
Keine Zusammenfassungen — immer einzelne, isolierte Claims.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class ClaimType(str, Enum):
|
||||
"""Klassifikation eines Claims nach seinem Epistemischen Status."""
|
||||
|
||||
FACT = "fact"
|
||||
OPINION = "opinion"
|
||||
PREDICTION = "prediction"
|
||||
RECOMMENDATION = "recommendation"
|
||||
CLAIM = "claim"
|
||||
|
||||
|
||||
class Claim(BaseModel):
|
||||
"""Eine atomare, überprüfbare Behauptung aus einer Quelle.
|
||||
|
||||
Felder:
|
||||
id: UUID — Primärschlüssel
|
||||
research_run_id: UUID — Zuordnung zum Research-Run
|
||||
source_id: UUID — Quelle, aus der der Claim extrahiert wurde
|
||||
claim_text: str — Der atomare Claim-Text (NOT NULL)
|
||||
evidence_span: str — Das exakte Zitat aus dem Original
|
||||
claim_type: ClaimType — Typisierung
|
||||
source_url: str — URL der Quelle
|
||||
confidence: float 0.0-1.0 — Wie sicher ist der Claim?
|
||||
metadata: dict — Zusätzliche Kontextdaten
|
||||
created_at: datetime — Erstellungszeitpunkt
|
||||
"""
|
||||
|
||||
id: UUID = Field(default_factory=uuid4)
|
||||
research_run_id: UUID = Field(
|
||||
..., description="Research-Run-UUID für Gruppierung"
|
||||
)
|
||||
source_id: UUID = Field(
|
||||
..., description="Source-UUID, aus der dieser Claim stammt"
|
||||
)
|
||||
claim_text: str = Field(
|
||||
..., min_length=1, description="Der atomare Claim-Text"
|
||||
)
|
||||
evidence_span: str = Field(
|
||||
..., description="Exakter Textabschnitt im Original als Evidenz"
|
||||
)
|
||||
claim_type: ClaimType = Field(default=ClaimType.CLAIM)
|
||||
source_url: str = Field(
|
||||
..., description="URL der Quelle"
|
||||
)
|
||||
confidence: float = Field(
|
||||
default=1.0, ge=0.0, le=1.0, description="Confidence 0-1"
|
||||
)
|
||||
metadata: dict[str, Any] = Field(
|
||||
default_factory=dict, description="Zusätzliche Metadaten"
|
||||
)
|
||||
created_at: datetime = Field(
|
||||
default_factory=datetime.utcnow
|
||||
)
|
||||
|
||||
@field_validator("claim_text")
|
||||
@classmethod
|
||||
def claim_text_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("claim_text darf nicht leer sein")
|
||||
return v
|
||||
|
||||
@field_validator("evidence_span")
|
||||
@classmethod
|
||||
def evidence_span_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("evidence_span darf nicht leer sein")
|
||||
return v
|
||||
|
||||
@field_validator("source_url")
|
||||
@classmethod
|
||||
def source_url_not_empty(cls, v: str) -> str:
|
||||
if not v.strip():
|
||||
raise ValueError("source_url darf nicht leer sein")
|
||||
return v
|
||||
|
||||
model_config = {"frozen": True}
|
||||
|
||||
|
||||
class ClaimExtractionResult(BaseModel):
|
||||
"""Ergebnis einer Claim-Extraktion pro Dokument."""
|
||||
|
||||
source_id: UUID
|
||||
source_url: str
|
||||
research_run_id: UUID
|
||||
claims: list[Claim] = Field(default_factory=list)
|
||||
total_tokens: int = 0
|
||||
extraction_tool: str = "llm"
|
||||
metadata: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
description="Extraktions-Metadaten (z.B. Token-Anzahl, Dauer)"
|
||||
)
|
||||
Reference in New Issue
Block a user