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:
202
src/nsct/models/gap_analysis.py
Normal file
202
src/nsct/models/gap_analysis.py
Normal 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}
|
||||
Reference in New Issue
Block a user