重写为 AI 情报 Agent 并接入 Jenkins 流水线
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user