update:合并非tts代码

This commit is contained in:
hrz
2025-05-21 13:18:12 +08:00
parent ede2bc6a4e
commit 851365fb58
65 changed files with 5423 additions and 1943 deletions
@@ -8,10 +8,10 @@ from plugins_func.register import register_function, ToolType, ActionResponse, A
TAG = __name__
logger = setup_logging()
GET_NEWS_FUNCTION_DESC = {
GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC = {
"type": "function",
"function": {
"name": "get_news",
"name": "get_news_from_chinanews",
"description": (
"获取最新新闻,随机选择一条新闻进行播报。"
"用户可以指定新闻类型,如社会新闻、科技新闻、国际新闻等。"
@@ -23,20 +23,20 @@ GET_NEWS_FUNCTION_DESC = {
"properties": {
"category": {
"type": "string",
"description": "新闻类别,例如社会、科技、国际。可选参数,如果不提供则使用默认类别"
"description": "新闻类别,例如社会、科技、国际。可选参数,如果不提供则使用默认类别",
},
"detail": {
"type": "boolean",
"description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容"
"description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容",
},
"lang": {
"type": "string",
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
}
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN",
},
},
"required": ["lang"]
}
}
"required": ["lang"],
},
},
}
@@ -51,18 +51,30 @@ def fetch_news_from_rss(rss_url):
# 查找所有item元素(新闻条目)
news_items = []
for item in root.findall('.//item'):
title = item.find('title').text if item.find('title') is not None else "无标题"
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 "未知时间"
for item in root.findall(".//item"):
title = (
item.find("title").text if item.find("title") is not None else "无标题"
)
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
})
news_items.append(
{
"title": title,
"link": link,
"description": description,
"pubDate": pubDate,
}
)
return news_items
except Exception as e:
@@ -76,18 +88,24 @@ def fetch_news_detail(url):
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
soup = BeautifulSoup(response.content, "html.parser")
# 尝试提取正文内容 (这里的选择器需要根据实际网站结构调整)
content_div = soup.select_one('.content_desc, .content, article, .article-content')
content_div = soup.select_one(
".content_desc, .content, article, .article-content"
)
if content_div:
paragraphs = content_div.find_all('p')
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
paragraphs = content_div.find_all("p")
content = "\n".join(
[p.get_text().strip() for p in paragraphs if p.get_text().strip()]
)
return content
else:
# 如果找不到特定的内容区域,尝试获取所有段落
paragraphs = soup.find_all('p')
content = '\n'.join([p.get_text().strip() for p in paragraphs if p.get_text().strip()])
paragraphs = soup.find_all("p")
content = "\n".join(
[p.get_text().strip() for p in paragraphs if p.get_text().strip()]
)
return content[:2000] # 限制长度
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
@@ -111,7 +129,7 @@ def map_category(category_text):
"财经": "finance",
"财经新闻": "finance",
"金融": "finance",
"经济": "finance"
"经济": "finance",
}
# 转换为小写并去除空格
@@ -121,20 +139,36 @@ def map_category(category_text):
return category_map.get(normalized_category, category_text)
@register_function('get_news', GET_NEWS_FUNCTION_DESC, ToolType.SYSTEM_CTL)
def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_CN"):
@register_function(
"get_news_from_chinanews",
GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC,
ToolType.SYSTEM_CTL,
)
def get_news_from_chinanews(
conn, category: str = None, detail: bool = False, lang: str = "zh_CN"
):
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
try:
# 如果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:
return ActionResponse(Action.REQLLM, "抱歉,没有找到最近查询的新闻,请先获取一条新闻。", None)
if (
not hasattr(conn, "last_news_link")
or not conn.last_news_link
or "link" not in conn.last_news_link
):
return ActionResponse(
Action.REQLLM,
"抱歉,没有找到最近查询的新闻,请先获取一条新闻。",
None,
)
link = conn.last_news_link.get('link')
title = conn.last_news_link.get('title', '未知标题')
link = conn.last_news_link.get("link")
title = conn.last_news_link.get("title", "未知标题")
if link == '#':
return ActionResponse(Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None)
if link == "#":
return ActionResponse(
Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None
)
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
@@ -142,8 +176,11 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
detail_content = fetch_news_detail(link)
if not detail_content or detail_content == "无法获取详细内容":
return ActionResponse(Action.REQLLM,
f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。", None)
return ActionResponse(
Action.REQLLM,
f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。",
None,
)
# 构建详情报告
detail_report = (
@@ -158,8 +195,10 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
# 否则,获取新闻列表并随机选择一条
# 从配置中获取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")
rss_config = conn.config["plugins"]["get_news_from_chinanews"]
default_rss_url = rss_config.get(
"default_rss_url", "https://www.chinanews.com.cn/rss/society.xml"
)
# 将用户输入的类别映射到配置中的类别键
mapped_category = map_category(category)
@@ -169,23 +208,27 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
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}")
logger.bind(tag=TAG).info(
f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}"
)
# 获取新闻列表
news_items = fetch_news_from_rss(rss_url)
if not news_items:
return ActionResponse(Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None)
return ActionResponse(
Action.REQLLM, "抱歉,未能获取到新闻信息,请稍后再试。", None
)
# 随机选择一条新闻
selected_news = random.choice(news_items)
# 保存当前新闻链接到连接对象,以便后续查询详情
if not hasattr(conn, 'last_news_link'):
if not hasattr(conn, "last_news_link"):
conn.last_news_link = {}
conn.last_news_link = {
'link': selected_news.get('link', '#'),
'title': selected_news.get('title', '未知标题')
"link": selected_news.get("link", "#"),
"title": selected_news.get("title", "未知标题"),
}
# 构建新闻报告
@@ -203,4 +246,6 @@ def get_news(conn, category: str = None, detail: bool = False, lang: str = "zh_C
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
return ActionResponse(Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None)
return ActionResponse(
Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None
)
@@ -0,0 +1,214 @@
import random
import requests
import json
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from markitdown import MarkItDown
TAG = __name__
logger = setup_logging()
# 新闻来源字典,包含名称和对应的API ID
NEWS_SOURCES = {
"thepaper": "澎湃新闻",
"baidu": "百度热搜",
"cls-depth": "财联社",
}
# 动态生成新闻源描述
def generate_news_sources_description():
sources_desc = []
for source_id, source_name in NEWS_SOURCES.items():
sources_desc.append(f"{source_name}({source_id})")
return "".join(sources_desc)
GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC = {
"type": "function",
"function": {
"name": "get_news_from_newsnow",
"description": (
"获取最新新闻,随机选择一条新闻进行播报。"
f"用户可以选择不同的新闻源,如{generate_news_sources_description()}等。"
"如果没有指定,默认从澎湃新闻获取。"
"用户可以要求获取详细内容,此时会获取新闻的详细内容。"
),
"parameters": {
"type": "object",
"properties": {
"source": {
"type": "string",
"description": f"新闻源,例如{generate_news_sources_description()}等。可选参数,如果不提供则使用默认新闻源",
},
"detail": {
"type": "boolean",
"description": "是否获取详细内容,默认为false。如果为true,则获取上一条新闻的详细内容",
},
"lang": {
"type": "string",
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN",
},
},
"required": ["lang"],
},
},
}
def fetch_news_from_api(conn, source="thepaper"):
"""从API获取新闻列表"""
try:
api_url = f"https://newsnow.busiyi.world/api/s?id={source}"
if conn.config["plugins"].get("get_news_from_newsnow") and conn.config[
"plugins"
]["get_news_from_newsnow"].get("url"):
api_url = conn.config["plugins"]["get_news_from_newsnow"]["url"] + source
response = requests.get(api_url, timeout=10)
response.raise_for_status()
data = response.json()
if "items" in data:
return data["items"]
else:
logger.bind(tag=TAG).error(f"获取新闻API响应格式错误: {data}")
return []
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻API失败: {e}")
return []
def fetch_news_detail(url):
"""获取新闻详情页内容并使用MarkItDown清理HTML"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
# 使用MarkItDown清理HTML内容
md = MarkItDown(enable_plugins=False)
result = md.convert(response)
# 获取清理后的文本内容
clean_text = result.text_content
# 如果清理后的内容为空,返回提示信息
if not clean_text or len(clean_text.strip()) == 0:
logger.bind(tag=TAG).warning(f"清理后的新闻内容为空: {url}")
return "无法解析新闻详情内容,可能是网站结构特殊或内容受限。"
return clean_text
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻详情失败: {e}")
return "无法获取详细内容"
@register_function(
"get_news_from_newsnow",
GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC,
ToolType.SYSTEM_CTL,
)
def get_news_from_newsnow(
conn, source: str = "thepaper", detail: bool = False, lang: str = "zh_CN"
):
"""获取新闻并随机选择一条进行播报,或获取上一条新闻的详细内容"""
try:
# 如果detail为True,获取上一条新闻的详细内容
detail = str(detail).lower() == "true"
if detail:
if (
not hasattr(conn, "last_newsnow_link")
or not conn.last_newsnow_link
or "url" not in conn.last_newsnow_link
):
return ActionResponse(
Action.REQLLM,
"抱歉,没有找到最近查询的新闻,请先获取一条新闻。",
None,
)
url = conn.last_newsnow_link.get("url")
title = conn.last_newsnow_link.get("title", "未知标题")
source_id = conn.last_newsnow_link.get("source_id", "thepaper")
source_name = NEWS_SOURCES.get(source_id, "未知来源")
if not url or url == "#":
return ActionResponse(
Action.REQLLM, "抱歉,该新闻没有可用的链接获取详细内容。", None
)
logger.bind(tag=TAG).debug(
f"获取新闻详情: {title}, 来源: {source_name}, URL={url}"
)
# 获取新闻详情
detail_content = fetch_news_detail(url)
if not detail_content or detail_content == "无法获取详细内容":
return ActionResponse(
Action.REQLLM,
f"抱歉,无法获取《{title}》的详细内容,可能是链接已失效或网站结构发生变化。",
None,
)
# 构建详情报告
detail_report = (
f"根据下列数据,用{lang}回应用户的新闻详情查询请求:\n\n"
f"新闻标题: {title}\n"
# f"新闻来源: {source_name}\n"
f"详细内容: {detail_content}\n\n"
f"(请对上述新闻内容进行总结,提取关键信息,以自然、流畅的方式向用户播报,"
f"不要提及这是总结,就像是在讲述一个完整的新闻故事)"
)
return ActionResponse(Action.REQLLM, detail_report, None)
# 否则,获取新闻列表并随机选择一条
# 验证新闻源是否有效,如果无效则使用默认源
if source not in NEWS_SOURCES:
logger.bind(tag=TAG).warning(f"无效的新闻源: {source},使用默认源thepaper")
source = "thepaper"
source_name = NEWS_SOURCES.get(source, "澎湃新闻")
logger.bind(tag=TAG).info(f"获取新闻: 新闻源={source}({source_name})")
# 获取新闻列表
news_items = fetch_news_from_api(conn, source)
if not news_items:
return ActionResponse(
Action.REQLLM,
f"抱歉,未能从{source_name}获取到新闻信息,请稍后再试或尝试其他新闻源。",
None,
)
# 随机选择一条新闻
selected_news = random.choice(news_items)
# 保存当前新闻链接到连接对象,以便后续查询详情
if not hasattr(conn, "last_newsnow_link"):
conn.last_newsnow_link = {}
conn.last_newsnow_link = {
"url": selected_news.get("url", "#"),
"title": selected_news.get("title", "未知标题"),
"source_id": source,
}
# 构建新闻报告
news_report = (
f"根据下列数据,用{lang}回应用户的新闻查询请求:\n\n"
f"新闻标题: {selected_news['title']}\n"
# f"新闻来源: {source_name}\n"
f"(请以自然、流畅的方式向用户播报这条新闻标题,"
f"提示用户可以要求获取详细内容,此时会获取新闻的详细内容。)"
)
return ActionResponse(Action.REQLLM, news_report, None)
except Exception as e:
logger.bind(tag=TAG).error(f"获取新闻出错: {e}")
return ActionResponse(
Action.REQLLM, "抱歉,获取新闻时发生错误,请稍后再试。", None
)
@@ -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()
@@ -12,55 +13,104 @@ GET_WEATHER_FUNCTION_DESC = {
"name": "get_weather",
"description": (
"获取某个地点的天气,用户应提供一个位置,比如用户说杭州天气,参数为:杭州。"
"如果用户说的是省份,默认用省会城市。如果用户说的不是省份或城市而是一个地名,"
"默认用该地所在省份的省会城市。"
"如果用户说的是省份,默认用省会城市。如果用户说的不是省份或城市而是一个地名,默认用该地所在省份的省会城市。"
"如果用户没有指明地点,说“天气怎么样”,”今天天气如何“,location参数为空"
),
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "地点名,例如杭州。可选参数,如果不提供则不传"
"description": "地点名,例如杭州。可选参数,如果不提供则不传",
},
"lang": {
"type": "string",
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN"
}
"description": "返回用户使用的语言code,例如zh_CN/zh_HK/en_US/ja_JP等,默认zh_CN",
},
},
"required": ["lang"]
}
}
"required": ["lang"],
},
},
}
HEADERS = {
'User-Agent': (
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36'
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36"
)
}
# 天气代码 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": "未知"
"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"
def fetch_city_info(location, api_key, api_host):
url = f"https://{api_host}/geo/v2/city/lookup?key={api_key}&location={location}&lang=zh"
response = requests.get(url, headers=HEADERS).json()
return response.get('location', [])[0] if response.get('location') else None
return response.get("location", [])[0] if response.get("location") else None
def fetch_weather_page(url):
@@ -72,10 +122,14 @@ def parse_weather_info(soup):
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 "未知"
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"):
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]
@@ -84,7 +138,9 @@ def parse_weather_info(soup):
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_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)
@@ -93,31 +149,47 @@ def parse_weather_info(soup):
return city_name, current_abstract, current_basic, temps_list
@register_function('get_weather', GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
@register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
api_key = conn.config["plugins"]["get_weather"]["api_key"]
api_host = conn.config["plugins"]["get_weather"].get("api_host", "mj7p3y7naa.re.qweatherapi.com")
api_key = conn.config["plugins"]["get_weather"].get("api_key", "a861d0d5e7bf4ee1a83d9a9e4f96d4da")
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(location, api_key)
client_ip = conn.client_ip
# 优先使用用户提供的location参数
if not location:
# 通过客户端IP解析城市
if client_ip:
# 动态解析IP对应的城市信息
ip_info = get_ip_info(client_ip, logger)
location = ip_info.get("city") if ip_info and "city" in ip_info else None
else:
# 若IP解析失败或无IP,使用默认位置
location = default_location
city_info = fetch_city_info(location, api_key, api_host)
if not city_info:
return ActionResponse(Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None)
soup = fetch_weather_page(city_info['fxLink'])
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)
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)
@@ -1,34 +1,43 @@
from plugins_func.register import register_function,ToolType, ActionResponse, Action
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
handle_exit_intent_function_desc = {
"type": "function",
"function": {
"name": "handle_exit_intent",
"description": "当用户想结束对话或需要退出系统时调用",
"parameters": {
"type": "object",
"properties": {
"say_goodbye": {
"type": "string",
"description": "和用户友好结束对话的告别语"
}
},
"required": ["say_goodbye"]
}
"type": "function",
"function": {
"name": "handle_exit_intent",
"description": "当用户想结束对话或需要退出系统时调用",
"parameters": {
"type": "object",
"properties": {
"say_goodbye": {
"type": "string",
"description": "和用户友好结束对话的告别语",
}
}
},
"required": ["say_goodbye"],
},
},
}
@register_function('handle_exit_intent', handle_exit_intent_function_desc, ToolType.SYSTEM_CTL)
def handle_exit_intent(conn, say_goodbye: str):
@register_function(
"handle_exit_intent", handle_exit_intent_function_desc, ToolType.SYSTEM_CTL
)
def handle_exit_intent(conn, say_goodbye: str | None = None):
# 处理退出意图
try:
if say_goodbye is None:
say_goodbye = "再见,祝您生活愉快!"
conn.close_after_chat = True
logger.bind(tag=TAG).info(f"退出意图已处理:{say_goodbye}")
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye)
return ActionResponse(
action=Action.RESPONSE, result="退出意图已处理", response=say_goodbye
)
except Exception as e:
logger.bind(tag=TAG).error(f"处理退出意图错误: {e}")
return ActionResponse(action=Action.NONE, result="退出意图处理失败", response="")
return ActionResponse(
action=Action.NONE, result="退出意图处理失败", response=""
)
@@ -8,10 +8,12 @@ HASS_CACHE = {}
def append_devices_to_prompt(conn):
if conn.use_function_call_mode:
funcs = conn.config["Intent"]["function_call"].get("functions", [])
if conn.intent_type == "function_call":
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
"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
@@ -25,8 +27,10 @@ def append_devices_to_prompt(conn):
def initialize_hass_handler(conn):
global HASS_CACHE
if HASS_CACHE == {}:
if conn.use_function_call_mode:
funcs = conn.config["Intent"]["function_call"].get("functions", [])
if conn.load_function_plugin:
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
"functions", []
)
if "hass_get_state" in funcs or "hass_set_state" in funcs:
HASS_CACHE["base_url"] = conn.config["plugins"]["home_assistant"].get(
"base_url"
@@ -1,51 +1,56 @@
from plugins_func.register import register_function,ToolType, ActionResponse, Action
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
from plugins_func.register import register_function, ToolType, ActionResponse, Action
plugin_loader_function_desc = {
"type": "function",
"function": {
"name": "plugin_loader",
"description": "当用户想加载或卸载插件/function时,调用此函数:支持的插件列表为[plugins]",
"parameters": {
"type": "object",
"properties": {
"oper": {
"type": "string",
"description": "load or unload"
},
"name":{
"type": "string",
"description": "要加载或卸载的插件名字"
}
},
"required": ["oper","name"]
}
}
}
"type": "function",
"function": {
"name": "plugin_loader",
"description": "当用户想加载或卸载插件/function时,调用此函数:支持的插件列表为[plugins]",
"parameters": {
"type": "object",
"properties": {
"oper": {"type": "string", "description": "load or unload"},
"name": {"type": "string", "description": "要加载或卸载的插件名字"},
},
"required": ["oper", "name"],
},
},
}
@register_function('plugin_loader', plugin_loader_function_desc, ToolType.SYSTEM_CTL)
@register_function("plugin_loader", plugin_loader_function_desc, ToolType.SYSTEM_CTL)
def plugin_loader(conn, oper: str, name: str):
"""插件加载"""
if oper not in ["load", "unload"]:
return ActionResponse(action=Action.RESPONSE, result="插件操作失败", response="不支持的操作")
return ActionResponse(
action=Action.RESPONSE, result="插件操作失败", response="不支持的操作"
)
cur_support = conn.func_handler.current_support_functions()
if oper == "load":
if name in cur_support:
return ActionResponse(action=Action.RESPONSE, result="插件加载失败", response=f"{name}插件已加载,无需重复加载")
return ActionResponse(
action=Action.RESPONSE,
result="插件加载失败",
response=f"{name}插件已加载,无需重复加载",
)
func = conn.func_handler.function_registry.register_function(name)
if not func:
return ActionResponse(action=Action.RESPONSE, result="插件加载失败", response="插件未找到")
return ActionResponse(
action=Action.RESPONSE, result="插件加载失败", response="插件未找到"
)
res = f"{name}插件加载成功"
else:
if name not in cur_support:
return ActionResponse(action=Action.RESPONSE, result="插件卸载失败", response=f"{name}插件未加载")
return ActionResponse(
action=Action.RESPONSE,
result="插件卸载失败",
response=f"{name}插件未加载",
)
bOK = conn.func_handler.function_registry.unregister_function(name)
if not bOK:
return ActionResponse(action=Action.RESPONSE, result="插件卸载失败", response="插件未找到")
return ActionResponse(
action=Action.RESPONSE, result="插件卸载失败", response="插件未找到"
)
res = f"{name}插件卸载成功"
conn.func_handler.upload_functions_desc()
return ActionResponse(action=Action.RESPONSE, result="插件操作成功", response=res)
+24 -10
View File
@@ -10,7 +10,10 @@ class ToolType(Enum):
NONE = (1, "调用完工具后,不做其他操作")
WAIT = (2, "调用工具,等待函数返回")
CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责")
SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数")
SYSTEM_CTL = (
4,
"系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数",
)
IOT_CTL = (5, "IOT设备控制,需要传递conn参数")
MCP_CLIENT = (6, "MCP客户端")
@@ -30,12 +33,14 @@ class Action(Enum):
self.code = code
self.message = message
class ActionResponse:
def __init__(self, action: Action, result, response):
self.action = action # 动作类型
self.result = result # 动作产生的结果
self.response = response # 直接回复的内容
class FunctionItem:
def __init__(self, name, description, func, type):
self.name = name
@@ -43,40 +48,49 @@ class FunctionItem:
self.func = func
self.type = type
class DeviceTypeRegistry:
"""设备类型注册表,用于管理IOT设备类型及其函数"""
def __init__(self):
self.type_functions = {} # type_signature -> {func_name: FunctionItem}
def generate_device_type_id(self, descriptor):
"""通过设备能力描述生成类型ID"""
properties = sorted(descriptor["properties"].keys())
methods = sorted(descriptor["methods"].keys())
# 使用属性和方法的组合作为设备类型的唯一标识
type_signature = f"{descriptor['name']}:{','.join(properties)}:{','.join(methods)}"
type_signature = (
f"{descriptor['name']}:{','.join(properties)}:{','.join(methods)}"
)
return type_signature
def get_device_functions(self, type_id):
"""获取设备类型对应的所有函数"""
return self.type_functions.get(type_id, {})
def register_device_type(self, type_id, functions):
"""注册设备类型及其函数"""
if type_id not in self.type_functions:
self.type_functions[type_id] = functions
# 初始化函数注册字典
all_function_registry = {}
device_type_registry = DeviceTypeRegistry()
def register_function(name, desc, type=None):
"""注册函数到函数注册字典的装饰器"""
def decorator(func):
all_function_registry[name] = FunctionItem(name, desc, func, type)
logger.bind(tag=TAG).debug(f"函数 '{name}' 已加载,可以注册使用")
return func
return decorator
class FunctionRegistry:
def __init__(self):
self.function_registry = {}
@@ -89,9 +103,9 @@ class FunctionRegistry:
self.logger.bind(tag=TAG).error(f"函数 '{name}' 未找到")
return None
self.function_registry[name] = func
self.logger.bind(tag=TAG).info(f"函数 '{name}' 注册成功")
self.logger.bind(tag=TAG).debug(f"函数 '{name}' 注册成功")
return func
def unregister_function(self, name):
# 注销函数,检测是否存在
if name not in self.function_registry:
@@ -103,9 +117,9 @@ class FunctionRegistry:
def get_function(self, name):
return self.function_registry.get(name)
def get_all_functions(self):
return self.function_registry
def get_all_function_desc(self):
return [func.description for _, func in self.function_registry.items()]
return [func.description for _, func in self.function_registry.items()]