ai_util.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. # -*- coding: utf-8 -*-
  2. from typing import AsyncGenerator
  3. from openai import AsyncOpenAI, OpenAI
  4. from openai.types.chat.chat_completion import ChatCompletion
  5. import httpx
  6. from app.config.setting import settings
  7. from app.core.logger import log
  8. class AIClient:
  9. """
  10. AI客户端类,用于与OpenAI API交互。
  11. """
  12. def __init__(self):
  13. self.model = settings.OPENAI_MODEL
  14. # 创建一个不带冲突参数的httpx客户端
  15. self.http_client = httpx.AsyncClient(
  16. timeout=30.0,
  17. follow_redirects=True
  18. )
  19. # 使用自定义的http客户端
  20. self.client = AsyncOpenAI(
  21. api_key=settings.OPENAI_API_KEY,
  22. base_url=settings.OPENAI_BASE_URL,
  23. http_client=self.http_client
  24. )
  25. def _friendly_error_message(self, e: Exception) -> str:
  26. """将 OpenAI 或网络异常转换为友好的中文提示。"""
  27. # 尝试获取状态码与错误体
  28. status_code = getattr(e, "status_code", None)
  29. body = getattr(e, "body", None)
  30. message = None
  31. error_type = None
  32. error_code = None
  33. try:
  34. if isinstance(body, dict) and "error" in body:
  35. err = body.get("error") or {}
  36. error_type = err.get("type")
  37. error_code = err.get("code")
  38. message = err.get("message")
  39. except Exception:
  40. # 忽略解析失败
  41. pass
  42. text = str(e)
  43. msg = message or text
  44. # 特定错误映射
  45. # 欠费/账户状态异常
  46. if (error_code == "Arrearage") or (error_type == "Arrearage") or ("in good standing" in (msg or "")):
  47. return "账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。"
  48. # 鉴权失败
  49. if status_code == 401 or "invalid api key" in msg.lower():
  50. return "鉴权失败,API Key 无效或已过期。请检查系统配置中的 API Key。"
  51. # 权限不足或被拒绝
  52. if status_code == 403 or error_type in {"PermissionDenied", "permission_denied"}:
  53. return "访问被拒绝,权限不足或账号受限。请检查账户权限设置。"
  54. # 配额不足或限流
  55. if status_code == 429 or error_type in {"insufficient_quota", "rate_limit_exceeded"}:
  56. return "请求过于频繁或配额已用尽。请稍后重试或提升账户配额。"
  57. # 客户端错误
  58. if status_code == 400:
  59. return f"请求参数错误或服务拒绝:{message or '请检查输入内容。'}"
  60. # 服务端错误
  61. if status_code in {500, 502, 503, 504}:
  62. return "服务暂时不可用,请稍后重试。"
  63. # 默认兜底
  64. return f"处理您的请求时出现错误:{msg}"
  65. async def process(self, query: str) -> AsyncGenerator[str, None]:
  66. """
  67. 处理查询并返回流式响应
  68. 参数:
  69. - query (str): 用户查询。
  70. 返回:
  71. - AsyncGenerator[str, None]: 流式响应内容。
  72. """
  73. system_prompt = """你是一个有用的AI助手,可以帮助用户回答问题和提供帮助。请用中文回答用户的问题。"""
  74. try:
  75. # 使用 await 调用异步客户端
  76. response = await self.client.chat.completions.create(
  77. model=self.model,
  78. messages=[
  79. {"role": "system", "content": system_prompt},
  80. {"role": "user", "content": query}
  81. ],
  82. stream=True
  83. )
  84. # 流式返回响应
  85. async for chunk in response:
  86. if chunk.choices and chunk.choices[0].delta.content:
  87. yield chunk.choices[0].delta.content
  88. except Exception as e:
  89. # 记录详细错误,返回友好提示
  90. log.error(f"AI处理查询失败: {str(e)}")
  91. yield self._friendly_error_message(e)
  92. async def close(self) -> None:
  93. """
  94. 关闭客户端连接
  95. """
  96. if hasattr(self, 'client'):
  97. await self.client.close()
  98. if hasattr(self, 'http_client'):
  99. await self.http_client.aclose()