from __future__ import annotations import logging from datetime import date from typing import Optional from sqlalchemy.orm import Session from app.agent.report_writer import ReportWriter from app.core.config import Settings from app.core.timezone import beijing_today from app.db.repository import AgentRepository from app.integrations.github_client import GitHubHotProjectClient from app.integrations.news_search_client import NewsSearchClient from app.llm.grok_client import GrokIntelligenceClient from app.schemas.agent import RunResponse, SignalItem logger = logging.getLogger(__name__) class AgentRunner: """Coordinates source collection, persistence, and report writing.""" def __init__(self, db: Session, settings: Settings) -> None: self.repo = AgentRepository(db) self.db = db self.github = GitHubHotProjectClient(settings) self.news = NewsSearchClient(settings) self.grok = GrokIntelligenceClient(settings) self.writer = ReportWriter(settings.report_dir) def run_daily(self, run_date: Optional[date] = None) -> RunResponse: # Runner统一编排候选采集、模型研判和持久化,保证一次运行只有一种业务路径。 current_date = run_date or beijing_today() request_payload = { "run_date": current_date.isoformat(), "mode": "grok_agent", } run = self.repo.create_run(request_payload=request_payload) logger.info("agent_run_started run_id=%s run_date=%s mode=grok_agent", run.id, current_date) try: topics = self.repo.list_enabled_topics() news_candidates = self.news.collect_ai_news(run_date=current_date) github_projects = self.github.collect_hot_projects(run_date=current_date) result = self.grok.collect_daily_intelligence( topics=topics, news_candidates=news_candidates, github_projects=github_projects, run_date=current_date, ) signal_items = self._compose_run_signals( news_candidates=news_candidates, github_projects=github_projects, model_signals=result.signals, ) signals = self.repo.save_signals(run=run, items=signal_items) report_path = self.writer.write_daily_report(current_date, result.report_markdown) self.repo.save_report( run=run, title=result.title, content_md=result.report_markdown, file_path=str(report_path), ) self.repo.mark_run_complete( run=run, raw_response={ "mode": "grok_agent", "topic_count": len(topics), "news_candidate_count": len(news_candidates), "github_project_count": len(github_projects), "model_signal_count": len(result.signals), "saved_signal_count": len(signal_items), "model": self.grok.settings.llm_model, "model_response": result.raw_response, }, ) self.db.commit() logger.info( "agent_run_completed run_id=%s signals=%s report_path=%s", run.id, len(signals), report_path, ) return RunResponse(run_id=run.id, status=run.status, report_path=str(report_path)) except Exception as exc: self.repo.mark_run_failed(run=run, error=str(exc)) self.db.commit() logger.exception("agent_run_failed run_id=%s error=%s", run.id, exc) raise def _compose_run_signals( self, news_candidates: list[SignalItem], github_projects: list[SignalItem], model_signals: list[SignalItem], ) -> list[SignalItem]: # 入库清单以采集候选为准,模型同链接结果只增强摘要和重要度,避免日报模型漏选导致信号消失。 model_by_url = {item.source_url: item for item in model_signals} composed = [] for candidate in [*news_candidates, *github_projects]: model_item = model_by_url.get(candidate.source_url) if model_item is None: composed.append(candidate) continue composed.append( candidate.model_copy( update={ "topic": model_item.topic, "summary": model_item.summary, "importance": model_item.importance, "entities": model_item.entities or candidate.entities, } ) ) return composed