Files
planet/backend/app/services/bgp_ai_brief.py

148 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from collections import Counter
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.bgp import BGP_SOURCES
from app.models.bgp_anomaly import BGPAnomaly
from app.models.bgp_incident import BGPIncident
from app.models.bgp_observation import BGPObservation
from app.schemas.ai import SituationalAnalysisRequest
from app.services.bgp_collectors import build_bgp_collector_coverage
def _format_counter(counter: dict[str, int], empty_text: str = "") -> str:
if not counter:
return empty_text
return "".join(f"{key} {value}" for key, value in counter.items())
def _severity_rank(value: str | None) -> int:
order = {
"critical": 0,
"high": 1,
"medium": 2,
"low": 3,
"info": 4,
}
return order.get((value or "").lower(), 99)
async def build_bgp_brief_request(
db: AsyncSession,
*,
incident_limit: int = 5,
anomaly_limit: int = 6,
collector_limit: int = 5,
) -> SituationalAnalysisRequest:
incidents_result = await db.execute(
select(BGPIncident)
.order_by(BGPIncident.created_at.desc(), BGPIncident.id.desc())
.limit(max(incident_limit, 1))
)
anomalies_result = await db.execute(
select(BGPAnomaly)
.order_by(BGPAnomaly.created_at.desc(), BGPAnomaly.id.desc())
.limit(max(anomaly_limit, 1))
)
observations_result = await db.execute(
select(BGPObservation).where(BGPObservation.source.in_(BGP_SOURCES))
)
incident_count_result = await db.execute(select(func.count(BGPIncident.id)))
anomaly_count_result = await db.execute(select(func.count(BGPAnomaly.id)))
incidents = incidents_result.scalars().all()
anomalies = anomalies_result.scalars().all()
observations = observations_result.scalars().all()
collectors = await build_bgp_collector_coverage(db, source_filter=BGP_SOURCES)
total_incidents = incident_count_result.scalar() or 0
total_anomalies = anomaly_count_result.scalar() or 0
total_observations = len(observations)
active_collectors = [item for item in collectors if item["observation_count"] > 0]
incident_status_counts = Counter((item.status or "unknown") for item in incidents)
incident_severity_counts = Counter((item.severity or "unknown") for item in incidents)
incident_type_counts = Counter((item.incident_type or "unknown") for item in incidents)
anomaly_type_counts = Counter((item.anomaly_type or "unknown") for item in anomalies)
event_type_counts = Counter((item.event_type or "unknown") for item in observations)
top_collectors = sorted(
active_collectors,
key=lambda item: (
-int(item["recent_24h_observation_count"]),
-int(item["observation_count"]),
str(item["collector"]),
),
)[: max(collector_limit, 1)]
observations_lines: list[str] = [
f"当前共有 {total_incidents} 起 BGP incidents、{total_anomalies} 条 anomalies、{total_observations} 条原始观测事件。",
f"活跃观测站 {len(active_collectors)} 个;近 24 小时事件数合计 {sum(int(item['recent_24h_observation_count']) for item in active_collectors)}",
f"最近 incidents 严重度分布:{_format_counter(dict(sorted(incident_severity_counts.items(), key=lambda item: _severity_rank(item[0]))))}",
f"最近 incidents 状态分布:{_format_counter(dict(incident_status_counts))}",
f"最近 incidents 类型分布:{_format_counter(dict(incident_type_counts.most_common(5)))}",
f"最近 anomalies 类型分布:{_format_counter(dict(anomaly_type_counts.most_common(6)))}",
f"观测事件类型分布:{_format_counter(dict(event_type_counts.most_common(6)))}",
]
if incidents:
observations_lines.append(
"最近 incident 摘要:" + "".join(
[
f"{item.incident_type} / {item.severity} / {item.status}"
f" / 前缀 {', '.join(item.affected_prefixes[:2]) if item.affected_prefixes else '-'}"
f" / 观测站 {len(item.affected_collectors or [])}"
for item in incidents
]
)
)
if anomalies:
observations_lines.append(
"最近 anomaly 摘要:" + "".join(
[
f"{item.anomaly_type} / {item.severity}"
f" / 前缀 {item.prefix or '-'}"
f" / ASN {item.new_origin_asn or item.origin_asn or '-'}"
for item in anomalies
]
)
)
if top_collectors:
observations_lines.append(
"重点观测站:" + "".join(
[
f"{item['collector']} ({', '.join([part for part in [item.get('city'), item.get('country')] if part]) or '未知位置'})"
f" / 近24h {item['recent_24h_observation_count']}"
f" / 前缀 {item['prefix_count']}"
for item in top_collectors
]
)
)
return SituationalAnalysisRequest(
title="BGP 态势 AI 简报",
objective="基于当前 BGP incidents、anomalies、原始观测事件与观测站覆盖情况生成一份面向操作员的简明态势简报突出当前风险、证据和优先动作。",
observations=observations_lines,
constraints=[
"明确区分事实、推断与建议。",
"优先指出需要立即关注的高严重度 incident 或异常模式。",
"结论应服务值班排障,不要写成泛泛的模型演示文案。",
"如果证据不足,要明确指出缺失数据。",
],
context={
"source": "bgp-overview",
"incident_total": total_incidents,
"anomaly_total": total_anomalies,
"observation_total": total_observations,
"active_collectors": len(active_collectors),
"top_incident_types": dict(incident_type_counts.most_common(5)),
"top_anomaly_types": dict(anomaly_type_counts.most_common(6)),
"top_event_types": dict(event_type_counts.most_common(6)),
},
)