mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 09:03:54 +08:00
Merge branch 'model-config-ui' into feat/volces-ai-gateway
This commit is contained in:
@@ -69,6 +69,11 @@ def get_config_from_api(config):
|
||||
"url": config["manager-api"].get("url", ""),
|
||||
"secret": config["manager-api"].get("secret", ""),
|
||||
}
|
||||
if config.get("server"):
|
||||
config_data["server"] = {
|
||||
"ip": config["server"].get("ip", ""),
|
||||
"port": config["server"].get("port", ""),
|
||||
}
|
||||
return config_data
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
from loguru import logger
|
||||
from config.config_loader import load_config
|
||||
|
||||
SERVER_VERSION = "0.3.9"
|
||||
SERVER_VERSION = "0.3.11"
|
||||
|
||||
|
||||
def get_module_abbreviation(module_name, module_dict):
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
# 先启动manager-api和manager-web,注册一个账号,第一个注册的账号为管理员
|
||||
# 使用管理员,进入【参数管理】页面,找到【server.secret】,复制它到参数值,注意每次从零部署,server.secret都会变化
|
||||
# 打开本data目录下的.config.yaml文件,修改manager-api.secret为刚才复制出来的server.secret
|
||||
server:
|
||||
ip: 0.0.0.0
|
||||
port: 8000
|
||||
manager-api:
|
||||
# 你的manager-api的地址,最好使用局域网ip
|
||||
url: http://127.0.0.1:8002/xiaozhi
|
||||
|
||||
@@ -226,12 +226,12 @@ class ConnectionHandler:
|
||||
"""加载意图识别"""
|
||||
self._initialize_intent()
|
||||
"""加载位置信息"""
|
||||
self.client_ip_info = get_ip_info(self.client_ip, self.logger)
|
||||
if self.client_ip_info is not None and "city" in self.client_ip_info:
|
||||
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
|
||||
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
|
||||
|
||||
self.dialogue.update_system_message(self.prompt)
|
||||
# self.client_ip_info = get_ip_info(self.client_ip, self.logger)
|
||||
# if self.client_ip_info is not None and "city" in self.client_ip_info:
|
||||
# self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
|
||||
# self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
|
||||
#
|
||||
# self.dialogue.update_system_message(self.prompt)
|
||||
|
||||
def _initialize_private_config(self):
|
||||
read_config_from_api = self.config.get("read_config_from_api", False)
|
||||
|
||||
@@ -6,6 +6,7 @@ from core.providers.llm.base import LLMProviderBase
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
self.api_key = config["api_key"]
|
||||
@@ -19,21 +20,25 @@ class LLMProvider(LLMProviderBase):
|
||||
# 处理dialogue
|
||||
if self.is_No_prompt:
|
||||
dialogue.pop(0)
|
||||
logger.bind(tag=TAG).debug(f"【阿里百练API服务】处理后的dialogue: {dialogue}")
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"【阿里百练API服务】处理后的dialogue: {dialogue}"
|
||||
)
|
||||
|
||||
# 构造调用参数
|
||||
call_params = {
|
||||
"api_key": self.api_key,
|
||||
"app_id": self.app_id,
|
||||
"session_id": session_id,
|
||||
"messages": dialogue
|
||||
"messages": dialogue,
|
||||
}
|
||||
if self.memory_id != False:
|
||||
# 百练memory需要prompt参数
|
||||
prompt = dialogue[-1].get("content")
|
||||
call_params["memory_id"] = self.memory_id
|
||||
call_params["prompt"] = prompt
|
||||
logger.bind(tag=TAG).debug(f"【阿里百练API服务】处理后的prompt: {prompt}")
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"【阿里百练API服务】处理后的prompt: {prompt}"
|
||||
)
|
||||
|
||||
responses = Application.call(**call_params)
|
||||
if responses.status_code != HTTPStatus.OK:
|
||||
@@ -44,9 +49,15 @@ class LLMProvider(LLMProviderBase):
|
||||
)
|
||||
yield "【阿里百练API服务响应异常】"
|
||||
else:
|
||||
logger.bind(tag=TAG).debug(f"【阿里百练API服务】构造参数: {call_params}")
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"【阿里百练API服务】构造参数: {call_params}"
|
||||
)
|
||||
yield responses.output.text
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"【阿里百练API服务】响应异常: {e}")
|
||||
yield "【LLM服务响应异常】"
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
logger.bind(tag=TAG).info(f"阿里百练暂未实现完整的工具调用(function call)")
|
||||
return self.response(session_id, dialogue)
|
||||
|
||||
@@ -21,37 +21,36 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
# 发起流式请求
|
||||
with requests.post(
|
||||
f"{self.base_url}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
json={
|
||||
"stream": True,
|
||||
"chatId": session_id,
|
||||
"detail": self.detail,
|
||||
"variables": self.variables,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": last_msg["content"]
|
||||
}
|
||||
]
|
||||
},
|
||||
stream=True
|
||||
f"{self.base_url}/chat/completions",
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
json={
|
||||
"stream": True,
|
||||
"chatId": session_id,
|
||||
"detail": self.detail,
|
||||
"variables": self.variables,
|
||||
"messages": [{"role": "user", "content": last_msg["content"]}],
|
||||
},
|
||||
stream=True,
|
||||
) as r:
|
||||
for line in r.iter_lines():
|
||||
if line:
|
||||
try:
|
||||
if line.startswith(b'data: '):
|
||||
if line[6:].decode('utf-8') == '[DONE]':
|
||||
if line.startswith(b"data: "):
|
||||
if line[6:].decode("utf-8") == "[DONE]":
|
||||
break
|
||||
|
||||
data = json.loads(line[6:])
|
||||
if 'choices' in data and len(data['choices']) > 0:
|
||||
delta = data['choices'][0].get('delta', {})
|
||||
if delta and 'content' in delta and delta['content'] is not None:
|
||||
content = delta['content']
|
||||
if '<think>' in content:
|
||||
if "choices" in data and len(data["choices"]) > 0:
|
||||
delta = data["choices"][0].get("delta", {})
|
||||
if (
|
||||
delta
|
||||
and "content" in delta
|
||||
and delta["content"] is not None
|
||||
):
|
||||
content = delta["content"]
|
||||
if "<think>" in content:
|
||||
continue
|
||||
if '</think>' in content:
|
||||
if "</think>" in content:
|
||||
continue
|
||||
yield content
|
||||
|
||||
@@ -62,4 +61,8 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
||||
yield "【服务响应异常】"
|
||||
yield "【服务响应异常】"
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
logger.bind(tag=TAG).info(f"fastgpt暂未实现完整的工具调用(function call)")
|
||||
return self.response(session_id, dialogue)
|
||||
|
||||
@@ -134,3 +134,7 @@ class LLMProvider(LLMProviderBase):
|
||||
yield f"JSON解码错误:{e}"
|
||||
except Exception as e:
|
||||
yield f"发生错误:{e}"
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
logger.bind(tag=TAG).info(f"gemini暂未实现完整的工具调用(function call)")
|
||||
return self.response(session_id, dialogue)
|
||||
|
||||
@@ -26,13 +26,19 @@ class TTSProviderBase(ABC):
|
||||
max_repeat_time = 5
|
||||
text = MarkdownCleaner.clean_markdown(text)
|
||||
while not os.path.exists(tmp_file) and max_repeat_time > 0:
|
||||
asyncio.run(self.text_to_speak(text, tmp_file))
|
||||
try:
|
||||
asyncio.run(self.text_to_speak(text, tmp_file))
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"语音生成失败: {text},错误: {e}")
|
||||
if not os.path.exists(tmp_file):
|
||||
max_repeat_time = max_repeat_time - 1
|
||||
logger.bind(tag=TAG).error(f"语音生成失败: {text}:{tmp_file},再试{max_repeat_time}次")
|
||||
max_repeat_time -= 1
|
||||
if max_repeat_time > 0:
|
||||
logger.bind(tag=TAG).error(f"再试{max_repeat_time}次")
|
||||
|
||||
if max_repeat_time > 0:
|
||||
logger.bind(tag=TAG).info(f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次")
|
||||
logger.bind(tag=TAG).info(
|
||||
f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次"
|
||||
)
|
||||
|
||||
return tmp_file
|
||||
except Exception as e:
|
||||
@@ -48,9 +54,11 @@ class TTSProviderBase(ABC):
|
||||
# 获取文件后缀名
|
||||
file_type = os.path.splitext(audio_file_path)[1]
|
||||
if file_type:
|
||||
file_type = file_type.lstrip('.')
|
||||
file_type = file_type.lstrip(".")
|
||||
# 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞
|
||||
audio = AudioSegment.from_file(audio_file_path, format=file_type, parameters=["-nostdin"])
|
||||
audio = AudioSegment.from_file(
|
||||
audio_file_path, format=file_type, parameters=["-nostdin"]
|
||||
)
|
||||
|
||||
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
|
||||
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
|
||||
@@ -72,11 +80,11 @@ class TTSProviderBase(ABC):
|
||||
# 按帧处理所有音频数据(包括最后一帧可能补零)
|
||||
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
|
||||
# 获取当前帧的二进制数据
|
||||
chunk = raw_data[i:i + frame_size * 2]
|
||||
chunk = raw_data[i : i + frame_size * 2]
|
||||
|
||||
# 如果最后一帧不足,补零
|
||||
if len(chunk) < frame_size * 2:
|
||||
chunk += b'\x00' * (frame_size * 2 - len(chunk))
|
||||
chunk += b"\x00" * (frame_size * 2 - len(chunk))
|
||||
|
||||
# 转换为numpy数组处理
|
||||
np_frame = np.frombuffer(chunk, dtype=np.int16)
|
||||
|
||||
@@ -24,7 +24,7 @@ class WebSocketServer:
|
||||
|
||||
async def start(self):
|
||||
server_config = self.config["server"]
|
||||
host = server_config["ip"]
|
||||
host = server_config.get("ip", "0.0.0.0")
|
||||
port = int(server_config.get("port", 8000))
|
||||
|
||||
self.logger.bind(tag=TAG).info(
|
||||
|
||||
@@ -47,6 +47,10 @@ services:
|
||||
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
|
||||
- SPRING_DATA_REDIS_HOST=xiaozhi-esp32-server-redis
|
||||
- SPRING_DATA_REDIS_PORT=6379
|
||||
volumes:
|
||||
# 配置文件目录
|
||||
- ./uploadfile:/app/uploadfile
|
||||
|
||||
xiaozhi-esp32-server-db:
|
||||
image: mysql:latest
|
||||
container_name: xiaozhi-esp32-server-db
|
||||
@@ -59,7 +63,7 @@ services:
|
||||
networks:
|
||||
- default
|
||||
expose:
|
||||
- "3306"
|
||||
- 3306
|
||||
volumes:
|
||||
- ./mysql/data:/var/lib/mysql
|
||||
environment:
|
||||
|
||||
@@ -2,6 +2,7 @@ import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from core.utils.util import get_ip_info
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -11,9 +12,12 @@ GET_WEATHER_FUNCTION_DESC = {
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": (
|
||||
"获取某个地点的天气,用户应提供一个位置,比如用户说杭州天气,参数为:杭州。"
|
||||
"获取某个地点的天气信息。当用户询问与天气相关的问题(例如'今天的天气怎么样?'、'明天会下雨吗?'、'广州天气如何?'等),"
|
||||
"或对话中包含'天气'字眼时,调用此功能。用户可以提供具体位置(如城市名),"
|
||||
"如果未提供位置,则自动获取用户当前位置查询天气。"
|
||||
"如果用户说的是省份,默认用省会城市。如果用户说的不是省份或城市而是一个地名,"
|
||||
"默认用该地所在省份的省会城市。"
|
||||
"当IP解析失败会使用默认地址"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
@@ -57,6 +61,7 @@ WEATHER_CODE_MAP = {
|
||||
"900": "热", "901": "冷", "999": "未知"
|
||||
}
|
||||
|
||||
|
||||
def fetch_city_info(location, api_key):
|
||||
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
||||
response = requests.get(url, headers=HEADERS).json()
|
||||
@@ -97,27 +102,47 @@ def parse_weather_info(soup):
|
||||
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
||||
api_key = conn.config["plugins"]["get_weather"]["api_key"]
|
||||
default_location = conn.config["plugins"]["get_weather"]["default_location"]
|
||||
location = location or conn.client_ip_info.get("city") or default_location
|
||||
logger.bind(tag=TAG).debug(f"获取天气: {location}")
|
||||
print("用户设置的default_location为:", default_location)
|
||||
client_ip = conn.client_ip
|
||||
print("用户提供的location为:", location)
|
||||
# 优先使用用户提供的location参数
|
||||
if not location:
|
||||
# 通过客户端IP解析城市
|
||||
if client_ip:
|
||||
# 动态解析IP对应的城市信息
|
||||
ip_info = get_ip_info(client_ip, logger)
|
||||
print("解析用户IP后的城市为:", ip_info)
|
||||
location = ip_info.get("city") if ip_info and "city" in ip_info else None
|
||||
else:
|
||||
# 若IP解析失败或无IP,使用默认位置
|
||||
location = default_location
|
||||
print("解析失败,将使用默认地址", location)
|
||||
|
||||
city_info = fetch_city_info(location, api_key)
|
||||
if not city_info:
|
||||
return ActionResponse(Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None)
|
||||
|
||||
soup = fetch_weather_page(city_info['fxLink'])
|
||||
if not soup:
|
||||
return ActionResponse(Action.REQLLM, None, "请求失败")
|
||||
|
||||
city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
|
||||
weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
|
||||
for i, (date, weather, high, low) in enumerate(temps_list):
|
||||
if high and low:
|
||||
weather_report += f"{date}: {low}到{high}, {weather}\n"
|
||||
weather_report += (
|
||||
f"当前天气: {current_abstract}\n"
|
||||
f"当前天气参数: {current_basic}\n"
|
||||
f"(确保只报告指定单日的天气情况,除非未来会出现异常天气;或者用户明确要求想要了解多日天气,如果未指定,默认报告今天的天气。"
|
||||
"参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
|
||||
)
|
||||
|
||||
return ActionResponse(Action.REQLLM, weather_report, None)
|
||||
print(f"当前查询的城市名称是: {city_name}")
|
||||
|
||||
weather_report = f"您查询的位置是:{city_name}\n\n当前天气: {current_abstract}\n"
|
||||
|
||||
# 添加有效的当前天气参数
|
||||
if current_basic:
|
||||
weather_report += "详细参数:\n"
|
||||
for key, value in current_basic.items():
|
||||
if value != "0": # 过滤无效值
|
||||
weather_report += f" · {key}: {value}\n"
|
||||
|
||||
# 添加7天预报
|
||||
weather_report += "\n未来7天预报:\n"
|
||||
for date, weather, high, low in temps_list:
|
||||
weather_report += f"{date}: {weather},气温 {low}~{high}\n"
|
||||
|
||||
# 提示语
|
||||
weather_report += "\n(如需某一天的具体天气,请告诉我日期)"
|
||||
|
||||
return ActionResponse(Action.REQLLM, weather_report, None)
|
||||
|
||||
@@ -13,7 +13,7 @@ def append_devices_to_prompt(conn):
|
||||
"functions", []
|
||||
)
|
||||
if "hass_get_state" in funcs or "hass_set_state" in funcs:
|
||||
prompt = "下面是我家智能设备,可以通过homeassistant控制\n"
|
||||
prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
||||
if len(devices) == 0:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user