Compare commits

..
21 Commits
Author SHA1 Message Date
欣南科技andGitHub bc2fc35cc9 Merge pull request #1642 from xinnan-tech/py_tts_listen
update:更新版本号
2025-06-20 17:42:53 +08:00
hrz 993d5395b2 update:更新版本号 2025-06-20 17:42:14 +08:00
欣南科技andGitHub 67f0b828ea Merge pull request #1641 from xinnan-tech/py_tts_listen
update:修复部分doubaoasr出现400错误的问题
2025-06-20 17:20:51 +08:00
hrz 879c1267b6 update:修复部分doubaoasr出现400错误的问题 2025-06-20 17:20:08 +08:00
hrzandGitHub 46c53e36a6 Merge pull request #1640 from xinnan-tech/py_tts_listen
fix: 反复打断任务没有被清除,vad四帧语音识别
2025-06-20 17:17:19 +08:00
hrz 09f6605cfe update:更新版本号 2025-06-20 17:17:09 +08:00
Sakura-RanChen 12cee4027a fix: 反复打断任务没有被清除,vad四帧语音识别 2025-06-20 16:25:08 +08:00
欣南科技andGitHub 979fea0d60 Merge pull request #1627 from xinnan-tech/update-remark
更新智控台两款意图识别的说明
2025-06-19 16:56:38 +08:00
hrz 94553c54bd 更新智控台两款意图识别的说明 2025-06-19 16:53:39 +08:00
hrzandGitHub 248db31c8b Merge pull request #1618 from xinnan-tech/py_bug_fix
update:添加MCP重连机制
2025-06-18 22:40:25 +08:00
hrzandGitHub 24bfa1ca15 Merge pull request #1615 from xinnan-tech/py_fix_type
fix: 豆包流式decode错误
2025-06-18 17:46:47 +08:00
欣南科技andGitHub 1e97a8febc Merge pull request #1620 from xinnan-tech/hot-fix
update:修复mcp返回json的bug
2025-06-18 17:28:51 +08:00
hrz 2742f2e1ff update:修复mcp返回json的bug 2025-06-18 17:28:21 +08:00
Sakura-RanChen 0a5ae70a7c 二次错误提醒 2025-06-18 16:32:58 +08:00
CGD 22d53bd36e update:添加MCP重连机制 2025-06-18 16:29:28 +08:00
Sakura-RanChen ebf68929ce update: 错误信息读取 2025-06-18 14:03:18 +08:00
Sakura-RanChen ef3b373211 fix: 豆包流式decode错误 2025-06-18 11:06:11 +08:00
hrzandGitHub 5012a51e1d Merge pull request #1609 from xinnan-tech/py_fix_type
Py fix type
2025-06-18 09:39:38 +08:00
Sakura-RanChen fb1f476a3c fix: 智控台切换默认视觉模型报错 2025-06-18 09:24:52 +08:00
Sakura-RanChen 1a31c8cd1d update: 视觉模块直接识别输出,不经过二次LLM 2025-06-17 16:13:24 +08:00
Sakura-RanChen d8bf5cdedf fix: openAi schema不支持list 2025-06-16 16:26:49 +08:00
14 changed files with 196 additions and 25 deletions
@@ -227,7 +227,7 @@ public interface Constant {
/** /**
* 版本号 * 版本号
*/ */
public static final String VERSION = "0.5.7"; public static final String VERSION = "0.5.8";
/** /**
* 无效固件URL * 无效固件URL
@@ -56,6 +56,9 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
wrapper.set("tts_model_id", modelId); wrapper.set("tts_model_id", modelId);
wrapper.set("tts_voice_id", null); wrapper.set("tts_voice_id", null);
break; break;
case "VLLM":
wrapper.set("vllm_model_id", modelId);
break;
case "MEMORY": case "MEMORY":
wrapper.set("mem_model_id", modelId); wrapper.set("mem_model_id", modelId);
break; break;
@@ -0,0 +1,19 @@
-- LLM意图识别配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = 'LLM意图识别配置说明:
1. 使用独立的LLM进行意图识别
2. 默认使用selected_module.LLM的模型
3. 可以配置使用独立的LLM(如免费的ChatGLMLLM
4. 通用性强,但会增加处理时间
配置说明:
1. 在llm字段中指定使用的LLM模型
2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm';
-- 函数调用意图识别配置说明
UPDATE `ai_model_config` SET
`doc_link` = NULL,
`remark` = '函数调用意图识别配置说明:
1. 使用LLM的function_call功能进行意图识别
2. 需要所选择的LLM支持function_call
3. 按需调用工具,处理速度快' WHERE `id` = 'Intent_function_call';
@@ -211,4 +211,11 @@ databaseChangeLog:
changes: changes:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202506161101.sql path: classpath:db/changelog/202506161101.sql
- changeSet:
id: 202506191643
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506191643.sql
+1 -1
View File
@@ -5,7 +5,7 @@ from config.config_loader import load_config
from config.settings import check_config_file from config.settings import check_config_file
from datetime import datetime from datetime import datetime
SERVER_VERSION = "0.5.7" SERVER_VERSION = "0.5.8"
_logger_initialized = False _logger_initialized = False
@@ -8,6 +8,7 @@ from config.config_loader import get_private_config_from_api
from core.utils.auth import AuthToken from core.utils.auth import AuthToken
import base64 import base64
from typing import Tuple, Optional from typing import Tuple, Optional
from plugins_func.register import Action
TAG = __name__ TAG = __name__
@@ -122,7 +123,8 @@ class VisionHandler:
return_json = { return_json = {
"success": True, "success": True,
"result": result, "action": Action.RESPONSE.name,
"response": result,
} }
response = web.Response( response = web.Response(
+28 -5
View File
@@ -115,6 +115,7 @@ class ConnectionHandler:
self.client_have_voice_last_time = 0.0 self.client_have_voice_last_time = 0.0
self.client_no_voice_last_time = 0.0 self.client_no_voice_last_time = 0.0
self.client_voice_stop = False self.client_voice_stop = False
self.client_voice_frame_count = 0
# asr相关变量 # asr相关变量
# 因为实际部署时可能会用到公共的本地ASR,不能把变量暴露给公共ASR # 因为实际部署时可能会用到公共的本地ASR,不能把变量暴露给公共ASR
@@ -627,8 +628,8 @@ class ConnectionHandler:
) )
memory_str = future.result() memory_str = future.result()
uuid_str = str(uuid.uuid4()).replace("-", "") self.sentence_id = str(uuid.uuid4().hex)
self.sentence_id = uuid_str
if self.intent_type == "function_call" and functions is not None: if self.intent_type == "function_call" and functions is not None:
# 使用支持functions的streaming接口 # 使用支持functions的streaming接口
@@ -751,9 +752,31 @@ class ConnectionHandler:
self.loop, self.loop,
).result() ).result()
self.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}") self.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}")
result = ActionResponse(
action=Action.REQLLM, result=result, response="" resultJson = None
) if isinstance(result, str):
try:
resultJson = json.loads(result)
except Exception as e:
self.logger.bind(tag=TAG).error(
f"解析MCP工具返回结果失败: {e}"
)
# 视觉大模型不经过二次LLM处理
if (
resultJson is not None
and isinstance(resultJson, dict)
and "action" in resultJson
):
result = ActionResponse(
action=Action[resultJson["action"]],
result=None,
response=resultJson.get("response", ""),
)
else:
result = ActionResponse(
action=Action.REQLLM, result=result, response=""
)
except Exception as e: except Exception as e:
self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}") self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}")
result = ActionResponse( result = ActionResponse(
+21
View File
@@ -80,6 +80,27 @@ class MCPClient:
fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop) fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop)
return await asyncio.wrap_future(fut) return await asyncio.wrap_future(fut)
def is_connected(self) -> bool:
"""检查MCP客户端是否连接正常
Returns:
bool: 如果客户端已连接并正常工作,返回True,否则返回False
"""
# 检查工作任务是否存在
if self._worker_task is None:
return False
# 检查工作任务是否已经完成或取消
if self._worker_task.done():
return False
# 检查会话是否存在
if self.session is None:
return False
# 所有检查都通过,连接正常
return True
async def _worker(self): async def _worker(self):
async with AsyncExitStack() as stack: async with AsyncExitStack() as stack:
try: try:
+58 -5
View File
@@ -100,7 +100,7 @@ class MCPManager:
return False return False
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any: async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
"""执行工具调用 """执行工具调用,失败时会尝试重新连接
Args: Args:
tool_name: 工具名称 tool_name: 工具名称
arguments: 工具参数 arguments: 工具参数
@@ -112,11 +112,64 @@ class MCPManager:
self.conn.logger.bind(tag=TAG).info( self.conn.logger.bind(tag=TAG).info(
f"Executing tool {tool_name} with arguments: {arguments}" f"Executing tool {tool_name} with arguments: {arguments}"
) )
for client in self.client.values():
max_retries = 3 # 最大重试次数
retry_interval = 2 # 重试间隔(秒)
# 找到对应的客户端
client_name = None
target_client = None
for name, client in self.client.items():
if client.has_tool(tool_name): if client.has_tool(tool_name):
return await client.call_tool(tool_name, arguments) client_name = name
target_client = client
raise ValueError(f"Tool {tool_name} not found in any MCP server") break
if not target_client:
raise ValueError(f"Tool {tool_name} not found in any MCP server")
# 带重试机制的工具调用
for attempt in range(max_retries):
try:
return await target_client.call_tool(tool_name, arguments)
except Exception as e:
# 最后一次尝试失败时直接抛出异常
if attempt == max_retries - 1:
raise
self.conn.logger.bind(tag=TAG).warning(
f"执行工具 {tool_name} 失败 (尝试 {attempt+1}/{max_retries}): {e}"
)
# 尝试重新连接
self.conn.logger.bind(tag=TAG).info(
f"重试前尝试重新连接 MCP 客户端 {client_name}"
)
try:
# 关闭旧的连接
await target_client.cleanup()
# 重新初始化客户端
config = self.load_config()
if client_name in config:
client = MCPClient(config[client_name])
await client.initialize()
self.client[client_name] = client
target_client = client
self.conn.logger.bind(tag=TAG).info(
f"成功重新连接 MCP 客户端: {client_name}"
)
else:
self.conn.logger.bind(tag=TAG).error(
f"Cannot reconnect MCP client {client_name}: config not found"
)
except Exception as reconnect_error:
self.conn.logger.bind(tag=TAG).error(
f"Failed to reconnect MCP client {client_name}: {reconnect_error}"
)
# 等待一段时间再重试
await asyncio.sleep(retry_interval)
async def cleanup_all(self) -> None: async def cleanup_all(self) -> None:
"""依次关闭所有 MCPClient,不让异常阻断整体流程。""" """依次关闭所有 MCPClient,不让异常阻断整体流程。"""
@@ -93,9 +93,7 @@ class ASRProvider(ASRProviderBase):
# 检查初始化响应 # 检查初始化响应
if "code" in result and result["code"] != 1000: if "code" in result and result["code"] != 1000:
error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('message', '未知错误')}" error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('error', '未知错误')}"
if "payload_msg" in result:
error_msg += f"\n详细错误信息: {json.dumps(result['payload_msg'], ensure_ascii=False)}"
logger.bind(tag=TAG).error(error_msg) logger.bind(tag=TAG).error(error_msg)
raise Exception(error_msg) raise Exception(error_msg)
@@ -256,7 +254,6 @@ class ASRProvider(ASRProviderBase):
"X-Api-Access-Key": self.access_token, "X-Api-Access-Key": self.access_token,
"X-Api-Resource-Id": "volc.bigasr.sauc.duration", "X-Api-Resource-Id": "volc.bigasr.sauc.duration",
"X-Api-Connect-Id": str(uuid.uuid4()), "X-Api-Connect-Id": str(uuid.uuid4()),
"Host": "openspeech.bytedance.com",
} }
def generate_header( def generate_header(
@@ -309,9 +306,14 @@ class ASRProvider(ASRProviderBase):
# 如果是错误响应 # 如果是错误响应
if message_type == 0x0F: # SERVER_ERROR_RESPONSE if message_type == 0x0F: # SERVER_ERROR_RESPONSE
code = int.from_bytes(header[4:8], "big", signed=False) code = int.from_bytes(res[4:8], "big", signed=False)
error_msg = res[8:].decode("utf-8") msg_length = int.from_bytes(res[8:12], "big", signed=False)
return {"code": code, "error": error_msg} error_msg = json.loads(res[12:].decode("utf-8"))
return {
"code": code,
"msg_length": msg_length,
"payload_msg": error_msg,
}
# 获取JSON数据(跳过12字节头部) # 获取JSON数据(跳过12字节头部)
try: try:
@@ -5,6 +5,7 @@ import os
import yaml import yaml
from config.config_loader import get_project_dir from config.config_loader import get_project_dir
from config.manage_api_client import save_mem_local_short from config.manage_api_client import save_mem_local_short
from core.utils.util import check_model_key
short_term_memory_prompt = """ short_term_memory_prompt = """
@@ -145,6 +146,10 @@ class MemoryProvider(MemoryProviderBase):
# 打印使用的模型信息 # 打印使用的模型信息
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__)) model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
logger.bind(tag=TAG).debug(f"使用记忆保存模型: {model_info}") logger.bind(tag=TAG).debug(f"使用记忆保存模型: {model_info}")
api_key = getattr(self.llm, "api_key", None)
memory_key_msg = check_model_key("记忆总结专用LLM", api_key)
if memory_key_msg:
logger.bind(tag=TAG).error(memory_key_msg)
if self.llm is None: if self.llm is None:
logger.bind(tag=TAG).error("LLM is not set for memory provider") logger.bind(tag=TAG).error("LLM is not set for memory provider")
return None return None
@@ -12,6 +12,8 @@ from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
from core.handle.abortHandle import handleAbortMessage from core.handle.abortHandle import handleAbortMessage
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
from asyncio import Task
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -141,6 +143,7 @@ class TTSProvider(TTSProviderBase):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.ws = None self.ws = None
self.interface_type = InterfaceType.DUAL_STREAM self.interface_type = InterfaceType.DUAL_STREAM
self._monitor_task = None # 监听任务引用
self.appId = config.get("appid") self.appId = config.get("appid")
self.access_token = config.get("access_token") self.access_token = config.get("access_token")
self.cluster = config.get("cluster") self.cluster = config.get("cluster")
@@ -270,8 +273,7 @@ class TTSProvider(TTSProviderBase):
try: try:
# 建立新连接 # 建立新连接
if self.ws is None: if self.ws is None:
await handleAbortMessage(self.conn) logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
logger.bind(tag=TAG).error(f"WebSocket连接不存在,终止发送文本")
return return
# 过滤Markdown # 过滤Markdown
@@ -293,6 +295,25 @@ class TTSProvider(TTSProviderBase):
async def start_session(self, session_id): async def start_session(self, session_id):
logger.bind(tag=TAG).info(f"开始会话~~{session_id}") logger.bind(tag=TAG).info(f"开始会话~~{session_id}")
try: try:
task = self._monitor_task
if (
task is not None
and isinstance(task, Task)
and not task.done()
):
logger.bind(tag=TAG).info("等待上一个监听任务结束...")
if self.ws is not None:
logger.bind(tag=TAG).info("强制关闭上一个WebSocket连接以唤醒监听任务...")
try:
await self.ws.close()
except Exception as e:
logger.bind(tag=TAG).warning(f"关闭上一个ws异常: {e}")
self.ws = None
try:
await asyncio.wait_for(task, timeout=8)
except Exception as e:
logger.bind(tag=TAG).warning(f"等待监听任务异常: {e}")
self._monitor_task = None
# 建立新连接 # 建立新连接
await self._ensure_connection() await self._ensure_connection()
@@ -463,6 +484,8 @@ class TTSProvider(TTSProviderBase):
except: except:
pass pass
self.ws = None self.ws = None
# 监听任务退出时清理引用
self._monitor_task = None
async def send_event( async def send_event(
self, self,
@@ -35,6 +35,10 @@ class VADProvider(VADProviderBase):
pcm_frame = self.decoder.decode(opus_packet, 960) pcm_frame = self.decoder.decode(opus_packet, 960)
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区 conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
# 初始化帧计数器
if not hasattr(conn, "client_voice_frame_count"):
conn.client_voice_frame_count = 0
# 处理缓冲区中的完整帧(每次处理512采样点) # 处理缓冲区中的完整帧(每次处理512采样点)
client_have_voice = False client_have_voice = False
while len(conn.client_audio_buffer) >= 512 * 2: while len(conn.client_audio_buffer) >= 512 * 2:
@@ -50,7 +54,15 @@ class VADProvider(VADProviderBase):
# 检测语音活动 # 检测语音活动
with torch.no_grad(): with torch.no_grad():
speech_prob = self.model(audio_tensor, 16000).item() speech_prob = self.model(audio_tensor, 16000).item()
client_have_voice = speech_prob >= self.vad_threshold is_voice = speech_prob >= self.vad_threshold
if is_voice:
conn.client_voice_frame_count += 1
else:
conn.client_voice_frame_count = 0
# 只有连续4帧检测到语音才认为有语音
client_have_voice = conn.client_voice_frame_count >= 4
# 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话 # 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
if conn.client_have_voice and not client_have_voice: if conn.client_have_voice and not client_have_voice:
@@ -31,7 +31,8 @@ hass_set_state_function_desc = {
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false", "description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false",
}, },
"rgb_color": { "rgb_color": {
"type": "list", "type": "array",
"items": {"type": "integer"},
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值", "description": "只有在设置颜色时需要,这里填目标颜色的rgb值",
}, },
}, },