Expose research planning information

This commit is contained in:
faligam
2026-09-07 12:17:38 +02:00
parent 5f237d535f
commit 2c532bc9bb
4 changed files with 118 additions and 1 deletions

View File

@@ -235,6 +235,14 @@ class ReportResponse(BaseModel):
generated_at: str = ""
class PlanResponse(BaseModel):
"""GET /v1/research/{id}/plan — validated planner output for a run."""
research_id: str
plan: dict[str, Any] | None = None
available: bool = False
class ErrorResponse(BaseModel):
"""Fehlerantwort."""
@@ -397,6 +405,12 @@ async def _run_pipeline(run: _ResearchRunState, request: ResearchRequest, budget
research_id = run.research_id
stage_ctx = {"research_run_id": research_id}
def store_plan(plan: dict[str, Any]) -> None:
"""Make the validated plan available as soon as planning finishes."""
run.plan = plan
run.updated_at = _now()
_save_run(run)
logger.info(
"[pipeline] Starting background research: run_id=%s query='%s'",
research_id,
@@ -416,6 +430,7 @@ async def _run_pipeline(run: _ResearchRunState, request: ResearchRequest, budget
budget_config=budget,
depth=request.depth,
priority=Priority.NORMAL,
on_plan_created=store_plan,
)
stage_ctx["stage"] = "planning"
@@ -449,6 +464,10 @@ async def _run_pipeline(run: _ResearchRunState, request: ResearchRequest, budget
logger.info("[pipeline] run_id=%s stage=synthesizing", research_id)
result = await orchestrator.run()
# The plan is structured, validated output from the planner. Persist it
# independently from the final report so it remains inspectable even
# when a later pipeline step fails.
run.plan = result.get("plan")
if result.get("success"):
run.state = ResearchRunState.COMPLETED.value
@@ -629,6 +648,21 @@ async def get_evidence(research_id: str) -> EvidenceResponse:
)
@router.get(
"/v1/research/{research_id}/plan",
response_model=PlanResponse,
summary="Get the validated research plan for a research run",
)
async def get_plan(research_id: str) -> PlanResponse:
"""Return the planner's structured strategy, never its prompt or raw output."""
run = _load_run(research_id)
return PlanResponse(
research_id=run.research_id,
plan=run.plan,
available=run.plan is not None,
)
@router.get(
"/v1/research/{research_id}/report",
response_model=ReportResponse,

View File

@@ -11,7 +11,7 @@ import asyncio
import json
import logging
import time
from collections.abc import Mapping
from collections.abc import Callable, Mapping
from datetime import datetime, timezone
from typing import Any
from uuid import UUID, uuid4
@@ -71,6 +71,7 @@ class ResearchOrchestrator:
context_budget_config: ContextBudgetConfig | None = None,
depth: str = "normal",
priority: Priority = Priority.NORMAL,
on_plan_created: Callable[[dict[str, Any]], None] | None = None,
) -> None:
"""Initialisiere den Orchestrator.
@@ -90,12 +91,16 @@ class ResearchOrchestrator:
Suchtiefe ("quick", "normal", "deep").
priority : Priority
LLM-Anfrage-Priorität für den Research-Pipeline.
on_plan_created : callable | None
Optionaler Hook, der nach der validierten Planungsphase ausgeführt
wird. Der Hook erhält ausschließlich den strukturierten Plan.
"""
self._config = config
self._research_id = research_id
self._query = query
self._depth = depth
self._priority = priority
self._on_plan_created = on_plan_created
# Budget
if budget_config is not None:
@@ -450,6 +455,7 @@ class ResearchOrchestrator:
"failed_step": step_name,
"state": self._state_machine.current_state.value,
"report": None,
"plan": self._plan,
}
# Zeit-Tracking
@@ -469,6 +475,7 @@ class ResearchOrchestrator:
"failed_step": step_name,
"state": self._state_machine.current_state.value,
"report": None,
"plan": self._plan,
}
# Alle Schritte erfolgreich → COMPLETED
@@ -480,6 +487,7 @@ class ResearchOrchestrator:
"report": self._get_report(),
"state": self._state_machine.current_state.value,
"budget_usage": self._budget_tracker.get_usage(),
"plan": self._plan,
}
elapsed = time.monotonic() - _start
@@ -545,6 +553,8 @@ class ResearchOrchestrator:
metrics.observe(H_LLM_REQUEST_DURATION, elapsed)
self._plan = plan
if self._on_plan_created is not None:
self._on_plan_created(plan)
logger.info("Planning complete: topic=%s, queries=%d",
plan.get("topic", ""), len(plan.get("queries", [])))