from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field ExternalQueryKind = Literal["weather", "news", "company", "model", "market", "generic_web", "unknown"] ExternalAnswerContract = Literal["direct_fact", "summary_with_sources", "compare_with_sources", "clarify"] class ExternalQueryPlan(BaseModel): model_config = ConfigDict(extra="ignore") plan_version: str = "v1" query_kind: ExternalQueryKind = "unknown" answer_contract: ExternalAnswerContract = "summary_with_sources" freshness_required: bool = False topic: str = "" subject_entities: list[str] = Field(default_factory=list) location_hint: str | None = None time_range_hint: str | None = None preferred_domains: list[str] = Field(default_factory=list) search_queries: list[str] = Field(default_factory=list) result_limit: int = Field(default=3, ge=1, le=5) requires_multi_source: bool = False clarify_on_ambiguity: bool = False class ExternalEvidenceItem(BaseModel): model_config = ConfigDict(extra="ignore") source_type: str = "url" source_label: str = "" title: str = "" target_url: str = "" snippet: str = "" retrieved_at: str | None = None freshness_score: float = 0.0 authority_score: float = 0.0 grounded_fields: list[str] = Field(default_factory=list) raw_metadata: dict[str, Any] = Field(default_factory=dict) class ExternalAdapterResult(BaseModel): model_config = ConfigDict(extra="ignore") adapter_name: str status: str = "empty" evidence: list[ExternalEvidenceItem] = Field(default_factory=list) meta: dict[str, Any] = Field(default_factory=dict) class ExternalQueryExecution(BaseModel): model_config = ConfigDict(extra="ignore") plan: ExternalQueryPlan adapter_name: str evidence: list[ExternalEvidenceItem] = Field(default_factory=list) verifier: dict[str, Any] = Field(default_factory=dict) planner_meta: dict[str, Any] = Field(default_factory=dict) meta: dict[str, Any] = Field(default_factory=dict) def to_context_packet(self, *, question: str) -> "ExternalContextPacket": verifier = dict(self.verifier or {}) uncertainties: list[str] = [] reason_code = str(verifier.get("reason_code") or "").strip() if reason_code: uncertainties.append(reason_code) for item in list(verifier.get("missing_fields") or []): normalized = str(item or "").strip() if normalized and normalized not in uncertainties: uncertainties.append(normalized) return ExternalContextPacket( question=str(question or "").strip(), plan=self.plan, adapter_attempts=list((self.meta or {}).get("adapter_attempts") or []), verified_evidence=list(self.evidence or []) if bool(verifier.get("ok")) else [], verifier=verifier, uncertainties=uncertainties, ) class ExternalContextPacket(BaseModel): model_config = ConfigDict(extra="ignore") question: str = "" plan: ExternalQueryPlan adapter_attempts: list[dict[str, Any]] = Field(default_factory=list) verified_evidence: list[ExternalEvidenceItem] = Field(default_factory=list) verifier: dict[str, Any] = Field(default_factory=dict) uncertainties: list[str] = Field(default_factory=list)