Files
Bubbles/ai_providers/ai_kimi.py
zihanjian f1912a5b84 KIMI
2025-11-08 17:16:03 +08:00

267 lines
10 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import logging
import time
from typing import List
import httpx
from openai import APIConnectionError, APIError, AuthenticationError, OpenAI
try:
from function.func_summary import MessageSummary
except ImportError: # pragma: no cover - fallback when typing
MessageSummary = object
class Kimi:
"""Moonshot Kimi provider (兼容OpenAI SDK)"""
def __init__(self, conf: dict, message_summary_instance: MessageSummary = None, bot_wxid: str = None) -> None:
key = conf.get("key")
api = conf.get("api", "https://api.moonshot.cn/v1")
proxy = conf.get("proxy")
prompt = conf.get("prompt")
self.model = conf.get("model", "kimi-k2")
self.max_history_messages = conf.get("max_history_messages", 30)
self.show_reasoning = bool(conf.get("show_reasoning", False))
self.LOG = logging.getLogger("Kimi")
self.message_summary = message_summary_instance
self.bot_wxid = bot_wxid
if not self.message_summary:
self.LOG.warning("MessageSummary 实例未提供给 Kimi上下文功能将不可用")
if not self.bot_wxid:
self.LOG.warning("bot_wxid 未提供给 Kimi可能无法正确识别机器人自身消息")
if proxy:
self.client = OpenAI(api_key=key, base_url=api, http_client=httpx.Client(proxy=proxy))
else:
self.client = OpenAI(api_key=key, base_url=api)
self.system_content_msg = {
"role": "system",
"content": prompt or "你是 Kimi一个由 Moonshot AI 打造的贴心助手。"
}
def __repr__(self) -> str:
return "Kimi"
@staticmethod
def value_check(conf: dict) -> bool:
if conf and conf.get("key"):
return True
return False
def get_answer(
self,
question: str,
wxid: str,
system_prompt_override=None,
specific_max_history=None,
tools=None,
tool_handler=None,
tool_choice=None,
tool_max_iterations: int = 10
) -> str:
api_messages = []
effective_system_prompt = system_prompt_override if system_prompt_override else self.system_content_msg.get("content")
if effective_system_prompt:
api_messages.append({"role": "system", "content": effective_system_prompt})
now_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
api_messages.append({"role": "system", "content": f"Current time is: {now_time}"})
if self.message_summary and self.bot_wxid:
history = self.message_summary.get_messages(wxid)
limit_to_use = specific_max_history if specific_max_history is not None else self.max_history_messages
try:
limit_to_use = int(limit_to_use) if limit_to_use is not None else None
except (TypeError, ValueError):
limit_to_use = self.max_history_messages
if limit_to_use is not None and limit_to_use > 0:
history = history[-limit_to_use:]
elif limit_to_use == 0:
history = []
for msg in history:
role = "assistant" if msg.get("sender_wxid") == self.bot_wxid else "user"
content = msg.get("content") or ""
if not content:
continue
if role == "user":
sender_name = msg.get("sender", "未知用户")
formatted_content = f"{sender_name}: {content}"
api_messages.append({"role": role, "content": formatted_content})
else:
api_messages.append({"role": role, "content": content})
else:
self.LOG.debug(f"wxid={wxid} 无法加载历史记录message_summary 或 bot_wxid 未设置)")
if question:
api_messages.append({"role": "user", "content": question})
if tools and not tool_handler:
self.LOG.warning("Kimi: 提供了 tools 但没有 tool_handler忽略工具调用。")
tools = None
try:
response_text, reasoning_text = self._execute_with_tools(
api_messages=api_messages,
tools=tools,
tool_handler=tool_handler,
tool_choice=tool_choice,
tool_max_iterations=tool_max_iterations
)
if (
self.show_reasoning
and reasoning_text
and isinstance(reasoning_text, str)
and reasoning_text.strip()
):
reasoning_output = reasoning_text.strip()
final_answer = response_text.strip() if isinstance(response_text, str) else response_text
return f"【思考过程】\n{reasoning_output}\n\n【最终回答】\n{final_answer}"
return response_text
except AuthenticationError:
self.LOG.error("Kimi API 认证失败,请检查 API 密钥是否正确")
return "Kimi API 认证失败,请检查配置。"
except APIConnectionError:
self.LOG.error("无法连接到 Kimi API请检查网络或代理设置")
return "无法连接到 Kimi 服务,请稍后再试。"
except APIError as api_err:
self.LOG.error(f"Kimi API 返回错误:{api_err}")
return f"Kimi API 错误:{api_err}"
except Exception as exc:
self.LOG.error(f"Kimi 处理请求时出现未知错误:{exc}", exc_info=True)
return "处理请求时出现未知错误,请稍后再试。"
def _execute_with_tools(
self,
api_messages,
tools=None,
tool_handler=None,
tool_choice=None,
tool_max_iterations: int = 10
):
iterations = 0
params_base = {"model": self.model}
runtime_tools = tools if tools and isinstance(tools, list) else None
runtime_tool_choice = tool_choice
reasoning_segments: List[str] = []
while True:
params = dict(params_base)
params["messages"] = api_messages
if runtime_tools:
params["tools"] = runtime_tools
if runtime_tool_choice:
params["tool_choice"] = runtime_tool_choice
response = self.client.chat.completions.create(**params)
choice = response.choices[0]
message = choice.message
finish_reason = choice.finish_reason
reasoning_chunk = self._extract_reasoning_text(message)
if reasoning_chunk:
reasoning_segments.append(reasoning_chunk)
if (
runtime_tools
and message
and getattr(message, "tool_calls", None)
and finish_reason == "tool_calls"
and tool_handler
):
iterations += 1
api_messages.append({
"role": "assistant",
"content": message.content or "",
"tool_calls": message.tool_calls
})
if tool_max_iterations is not None and iterations > max(tool_max_iterations, 0):
api_messages.append({
"role": "system",
"content": "你已经达到允许的最大工具调用次数,请根据现有信息直接给出最终回答。"
})
runtime_tool_choice = "none"
continue
for tool_call in message.tool_calls:
tool_name = tool_call.function.name
raw_arguments = tool_call.function.arguments or "{}"
try:
parsed_arguments = json.loads(raw_arguments)
except json.JSONDecodeError:
parsed_arguments = {"_raw": raw_arguments}
try:
tool_output = tool_handler(tool_name, parsed_arguments)
except Exception as handler_exc:
self.LOG.error(f"工具 {tool_name} 执行失败: {handler_exc}", exc_info=True)
tool_output = json.dumps(
{"error": f"{tool_name} failed: {handler_exc.__class__.__name__}"},
ensure_ascii=False
)
if not isinstance(tool_output, str):
tool_output = json.dumps(tool_output, ensure_ascii=False)
api_messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": tool_output
})
runtime_tool_choice = None
continue
response_text = message.content if message and message.content else ""
if response_text.startswith("\n\n"):
response_text = response_text[2:]
response_text = response_text.replace("\n\n", "\n")
reasoning_text = "\n".join(seg for seg in reasoning_segments if seg).strip()
return response_text, reasoning_text
def _extract_reasoning_text(self, message) -> str:
"""Moonshot 在 ChatCompletionMessage 上挂载 reasoning_content 字段"""
if not message:
return ""
raw_reasoning = getattr(message, "reasoning_content", None)
if not raw_reasoning:
return ""
def _normalize_segment(segment) -> str:
if isinstance(segment, str):
return segment
if isinstance(segment, dict):
return segment.get("content") or segment.get("text") or ""
if isinstance(segment, list):
return "\n".join(filter(None, (_normalize_segment(item) for item in segment)))
return str(segment) if segment is not None else ""
if isinstance(raw_reasoning, list):
segments = []
for part in raw_reasoning:
normalized = _normalize_segment(part)
if normalized:
segments.append(normalized)
return "\n".join(segments).strip()
return _normalize_segment(raw_reasoning).strip()
__all__ = ["Kimi"]