提示词这一侧不再自己存天气
原来提示词这条路自己存了一份,插件给的失败提示被当成天气报告存了 8 小时,网络好了也出不来天气。现在这边不存了,成功和失败都交给插件那边的缓存管:成功的存 8 小时,失败的存 5 分钟。存量空条目照旧清掉,管理员配好凭据后下一次问就能出天气。
This commit is contained in:
1 parent
b77ef426e3
commit
8a0d933074
2 files changed
+91
-44
No files matched your search
@@ -64,8 +64,8 @@ class PromptManager:
|
||||
self.logger = logger or setup_logging()
|
||||
self.base_prompt_template = None
|
||||
self.last_update_time = 0
|
||||
# 天气取不到时给用户的一句话,提示词要用它占位
|
||||
self._weather_notice = ""
|
||||
# 这次取到的天气内容,取不到时是一句给用户的说明
|
||||
self._weather_content = ""
|
||||
|
||||
# 导入全局缓存管理器
|
||||
from core.utils.cache.manager import cache_manager, CacheType
|
||||
@@ -172,17 +172,19 @@ class PromptManager:
|
||||
return "未知位置"
|
||||
|
||||
def _get_weather_info(self, conn: "ConnectionHandler", location: str) -> str:
|
||||
"""获取天气信息"""
|
||||
"""获取天气信息
|
||||
|
||||
<p>
|
||||
这一路不再自己存天气。插件那边存了:成功的报告 8 小时,失败的提示
|
||||
5 分钟。这边再存一份,存的是插件给什么就存什么,失败提示会被存成
|
||||
8 小时,网络恢复了、凭据配好了也出不来天气。
|
||||
"""
|
||||
try:
|
||||
# 先从缓存获取
|
||||
cached_weather = self.cache_manager.get(self.CacheType.WEATHER, location)
|
||||
if cached_weather:
|
||||
return cached_weather
|
||||
# 缓存里留着空条目说明上次没取到,留着会挡住下一次取,
|
||||
# 删掉重来,开发者把凭据配好之后下一次就该出天气
|
||||
# 以前这条路过缓存,存进去的条目是空的时候,管理员把凭据配好之后
|
||||
# 还得等它过期才出天气。没人读它了,清掉
|
||||
self.cache_manager.delete(self.CacheType.WEATHER, location)
|
||||
|
||||
# 缓存未命中,调用 async get_weather 函数
|
||||
# 调用 async get_weather 函数
|
||||
# Windows ProactorEventLoop 不支持 run_coroutine_threadsafe().result()
|
||||
# 因此用 call_soon_threadsafe 提交任务 + threading.Event 等待结果
|
||||
# 注意:Event.wait() 只阻塞当前线程池线程,不阻塞主事件循环
|
||||
@@ -212,25 +214,23 @@ class PromptManager:
|
||||
if isinstance(result, ActionResponse):
|
||||
weather_report = result.result
|
||||
if weather_report:
|
||||
self.cache_manager.set(
|
||||
self.CacheType.WEATHER, location, weather_report
|
||||
)
|
||||
self._weather_notice = ""
|
||||
# 成功还是失败,插件那边分得很清楚:成功的存 8 小时,
|
||||
# 失败的只存 5 分钟。这边原样用,不另存
|
||||
self._weather_content = weather_report
|
||||
return weather_report
|
||||
# 凭据没配好时插件给不出报告。这种结果不写进缓存:
|
||||
# 空条目留着的话,开发者配好凭据后还得等缓存过期才出天气
|
||||
# 凭据没配好时插件给不出报告
|
||||
if result.action == Action.RESPONSE:
|
||||
self._weather_notice = WEATHER_NOT_CONFIGURED_NOTICE
|
||||
self._weather_content = WEATHER_NOT_CONFIGURED_NOTICE
|
||||
else:
|
||||
self._weather_notice = WEATHER_UNAVAILABLE_NOTICE
|
||||
return self._weather_notice
|
||||
self._weather_notice = WEATHER_UNAVAILABLE_NOTICE
|
||||
return self._weather_notice
|
||||
self._weather_content = WEATHER_UNAVAILABLE_NOTICE
|
||||
return self._weather_content
|
||||
self._weather_content = WEATHER_UNAVAILABLE_NOTICE
|
||||
return self._weather_content
|
||||
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"获取天气信息失败: {e}")
|
||||
self._weather_notice = WEATHER_UNAVAILABLE_NOTICE
|
||||
return self._weather_notice
|
||||
self._weather_content = WEATHER_UNAVAILABLE_NOTICE
|
||||
return self._weather_content
|
||||
|
||||
def update_context_info(self, conn, client_ip: str):
|
||||
"""同步更新上下文信息"""
|
||||
@@ -270,17 +270,14 @@ class PromptManager:
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"更新上下文信息失败: {e}")
|
||||
|
||||
def _get_weather_context(self, location: str) -> str:
|
||||
def _get_weather_context(self) -> str:
|
||||
"""给提示词用的天气内容
|
||||
|
||||
<p>
|
||||
缓存里没有有效报告时给一句说明,不留空。占位符空着的话,模型会自己
|
||||
编一个天气出来。
|
||||
天气没取到时给一句说明,不留空。占位符空着的话,模型会自己编一个
|
||||
天气出来。
|
||||
"""
|
||||
cached_weather = self.cache_manager.get(self.CacheType.WEATHER, location)
|
||||
if cached_weather:
|
||||
return cached_weather
|
||||
return self._weather_notice or WEATHER_UNAVAILABLE_NOTICE
|
||||
return self._weather_content or WEATHER_UNAVAILABLE_NOTICE
|
||||
|
||||
def build_enhanced_prompt(
|
||||
self, user_prompt: str, device_id: str, client_ip: str = None, *args, **kwargs
|
||||
@@ -295,7 +292,6 @@ class PromptManager:
|
||||
|
||||
# 获取缓存的上下文信息
|
||||
local_address = ""
|
||||
weather_info = WEATHER_UNAVAILABLE_NOTICE
|
||||
|
||||
if client_ip:
|
||||
# 获取位置信息(从全局缓存)
|
||||
@@ -303,9 +299,8 @@ class PromptManager:
|
||||
self.cache_manager.get(self.CacheType.LOCATION, client_ip) or ""
|
||||
)
|
||||
|
||||
# 获取天气信息(从全局缓存)
|
||||
if local_address:
|
||||
weather_info = self._get_weather_context(local_address)
|
||||
# 获取天气信息(取天气的那一步已经把内容放在管理器上了)
|
||||
weather_info = self._get_weather_context()
|
||||
|
||||
# 获取TTS选择的语言,默认值为中文
|
||||
language = (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
天气有两条路进模型:工具调用那条走插件,提示词那条走 {weather_info} 占位符。
|
||||
这条用例盯的是占位符那条:凭据没配好时它得给用户一句说明,不能留空让模型
|
||||
自己编天气;取到的报告也不能被空条目挡住。
|
||||
自己编天气;取到的报告和失败提示也不能被塞进长缓存。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -18,6 +18,7 @@ _PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
# 提示词里不能出现这个路径,模型会照着念给终端用户
|
||||
CREDENTIAL_HINT = "尚未配置和风天气 api_key,请参考 docs/weather-integration.md 申请后填入"
|
||||
WEATHER_REPORT = "您查询的位置是:北京\n\n当前天气: 晴\n"
|
||||
FAILURE_MESSAGE = "暂时查不到 北京 的天气(城市未找到或天气服务不可用),请稍后再试"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -67,18 +68,32 @@ class _FakeConn:
|
||||
self.device_id = "test-device"
|
||||
|
||||
|
||||
def _install_fake_plugin(monkeypatch):
|
||||
"""把真插件换成假的:按有没有 api_key 走两种返回"""
|
||||
def _install_fake_plugin(monkeypatch, responses=None):
|
||||
"""把真插件换成假的:按有没有 api_key 走两种返回
|
||||
|
||||
<p>
|
||||
responses 给了就按顺序回,回完最后一条就一直回最后一条。返回调用次数,
|
||||
用例要看插件被调了几次。
|
||||
"""
|
||||
import plugins_func.functions.get_weather as weather_module
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
|
||||
calls = []
|
||||
|
||||
async def _fake_get_weather(conn, location=None, lang="zh_CN"):
|
||||
api_key = str(conn.config.get("plugins", {}).get("get_weather", {}).get("api_key") or "")
|
||||
api_key = str(
|
||||
conn.config.get("plugins", {}).get("get_weather", {}).get("api_key") or ""
|
||||
)
|
||||
calls.append(location)
|
||||
if not api_key.strip():
|
||||
return ActionResponse(Action.RESPONSE, None, CREDENTIAL_HINT)
|
||||
if responses:
|
||||
index = min(len(calls) - 1, len(responses) - 1)
|
||||
return responses[index]
|
||||
return ActionResponse(Action.REQLLM, WEATHER_REPORT, None)
|
||||
|
||||
monkeypatch.setattr(weather_module, "get_weather", _fake_get_weather)
|
||||
return calls
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -135,13 +150,13 @@ def test_unconfigured_weather_gives_notice_and_caches_nothing(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stale", [None, ""])
|
||||
def test_empty_cached_entry_does_not_block_weather(
|
||||
def test_empty_cached_entry_is_cleared_and_weather_is_fetched_again(
|
||||
monkeypatch, _manager, _cache, stale
|
||||
):
|
||||
"""缓存里留着空条目时,配好凭据后下一次就该出天气"""
|
||||
"""缓存里已经有空条目时,配好凭据后下一次就该出天气"""
|
||||
from core.utils.cache.manager import CacheType
|
||||
|
||||
_install_fake_plugin(monkeypatch)
|
||||
calls = _install_fake_plugin(monkeypatch)
|
||||
loop = _FakeLoop()
|
||||
conn = _FakeConn(loop, api_key="not-a-real-key")
|
||||
_cache.set(CacheType.LOCATION, conn.client_ip, "北京")
|
||||
@@ -153,13 +168,49 @@ def test_empty_cached_entry_does_not_block_weather(
|
||||
loop.close()
|
||||
|
||||
assert "当前天气: 晴" in prompt
|
||||
assert _cache.get(CacheType.WEATHER, "北京") == WEATHER_REPORT
|
||||
# 存量空条目被清掉了,没有留在缓存里
|
||||
assert _cache.get(CacheType.WEATHER, "北京") is None
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_failed_weather_is_not_cached_and_is_fetched_again(
|
||||
monkeypatch, _manager, _cache
|
||||
):
|
||||
"""插件说查不到天气时,那句话不能进长缓存,下一次要重新取"""
|
||||
from core.utils.cache.manager import CacheType
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
|
||||
calls = _install_fake_plugin(
|
||||
monkeypatch,
|
||||
responses=[
|
||||
ActionResponse(Action.REQLLM, FAILURE_MESSAGE, None),
|
||||
ActionResponse(Action.REQLLM, WEATHER_REPORT, None),
|
||||
],
|
||||
)
|
||||
loop = _FakeLoop()
|
||||
conn = _FakeConn(loop, api_key="not-a-real-key")
|
||||
_cache.set(CacheType.LOCATION, conn.client_ip, "北京")
|
||||
|
||||
try:
|
||||
first_prompt = _build_prompt(_manager, conn)
|
||||
# 失败提示进提示词,用户能听到「暂时查不到」
|
||||
assert "暂时查不到" in first_prompt
|
||||
# 但它不能被存进 8 小时那档缓存,不然网络恢复了就出不来天气
|
||||
assert _cache.get(CacheType.WEATHER, "北京") is None
|
||||
|
||||
second_prompt = _build_prompt(_manager, conn)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
# 下一次重新取,插件被调了两次
|
||||
assert len(calls) == 2
|
||||
assert "当前天气: 晴" in second_prompt
|
||||
|
||||
|
||||
def test_configured_weather_report_goes_to_placeholder(
|
||||
monkeypatch, _manager, _cache
|
||||
):
|
||||
"""凭据齐全:报告照旧进占位符,也照旧进缓存"""
|
||||
"""凭据齐全:报告照旧进占位符,缓存由插件那边管"""
|
||||
from core.utils.cache.manager import CacheType
|
||||
|
||||
_install_fake_plugin(monkeypatch)
|
||||
@@ -174,4 +225,5 @@ def test_configured_weather_report_goes_to_placeholder(
|
||||
|
||||
assert "当前天气: 晴" in prompt
|
||||
assert "未配置" not in prompt
|
||||
assert _cache.get(CacheType.WEATHER, "北京") == WEATHER_REPORT
|
||||
# 提示词这一侧不再自己存一份,成功那份由插件按 8 小时存
|
||||
assert _cache.get(CacheType.WEATHER, "北京") is None
|
||||
Reference in new issue
Block a user