标签管理: - 新增 tags/tag_assignments 持久化层、迁移、/api/tags 路由 - 标签分智能体/技能/定时任务三类, 仅同类型可互打, 仅 admin 可增删改与打标签 - 三类资源列表支持按名称模糊检索与按标签筛选(走后端查询参数) - 前端新增标签管理页、TagPickerDialog(点击即时绑定)、彩色标签 chip 问答前缀: - 前缀不再拼入消息 content, 改存 additional_kwargs 由 PromptPrefixMiddleware 在调模型前临时注入, 用户界面看不到前缀但模型仍收到 - 修正 thread_existed 闸门导致首轮前缀被跳过的问题 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
5.7 KiB
Python
150 lines
5.7 KiB
Python
"""Tests for the admin-configurable prompt-prefix toggle."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from langchain_core.messages import AIMessage, HumanMessage
|
|
|
|
from app.gateway.services import _prepend_prompt_prefix, _prompt_prefix_opted_in
|
|
from deerflow.config import system_settings as ss
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_runtime_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
|
|
"""Redirect ``runtime_home()`` to a per-test tmp dir."""
|
|
|
|
def fake_home() -> Path:
|
|
return tmp_path
|
|
|
|
monkeypatch.setattr(ss, "runtime_home", fake_home)
|
|
return tmp_path
|
|
|
|
|
|
def test_load_returns_defaults_when_missing(tmp_runtime_home: Path) -> None:
|
|
settings = ss.load_system_settings()
|
|
assert settings.prompt_prefix.enabled is False
|
|
assert settings.prompt_prefix.prompt_prefix == ""
|
|
|
|
|
|
def test_save_then_load_roundtrips(tmp_runtime_home: Path) -> None:
|
|
settings = ss.SystemSettings(
|
|
prompt_prefix=ss.PromptPrefixSettings(
|
|
enabled=True,
|
|
switch_label="加入立场前缀",
|
|
prompt_prefix="你需要站在中方角度",
|
|
)
|
|
)
|
|
ss.save_system_settings(settings)
|
|
|
|
file_path = tmp_runtime_home / "system_settings.json"
|
|
assert file_path.is_file()
|
|
saved = json.loads(file_path.read_text(encoding="utf-8"))
|
|
assert saved["prompt_prefix"]["prompt_prefix"] == "你需要站在中方角度"
|
|
|
|
reloaded = ss.load_system_settings()
|
|
assert reloaded.prompt_prefix.enabled is True
|
|
assert reloaded.prompt_prefix.switch_label == "加入立场前缀"
|
|
assert reloaded.prompt_prefix.prompt_prefix == "你需要站在中方角度"
|
|
|
|
|
|
def test_prepend_prefix_tags_first_human_without_touching_content() -> None:
|
|
graph_input = {"messages": [HumanMessage(content="评价一下中美贸易")]}
|
|
out = _prepend_prompt_prefix(graph_input, "你需要站在中方角度")
|
|
msg = out["messages"][0]
|
|
assert isinstance(msg, HumanMessage)
|
|
# Content stays clean so the frontend never shows the prefix.
|
|
assert msg.content == "评价一下中美贸易"
|
|
# The prefix rides along in additional_kwargs for the middleware to inject.
|
|
assert msg.additional_kwargs["prompt_prefix"] == "你需要站在中方角度"
|
|
|
|
|
|
def test_prepend_prefix_preserves_block_content() -> None:
|
|
graph_input = {
|
|
"messages": [
|
|
HumanMessage(content=[{"type": "text", "text": "评价一下中美贸易"}])
|
|
]
|
|
}
|
|
out = _prepend_prompt_prefix(graph_input, "你需要站在中方角度")
|
|
msg = out["messages"][0]
|
|
assert msg.content == [{"type": "text", "text": "评价一下中美贸易"}]
|
|
assert msg.additional_kwargs["prompt_prefix"] == "你需要站在中方角度"
|
|
|
|
|
|
def test_prepend_prefix_only_touches_first_human() -> None:
|
|
graph_input = {
|
|
"messages": [
|
|
AIMessage(content="hi"),
|
|
HumanMessage(content="q1"),
|
|
HumanMessage(content="q2"),
|
|
]
|
|
}
|
|
out = _prepend_prompt_prefix(graph_input, "PREFIX")
|
|
# first human is index 1; index 2 must remain untouched
|
|
assert out["messages"][1].additional_kwargs.get("prompt_prefix") == "PREFIX"
|
|
assert out["messages"][1].content == "q1"
|
|
assert out["messages"][2].additional_kwargs.get("prompt_prefix") is None
|
|
assert out["messages"][2].content == "q2"
|
|
|
|
|
|
def test_prepend_prefix_no_messages_is_noop() -> None:
|
|
assert _prepend_prompt_prefix({"messages": []}, "X") == {"messages": []}
|
|
assert _prepend_prompt_prefix({}, "X") == {}
|
|
|
|
|
|
def test_prompt_prefix_middleware_injects_prefix_for_model_only() -> None:
|
|
"""The middleware prepends the prefix to the request copy, not to state."""
|
|
from deerflow.agents.middlewares.prompt_prefix_middleware import PromptPrefixMiddleware
|
|
|
|
mw = PromptPrefixMiddleware()
|
|
tagged = HumanMessage(
|
|
content="评价一下中美贸易",
|
|
additional_kwargs={"prompt_prefix": "你需要站在中方角度"},
|
|
)
|
|
patched = PromptPrefixMiddleware._apply([tagged])
|
|
assert patched is not None
|
|
assert patched[0].content == "你需要站在中方角度\n\n评价一下中美贸易"
|
|
# The original message in state is untouched.
|
|
assert tagged.content == "评价一下中美贸易"
|
|
assert mw is not None
|
|
|
|
|
|
def test_prompt_prefix_middleware_noop_without_tag() -> None:
|
|
from deerflow.agents.middlewares.prompt_prefix_middleware import PromptPrefixMiddleware
|
|
|
|
plain = HumanMessage(content="just a question")
|
|
assert PromptPrefixMiddleware._apply([plain]) is None
|
|
|
|
|
|
def test_prompt_prefix_middleware_block_content() -> None:
|
|
from deerflow.agents.middlewares.prompt_prefix_middleware import PromptPrefixMiddleware
|
|
|
|
tagged = HumanMessage(
|
|
content=[{"type": "text", "text": "评价一下中美贸易"}],
|
|
additional_kwargs={"prompt_prefix": "你需要站在中方角度"},
|
|
)
|
|
patched = PromptPrefixMiddleware._apply([tagged])
|
|
assert patched is not None
|
|
assert patched[0].content[0] == {"type": "text", "text": "你需要站在中方角度"}
|
|
assert patched[0].content[1] == {"type": "text", "text": "评价一下中美贸易"}
|
|
|
|
|
|
def test_opted_in_via_context() -> None:
|
|
body = SimpleNamespace(context={"prompt_prefix_enabled": True}, metadata=None)
|
|
assert _prompt_prefix_opted_in(body) is True
|
|
|
|
|
|
def test_opted_in_via_metadata() -> None:
|
|
body = SimpleNamespace(context=None, metadata={"prompt_prefix_enabled": True})
|
|
assert _prompt_prefix_opted_in(body) is True
|
|
|
|
|
|
def test_not_opted_in_by_default() -> None:
|
|
body = SimpleNamespace(context=None, metadata=None)
|
|
assert _prompt_prefix_opted_in(body) is False
|
|
body2 = SimpleNamespace(context={"mode": "ultra"}, metadata={})
|
|
assert _prompt_prefix_opted_in(body2) is False
|