from __future__ import annotations from datetime import datetime from hashlib import sha256 from typing import Optional from sqlalchemy import select from sqlalchemy.orm import Session from app.db.models import AgentRun, Report, Signal, Topic from app.schemas.agent import SignalItem class AgentRepository: """Database boundary for the agent runner.""" def __init__(self, db: Session) -> None: self.db = db def list_enabled_topics(self) -> list[Topic]: # 启用主题是Agent每次运行时使用的长期目标。 return list(self.db.scalars(select(Topic).where(Topic.enabled.is_(True))).all()) def create_run(self, request_payload: dict) -> AgentRun: # 运行先进入running状态,情报和日报保存完成后再标记完成。 run = AgentRun(status="running", request_payload=request_payload) self.db.add(run) self.db.flush() return run def mark_run_complete(self, run: AgentRun, raw_response: dict) -> None: run.status = "completed" run.finished_at = datetime.utcnow() run.raw_response = raw_response self.db.flush() def mark_run_failed(self, run: AgentRun, error: str) -> None: run.status = "failed" run.finished_at = datetime.utcnow() run.error = error self.db.flush() def save_signals(self, run: AgentRun, items: list[SignalItem]) -> list[Signal]: # 相同来源链接只保存一次,避免日报和仪表盘重复出现同一条情报。 saved: list[Signal] = [] for item in items: signal = self.save_or_update_signal(run=run, item=item) if signal is None: continue saved.append(signal) return saved def save_or_update_signal(self, run: AgentRun, item: SignalItem) -> Optional[Signal]: # 同一来源链接只保留一条情报,流式接口和普通接口共用这条去重规则。 source_url_hash = self._source_url_hash(item.source_url) existing = self.db.scalar(select(Signal).where(Signal.source_url_hash == source_url_hash)) if existing and existing.run_id != run.id: return None if existing: existing.topic = item.topic existing.title = item.title existing.summary = item.summary existing.source_name = item.source_name existing.published_at = item.published_at existing.importance = item.importance existing.entities = item.entities self.db.flush() return existing signal = Signal( run_id=run.id, topic=item.topic, title=item.title, summary=item.summary, source_url=item.source_url, source_url_hash=source_url_hash, source_name=item.source_name, published_at=item.published_at, importance=item.importance, entities=item.entities, ) self.db.add(signal) self.db.flush() return signal def _source_url_hash(self, source_url: str) -> str: # MySQL不适合直接唯一索引长URL,使用SHA-256作为去重键。 return sha256(source_url.encode("utf-8")).hexdigest() def save_report(self, run: AgentRun, title: str, content_md: str, file_path: str) -> Report: # 文件路径便于人工打开Markdown产物,MySQL保存可查询正文。 report = Report(run_id=run.id, title=title, content_md=content_md, file_path=file_path) self.db.add(report) self.db.flush() return report