Add dynamic variable support to Assistant model and related components
- Introduce dynamic variable definitions in AssistantConfig and Assistant models, allowing for flexible prompt customization. - Implement validation for dynamic variable names and types in the schema. - Update backend services and routes to handle dynamic variables in assistant configurations and runtime processing. - Enhance frontend components to support dynamic variable definitions, including a new editor for managing variables. - Add tests to ensure proper functionality and validation of dynamic variables in various scenarios.
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from models import AssistantConfig, RuntimeTool
|
||||
from pipecat.frames.frames import (
|
||||
@@ -20,6 +21,7 @@ from services.brains.dify_llm import (
|
||||
normalize_api_base,
|
||||
)
|
||||
from services.brains.workflow_brain import WorkflowBrain
|
||||
from services.runtime_variables import prepare_dynamic_config
|
||||
|
||||
|
||||
class FakeLLM:
|
||||
@@ -94,6 +96,21 @@ class BrainRegistryTests(unittest.TestCase):
|
||||
runtimeMode="realtime",
|
||||
)
|
||||
|
||||
def test_prompt_realtime_keeps_dynamic_variable_definitions(self):
|
||||
assistant = AssistantUpsert(
|
||||
name="realtime prompt",
|
||||
type="prompt",
|
||||
runtimeMode="realtime",
|
||||
dynamicVariableDefinitions={
|
||||
"user_name": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"default": None,
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertIn("user_name", assistant.dynamic_variable_definitions)
|
||||
|
||||
|
||||
class DifyHelpersTests(unittest.TestCase):
|
||||
def test_normalize_api_base(self):
|
||||
@@ -176,6 +193,24 @@ class DifyLLMServiceTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
|
||||
class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_realtime_prompt_brain_renders_dynamic_variables(self):
|
||||
cfg = prepare_dynamic_config(
|
||||
AssistantConfig(
|
||||
type="prompt",
|
||||
runtimeMode="realtime",
|
||||
prompt="服务用户 {{user_name}}",
|
||||
greeting="您好,{{user_name}}",
|
||||
dynamic_variable_definitions={
|
||||
"user_name": {"type": "string", "required": True}
|
||||
},
|
||||
),
|
||||
{"user_name": "王先生"},
|
||||
assistant_id="asst_realtime",
|
||||
)
|
||||
brain = build_brain(cfg)
|
||||
self.assertEqual(brain.system_prompt(cfg), "服务用户 王先生")
|
||||
self.assertEqual(await brain.greeting(cfg), "您好,王先生")
|
||||
|
||||
async def test_end_call_tool_is_owned_by_prompt_brain(self):
|
||||
brain = build_brain(
|
||||
AssistantConfig(
|
||||
@@ -233,6 +268,111 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertTrue(call_end.finished)
|
||||
self.assertEqual(params.result["action"], "ending_call")
|
||||
|
||||
async def test_http_tool_renders_secrets_and_updates_prompt_variable(self):
|
||||
requests = []
|
||||
|
||||
class FakeResponse:
|
||||
status_code = 200
|
||||
content = b'{"order":{"status":"paid"}}'
|
||||
|
||||
def raise_for_status(self):
|
||||
return None
|
||||
|
||||
def json(self):
|
||||
return {"order": {"status": "paid"}}
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, **_kwargs):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args):
|
||||
return None
|
||||
|
||||
async def request(self, method, url, **kwargs):
|
||||
requests.append((method, url, kwargs))
|
||||
return FakeResponse()
|
||||
|
||||
cfg = prepare_dynamic_config(
|
||||
AssistantConfig(
|
||||
type="prompt",
|
||||
runtimeMode="pipeline",
|
||||
prompt="订单状态:{{order_status}}",
|
||||
dynamic_variable_definitions={
|
||||
"order_status": {"type": "string", "default": "unknown"}
|
||||
},
|
||||
tools=[
|
||||
RuntimeTool(
|
||||
id="lookup",
|
||||
name="查询订单",
|
||||
function_name="lookup_order",
|
||||
type="http",
|
||||
description="查询订单状态",
|
||||
definition={
|
||||
"config": {
|
||||
"method": "GET",
|
||||
"url": "https://example.test/orders/{order_id}",
|
||||
"headers": {"Authorization": "Bearer {{secret__token}}"},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "order_id",
|
||||
"type": "string",
|
||||
"location": "path",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"type": "string",
|
||||
"location": "header",
|
||||
"required": False,
|
||||
},
|
||||
],
|
||||
"dynamic_variable_assignments": {
|
||||
"order_status": "response.order.status"
|
||||
},
|
||||
}
|
||||
},
|
||||
secrets={"dynamic_variables": {"secret__token": "server-token"}},
|
||||
)
|
||||
],
|
||||
),
|
||||
{},
|
||||
assistant_id="asst_1",
|
||||
)
|
||||
brain = build_brain(cfg)
|
||||
llm = FakeLLM()
|
||||
prompts = []
|
||||
visible_tools = []
|
||||
|
||||
async def queue_frame(_frame):
|
||||
pass
|
||||
|
||||
await brain.setup(
|
||||
cfg,
|
||||
BrainRuntime(
|
||||
context=LLMContext(messages=[]),
|
||||
llm=llm,
|
||||
queue_frame=queue_frame,
|
||||
set_system_prompt=prompts.append,
|
||||
set_tools=lambda tools: visible_tools.extend(tools or []),
|
||||
call_end=FakeCallEnd(),
|
||||
),
|
||||
)
|
||||
params = FakeFunctionParams(
|
||||
{"order_id": "A/1", "Authorization": "attacker-value"}
|
||||
)
|
||||
with patch("services.brains.prompt_brain.httpx.AsyncClient", FakeClient):
|
||||
await llm.functions["lookup_order"](params)
|
||||
|
||||
self.assertEqual(requests[0][1], "https://example.test/orders/A%2F1")
|
||||
self.assertEqual(
|
||||
requests[0][2]["headers"]["Authorization"], "Bearer server-token"
|
||||
)
|
||||
self.assertEqual(params.result["updated_variables"], ["order_status"])
|
||||
self.assertEqual(prompts[-1], "订单状态:paid")
|
||||
|
||||
|
||||
class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_transition_and_end_are_owned_by_workflow_brain(self):
|
||||
|
||||
Reference in New Issue
Block a user