223 lines
7.4 KiB
Python
223 lines
7.4 KiB
Python
import unittest
|
|
|
|
from models import AssistantConfig, RuntimeModelResource
|
|
from pipecat.frames.frames import LLMContextFrame
|
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
|
from pipecat.processors.frame_processor import FrameDirection
|
|
from services.pipecat.pipeline import (
|
|
KNOWLEDGE_CONTEXT_MARKER,
|
|
KnowledgeRetrievalProcessor,
|
|
UserTurnRoutingProcessor,
|
|
_knowledge_tool_description,
|
|
_workflow_vision_uses_main_llm,
|
|
)
|
|
|
|
|
|
class KnowledgeToolDescriptionTest(unittest.TestCase):
|
|
def test_includes_bound_knowledge_scope(self):
|
|
description = _knowledge_tool_description(
|
|
AssistantConfig(
|
|
knowledge_base_name="产品服务知识库",
|
|
knowledge_base_description="产品价格、售后政策和退换货条件",
|
|
)
|
|
)
|
|
|
|
self.assertIn("知识库名称:产品服务知识库", description)
|
|
self.assertIn("资料适用范围:产品价格、售后政策和退换货条件", description)
|
|
self.assertIn("与该范围无关的问题不要调用", description)
|
|
|
|
def test_falls_back_when_metadata_is_empty(self):
|
|
description = _knowledge_tool_description(AssistantConfig())
|
|
|
|
self.assertEqual(
|
|
description,
|
|
"在当前助手绑定的知识库中检索与问题最相关的资料片段。",
|
|
)
|
|
|
|
def test_compacts_and_limits_description(self):
|
|
description = _knowledge_tool_description(
|
|
AssistantConfig(knowledge_base_description=("范围\n 内容 " * 200))
|
|
)
|
|
|
|
self.assertNotIn("\n ", description)
|
|
self.assertLess(len(description), 1000)
|
|
|
|
def test_workflow_knowledge_uses_system_role(self):
|
|
processor = KnowledgeRetrievalProcessor(None)
|
|
messages = [
|
|
{"role": "assistant", "content": "你好"},
|
|
{
|
|
"role": "developer",
|
|
"content": f"{KNOWLEDGE_CONTEXT_MARKER}\n旧检索结果",
|
|
},
|
|
]
|
|
|
|
processor._set_context(
|
|
messages,
|
|
f"{KNOWLEDGE_CONTEXT_MARKER}\n新检索结果",
|
|
)
|
|
|
|
self.assertEqual(messages[0]["role"], "system")
|
|
self.assertIn("新检索结果", messages[0]["content"])
|
|
self.assertFalse(any(message["role"] == "developer" for message in messages))
|
|
|
|
|
|
class UserTurnRoutingProcessorTest(unittest.IsolatedAsyncioTestCase):
|
|
async def test_routes_each_user_message_once_before_response_run(self):
|
|
class FakeBrain:
|
|
def __init__(self):
|
|
self.turns = []
|
|
|
|
async def on_user_turn_end(self, content, user_message=None):
|
|
self.turns.append((content, user_message))
|
|
return True
|
|
|
|
brain = FakeBrain()
|
|
processor = UserTurnRoutingProcessor(brain)
|
|
forwarded = []
|
|
|
|
async def push_frame(frame, direction):
|
|
forwarded.append((frame, direction))
|
|
|
|
processor.push_frame = push_frame
|
|
context = LLMContext(messages=[{"role": "user", "content": "我叫李白"}])
|
|
frame = LLMContextFrame(context)
|
|
|
|
await processor.process_frame(frame, FrameDirection.DOWNSTREAM)
|
|
self.assertEqual(
|
|
brain.turns,
|
|
[("我叫李白", {"role": "user", "content": "我叫李白"})],
|
|
)
|
|
self.assertEqual(forwarded, [])
|
|
|
|
# A queued LLMRunFrame after the transition uses the same context. It
|
|
# must reach the target Agent without invoking routing a second time.
|
|
await processor.process_frame(frame, FrameDirection.DOWNSTREAM)
|
|
self.assertEqual(
|
|
brain.turns,
|
|
[("我叫李白", {"role": "user", "content": "我叫李白"})],
|
|
)
|
|
self.assertEqual(forwarded, [(frame, FrameDirection.DOWNSTREAM)])
|
|
|
|
async def test_routes_multimodal_user_message_by_its_text_part(self):
|
|
class FakeBrain:
|
|
def __init__(self):
|
|
self.turns = []
|
|
|
|
async def on_user_turn_end(self, content, user_message=None):
|
|
self.turns.append((content, user_message))
|
|
return False
|
|
|
|
brain = FakeBrain()
|
|
processor = UserTurnRoutingProcessor(brain)
|
|
processor.push_frame = lambda *_args, **_kwargs: _async_none()
|
|
context = LLMContext(
|
|
messages=[
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "看看这张照片"},
|
|
{
|
|
"type": "image_url",
|
|
"image_url": {"url": "data:image/jpeg;base64,AA=="},
|
|
},
|
|
],
|
|
}
|
|
]
|
|
)
|
|
|
|
await processor.process_frame(
|
|
LLMContextFrame(context),
|
|
FrameDirection.DOWNSTREAM,
|
|
)
|
|
|
|
self.assertEqual(
|
|
brain.turns,
|
|
[
|
|
(
|
|
"看看这张照片",
|
|
{
|
|
"role": "user",
|
|
"content": [
|
|
{"type": "text", "text": "看看这张照片"},
|
|
{
|
|
"type": "image_url",
|
|
"image_url": {
|
|
"url": "data:image/jpeg;base64,AA=="
|
|
},
|
|
},
|
|
],
|
|
},
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
class WorkflowVisionModeTest(unittest.TestCase):
|
|
def test_uses_active_agent_llm_only_without_auxiliary_model(self):
|
|
cfg = AssistantConfig(
|
|
type="workflow",
|
|
workflow_model_resources={
|
|
"agent_llm": RuntimeModelResource(
|
|
id="agent_llm",
|
|
name="视觉 Agent",
|
|
capability="LLM",
|
|
interface_type="openai-llm",
|
|
support_image_input=True,
|
|
)
|
|
},
|
|
)
|
|
|
|
self.assertTrue(
|
|
_workflow_vision_uses_main_llm(
|
|
cfg,
|
|
{
|
|
"enabled": True,
|
|
"llm_resource_id": "agent_llm",
|
|
"vision_model_resource_id": None,
|
|
},
|
|
)
|
|
)
|
|
self.assertFalse(
|
|
_workflow_vision_uses_main_llm(
|
|
cfg,
|
|
{
|
|
"enabled": True,
|
|
"llm_resource_id": "agent_llm",
|
|
"vision_model_resource_id": "auxiliary_vision",
|
|
},
|
|
)
|
|
)
|
|
|
|
def test_rejects_a_non_visual_active_agent_llm(self):
|
|
cfg = AssistantConfig(
|
|
type="workflow",
|
|
workflow_model_resources={
|
|
"text_llm": RuntimeModelResource(
|
|
id="text_llm",
|
|
name="文本 Agent",
|
|
capability="LLM",
|
|
interface_type="openai-llm",
|
|
support_image_input=False,
|
|
)
|
|
},
|
|
)
|
|
|
|
with self.assertRaisesRegex(ValueError, "不支持图片输入"):
|
|
_workflow_vision_uses_main_llm(
|
|
cfg,
|
|
{
|
|
"enabled": True,
|
|
"llm_resource_id": "text_llm",
|
|
"vision_model_resource_id": None,
|
|
},
|
|
)
|
|
|
|
|
|
async def _async_none():
|
|
return None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|