mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 15:13:55 +08:00
fix:已经使用了intent_llm,不应该再回到functioncall的chat中
This commit is contained in:
@@ -210,11 +210,6 @@ class ConnectionHandler:
|
||||
intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config)
|
||||
self.logger.bind(tag=TAG).info(f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}")
|
||||
|
||||
# 记录额外的模型信息
|
||||
model_name = intent_llm_config.get("model_name", "未指定")
|
||||
base_url = intent_llm_config.get("base_url", "未指定")
|
||||
self.logger.bind(tag=TAG).info(f"意图识别LLM详细信息 - 模型名称: {model_name}, 服务地址: {base_url}")
|
||||
|
||||
self.intent.set_llm(intent_llm)
|
||||
else:
|
||||
# 否则使用主LLM
|
||||
@@ -337,7 +332,7 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
||||
return True
|
||||
|
||||
def chat_with_function_calling(self, query, tool_call=False, is_weather_query=False, is_news_query=False):
|
||||
def chat_with_function_calling(self, query, tool_call=False):
|
||||
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
|
||||
"""Chat with function calling for intent detection using streaming"""
|
||||
if self.isNeedAuth():
|
||||
@@ -355,7 +350,7 @@ class ConnectionHandler:
|
||||
functions = self.func_handler.get_functions()
|
||||
|
||||
response_message = []
|
||||
processed_chars = 0
|
||||
processed_chars = 0 # 跟踪已处理的字符位置
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
@@ -364,58 +359,14 @@ class ConnectionHandler:
|
||||
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
|
||||
memory_str = future.result()
|
||||
|
||||
# 为天气查询添加特殊处理
|
||||
if is_weather_query:
|
||||
self.logger.bind(tag=TAG).info(f"检测到天气查询,添加特殊指令")
|
||||
# 获取对话历史
|
||||
dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str)
|
||||
|
||||
# 找到最后一条tool消息(可能是天气数据)
|
||||
for i in range(len(dialogue_with_memory) - 1, -1, -1):
|
||||
if dialogue_with_memory[i].get("role") == "tool" and "当前天气" in dialogue_with_memory[i].get("content", ""):
|
||||
# 添加特殊指令
|
||||
dialogue_with_memory.append({
|
||||
"role": "system",
|
||||
"content": "请根据上面的天气数据,以简洁友好的方式回答用户的天气查询。直接告诉用户当前天气状况、温度以及可能需要的建议,不要提及数据来源或解释你是如何获取这些信息的。"
|
||||
})
|
||||
self.logger.bind(tag=TAG).info(f"已添加天气查询特殊指令")
|
||||
break
|
||||
|
||||
# 使用支持functions的streaming接口并传入修改后的对话历史
|
||||
llm_responses = self.llm.response_with_functions(
|
||||
self.session_id,
|
||||
dialogue_with_memory,
|
||||
functions=functions
|
||||
)
|
||||
# 为新闻查询添加特殊处理
|
||||
elif is_news_query:
|
||||
self.logger.bind(tag=TAG).info(f"检测到新闻查询,添加特殊指令")
|
||||
# 获取对话历史
|
||||
dialogue_with_memory = self.dialogue.get_llm_dialogue_with_memory(memory_str)
|
||||
|
||||
# 找到最后一条tool消息(可能是新闻数据)
|
||||
for i in range(len(dialogue_with_memory) - 1, -1, -1):
|
||||
if dialogue_with_memory[i].get("role") == "tool" and "新闻" in dialogue_with_memory[i].get("content", ""):
|
||||
# 添加特殊指令
|
||||
dialogue_with_memory.append({
|
||||
"role": "system",
|
||||
"content": "请根据上面的新闻数据,以简洁友好的方式回答用户的新闻查询。直接告诉用户新闻内容,不要提及数据来源或解释你是如何获取这些信息的。保持新闻播报的语气和风格。"
|
||||
})
|
||||
self.logger.bind(tag=TAG).info(f"已添加新闻查询特殊指令")
|
||||
break
|
||||
|
||||
# 使用支持functions的streaming接口并传入修改后的对话历史
|
||||
llm_responses = self.llm.response_with_functions(
|
||||
self.session_id,
|
||||
dialogue_with_memory,
|
||||
functions=functions
|
||||
)
|
||||
else:
|
||||
llm_responses = self.llm.response_with_functions(
|
||||
self.session_id,
|
||||
self.dialogue.get_llm_dialogue_with_memory(memory_str),
|
||||
functions=functions
|
||||
)
|
||||
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
|
||||
|
||||
# 使用支持functions的streaming接口
|
||||
llm_responses = self.llm.response_with_functions(
|
||||
self.session_id,
|
||||
self.dialogue.get_llm_dialogue_with_memory(memory_str),
|
||||
functions=functions
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
|
||||
return None
|
||||
@@ -431,6 +382,9 @@ class ConnectionHandler:
|
||||
content_arguments = ""
|
||||
for response in llm_responses:
|
||||
content, tools_call = response
|
||||
if "content" in response:
|
||||
content = response["content"]
|
||||
tools_call = None
|
||||
if content is not None and len(content) > 0:
|
||||
if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content):
|
||||
tool_call_flag = True
|
||||
@@ -533,126 +487,36 @@ class ConnectionHandler:
|
||||
return True
|
||||
|
||||
def _handle_function_result(self, result, function_call_data, text_index):
|
||||
self.logger.bind(tag=TAG).info(f"处理函数调用结果,动作类型: {result.action.name if result.action else 'None'}")
|
||||
|
||||
# 检查是否有备用直接回复
|
||||
direct_response = getattr(result, 'response', None)
|
||||
|
||||
if result.action == Action.RESPONSE: # 直接回复前端
|
||||
text = result.response
|
||||
self.logger.bind(tag=TAG).info(f"函数返回直接回复: {text[:100] if text else 'None'}...")
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||
self.logger.bind(tag=TAG).info(f"处理REQLLM动作,需要进一步处理结果")
|
||||
|
||||
text = result.result
|
||||
function_id = function_call_data["id"]
|
||||
function_name = function_call_data["name"]
|
||||
function_arguments = function_call_data["arguments"]
|
||||
|
||||
if text is not None and len(text) > 0:
|
||||
self.logger.bind(tag=TAG).info(f"函数返回结果长度: {len(text)}, 前100字符: {text[:100]}...")
|
||||
|
||||
# 特殊处理天气查询
|
||||
if function_name == "get_weather":
|
||||
self.logger.bind(tag=TAG).info(f"检测到天气查询结果,使用特殊处理")
|
||||
|
||||
# 记录工具调用到对话历史
|
||||
self.dialogue.put(Message(role='assistant',
|
||||
tool_calls=[{"id": function_id,
|
||||
"function": {"arguments": function_arguments,
|
||||
"name": function_name},
|
||||
"type": 'function',
|
||||
"index": 0}]))
|
||||
|
||||
# 记录工具返回结果到对话历史
|
||||
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
|
||||
|
||||
try:
|
||||
# 使用天气数据生成回复
|
||||
self.chat_with_function_calling(text, tool_call=True, is_weather_query=True)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"处理天气查询数据失败: {e}")
|
||||
if direct_response:
|
||||
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
|
||||
self.recode_first_last_text(direct_response, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=direct_response))
|
||||
# 特殊处理新闻查询
|
||||
elif function_name == "get_news":
|
||||
self.logger.bind(tag=TAG).info(f"检测到新闻查询结果,使用特殊处理")
|
||||
|
||||
# 记录工具调用到对话历史
|
||||
self.dialogue.put(Message(role='assistant',
|
||||
tool_calls=[{"id": function_id,
|
||||
"function": {"arguments": function_arguments,
|
||||
"name": function_name},
|
||||
"type": 'function',
|
||||
"index": 0}]))
|
||||
|
||||
# 记录工具返回结果到对话历史
|
||||
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
|
||||
|
||||
try:
|
||||
# 使用新闻数据生成回复,设置is_news_query=True
|
||||
self.chat_with_function_calling(text, tool_call=True, is_news_query=True)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"处理新闻查询数据失败: {e}")
|
||||
if direct_response:
|
||||
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
|
||||
self.recode_first_last_text(direct_response, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=direct_response))
|
||||
else:
|
||||
# 其他类型的函数调用
|
||||
self.dialogue.put(Message(role='assistant',
|
||||
tool_calls=[{"id": function_id,
|
||||
"function": {"arguments": function_arguments,
|
||||
"name": function_name},
|
||||
"type": 'function',
|
||||
"index": 0}]))
|
||||
|
||||
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
|
||||
|
||||
try:
|
||||
self.chat_with_function_calling(text, tool_call=True)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"处理函数调用结果失败: {e}")
|
||||
if direct_response:
|
||||
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
|
||||
self.recode_first_last_text(direct_response, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=direct_response))
|
||||
else:
|
||||
self.logger.bind(tag=TAG).warning(f"函数返回结果为空")
|
||||
if direct_response:
|
||||
self.logger.bind(tag=TAG).info(f"使用备用直接回复: {direct_response}")
|
||||
self.recode_first_last_text(direct_response, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, direct_response, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=direct_response))
|
||||
else:
|
||||
error_text = f"抱歉,我无法获取{function_name}的结果,请稍后再试。"
|
||||
self.recode_first_last_text(error_text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, error_text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=error_text))
|
||||
text = result.result
|
||||
if text is not None and len(text) > 0:
|
||||
function_id = function_call_data["id"]
|
||||
function_name = function_call_data["name"]
|
||||
function_arguments = function_call_data["arguments"]
|
||||
self.dialogue.put(Message(role='assistant',
|
||||
tool_calls=[{"id": function_id,
|
||||
"function": {"arguments": function_arguments,
|
||||
"name": function_name},
|
||||
"type": 'function',
|
||||
"index": 0}]))
|
||||
|
||||
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
|
||||
self.chat_with_function_calling(text, tool_call=True)
|
||||
elif result.action == Action.NOTFOUND:
|
||||
text = result.result
|
||||
self.logger.bind(tag=TAG).info(f"未找到对应函数: {text}")
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
else:
|
||||
text = result.result
|
||||
self.logger.bind(tag=TAG).info(f"其他动作类型,直接返回结果: {text[:100] if text else None}...")
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
|
||||
@@ -91,11 +91,18 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
def process_function_call():
|
||||
conn.dialogue.put(Message(role="user", content=original_text))
|
||||
result = conn.func_handler.handle_llm_function_call(conn, function_call_data)
|
||||
if result:
|
||||
if result and function_name != 'play_music':
|
||||
# 获取当前最新的文本索引
|
||||
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
|
||||
# 处理函数调用结果
|
||||
conn._handle_function_result(result, function_call_data, text_index)
|
||||
text = result.response
|
||||
if text is None:
|
||||
text = result.result
|
||||
if text is not None:
|
||||
text_index = conn.tts_last_text_index + 1 if hasattr(conn, 'tts_last_text_index') else 0
|
||||
conn.recode_first_last_text(text, text_index)
|
||||
future = conn.executor.submit(conn.speak_and_play, text, text_index)
|
||||
conn.llm_finish_task = True
|
||||
conn.tts_queue.put(future)
|
||||
conn.dialogue.put(Message(role="assistant", content=text))
|
||||
|
||||
# 将函数执行放在线程池中
|
||||
conn.executor.submit(process_function_call)
|
||||
|
||||
@@ -23,13 +23,8 @@ class IntentProviderBase(ABC):
|
||||
self.llm = llm
|
||||
# 获取模型名称和类型信息
|
||||
model_name = getattr(llm, 'model_name', str(llm.__class__.__name__))
|
||||
model_type = getattr(llm, 'type', 'unknown')
|
||||
# 记录更详细的日志
|
||||
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}, 类型: {model_type}")
|
||||
# 尝试获取模型基础URL
|
||||
base_url = getattr(llm, 'base_url', 'N/A')
|
||||
if base_url != 'N/A':
|
||||
logger.bind(tag=TAG).debug(f"意图识别LLM基础URL: {base_url}")
|
||||
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}")
|
||||
|
||||
@abstractmethod
|
||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||
|
||||
@@ -45,10 +45,10 @@ def fetch_news_from_rss(rss_url):
|
||||
try:
|
||||
response = requests.get(rss_url)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
# 解析XML
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
|
||||
# 查找所有item元素(新闻条目)
|
||||
news_items = []
|
||||
for item in root.findall('.//item'):
|
||||
@@ -56,14 +56,14 @@ def fetch_news_from_rss(rss_url):
|
||||
link = item.find('link').text if item.find('link') is not None else "#"
|
||||
description = item.find('description').text if item.find('description') is not None else "无描述"
|
||||
pubDate = item.find('pubDate').text if item.find('pubDate') is not None else "未知时间"
|
||||
|
||||
|
||||
news_items.append({
|
||||
'title': title,
|
||||
'link': link,
|
||||
'description': description,
|
||||
'pubDate': pubDate
|
||||
})
|
||||
|
||||
|
||||
return news_items
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取RSS新闻失败: {e}")
|
||||
@@ -75,9 +75,9 @@ def fetch_news_detail(url):
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
soup = BeautifulSoup(response.content, 'html.parser')
|
||||
|
||||
|
||||
# 尝试提取正文内容 (这里的选择器需要根据实际网站结构调整)
|
||||
content_div = soup.select_one('.content_desc, .content, article, .article-content')
|
||||
if content_div:
|
||||
@@ -98,7 +98,7 @@ def map_category(category_text):
|
||||
"""将用户输入的中文类别映射到配置文件中的类别键"""
|
||||
if not category_text:
|
||||
return None
|
||||
|
||||
|
||||
# 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件
|
||||
category_map = {
|
||||
# 社会新闻
|
||||
@@ -113,10 +113,10 @@ def map_category(category_text):
|
||||
"金融": "finance",
|
||||
"经济": "finance"
|
||||
}
|
||||
|
||||
|
||||
# 转换为小写并去除空格
|
||||
normalized_category = category_text.lower().strip()
|
||||
|
||||
|
||||
# 返回映射结果,如果没有匹配项则返回原始输入
|
||||
return category_map.get(normalized_category, category_text)
|
||||
|
||||
@@ -128,25 +128,23 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
|
||||
# 如果detail为True,获取上一条新闻的详细内容
|
||||
if detail:
|
||||
if not hasattr(conn, 'last_news_link') or not conn.last_news_link or 'link' not in conn.last_news_link:
|
||||
direct_response = "抱歉,没有找到最近查询的新闻,请先获取一条新闻。"
|
||||
return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", direct_response)
|
||||
|
||||
return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
|
||||
|
||||
link = conn.last_news_link.get('link')
|
||||
title = conn.last_news_link.get('title', '未知标题')
|
||||
|
||||
|
||||
if link == '#':
|
||||
direct_response = "抱歉,该新闻没有可用的链接获取详细内容。"
|
||||
return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", direct_response)
|
||||
|
||||
return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
|
||||
|
||||
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
|
||||
|
||||
|
||||
# 获取新闻详情
|
||||
detail_content = fetch_news_detail(link)
|
||||
|
||||
|
||||
if not detail_content or detail_content == "无法获取详细内容":
|
||||
direct_response = f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。"
|
||||
return ActionResponse(Action.REQLLM, f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", direct_response)
|
||||
|
||||
return ActionResponse(Action.REQLLM,
|
||||
f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
|
||||
|
||||
# 构建详情报告
|
||||
detail_report = (
|
||||
f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n"
|
||||
@@ -155,37 +153,33 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
|
||||
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
|
||||
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
|
||||
)
|
||||
|
||||
# 构造一个直接回复,简要概括新闻内容
|
||||
direct_response = f"以下是《{title}》的详细内容:{detail_content[:200]}...(内容过长已省略)"
|
||||
|
||||
return ActionResponse(Action.REQLLM, detail_report, direct_response)
|
||||
|
||||
|
||||
return ActionResponse(Action.REQLLM, detail_report, None)
|
||||
|
||||
# 否则,获取新闻列表并随机选择一条
|
||||
# 从配置中获取RSS URL
|
||||
rss_config = conn.config["plugins"]["get_news"]
|
||||
default_rss_url = rss_config.get("default_rss_url", "https://www.chinanews.com.cn/rss/society.xml")
|
||||
|
||||
|
||||
# 将用户输入的类别映射到配置中的类别键
|
||||
mapped_category = map_category(category)
|
||||
|
||||
|
||||
# 如果提供了类别,尝试从配置中获取对应的URL
|
||||
rss_url = default_rss_url
|
||||
if mapped_category and mapped_category in rss_config.get("category_urls", {}):
|
||||
rss_url = rss_config["category_urls"][mapped_category]
|
||||
|
||||
|
||||
logger.bind(tag=TAG).info(f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}")
|
||||
|
||||
|
||||
# 获取新闻列表
|
||||
news_items = fetch_news_from_rss(rss_url)
|
||||
|
||||
|
||||
if not news_items:
|
||||
direct_response = "抱歉,未能获取到新闻信息,请稍后再试。"
|
||||
return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", direct_response)
|
||||
|
||||
return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
|
||||
|
||||
# 随机选择一条新闻
|
||||
selected_news = random.choice(news_items)
|
||||
|
||||
|
||||
# 保存当前新闻链接到连接对象,以便后续查询详情
|
||||
if not hasattr(conn, 'last_news_link'):
|
||||
conn.last_news_link = {}
|
||||
@@ -193,7 +187,7 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
|
||||
'link': selected_news.get('link', '#'),
|
||||
'title': selected_news.get('title', '未知标题')
|
||||
}
|
||||
|
||||
|
||||
# 构建新闻报告
|
||||
news_report = (
|
||||
f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n"
|
||||
@@ -204,17 +198,9 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
|
||||
f"直接读出新闻即可,不需要额外多余的内容。"
|
||||
f"如果用户询问更多详情,告知用户可以说'请详细介绍这条新闻'获取更多内容)"
|
||||
)
|
||||
|
||||
# 构造一个直接回复版本,简要播报新闻
|
||||
direct_response = (
|
||||
f"最新{mapped_category or ''}新闻:{selected_news['title']}。"
|
||||
f"{selected_news['description'][:150]}..."
|
||||
f"如果您想了解更多详情,可以说'请详细介绍这条新闻'。"
|
||||
)
|
||||
|
||||
return ActionResponse(Action.REQLLM, news_report, direct_response)
|
||||
|
||||
|
||||
return ActionResponse(Action.REQLLM, news_report, None)
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
|
||||
direct_response = "抱歉,获取新闻时发生错误,请稍后再试。"
|
||||
return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", direct_response)
|
||||
return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
|
||||
@@ -2,7 +2,6 @@ import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
import json
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -40,158 +39,85 @@ HEADERS = {
|
||||
)
|
||||
}
|
||||
|
||||
# 天气代码 https://dev.qweather.com/docs/resource/icons/#weather-icons
|
||||
WEATHER_CODE_MAP = {
|
||||
"100": "晴", "101": "多云", "102": "少云", "103": "晴间多云", "104": "阴",
|
||||
"150": "晴", "151": "多云", "152": "少云", "153": "晴间多云",
|
||||
"300": "阵雨", "301": "强阵雨", "302": "雷阵雨", "303": "强雷阵雨", "304": "雷阵雨伴有冰雹",
|
||||
"305": "小雨", "306": "中雨", "307": "大雨", "308": "极端降雨", "309": "毛毛雨/细雨",
|
||||
"310": "暴雨", "311": "大暴雨", "312": "特大暴雨", "313": "冻雨", "314": "小到中雨",
|
||||
"315": "中到大雨", "316": "大到暴雨", "317": "暴雨到大暴雨", "318": "大暴雨到特大暴雨",
|
||||
"350": "阵雨", "351": "强阵雨", "399": "雨",
|
||||
"400": "小雪", "401": "中雪", "402": "大雪", "403": "暴雪", "404": "雨夹雪",
|
||||
"405": "雨雪天气", "406": "阵雨夹雪", "407": "阵雪", "408": "小到中雪", "409": "中到大雪", "410": "大到暴雪",
|
||||
"456": "阵雨夹雪", "457": "阵雪", "499": "雪",
|
||||
"500": "薄雾", "501": "雾", "502": "霾", "503": "扬沙", "504": "浮尘",
|
||||
"507": "沙尘暴", "508": "强沙尘暴",
|
||||
"509": "浓雾", "510": "强浓雾", "511": "中度霾", "512": "重度霾", "513": "严重霾", "514": "大雾", "515": "特强浓雾",
|
||||
"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"
|
||||
logger.bind(tag=TAG).info(f"正在请求城市信息API,URL: {url}")
|
||||
try:
|
||||
response = requests.get(url, headers=HEADERS)
|
||||
logger.bind(tag=TAG).info(f"城市信息API响应状态码: {response.status_code}")
|
||||
json_data = response.json()
|
||||
logger.bind(tag=TAG).debug(f"城市信息API响应: {json.dumps(json_data, ensure_ascii=False)}")
|
||||
return json_data.get('location', [])[0] if json_data.get('location') else None
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取城市信息失败: {str(e)}")
|
||||
return None
|
||||
response = requests.get(url, headers=HEADERS).json()
|
||||
return response.get('location', [])[0] if response.get('location') else None
|
||||
|
||||
|
||||
def fetch_weather_page(url):
|
||||
logger.bind(tag=TAG).info(f"正在请求天气页面,URL: {url}")
|
||||
try:
|
||||
response = requests.get(url, headers=HEADERS)
|
||||
logger.bind(tag=TAG).info(f"天气页面响应状态码: {response.status_code}")
|
||||
if not response.ok:
|
||||
logger.bind(tag=TAG).error(f"获取天气页面失败: {response.text[:200]}")
|
||||
return BeautifulSoup(response.text, "html.parser") if response.ok else None
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"获取天气页面失败: {str(e)}")
|
||||
return None
|
||||
response = requests.get(url, headers=HEADERS)
|
||||
return BeautifulSoup(response.text, "html.parser") if response.ok else None
|
||||
|
||||
|
||||
def parse_weather_info(soup):
|
||||
try:
|
||||
city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True)
|
||||
logger.bind(tag=TAG).info(f"解析到城市名称: {city_name}")
|
||||
city_name = soup.select_one("h1.c-submenu__location").get_text(strip=True)
|
||||
|
||||
current_abstract = soup.select_one(".c-city-weather-current .current-abstract")
|
||||
current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知"
|
||||
logger.bind(tag=TAG).info(f"当前天气概况: {current_abstract}")
|
||||
current_abstract = soup.select_one(".c-city-weather-current .current-abstract")
|
||||
current_abstract = current_abstract.get_text(strip=True) if current_abstract else "未知"
|
||||
|
||||
current_basic = {}
|
||||
for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"):
|
||||
parts = item.get_text(strip=True, separator=" ").split(" ")
|
||||
if len(parts) == 2:
|
||||
key, value = parts[1], parts[0]
|
||||
current_basic[key] = value
|
||||
logger.bind(tag=TAG).info(f"当前天气详情: {json.dumps(current_basic, ensure_ascii=False)}")
|
||||
current_basic = {}
|
||||
for item in soup.select(".c-city-weather-current .current-basic .current-basic___item"):
|
||||
parts = item.get_text(strip=True, separator=" ").split(" ")
|
||||
if len(parts) == 2:
|
||||
key, value = parts[1], parts[0]
|
||||
current_basic[key] = value
|
||||
|
||||
temps_list = []
|
||||
for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
|
||||
date = row.select_one(".date-bg .date").get_text(strip=True)
|
||||
temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
|
||||
high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
|
||||
temps_list.append((date, high_temp, low_temp))
|
||||
logger.bind(tag=TAG).info(f"获取到未来7天温度: {temps_list}")
|
||||
temps_list = []
|
||||
for row in soup.select(".city-forecast-tabs__row")[:7]: # 取前7天的数据
|
||||
date = row.select_one(".date-bg .date").get_text(strip=True)
|
||||
weather_code = row.select_one(".date-bg .icon")["src"].split("/")[-1].split(".")[0]
|
||||
weather = WEATHER_CODE_MAP.get(weather_code, "未知")
|
||||
temps = [span.get_text(strip=True) for span in row.select(".tmp-cont .temp")]
|
||||
high_temp, low_temp = (temps[0], temps[-1]) if len(temps) >= 2 else (None, None)
|
||||
temps_list.append((date, weather, high_temp, low_temp))
|
||||
|
||||
return city_name, current_abstract, current_basic, temps_list
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"解析天气信息失败: {str(e)}")
|
||||
return "未知城市", "未知天气", {}, []
|
||||
return city_name, current_abstract, current_basic, temps_list
|
||||
|
||||
|
||||
@register_function('get_weather', GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
||||
logger.bind(tag=TAG).info(f"===== 天气查询函数开始执行 =====")
|
||||
logger.bind(tag=TAG).info(f"查询参数: location={location}, lang={lang}")
|
||||
|
||||
try:
|
||||
api_key = conn.config["plugins"]["get_weather"]["api_key"]
|
||||
default_location = conn.config["plugins"]["get_weather"]["default_location"]
|
||||
|
||||
# 位置参数处理逻辑优化
|
||||
# 1. 如果传入了明确的位置参数且不为None/空字符串,则使用该参数
|
||||
# 2. 否则,尝试使用客户端IP地址对应的城市信息
|
||||
# 3. 如果上述都无效,则使用配置文件中的默认位置
|
||||
if location and location.strip():
|
||||
final_location = location.strip()
|
||||
logger.bind(tag=TAG).info(f"使用用户明确指定的位置: {final_location}")
|
||||
elif conn.client_ip_info and "city" in conn.client_ip_info and conn.client_ip_info["city"]:
|
||||
final_location = conn.client_ip_info["city"]
|
||||
logger.bind(tag=TAG).info(f"使用IP地址解析的位置: {final_location}")
|
||||
else:
|
||||
final_location = default_location
|
||||
logger.bind(tag=TAG).info(f"使用配置文件中的默认位置: {final_location}")
|
||||
|
||||
logger.bind(tag=TAG).info(f"最终使用的查询地点: {final_location}, API Key: {api_key}")
|
||||
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}")
|
||||
|
||||
city_info = fetch_city_info(final_location, api_key)
|
||||
if not city_info:
|
||||
error_msg = f"未找到相关的城市: {final_location},请确认地点是否正确"
|
||||
logger.bind(tag=TAG).error(error_msg)
|
||||
# 构造一个直接回复
|
||||
direct_response = f"抱歉,我未能找到\"{final_location}\"的天气信息,请确认地点名称是否正确,或者尝试查询其他城市的天气。"
|
||||
return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
|
||||
city_info = fetch_city_info(location, api_key)
|
||||
if not city_info:
|
||||
return ActionResponse(Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None)
|
||||
|
||||
logger.bind(tag=TAG).info(f"获取到城市信息: {json.dumps(city_info, ensure_ascii=False)}")
|
||||
logger.bind(tag=TAG).info(f"天气查询链接: {city_info['fxLink']}")
|
||||
|
||||
soup = fetch_weather_page(city_info['fxLink'])
|
||||
if not soup:
|
||||
error_msg = "请求天气页面失败"
|
||||
logger.bind(tag=TAG).error(error_msg)
|
||||
direct_response = f"抱歉,在获取{final_location}的天气信息时遇到了问题,请稍后再试。"
|
||||
return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
|
||||
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)
|
||||
|
||||
# 构建天气报告(给LLM使用的详细数据)
|
||||
weather_report = f"根据下列数据,用{lang}回应用户的查询天气请求:\n{city_name}未来7天天气:\n"
|
||||
for i, (date, high, low) in enumerate(temps_list):
|
||||
if high and low:
|
||||
weather_report += f"{date}: {low}到{high}\n"
|
||||
weather_report += (
|
||||
f"当前天气: {current_abstract}\n"
|
||||
f"当前天气参数: {current_basic}\n"
|
||||
f"(确保只报告指定单日的气温范围,除非用户明确要求想要了解多日天气,如果未指定,默认报告今天的温度范围。"
|
||||
"参数为0的值不需要报告给用户,每次都报告体感温度,根据语境选择合适的参数内容告知用户,并对参数给出相应评价)"
|
||||
)
|
||||
|
||||
# 同时构建一个直接可用的人性化天气回复
|
||||
# 这用作备用,防止LLM处理失败时能够直接回复用户
|
||||
today_temp = ""
|
||||
for date, high, low in temps_list:
|
||||
if "今天" in date or "今日" in date:
|
||||
today_temp = f"{low}到{high}"
|
||||
break
|
||||
if not today_temp and temps_list:
|
||||
today_temp = f"{temps_list[0][2]}到{temps_list[0][1]}"
|
||||
|
||||
feel_temp = current_basic.get("体感温度", "")
|
||||
humidity = current_basic.get("相对湿度", "")
|
||||
|
||||
direct_response = f"{city_name}今天{current_abstract},温度{today_temp}。"
|
||||
if feel_temp:
|
||||
direct_response += f"体感温度{feel_temp}。"
|
||||
if humidity:
|
||||
direct_response += f"相对湿度{humidity}。"
|
||||
|
||||
# 添加简单的建议
|
||||
if "雨" in current_abstract:
|
||||
direct_response += "外出请记得带伞。"
|
||||
elif temps_list and temps_list[0][1] and int(temps_list[0][1].replace("°", "")) > 30:
|
||||
direct_response += "天气炎热,注意防暑。"
|
||||
elif temps_list and temps_list[0][2] and int(temps_list[0][2].replace("°", "")) < 10:
|
||||
direct_response += "天气较冷,注意保暖。"
|
||||
|
||||
logger.bind(tag=TAG).info(f"构建的天气报告: {weather_report}")
|
||||
logger.bind(tag=TAG).info(f"备用直接回复: {direct_response}")
|
||||
logger.bind(tag=TAG).info(f"===== 天气查询函数执行完毕,返回结果 =====")
|
||||
|
||||
# 返回详细天气报告,但添加direct_response作为备用
|
||||
return ActionResponse(Action.REQLLM, weather_report, direct_response)
|
||||
except Exception as e:
|
||||
error_msg = f"查询天气时发生错误: {str(e)}"
|
||||
logger.bind(tag=TAG).error(error_msg)
|
||||
# 出错时也提供一个直接回复
|
||||
direct_response = f"抱歉,在获取天气信息时遇到了技术问题,请稍后再试。"
|
||||
return ActionResponse(Action.RESPONSE, None, direct_response) # 使用RESPONSE动作直接返回
|
||||
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)
|
||||
Reference in New Issue
Block a user