PMFlow/backend/app/services/external_query_service/schemas.py

129 lines
5.0 KiB
Python

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"]
ExternalRetrievalStatus = Literal["evidence_found", "verifier_blocked", "no_relevant_results", "retrieval_failed"]
ExternalImpactStatus = Literal["direct_match", "indirect_only", "no_direct_impact"]
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
fit_brief_summary: str = ""
project_scope_hint: str | None = None
recommendation_goal: str | None = None
project_object_anchors: list[str] = Field(default_factory=list)
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 derive_retrieval_status(self) -> ExternalRetrievalStatus:
verifier = dict(self.verifier or {})
attempts = list((self.meta or {}).get("adapter_attempts") or [])
if self.evidence and bool(verifier.get("ok")):
return "evidence_found"
if self.evidence and not bool(verifier.get("ok")):
return "verifier_blocked"
if any(
int(item.get("evidence_count") or 0) > 0 and not bool(item.get("verifier_ok"))
for item in attempts
if isinstance(item, dict)
):
return "verifier_blocked"
statuses = {
str(item.get("status") or "").strip().lower()
for item in attempts
if isinstance(item, dict) and str(item.get("status") or "").strip()
}
if any(status in {"success", "empty"} for status in statuses):
return "no_relevant_results"
return "retrieval_failed"
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,
retrieval_status=self.derive_retrieval_status(),
impact_status="no_direct_impact",
affected_signal_sources=[],
)
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)
retrieval_status: ExternalRetrievalStatus = "retrieval_failed"
impact_status: ExternalImpactStatus = "no_direct_impact"
affected_signal_sources: list[str] = Field(default_factory=list)