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:
@@ -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}
|
||||
Reference in New Issue
Block a user