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

@@ -317,12 +317,25 @@ class ResearchOrchestrator:
"planning",
"searching",
"fetching",
"extracting",
"analyzing",
"comparing",
"synthesizing",
]
# Gap-Analysis & iterative Suche (Stage 13)
gap_results = await self._run_gap_analysis_loop(
claims=self._claims,
sources=self._sources,
max_iterations=2,
)
if gap_results.get("gap_queries"):
self._search_results.extend(gap_results.get("gap_search_results", []))
logger.info("Gap iteration complete: %d gap queries, %d additional results",
len(gap_results.get("gap_queries", [])),
len(gap_results.get("gap_search_results", [])))
if gap_results.get("gap_claims"):
self._claims.extend(gap_results["gap_claims"])
for step_name in steps:
try:
# Budget prüfen vor jedem Schritt
@@ -841,4 +854,110 @@ class ResearchOrchestrator:
self._multi_search = None
self._llm_provider = None
self._budget_tracker = BudgetTracker(self._budget_config)
logger.info("Orchestrator reset to CREATED state")
logger.info("Orchestrator reset to CREATED state")
# ---------------------------------------------------------------
# Private: Gap Analysis Loop (Stage 13)
# ---------------------------------------------------------------
async def _run_gap_analysis_loop(
self,
claims: list[Claim],
sources: list[dict[str, Any]],
max_iterations: int,
) -> dict[str, Any]:
"""Gap-Analyse durchführen und bei Bedarf iterative Suchanfragen generieren.
Parameters
----------
claims : list[Claim]
Extrahierte Claims.
sources : list[dict]
Gesammelte Quellen.
max_iterations : int
Maximale Anzahl Iterationen.
Returns
-------
dict
Gap-Ergebnisse mit gap_queries, gap_search_results, gap_claims.
"""
from nsct.models.gap_analysis import IterationReport
from nsct.stages.stage13_gap_analysis import GapAnalysisEngine
result = {
"gap_queries": [],
"gap_search_results": [],
"gap_claims": [],
"report": None,
}
try:
engine = GapAnalysisEngine(config=self._config, max_iterations=max_iterations)
for iteration in range(1, max_iterations + 1):
report = engine.analyze(
claims=claims,
sources=sources,
iteration_number=iteration,
research_run_id=str(self._run.id) if self._run else "",
)
result["report"] = report
if not report.has_gaps:
logger.info("Gap analysis: no more gaps at iteration %d", iteration)
break
logger.info("Gap analysis iteration %d: %d findings, %d queries",
iteration, len(report.findings), len(report.gap_search_queries))
result["gap_queries"].extend(report.gap_search_queries)
# Führe Gap-Suchen aus
if report.gap_search_queries:
gap_results = await self._execute_gap_searches(
report.gap_search_queries,
max_iterations,
)
result["gap_search_results"].extend(gap_results.get("search_results", []))
if gap_results.get("claims"):
result["gap_claims"].extend(gap_results["claims"])
return result
except Exception as exc:
logger.warning("Gap analysis failed: %s", exc)
return result
async def _execute_gap_searches(
self,
gap_queries: list[Any],
_max_iterations: int,
) -> dict[str, Any]:
"""Gap-Suchanfragen ausführen und Ergebnisse sammeln."""
search_results = []
claims = []
try:
multi_search = self._get_multi_search()
search_tasks = []
for q in gap_queries:
query_text = q.query if hasattr(q, "query") else q.get("query", "")
language = q.language if hasattr(q, "language") else "de"
search_tasks.append(multi_search.search(query_text, language=language, max_results=3))
raw_results = await asyncio.gather(*search_tasks, return_exceptions=True)
for raw in raw_results:
if isinstance(raw, Exception):
logger.warning("Gap search failed: %s", raw)
continue
if isinstance(raw, list):
search_results.extend(raw)
except Exception as exc:
logger.warning("Gap search execution failed: %s", exc)
return {"search_results": search_results, "claims": claims}