feat(stage13): implement Iterative Research / Gap Analysis\n\nImplement Gap Analysis Engine (Stage 13):\n- GapAnalysisEngine: detect single-source claims, contradictions,\n missing primary sources, weak evidence\n- IterationReport: structured gap findings with severity & target\n- GapSearchQuery: derived search queries per gap finding\n- Integration into ResearchOrchestrator: runs gap analysis after\n extracting, then executes gap searches iteratively\n- 17 tests covering all analysis categories and edge cases

This commit is contained in:
Frerk Campen
2026-08-26 11:19:35 +00:00
committed by NSCT Agent
parent 2ef7b67002
commit 0b7a624bc8
4 changed files with 1196 additions and 2 deletions

View File

@@ -0,0 +1,202 @@
"""Pydantic v2 schemas — Iterative Research / Gap Analysis (Stage 13).
Definiert Gap-Analysis-Ergebnis, Gap-Finding, Iteration-Gap-Report
und GapSearchQuery für die iterative Lückenerkennung.
"""
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from typing import Any
from uuid import UUID, uuid4
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class GapCategory(str, Enum):
"""Kategorie einer festgestellten Lücke."""
SINGLE_SOURCE_CLAIM = "single_source_claim"
MISSING_PRIMARY_SOURCE = "missing_primary_source"
UNRESOLVED_CONTRADICTION = "unresolved_contradiction"
MISSING_COUNTER_EVIDENCE = "missing_counter_evidence"
WEAK_EVIDENCE = "weak_evidence"
GENERAL = "general"
class GapSeverity(str, Enum):
"""Schweregrad einer Lücke."""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class GapTarget(str, Enum):
"""Zieltyp der iterativen Suche."""
PRIMARY_SOURCE = "primary_source"
COUNTER_EVIDENCE = "counter_evidence"
SPECIALIST_SOURCE = "specialist_source"
GENERAL_EXPANSION = "general_expansion"
FACT_CHECK = "fact_check"
# ---------------------------------------------------------------------------
# GapFinding
# ---------------------------------------------------------------------------
class GapFinding(BaseModel):
"""Ein einzelnes festgestelltes Datenloch in den recherchierten Ergebnissen.
Felder
------
finding_id : UUID
Eindeutige ID des Findings.
category : GapCategory
Kategorie der Lücke.
severity : GapSeverity
Wie kritisch die Lücke ist.
description : str
Menschliche Beschreibung.
claim_ids : list[UUID]
Betroffene Claim-IDs (leer bei allgemeinen Lücken).
affected_sources : list[UUID]
Betroffene Source-IDs (leer bei allgemeinen Lücken).
confidence : float
Wie sicher ist die Einschätzung (0-1).
recommended_target : GapTarget
Welchen Suchfokus empfiehlt die Analyse.
reason : str
Begründung für dieses Finding.
metadata : dict
Zusätzliche Kontextdaten.
created_at : datetime
Erstellungszeitpunkt.
"""
finding_id: UUID = Field(default_factory=uuid4)
category: GapCategory = Field(..., description="Kategorie der Lücke.")
severity: GapSeverity = Field(default=GapSeverity.MEDIUM)
description: str = Field(
..., min_length=1, description="Menschliche Beschreibung der Lücke."
)
claim_ids: list[UUID] = Field(
default_factory=list, description="Betroffene Claim-IDs."
)
affected_sources: list[UUID] = Field(
default_factory=list, description="Betroffene Source-IDs."
)
confidence: float = Field(
default=0.7, ge=0.0, le=1.0, description="Sicherheit der Einschätzung (0-1)."
)
recommended_target: GapTarget = Field(
default=GapTarget.GENERAL_EXPANSION,
description="Empfohlener Suchfokus.",
)
reason: str = Field(
..., min_length=1, description="Begründung für dieses Finding."
)
metadata: dict[str, Any] = Field(
default_factory=dict, description="Zusätzliche Metadaten."
)
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
)
model_config = {"frozen": True}
# ---------------------------------------------------------------------------
# GapSearchQuery
# ---------------------------------------------------------------------------
class GapSearchQuery(BaseModel):
"""Eine aus einer Lücke abgeleitete Suchanfrage für den nächsten Research-Throughlauf.
Felder
------
query : str
Die eigentliche Suchanfrage.
reason : str
Warum wird diese Suche benötigt (Bezug zum GapFinding).
target : GapTarget
Zieltyp dieser Suche.
purpose : str
Warum wird diese Suche benötigt — Bezug zum GapFinding.
category : str
primaeary_source | counter_evidence | general_expansion | fact_check
language : str
Sprache (z.B. 'de' oder 'en').
"""
query: str = Field(..., min_length=1, description="Die Suchanfrage.")
reason: str = Field(
..., min_length=1, description="Warum diese Suche benötigt wird."
)
target: GapTarget = Field(
default=GapTarget.GENERAL_EXPANSION, description="Zieltyp der Suche."
)
purpose: str = Field(
..., min_length=1, description="Zweck — Bezug zum GapFinding."
)
category: str = Field(
default="general",
description="primary_source | counter_evidence | general | fact_check",
)
language: str = Field(
default="de", min_length=1, description="Sprache der Suche."
)
model_config = {"frozen": True}
# ---------------------------------------------------------------------------
# IterationReport
# ---------------------------------------------------------------------------
class IterationReport(BaseModel):
"""Zusammenfassung der iterativen Gap-Analyse nach einem Durchlauf.
Enthält alle gefundenen Lücken, die daraus abgeleiteten Suchanfragen
und eine Zusammenfassung der Research-Statistiken.
"""
research_run_id: UUID = Field(
..., description="UUID des Research-Runs."
)
iteration_number: int = Field(
..., ge=1, description="Numer der Iteration (1-based)."
)
max_iterations: int = Field(
..., ge=1, description="Maximal erlaubte Iterationen."
)
has_gaps: bool = Field(
..., description="True wenn noch nicht alle Lücken geschlossen sind."
)
findings: list[GapFinding] = Field(
default_factory=list, description="Alle festgestellten Lücken."
)
gap_search_queries: list[GapSearchQuery] = Field(
default_factory=list,
description="Aus den Lücken abgeleitete Suchanfragen für die nächste Runde.",
)
research_statistics: dict[str, Any] = Field(
default_factory=dict,
description="Statistiken der Research-Runde (Quellen, Claims, etc.).",
)
created_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
)
model_config = {"frozen": True}