mirror of
https://github.com/Zippland/Bubbles.git
synced 2026-02-20 01:09:51 +08:00
267 lines
10 KiB
Python
267 lines
10 KiB
Python
#!/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"]
|