feat:新增投顾agent和nl2sqlagent

This commit is contained in:
2026-09-13 16:19:24 +08:00
parent c80c6acac0
commit 163192bf55
122 changed files with 7488 additions and 362 deletions
+158
View File
@@ -0,0 +1,158 @@
"""NL2SQL 查询运行指标,供管理员诊断和后续监控采集。"""
from __future__ import annotations
from collections import Counter
from threading import Lock
import logging
from datetime import datetime
from sqlalchemy import text
logger = logging.getLogger("nl2sql.metrics")
HISTORY_METRICS_SQL = text(
"""
SELECT
COUNT(*) AS total,
COALESCE(SUM(status = 'success'), 0) AS success,
COALESCE(SUM(status = 'failed'), 0) AS failed,
COALESCE(SUM(status = 'timeout'), 0) AS timeout,
COALESCE(AVG(elapsed_ms), 0) AS average_elapsed_ms
FROM nl2sql_query_history
WHERE (:since IS NULL OR create_time >= :since)
"""
)
class QueryMetrics:
"""进程内查询计数器,重启后清零。"""
def __init__(self, *, slow_threshold_ms: float = 500):
self.slow_threshold_ms = slow_threshold_ms
self._lock = Lock()
self._total = 0
self._success = 0
self._failed = 0
self._cache_hits = 0
self._timeouts = 0
self._slow = 0
self._rate_limited = 0
self._elapsed_total = 0.0
self._failure_reasons: Counter[str] = Counter()
def record(
self,
*,
status: str,
elapsed_ms: float | None = None,
cache_hit: bool = False,
failure_reason: str | None = None,
) -> None:
with self._lock:
self._total += 1
if status == "success":
self._success += 1
else:
self._failed += 1
if failure_reason:
self._failure_reasons[failure_reason] += 1
if status == "timeout":
self._timeouts += 1
if cache_hit:
self._cache_hits += 1
if elapsed_ms is not None and elapsed_ms >= self.slow_threshold_ms:
self._slow += 1
if elapsed_ms is not None:
self._elapsed_total += elapsed_ms
def record_rate_limited(self) -> None:
"""记录一次因配额、并发或频率限制而拒绝的请求。"""
with self._lock:
self._rate_limited += 1
def snapshot(self) -> dict[str, float | int]:
with self._lock:
total = self._total
return {
"total": total,
"success": self._success,
"failed": self._failed,
"success_rate": self._success / total if total else 0.0,
"failure_rate": self._failed / total if total else 0.0,
"cache_hits": self._cache_hits,
"cache_hit_rate": self._cache_hits / total if total else 0.0,
"timeout_count": self._timeouts,
"slow_query_count": self._slow,
"rate_limited_count": self._rate_limited,
"average_elapsed_ms": self._elapsed_total / total if total else 0.0,
"failure_reasons": dict(sorted(self._failure_reasons.items())),
}
query_metrics = QueryMetrics()
def render_prometheus(metrics: dict | None = None) -> str:
"""将聚合指标导出为无外部依赖的 Prometheus 文本格式。"""
snapshot = metrics or query_metrics.snapshot()
lines = [
"# HELP nl2sql_queries_total NL2SQL 查询总数",
"# TYPE nl2sql_queries_total counter",
f"nl2sql_queries_total {snapshot.get('total', 0)}",
"# HELP nl2sql_queries_success_total NL2SQL 成功查询数",
"# TYPE nl2sql_queries_success_total counter",
f"nl2sql_queries_success_total {snapshot.get('success', 0)}",
"# HELP nl2sql_queries_failed_total NL2SQL 失败查询数",
"# TYPE nl2sql_queries_failed_total counter",
f"nl2sql_queries_failed_total {snapshot.get('failed', 0)}",
"# HELP nl2sql_queries_timeout_total NL2SQL 超时查询数",
"# TYPE nl2sql_queries_timeout_total counter",
f"nl2sql_queries_timeout_total {snapshot.get('timeout_count', 0)}",
"# HELP nl2sql_queries_cache_hits_total NL2SQL 缓存命中数",
"# TYPE nl2sql_queries_cache_hits_total counter",
f"nl2sql_queries_cache_hits_total {snapshot.get('cache_hits', 0)}",
"# HELP nl2sql_queries_rate_limited_total NL2SQL 限流拒绝数",
"# TYPE nl2sql_queries_rate_limited_total counter",
f"nl2sql_queries_rate_limited_total {snapshot.get('rate_limited_count', 0)}",
"# HELP nl2sql_queries_slow_total NL2SQL 慢查询数",
"# TYPE nl2sql_queries_slow_total counter",
f"nl2sql_queries_slow_total {snapshot.get('slow_query_count', 0)}",
"# HELP nl2sql_queries_average_elapsed_ms NL2SQL 平均耗时毫秒",
"# TYPE nl2sql_queries_average_elapsed_ms gauge",
f"nl2sql_queries_average_elapsed_ms {snapshot.get('average_elapsed_ms', 0.0)}",
]
for reason, count in sorted((snapshot.get("failure_reasons") or {}).items()):
safe_reason = str(reason).replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
lines.append(f'nl2sql_query_failures_total{{reason="{safe_reason}"}} {count}')
return "\n".join(lines) + "\n"
async def load_history_metrics(db, *, since: datetime | None = None) -> dict[str, int | float]:
"""从查询归档表聚合指标,不读取问题、SQL 或结果行。"""
result = await db.execute(HISTORY_METRICS_SQL, {"since": since})
row = result.mappings().one()
return {
"total": int(row.get("total", 0) or 0),
"success": int(row.get("success", 0) or 0),
"failed": int(row.get("failed", 0) or 0),
"timeout": int(row.get("timeout", 0) or 0),
"average_elapsed_ms": float(row.get("average_elapsed_ms", 0) or 0),
}
async def load_history_metrics_safely(db, *, since: datetime | None = None) -> dict[str, int | float | bool]:
"""历史指标读取失败时返回明确的不可用状态。"""
try:
result = await load_history_metrics(db, since=since)
return {**result, "available": True}
except Exception: # noqa: BLE001 指标故障不能阻断管理接口
logger.warning("NL2SQL 历史指标读取失败", exc_info=True)
return {
"total": 0,
"success": 0,
"failed": 0,
"timeout": 0,
"average_elapsed_ms": 0.0,
"available": False,
}