Expose research planning information
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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", [])))
|
||||
|
||||
Reference in New Issue
Block a user