重写为 AI 情报 Agent 并接入 Jenkins 流水线

This commit is contained in:
Codex
2026-07-08 21:41:46 +08:00
commit f471c2a08a
45 changed files with 2163 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Agent orchestration package."""
+16
View File
@@ -0,0 +1,16 @@
from datetime import date
from pathlib import Path
class ReportWriter:
"""把人可读日报写入本地文件。"""
def __init__(self, base_dir: Path) -> None:
self.base_dir = base_dir
def write_daily_report(self, run_date: date, content_md: str) -> Path:
# 日报按日期落盘,便于人工浏览和备份。
self.base_dir.mkdir(parents=True, exist_ok=True)
report_path = self.base_dir / f"{run_date.isoformat()}.md"
report_path.write_text(content_md, encoding="utf-8")
return report_path
+82
View File
@@ -0,0 +1,82 @@
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.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
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 date.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,
)
signals = self.repo.save_signals(run=run, items=result.signals)
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": 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
+30
View File
@@ -0,0 +1,30 @@
from apscheduler.schedulers.background import BackgroundScheduler
from sqlalchemy.orm import Session
from app.agent.runner import AgentRunner
from app.core.config import Settings
from app.db.session import SessionLocal
def run_scheduled_job(settings: Settings) -> None:
# 定时任务不在请求生命周期内,需要自己创建和关闭数据库会话。
db: Session = SessionLocal()
try:
AgentRunner(db=db, settings=settings).run_daily()
finally:
db.close()
def build_scheduler(settings: Settings) -> BackgroundScheduler:
# APScheduler负责每日后台运行,服务启动后自动挂载任务。
scheduler = BackgroundScheduler(timezone=settings.agent_timezone)
scheduler.add_job(
run_scheduled_job,
trigger="cron",
hour=settings.agent_cron_hour,
minute=settings.agent_cron_minute,
args=[settings],
id="daily-intelligence",
replace_existing=True,
)
return scheduler
+136
View File
@@ -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"