from __future__ import annotations import json from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path from typing import Any from uuid import uuid4 from app.core.config import ROOT_DIR from app.schemas.ai import BGPBriefRecordResponse, BGPBriefRecordSummary, SituationalAnalysisResponse _BRIEF_STORAGE_DIR = ROOT_DIR / "data" / "ai" / "bgp-briefs" _METADATA_PREFIX = "" _BRIEF_TITLE = "BGP AI 简报" @dataclass(slots=True) class _StoredBrief: id: str title: str provider: str model: str request_id: str | None generated_at: str content_markdown: str facts: list[str] context: dict[str, Any] path: Path def _ensure_storage_dir() -> Path: _BRIEF_STORAGE_DIR.mkdir(parents=True, exist_ok=True) return _BRIEF_STORAGE_DIR def _build_metadata_line(metadata: dict[str, Any]) -> str: return f"{_METADATA_PREFIX}{json.dumps(metadata, ensure_ascii=False)}{_METADATA_SUFFIX}" def _parse_brief_file(path: Path) -> _StoredBrief | None: try: raw_text = path.read_text(encoding="utf-8") except OSError: return None first_line, separator, remainder = raw_text.partition("\n") if not separator or not first_line.startswith(_METADATA_PREFIX) or not first_line.endswith(_METADATA_SUFFIX): return None metadata_payload = first_line[len(_METADATA_PREFIX) : -len(_METADATA_SUFFIX)] try: metadata = json.loads(metadata_payload) except json.JSONDecodeError: return None return _StoredBrief( id=str(metadata.get("id") or path.stem), title=str(metadata.get("title") or _BRIEF_TITLE), provider=str(metadata.get("provider") or "-"), model=str(metadata.get("model") or "-"), request_id=metadata.get("request_id"), generated_at=str(metadata.get("generated_at") or datetime.fromtimestamp(path.stat().st_mtime, UTC).isoformat()), content_markdown=remainder.lstrip("\n"), facts=list(metadata.get("facts") or []), context=dict(metadata.get("context") or {}), path=path, ) def list_bgp_brief_records(limit: int = 50) -> list[BGPBriefRecordSummary]: storage_dir = _ensure_storage_dir() records: list[_StoredBrief] = [] for path in storage_dir.glob("*.md"): parsed = _parse_brief_file(path) if parsed is not None: records.append(parsed) records.sort(key=lambda item: item.generated_at, reverse=True) return [ BGPBriefRecordSummary( id=item.id, title=item.title, provider=item.provider, model=item.model, request_id=item.request_id, generated_at=item.generated_at, ) for item in records[: max(limit, 1)] ] def get_bgp_brief_record(brief_id: str) -> BGPBriefRecordResponse | None: path = _ensure_storage_dir() / f"{brief_id}.md" parsed = _parse_brief_file(path) if parsed is None: return None return BGPBriefRecordResponse( id=parsed.id, title=parsed.title, provider=parsed.provider, model=parsed.model, request_id=parsed.request_id, generated_at=parsed.generated_at, content_markdown=parsed.content_markdown, facts=parsed.facts, context=parsed.context, ) def get_latest_bgp_brief_record() -> BGPBriefRecordResponse | None: summaries = list_bgp_brief_records(limit=1) if not summaries: return None return get_bgp_brief_record(summaries[0].id) def save_bgp_brief_record( analysis: SituationalAnalysisResponse, *, request_id: str | None, facts: list[str] | None = None, context: dict[str, Any] | None = None, generated_at: datetime | None = None, ) -> BGPBriefRecordResponse: created_at = generated_at or datetime.now(UTC) brief_id = f"{created_at.strftime('%Y%m%dT%H%M%SZ')}-{uuid4().hex[:8]}" path = _ensure_storage_dir() / f"{brief_id}.md" metadata = { "id": brief_id, "title": _BRIEF_TITLE, "provider": analysis.provider, "model": analysis.model, "request_id": request_id, "generated_at": created_at.isoformat(), "facts": facts or [], "context": context or {}, } markdown_text = f"{_build_metadata_line(metadata)}\n\n{analysis.content.rstrip()}\n" path.write_text(markdown_text, encoding="utf-8") return BGPBriefRecordResponse( id=brief_id, title=_BRIEF_TITLE, provider=analysis.provider, model=analysis.model, request_id=request_id, generated_at=created_at.isoformat(), content_markdown=analysis.content, facts=facts or [], context=context or {}, )