mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 08:33:53 +08:00
Merge branch 'py-bump-test' into dependabot/pip/main/xiaozhi-server/websockets-15.0.1
This commit is contained in:
@@ -67,7 +67,6 @@
|
||||
|
||||
<context>
|
||||
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||
- **设备ID:** {{device_id}}
|
||||
- **当前时间:** {{current_time}}
|
||||
- **今天日期:** {{today_date}} ({{today_weekday}})
|
||||
- **今天农历:** {{lunar_date}}
|
||||
|
||||
@@ -147,7 +147,15 @@ plugins:
|
||||
- ".wav"
|
||||
- ".p3"
|
||||
refresh_time: 300 # 刷新音乐列表的时间间隔,单位为秒
|
||||
|
||||
search_from_ragflow:
|
||||
# 知识库的描述信息,方便大语言模型知道什么时候调用
|
||||
description: "当用户问xxx时,调用本方法,使用知识库中的信息回答问题"
|
||||
# ragflow接口配置
|
||||
base_url: "http://192.168.0.8"
|
||||
# ragflow api访问令牌
|
||||
api_key: "ragflow-xxx"
|
||||
# ragflow知识库id
|
||||
dataset_ids: ["123456789"]
|
||||
# 声纹识别配置
|
||||
voiceprint:
|
||||
# 声纹接口地址
|
||||
@@ -239,6 +247,7 @@ Intent:
|
||||
functions:
|
||||
- change_role
|
||||
- get_weather
|
||||
# - search_from_ragflow
|
||||
# - get_news_from_chinanews
|
||||
- get_news_from_newsnow
|
||||
# play_music是服务器自带的音乐播放,hass_play_music是通过home assistant控制的独立外部程序音乐播放
|
||||
|
||||
@@ -68,6 +68,9 @@ def get_config_from_api(config):
|
||||
"vision_explain": config["server"].get("vision_explain", ""),
|
||||
"auth_key": config["server"].get("auth_key", ""),
|
||||
}
|
||||
# 如果服务器没有prompt_template,则从本地配置读取
|
||||
if not config_data.get("prompt_template"):
|
||||
config_data["prompt_template"] = config.get("prompt_template")
|
||||
return config_data
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
from datetime import datetime
|
||||
|
||||
SERVER_VERSION = "0.8.5"
|
||||
SERVER_VERSION = "0.8.7"
|
||||
_logger_initialized = False
|
||||
|
||||
|
||||
|
||||
@@ -22,4 +22,6 @@ manager-api:
|
||||
# 如果使用docker部署,请使用填写成 http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||
url: http://127.0.0.1:8002/xiaozhi
|
||||
# 你的manager-api的token,就是刚才复制出来的server.secret
|
||||
secret: 你的server.secret值
|
||||
secret: 你的server.secret值
|
||||
# 默认系统提示词模板文件
|
||||
prompt_template: agent-base-prompt.txt
|
||||
@@ -798,9 +798,9 @@ class ConnectionHandler:
|
||||
|
||||
if tools_call is not None and len(tools_call) > 0:
|
||||
tool_call_flag = True
|
||||
if tools_call[0].id is not None:
|
||||
if tools_call[0].id is not None and tools_call[0].id != "":
|
||||
function_id = tools_call[0].id
|
||||
if tools_call[0].function.name is not None:
|
||||
if tools_call[0].function.name is not None and tools_call[0].function.name != "":
|
||||
function_name = tools_call[0].function.name
|
||||
if tools_call[0].function.arguments is not None:
|
||||
function_arguments += tools_call[0].function.arguments
|
||||
|
||||
@@ -94,30 +94,36 @@ async def sendAudio(conn, audios, frame_duration=60):
|
||||
send_delay = conn.config.get("tts_audio_send_delay", -1) / 1000.0
|
||||
|
||||
if isinstance(audios, bytes):
|
||||
if conn.client_abort:
|
||||
return
|
||||
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
|
||||
# 获取或初始化流控状态
|
||||
if not hasattr(conn, "audio_flow_control"):
|
||||
# 重置流控状态,第一次读取和会话发生转变时
|
||||
if not hasattr(conn, "audio_flow_control") or conn.audio_flow_control.get("sentence_id") != conn.sentence_id:
|
||||
conn.audio_flow_control = {
|
||||
"last_send_time": 0,
|
||||
"packet_count": 0,
|
||||
"start_time": time.perf_counter(),
|
||||
"sequence": 0, # 添加序列号
|
||||
"sentence_id": conn.sentence_id,
|
||||
}
|
||||
|
||||
if conn.client_abort:
|
||||
return
|
||||
|
||||
conn.last_activity_time = time.time() * 1000
|
||||
|
||||
# 预缓冲:前5个包直接发送,不做延迟
|
||||
pre_buffer_count = 5
|
||||
flow_control = conn.audio_flow_control
|
||||
current_time = time.perf_counter()
|
||||
|
||||
if send_delay > 0:
|
||||
|
||||
if flow_control["packet_count"] < pre_buffer_count:
|
||||
# 预缓冲阶段,直接发送不延迟
|
||||
pass
|
||||
elif send_delay > 0:
|
||||
# 使用固定延迟
|
||||
await asyncio.sleep(send_delay)
|
||||
else:
|
||||
# 计算预期发送时间
|
||||
effective_packet = flow_control["packet_count"] - pre_buffer_count
|
||||
expected_time = flow_control["start_time"] + (
|
||||
flow_control["packet_count"] * frame_duration / 1000
|
||||
effective_packet * frame_duration / 1000
|
||||
)
|
||||
delay = expected_time - current_time
|
||||
if delay > 0:
|
||||
@@ -150,7 +156,7 @@ async def sendAudio(conn, audios, frame_duration=60):
|
||||
play_position = 0
|
||||
|
||||
# 执行预缓冲
|
||||
pre_buffer_frames = min(3, len(audios))
|
||||
pre_buffer_frames = min(5, len(audios))
|
||||
for i in range(pre_buffer_frames):
|
||||
if conn.conn_from_mqtt_gateway:
|
||||
# 计算时间戳和序列号
|
||||
|
||||
@@ -17,7 +17,6 @@ class LLMProvider(LLMProviderBase):
|
||||
self.base_url = config.get("base_url")
|
||||
else:
|
||||
self.base_url = config.get("url")
|
||||
# 增加timeout的配置项,单位为秒
|
||||
timeout = config.get("timeout", 300)
|
||||
self.timeout = int(timeout) if timeout else 300
|
||||
|
||||
@@ -48,8 +47,18 @@ class LLMProvider(LLMProviderBase):
|
||||
logger.bind(tag=TAG).error(model_key_msg)
|
||||
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=httpx.Timeout(self.timeout))
|
||||
|
||||
@staticmethod
|
||||
def normalize_dialogue(dialogue):
|
||||
"""自动修复 dialogue 中缺失 content 的消息"""
|
||||
for msg in dialogue:
|
||||
if "role" in msg and "content" not in msg:
|
||||
msg["content"] = ""
|
||||
return dialogue
|
||||
|
||||
def response(self, session_id, dialogue, **kwargs):
|
||||
try:
|
||||
dialogue = self.normalize_dialogue(dialogue)
|
||||
|
||||
responses = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=dialogue,
|
||||
@@ -65,17 +74,11 @@ class LLMProvider(LLMProviderBase):
|
||||
is_active = True
|
||||
for chunk in responses:
|
||||
try:
|
||||
# 检查是否存在有效的choice且content不为空
|
||||
delta = (
|
||||
chunk.choices[0].delta
|
||||
if getattr(chunk, "choices", None)
|
||||
else None
|
||||
)
|
||||
content = delta.content if hasattr(delta, "content") else ""
|
||||
delta = chunk.choices[0].delta if getattr(chunk, "choices", None) else None
|
||||
content = getattr(delta, "content", "") if delta else ""
|
||||
except IndexError:
|
||||
content = ""
|
||||
if content:
|
||||
# 处理标签跨多个chunk的情况
|
||||
if "<think>" in content:
|
||||
is_active = False
|
||||
content = content.split("<think>")[0]
|
||||
@@ -90,17 +93,18 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
try:
|
||||
dialogue = self.normalize_dialogue(dialogue)
|
||||
|
||||
stream = self.client.chat.completions.create(
|
||||
model=self.model_name, messages=dialogue, stream=True, tools=functions
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
# 检查是否存在有效的choice且content不为空
|
||||
if getattr(chunk, "choices", None):
|
||||
yield chunk.choices[0].delta.content, chunk.choices[
|
||||
0
|
||||
].delta.tool_calls
|
||||
# 存在 CompletionUsage 消息时,生成 Token 消耗 log
|
||||
delta = chunk.choices[0].delta
|
||||
content = getattr(delta, "content", "")
|
||||
tool_calls = getattr(delta, "tool_calls", None)
|
||||
yield content, tool_calls
|
||||
elif isinstance(getattr(chunk, "usage", None), CompletionUsage):
|
||||
usage_info = getattr(chunk, "usage", None)
|
||||
logger.bind(tag=TAG).info(
|
||||
|
||||
@@ -349,11 +349,6 @@ async def call_mcp_endpoint_tool(
|
||||
raise ValueError(f"参数处理失败: {str(e)}")
|
||||
raise e
|
||||
|
||||
# 加入MAC地址
|
||||
if mcp_client.conn and mcp_client.conn.device_id:
|
||||
arguments['mac_address'] = mcp_client.conn.device_id
|
||||
logger.bind(tag=TAG).info(f"已将设备MAC地址 {mcp_client.conn.device_id} 加入到MCP接入点工具调用参数中")
|
||||
|
||||
actual_name = mcp_client.name_mapping.get(tool_name, tool_name)
|
||||
payload = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -363,7 +358,9 @@ async def call_mcp_endpoint_tool(
|
||||
}
|
||||
|
||||
message = json.dumps(payload)
|
||||
logger.bind(tag=TAG).info(f"发送MCP接入点工具调用请求: {actual_name},参数: {json.dumps(arguments, ensure_ascii=False)}")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"发送MCP接入点工具调用请求: {actual_name},参数: {json.dumps(arguments, ensure_ascii=False)}"
|
||||
)
|
||||
await mcp_client.send_message(message)
|
||||
|
||||
try:
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import Optional, List, Dict, Any
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
from config.logger import setup_logging
|
||||
from core.utils.util import sanitize_tool_name
|
||||
|
||||
@@ -172,10 +173,33 @@ class ServerMCPClient:
|
||||
if "API_ACCESS_TOKEN" in self.config:
|
||||
headers["Authorization"] = f"Bearer {self.config['API_ACCESS_TOKEN']}"
|
||||
self.logger.bind(tag=TAG).warning(f"你正在使用旧过时的配置 API_ACCESS_TOKEN ,请在.mcp_server_settings.json中将API_ACCESS_TOKEN直接设置在headers中,例如 'Authorization': 'Bearer API_ACCESS_TOKEN'")
|
||||
sse_r, sse_w = await stack.enter_async_context(
|
||||
sse_client(self.config["url"], headers=headers, timeout=self.config.get("timeout", 5), sse_read_timeout=self.config.get("sse_read_timeout", 60 * 5))
|
||||
)
|
||||
read_stream, write_stream = sse_r, sse_w
|
||||
|
||||
# 根据transport类型选择不同的客户端,默认为SSE
|
||||
transport_type = self.config.get("transport", "sse")
|
||||
|
||||
if transport_type == "streamable-http" or transport_type == "http":
|
||||
# 使用 Streamable HTTP 传输
|
||||
http_r, http_w, get_session_id = await stack.enter_async_context(
|
||||
streamablehttp_client(
|
||||
url=self.config["url"],
|
||||
headers=headers,
|
||||
timeout=self.config.get("timeout", 30),
|
||||
sse_read_timeout=self.config.get("sse_read_timeout", 60 * 5),
|
||||
terminate_on_close=self.config.get("terminate_on_close", True)
|
||||
)
|
||||
)
|
||||
read_stream, write_stream = http_r, http_w
|
||||
else:
|
||||
# 使用传统的 SSE 传输
|
||||
sse_r, sse_w = await stack.enter_async_context(
|
||||
sse_client(
|
||||
url=self.config["url"],
|
||||
headers=headers,
|
||||
timeout=self.config.get("timeout", 5),
|
||||
sse_read_timeout=self.config.get("sse_read_timeout", 60 * 5)
|
||||
)
|
||||
)
|
||||
read_stream, write_stream = sse_r, sse_w
|
||||
|
||||
else:
|
||||
raise ValueError("MCP客户端配置必须包含'command'或'url'")
|
||||
|
||||
@@ -71,6 +71,19 @@ class ServerPluginExecutor(ToolExecutor):
|
||||
for func_name in all_required_functions:
|
||||
func_item = all_function_registry.get(func_name)
|
||||
if func_item:
|
||||
# 从函数注册中获取描述
|
||||
fun_description = (
|
||||
self.config.get("plugins", {})
|
||||
.get(func_name, {})
|
||||
.get("description", "")
|
||||
)
|
||||
if fun_description is not None and len(fun_description) > 0:
|
||||
if "function" in func_item.description and isinstance(
|
||||
func_item.description["function"], dict
|
||||
):
|
||||
func_item.description["function"][
|
||||
"description"
|
||||
] = fun_description
|
||||
tools[func_name] = ToolDefinition(
|
||||
name=func_name,
|
||||
description=func_item.description,
|
||||
|
||||
@@ -66,7 +66,9 @@ class PromptManager:
|
||||
def _load_base_template(self):
|
||||
"""加载基础提示词模板"""
|
||||
try:
|
||||
template_path = self.config.get("prompt_template", "agent-base-prompt.txt")
|
||||
template_path = self.config.get("prompt_template", None)
|
||||
if not template_path:
|
||||
template_path = "agent-base-prompt.txt"
|
||||
cache_key = f"prompt_template:{template_path}"
|
||||
|
||||
# 先从缓存获取
|
||||
@@ -117,8 +119,12 @@ class PromptManager:
|
||||
|
||||
def _get_current_time_info(self) -> tuple:
|
||||
"""获取当前时间信息"""
|
||||
from .current_time import get_current_date, get_current_weekday, get_current_lunar_date
|
||||
|
||||
from .current_time import (
|
||||
get_current_date,
|
||||
get_current_weekday,
|
||||
get_current_lunar_date,
|
||||
)
|
||||
|
||||
today_date = get_current_date()
|
||||
today_weekday = get_current_weekday()
|
||||
lunar_date = get_current_lunar_date() + "\n"
|
||||
@@ -192,9 +198,7 @@ class PromptManager:
|
||||
|
||||
try:
|
||||
# 获取最新的时间信息(不缓存)
|
||||
today_date, today_weekday, lunar_date = (
|
||||
self._get_current_time_info()
|
||||
)
|
||||
today_date, today_weekday, lunar_date = self._get_current_time_info()
|
||||
|
||||
# 获取缓存的上下文信息
|
||||
local_address = ""
|
||||
@@ -225,7 +229,9 @@ class PromptManager:
|
||||
weather_info=weather_info,
|
||||
emojiList=EMOJI_List,
|
||||
device_id=device_id,
|
||||
*args, **kwargs
|
||||
client_ip=client_ip,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
device_cache_key = f"device_prompt:{device_id}"
|
||||
self.cache_manager.set(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"后面不断测试补充好用的mcp服务,欢迎大家一起补充。",
|
||||
"记得删除注释行,des属性仅为说明,不会被解析。",
|
||||
"des和link属性,仅为说明安装方式,方便大家查看原始链接,不是必须项。",
|
||||
"当前支持stdio/sse两种模式。"
|
||||
"当前支持三种传输模式:stdio(标准输入输出), sse(Server-Sent Events), streamable-http(流式HTTP)。"
|
||||
],
|
||||
"mcpServers": {
|
||||
"Home Assistant": {
|
||||
@@ -41,7 +41,16 @@
|
||||
"url": "http://localhost:8080/sse",
|
||||
"headers": {
|
||||
"Authorization": "Bearer YOUR TOKEN"
|
||||
}
|
||||
},
|
||||
"des": "使用SSE传输模式(默认)"
|
||||
},
|
||||
"streamable-http-mcp-server": {
|
||||
"url": "http://localhost:8000/mcp",
|
||||
"transport": "streamable-http",
|
||||
"headers": {
|
||||
"Authorization": "Bearer YOUR TOKEN"
|
||||
},
|
||||
"des": "使用Streamable HTTP传输模式,适用于生产环境的Web部署"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import requests
|
||||
import sys
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
# 定义基础的函数描述模板
|
||||
SEARCH_FROM_RAGFLOW_FUNCTION_DESC = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_from_ragflow",
|
||||
"description": "从知识库中查询信息",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"question": {"type": "string", "description": "查询的问题"}},
|
||||
"required": ["question"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@register_function(
|
||||
"search_from_ragflow", SEARCH_FROM_RAGFLOW_FUNCTION_DESC, ToolType.SYSTEM_CTL
|
||||
)
|
||||
def search_from_ragflow(conn, question=None):
|
||||
# 确保字符串参数正确处理编码
|
||||
if question and isinstance(question, str):
|
||||
# 确保问题参数是UTF-8编码的字符串
|
||||
pass
|
||||
else:
|
||||
question = str(question) if question is not None else ""
|
||||
|
||||
base_url = conn.config["plugins"]["search_from_ragflow"].get("base_url", "")
|
||||
api_key = conn.config["plugins"]["search_from_ragflow"].get("api_key", "")
|
||||
dataset_ids = conn.config["plugins"]["search_from_ragflow"].get("dataset_ids", [])
|
||||
|
||||
url = base_url + "/api/v1/retrieval"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
|
||||
# 确保payload中的字符串都是UTF-8编码
|
||||
payload = {"question": question, "dataset_ids": dataset_ids}
|
||||
|
||||
try:
|
||||
# 使用ensure_ascii=False确保JSON序列化时正确处理中文
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
verify=False,
|
||||
)
|
||||
|
||||
# 显式设置响应的编码为utf-8
|
||||
response.encoding = "utf-8"
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
# 先获取文本内容,然后手动处理JSON解码
|
||||
response_text = response.text
|
||||
import json
|
||||
|
||||
result = json.loads(response_text)
|
||||
|
||||
if result.get("code") != 0:
|
||||
error_detail = response.get("error", {}).get("detail", "")
|
||||
# 安全地记录错误信息
|
||||
logger.bind(tag=TAG).error(
|
||||
"从RAGflow获取信息失败,原因:%s", str(error_detail)
|
||||
)
|
||||
return ActionResponse(Action.RESPONSE, None, "RAG接口返回异常")
|
||||
|
||||
chunks = result.get("data", {}).get("chunks", [])
|
||||
contents = []
|
||||
for chunk in chunks:
|
||||
content = chunk.get("content", "")
|
||||
if content:
|
||||
# 安全地处理内容字符串
|
||||
if isinstance(content, str):
|
||||
contents.append(content)
|
||||
elif isinstance(content, bytes):
|
||||
contents.append(content.decode("utf-8", errors="replace"))
|
||||
else:
|
||||
contents.append(str(content))
|
||||
|
||||
# 构建适合大模型的上下文内容(每段前加编号,段间两个换行)
|
||||
context_text = "\n\n".join(
|
||||
f"{i+1}. {c.strip()}" for i, c in enumerate(contents[:5])
|
||||
)
|
||||
if not context_text:
|
||||
context_text = "根据知识库查询结果,没有相关信息。"
|
||||
return ActionResponse(Action.REQLLM, context_text, None)
|
||||
|
||||
except Exception as e:
|
||||
# 使用安全的方式记录异常,避免编码问题
|
||||
logger.bind(tag=TAG).error("从RAGflow获取信息失败,原因:%s", str(e))
|
||||
return ActionResponse(Action.RESPONSE, None, "RAG接口返回异常")
|
||||
@@ -1,37 +1,40 @@
|
||||
pyyml==0.0.2
|
||||
# 本项目推荐环境是python3.10
|
||||
# 以下暂时不推荐升级的依赖
|
||||
torch==2.2.2
|
||||
silero_vad==6.0.0
|
||||
torchaudio==2.2.2
|
||||
# 以下是可升级的依赖
|
||||
pyyml==0.0.2
|
||||
silero_vad==6.1.0
|
||||
websockets==15.0.1
|
||||
opuslib_next==1.1.2
|
||||
opuslib_next==1.1.5
|
||||
numpy==1.26.4
|
||||
pydub==0.25.1
|
||||
funasr==1.2.3
|
||||
torchaudio==2.2.2
|
||||
openai==2.5.0
|
||||
funasr==1.2.7
|
||||
openai==2.7.1
|
||||
google-generativeai==0.8.5
|
||||
edge_tts==7.0.0
|
||||
httpx==0.27.2
|
||||
aiohttp==3.12.15
|
||||
aiohttp_cors==0.7.0
|
||||
ormsgpack==1.11.0
|
||||
ruamel.yaml==0.18.15
|
||||
edge_tts==7.2.3
|
||||
httpx==0.28.1
|
||||
aiohttp==3.13.2
|
||||
aiohttp_cors==0.8.1
|
||||
ormsgpack==1.12.0
|
||||
ruamel.yaml==0.18.16
|
||||
loguru==0.7.3
|
||||
requests==2.32.5
|
||||
cozepy==0.19.0
|
||||
cozepy==0.20.0
|
||||
mem0ai==1.0.0
|
||||
bs4==0.0.2
|
||||
modelscope==1.23.2
|
||||
sherpa_onnx==1.12.11
|
||||
mcp==1.13.1
|
||||
sherpa_onnx==1.12.15
|
||||
mcp==1.20.0
|
||||
cnlunar==0.2.0
|
||||
PySocks==1.7.1
|
||||
dashscope==1.24.6
|
||||
baidu-aip==4.16.13
|
||||
chardet==5.2.0
|
||||
aioconsole==0.8.1
|
||||
aioconsole==0.8.2
|
||||
markitdown==0.1.3
|
||||
mcp-proxy==0.8.2
|
||||
PyJWT==2.8.0
|
||||
mcp-proxy==0.10.0
|
||||
PyJWT==2.10.1
|
||||
psutil==7.0.0
|
||||
portalocker==3.2.0
|
||||
Jinja2==3.1.6
|
||||
|
||||
Reference in New Issue
Block a user