191 lines
7.6 KiB
Python
191 lines
7.6 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
|
|
|
|
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"]
|
|
RecommendationEvidenceMode = Literal["advisory", "verified", "needs_clarify"]
|
|
|
|
|
|
class ExternalQueryPlan(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
plan_version: str = "v1"
|
|
query_kind: ExternalQueryKind = "unknown"
|
|
interaction_level: Literal["status", "recommendation", "draft", "execute", "clarify"] | None = None
|
|
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=10)
|
|
requires_multi_source: bool = False
|
|
clarify_on_ambiguity: bool = False
|
|
fit_brief_summary: str = ""
|
|
project_binding: int | None = None
|
|
project_title: str | None = None
|
|
project_scope_hint: str | None = None
|
|
recommendation_goal: str | None = None
|
|
project_object_anchors: list[str] = Field(default_factory=list)
|
|
signal_source_objectives: 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)
|
|
|
|
|
|
class SignalSourceRecommendationCandidate(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
candidate_ref: str | None = None
|
|
name: str = ""
|
|
display_name: str = ""
|
|
reason: str = ""
|
|
business_value: str = ""
|
|
why_now: str = ""
|
|
source_origin: str = "recommendation"
|
|
materialization_payload: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
@model_validator(mode="after")
|
|
def _normalize_fields(self) -> "SignalSourceRecommendationCandidate":
|
|
display_name = str(self.display_name or self.name).strip()
|
|
name = str(self.name or display_name).strip()
|
|
reason = str(self.reason or self.business_value or self.why_now).strip()
|
|
business_value = str(self.business_value or reason or self.why_now).strip()
|
|
why_now = str(self.why_now or reason or business_value).strip()
|
|
payload = dict(self.materialization_payload or {})
|
|
if display_name and not payload.get("display_name"):
|
|
payload["display_name"] = display_name
|
|
if str(self.source_origin or "").strip() and not payload.get("source_origin"):
|
|
payload["source_origin"] = str(self.source_origin or "").strip()
|
|
if self.candidate_ref and not payload.get("candidate_ref"):
|
|
payload["candidate_ref"] = str(self.candidate_ref)
|
|
|
|
self.display_name = display_name
|
|
self.name = name or display_name
|
|
self.reason = reason
|
|
self.business_value = business_value
|
|
self.why_now = why_now
|
|
self.materialization_payload = payload
|
|
return self
|
|
|
|
|
|
class SignalSourceRecommendationPacket(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
candidate_set_id: str = ""
|
|
project_binding: int | None = None
|
|
resource_role: Literal["signal_source"] = "signal_source"
|
|
source_kind: Literal["external", "hybrid"] = "external"
|
|
evidence_mode: RecommendationEvidenceMode = "advisory"
|
|
candidates: list[SignalSourceRecommendationCandidate] = Field(default_factory=list)
|
|
caveat: str = ""
|
|
|
|
|
|
def normalize_signal_source_recommendation_payload(value: Any) -> dict[str, Any]:
|
|
if not isinstance(value, dict) or not value:
|
|
return {}
|
|
try:
|
|
return SignalSourceRecommendationPacket.model_validate(value).model_dump()
|
|
except Exception:
|
|
return {}
|