125 lines
5.2 KiB
Python
125 lines
5.2 KiB
Python
"""AI Agent 分析模块 - 调用 OpenAI 兼容接口分析诊断结果 (支持流式输出)"""
|
|
import json
|
|
import httpx
|
|
from backend.config import AI_API_BASE, AI_API_KEY, AI_MODEL
|
|
|
|
SYSTEM_PROMPT = """你是一位资深的 Kubernetes 运维专家和 SRE 工程师。你的任务是分析 K8S 集群诊断报告,找出根本原因并给出修复建议。
|
|
|
|
分析要求:
|
|
1. **问题定位**:明确指出每个问题的根本原因(Root Cause),不要只描述表面现象
|
|
2. **影响评估**:说明每个问题对业务的潜在影响
|
|
3. **修复方案**:给出具体的修复命令或操作步骤(kubectl 命令、YAML 修改等)
|
|
4. **优先级排序**:按紧急程度排序,先处理影响最大的问题
|
|
5. **预防建议**:给出避免类似问题再次发生的建议
|
|
|
|
输出格式:使用清晰的 Markdown,包含标题、列表、代码块。语言使用中文。"""
|
|
|
|
_HEADERS = {
|
|
"Authorization": f"Bearer {AI_API_KEY}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
async def _stream_chat(messages: list[dict]):
|
|
"""通用流式请求 - yield 文本 chunk"""
|
|
# connect=10s 快速失败; read=90s 防止 AI 服务无响应时前端无限卡死
|
|
timeout = httpx.Timeout(90, connect=10)
|
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
async with client.stream(
|
|
"POST",
|
|
f"{AI_API_BASE}/chat/completions",
|
|
headers=_HEADERS,
|
|
json={
|
|
"model": AI_MODEL,
|
|
"messages": messages,
|
|
"temperature": 0.3,
|
|
"max_tokens": 4096,
|
|
"stream": True,
|
|
},
|
|
) as resp:
|
|
# 非 2xx 直接抛异常 (由上层转成 error 事件)
|
|
resp.raise_for_status()
|
|
|
|
# 有些服务认证失败时返回 200 + JSON 错误体 (非 SSE),需要识别
|
|
ctype = resp.headers.get("content-type", "")
|
|
if "text/event-stream" not in ctype and "application/json" in ctype:
|
|
body = await resp.aread()
|
|
try:
|
|
err = json.loads(body.decode())
|
|
raise httpx.HTTPStatusError(
|
|
f"AI 服务返回错误: {err.get('error', body.decode()[:300])}",
|
|
request=resp.request,
|
|
response=resp,
|
|
)
|
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
raise httpx.HTTPStatusError(
|
|
f"AI 服务返回非流式响应: {body.decode()[:300]}",
|
|
request=resp.request,
|
|
response=resp,
|
|
)
|
|
|
|
async for line in resp.aiter_lines():
|
|
if not line.startswith("data: "):
|
|
continue
|
|
data_str = line[6:].strip()
|
|
if data_str == "[DONE]":
|
|
break
|
|
try:
|
|
chunk = json.loads(data_str)
|
|
delta = chunk["choices"][0].get("delta", {})
|
|
content = delta.get("content", "")
|
|
if content:
|
|
yield content
|
|
except (json.JSONDecodeError, KeyError, IndexError):
|
|
continue
|
|
|
|
|
|
async def analyze_with_ai_stream(report: str, question: str = ""):
|
|
"""流式 AI 分析诊断报告 - yield 文本 chunk"""
|
|
user_msg = f"以下是 K8S 集群诊断报告,请进行详细分析:\n\n{report}"
|
|
if question:
|
|
user_msg += f"\n\n用户额外问题:{question}"
|
|
|
|
messages = [
|
|
{"role": "system", "content": SYSTEM_PROMPT},
|
|
{"role": "user", "content": user_msg},
|
|
]
|
|
async for chunk in _stream_chat(messages):
|
|
yield chunk
|
|
|
|
|
|
async def chat_with_ai_stream(messages: list[dict], context: str = ""):
|
|
"""流式多轮对话 - yield 文本 chunk"""
|
|
msgs = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
if context:
|
|
msgs.append({"role": "system", "content": f"当前诊断上下文:\n{context}"})
|
|
msgs.extend(messages)
|
|
async for chunk in _stream_chat(msgs):
|
|
yield chunk
|
|
|
|
|
|
async def analyze_with_ai(report: str, question: str = "") -> dict:
|
|
"""非流式 AI 分析 (兼容旧接口)"""
|
|
try:
|
|
content = ""
|
|
async for chunk in analyze_with_ai_stream(report, question):
|
|
content += chunk
|
|
return {"success": True, "analysis": content, "model": AI_MODEL}
|
|
except httpx.HTTPStatusError as e:
|
|
return {"success": False, "analysis": f"AI 接口返回错误: {e.response.status_code} - {e.response.text[:500]}", "model": AI_MODEL}
|
|
except httpx.ConnectError:
|
|
return {"success": False, "analysis": f"无法连接 AI 服务 ({AI_API_BASE}),请检查 .env 中的 AI_API_BASE 配置", "model": AI_MODEL}
|
|
except Exception as e:
|
|
return {"success": False, "analysis": f"AI 分析失败: {str(e)}", "model": AI_MODEL}
|
|
|
|
|
|
async def chat_with_ai(messages: list[dict], context: str = "") -> dict:
|
|
"""非流式多轮对话 (兼容旧接口)"""
|
|
try:
|
|
content = ""
|
|
async for chunk in chat_with_ai_stream(messages, context):
|
|
content += chunk
|
|
return {"success": True, "reply": content}
|
|
except Exception as e:
|
|
return {"success": False, "reply": f"AI 对话失败: {str(e)}"}
|