from __future__ import annotations import json import logging from datetime import date from typing import Iterator, Optional from app.agent.report_writer import ReportWriter from app.core.config import Settings from app.db.models import AgentRun, Signal from app.db.repository import AgentRepository from app.db.session import SessionLocal from app.integrations.github_client import GitHubHotProjectClient from app.integrations.news_search_client import NewsSearchClient from app.llm.grok_client import GrokIntelligenceClient logger = logging.getLogger(__name__) def stream_daily_agent_events(settings: Settings, run_date: Optional[date] = None) -> Iterator[str]: """以SSE方式运行每日Agent,并在模型完成后逐条发送结构化情报。""" db = SessionLocal() repo = AgentRepository(db) current_date = run_date or date.today() run: Optional[AgentRun] = None try: request_payload = { "run_date": current_date.isoformat(), "mode": "grok_agent_stream", } run = repo.create_run(request_payload=request_payload) db.commit() logger.info("agent_stream_started run_id=%s run_date=%s mode=grok_agent", run.id, current_date) yield _sse("started", {"run_id": run.id, "status": run.status}) news = NewsSearchClient(settings) github = GitHubHotProjectClient(settings) grok = GrokIntelligenceClient(settings) topics = repo.list_enabled_topics() yield _sse("stage", {"run_id": run.id, "stage": "news"}) news_candidates = news.collect_ai_news(run_date=current_date) yield _sse("stage", {"run_id": run.id, "stage": "github"}) github_projects = github.collect_hot_projects(run_date=current_date) yield _sse("stage", {"run_id": run.id, "stage": "grok"}) result = grok.collect_daily_intelligence( topics=topics, news_candidates=news_candidates, github_projects=github_projects, run_date=current_date, ) saved_count = 0 final_signals = [] for item in result.signals: # 模型一次性完成筛选和摘要,SSE只负责把最终情报逐条落库并推给前端。 signal = repo.save_or_update_signal(run=run, item=item) if signal is None: continue db.commit() db.refresh(signal) final_signals.append(signal) saved_count += 1 yield _sse("candidate", {"signal": _signal_payload(signal), "saved_count": saved_count}) writer = ReportWriter(settings.report_dir) report_path = writer.write_daily_report(current_date, result.report_markdown) repo.save_report( run=run, title=result.title, content_md=result.report_markdown, file_path=str(report_path), ) repo.mark_run_complete( run=run, raw_response={ "mode": "grok_agent_stream", "topic_count": len(topics), "news_candidate_count": len(news_candidates), "github_project_count": len(github_projects), "model": settings.llm_model, "model_response": result.raw_response, }, ) db.commit() logger.info( "agent_stream_completed run_id=%s candidates=%s final_signals=%s report_path=%s", run.id, saved_count, len(final_signals), report_path, ) yield _sse( "completed", { "run_id": run.id, "status": "completed", "candidate_count": saved_count, "final_signal_count": len(final_signals), "report_path": str(report_path), }, ) except Exception as exc: if run is not None: repo.mark_run_failed(run=run, error=str(exc)) db.commit() logger.exception("agent_stream_failed run_id=%s error=%s", run.id, exc) yield _sse("failed", {"run_id": run.id, "status": "failed", "error": str(exc)}) else: logger.exception("agent_stream_failed_before_run error=%s", exc) yield _sse("failed", {"run_id": None, "status": "failed", "error": str(exc)}) finally: db.close() def _signal_payload(signal: Signal) -> dict: # SSE载荷只使用JSON安全字段,方便前端直接追加到列表。 return { "id": signal.id, "run_id": signal.run_id, "topic": signal.topic, "title": signal.title, "summary": signal.summary, "source_url": signal.source_url, "source_name": signal.source_name, "published_at": signal.published_at.isoformat() if signal.published_at else None, "importance": signal.importance, "entities": signal.entities, "created_at": signal.created_at.isoformat() if signal.created_at else None, } def _sse(event: str, data: dict) -> str: # 标准SSE帧让前端可以通过流式请求消费事件。 payload = json.dumps(data, ensure_ascii=False) return f"event: {event}\ndata: {payload}\n\n"