from __future__ import annotations from dataclasses import asdict, dataclass, field from datetime import datetime, timezone from typing import Any def now_iso() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat() @dataclass class MemoryFact: id: str kind: str text: str evidence: str confidence: float status: str = "candidate" created_at: str = field(default_factory=now_iso) updated_at: str = field(default_factory=now_iso) @classmethod def from_dict(cls, data: dict[str, Any]) -> "MemoryFact": return cls(**data) def to_dict(self) -> dict[str, Any]: return asdict(self) @dataclass class UserMemory: facts: list[MemoryFact] = field(default_factory=list) @classmethod def from_dict(cls, data: dict[str, Any]) -> "UserMemory": return cls([MemoryFact.from_dict(item) for item in data.get("facts", [])]) def to_dict(self) -> dict[str, Any]: return {"facts": [fact.to_dict() for fact in self.facts]} def accepted(self) -> list[MemoryFact]: return [fact for fact in self.facts if fact.status == "accepted"] def candidates(self) -> list[MemoryFact]: return [fact for fact in self.facts if fact.status == "candidate"] @dataclass class MemoryExtractionResult: candidates: list[MemoryFact] @dataclass class SanitizedTask: run_id: str original_question_hash: str sanitized_query: str neutral_keywords: list[str] removed_or_hidden_terms: list[str] user_approved_context: list[str] @dataclass class RouteDecision: route: str confidence: float reasons: list[str] @dataclass class SourceCandidate: title: str url: str snippet: str extracted_text: str | None = None source_type: str = "fixture" fetch_status: str = "fixture" @classmethod def from_dict(cls, data: dict[str, Any]) -> "SourceCandidate": return cls(**data) def to_dict(self) -> dict[str, Any]: return asdict(self) @dataclass class ExternalAgentResult: route: str summary: str sources: list[SourceCandidate] = field(default_factory=list) artifacts: list[str] = field(default_factory=list) errors: list[str] = field(default_factory=list) @dataclass class RankedRecommendation: title: str url: str | None score: float matched_memory: list[str] conflicts: list[str] why_it_fits: str risks: str next_step: str @dataclass class AgentTrace: run_id: str route_decision: RouteDecision private_memory_used: list[str] sanitized_task: SanitizedTask outbound_payload_preview: str external_result_summary: str final_reasoning_summary: str def to_dict(self) -> dict[str, Any]: return asdict(self)