mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-22 08:38:13 +02:00
chore: bump pipecat version and fix tests (#263)
* chore: bump pipecat version and fix tests * chore: add github workflow to run tests * fix: install reqirements.dev.txt in test script * fix: fix api-test action * feat: add integration test * test: add integration tests * test: add test for function call mute strategy
This commit is contained in:
parent
d256c6005c
commit
0e12c41fc7
76 changed files with 1776 additions and 670 deletions
|
|
@ -9,15 +9,15 @@ This module tests the full flow of:
|
|||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||
|
||||
from api.services.workflow.pipecat_engine_custom_tools import (
|
||||
CustomToolManager,
|
||||
get_function_schema,
|
||||
)
|
||||
from api.tests.conftest import MockToolModel
|
||||
from pipecat.adapters.schemas.function_schema import FunctionSchema
|
||||
from pipecat.adapters.schemas.tools_schema import ToolsSchema
|
||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||
|
||||
|
||||
def _update_llm_context(context, system_message, functions):
|
||||
|
|
@ -45,70 +45,65 @@ class TestCustomToolManagerContextIntegration:
|
|||
manager = CustomToolManager(mock_engine)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.get_organization_id_from_workflow_run"
|
||||
) as mock_get_org:
|
||||
mock_get_org.return_value = 1
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=sample_tools)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=sample_tools)
|
||||
# Get tool schemas via CustomToolManager - now returns FunctionSchema objects
|
||||
tool_uuids = ["weather-uuid-123", "booking-uuid-456", "lookup-uuid-789"]
|
||||
schemas = await manager.get_tool_schemas(tool_uuids)
|
||||
|
||||
# Get tool schemas via CustomToolManager - now returns FunctionSchema objects
|
||||
tool_uuids = ["weather-uuid-123", "booking-uuid-456", "lookup-uuid-789"]
|
||||
schemas = await manager.get_tool_schemas(tool_uuids)
|
||||
# Verify schemas were returned as FunctionSchema objects
|
||||
assert len(schemas) == 3
|
||||
assert all(isinstance(s, FunctionSchema) for s in schemas)
|
||||
|
||||
# Verify schemas were returned as FunctionSchema objects
|
||||
assert len(schemas) == 3
|
||||
assert all(isinstance(s, FunctionSchema) for s in schemas)
|
||||
# Create context with conversation history
|
||||
context = LLMContext()
|
||||
context.set_messages(
|
||||
[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I need to check the weather and book an appointment.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "I can help with both. Where would you like to check the weather?",
|
||||
},
|
||||
{"role": "user", "content": "San Francisco"},
|
||||
]
|
||||
)
|
||||
|
||||
# Create context with conversation history
|
||||
context = LLMContext()
|
||||
context.set_messages(
|
||||
[
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "I need to check the weather and book an appointment.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "I can help with both. Where would you like to check the weather?",
|
||||
},
|
||||
{"role": "user", "content": "San Francisco"},
|
||||
]
|
||||
)
|
||||
# Update context with new system message and tools
|
||||
# Now we can pass schemas directly since they're FunctionSchema objects
|
||||
new_system = {
|
||||
"role": "system",
|
||||
"content": "You are a scheduling assistant with access to weather and booking tools.",
|
||||
}
|
||||
_update_llm_context(context, new_system, schemas)
|
||||
|
||||
# Update context with new system message and tools
|
||||
# Now we can pass schemas directly since they're FunctionSchema objects
|
||||
new_system = {
|
||||
"role": "system",
|
||||
"content": "You are a scheduling assistant with access to weather and booking tools.",
|
||||
}
|
||||
_update_llm_context(context, new_system, schemas)
|
||||
# Verify context was updated correctly
|
||||
messages = context.messages
|
||||
assert len(messages) == 4
|
||||
assert (
|
||||
messages[0]["content"]
|
||||
== "You are a scheduling assistant with access to weather and booking tools."
|
||||
)
|
||||
assert messages[1]["role"] == "user"
|
||||
assert messages[3]["content"] == "San Francisco"
|
||||
|
||||
# Verify context was updated correctly
|
||||
messages = context.messages
|
||||
assert len(messages) == 4
|
||||
assert (
|
||||
messages[0]["content"]
|
||||
== "You are a scheduling assistant with access to weather and booking tools."
|
||||
)
|
||||
assert messages[1]["role"] == "user"
|
||||
assert messages[3]["content"] == "San Francisco"
|
||||
# Verify tools were set
|
||||
tools = context.tools
|
||||
assert tools is not None
|
||||
assert len(tools.standard_tools) == 3
|
||||
|
||||
# Verify tools were set
|
||||
tools = context.tools
|
||||
assert tools is not None
|
||||
assert len(tools.standard_tools) == 3
|
||||
|
||||
# Verify tool names
|
||||
tool_names = {t.name for t in tools.standard_tools}
|
||||
assert tool_names == {
|
||||
"get_weather",
|
||||
"book_appointment",
|
||||
"customer_lookup",
|
||||
}
|
||||
# Verify tool names
|
||||
tool_names = {t.name for t in tools.standard_tools}
|
||||
assert tool_names == {
|
||||
"get_weather",
|
||||
"book_appointment",
|
||||
"customer_lookup",
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_schemas_have_correct_properties(
|
||||
|
|
@ -118,39 +113,32 @@ class TestCustomToolManagerContextIntegration:
|
|||
manager = CustomToolManager(mock_engine)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.get_organization_id_from_workflow_run"
|
||||
) as mock_get_org:
|
||||
mock_get_org.return_value = 1
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=sample_tools)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=sample_tools)
|
||||
schemas = await manager.get_tool_schemas(
|
||||
["weather-uuid-123", "booking-uuid-456"]
|
||||
)
|
||||
|
||||
schemas = await manager.get_tool_schemas(
|
||||
["weather-uuid-123", "booking-uuid-456"]
|
||||
)
|
||||
# Find the booking schema - now using FunctionSchema attributes
|
||||
booking_schema = next(s for s in schemas if s.name == "book_appointment")
|
||||
|
||||
# Find the booking schema - now using FunctionSchema attributes
|
||||
booking_schema = next(
|
||||
s for s in schemas if s.name == "book_appointment"
|
||||
)
|
||||
# Verify parameter properties
|
||||
assert "customer_name" in booking_schema.properties
|
||||
assert "date" in booking_schema.properties
|
||||
assert "time" in booking_schema.properties
|
||||
assert "notes" in booking_schema.properties
|
||||
|
||||
# Verify parameter properties
|
||||
assert "customer_name" in booking_schema.properties
|
||||
assert "date" in booking_schema.properties
|
||||
assert "time" in booking_schema.properties
|
||||
assert "notes" in booking_schema.properties
|
||||
# Verify types
|
||||
assert booking_schema.properties["customer_name"]["type"] == "string"
|
||||
assert booking_schema.properties["date"]["type"] == "string"
|
||||
|
||||
# Verify types
|
||||
assert booking_schema.properties["customer_name"]["type"] == "string"
|
||||
assert booking_schema.properties["date"]["type"] == "string"
|
||||
|
||||
# Verify required
|
||||
assert "customer_name" in booking_schema.required
|
||||
assert "date" in booking_schema.required
|
||||
assert "time" in booking_schema.required
|
||||
assert "notes" not in booking_schema.required
|
||||
# Verify required
|
||||
assert "customer_name" in booking_schema.required
|
||||
assert "date" in booking_schema.required
|
||||
assert "time" in booking_schema.required
|
||||
assert "notes" not in booking_schema.required
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_update_with_builtin_and_custom_tools(
|
||||
|
|
@ -160,67 +148,62 @@ class TestCustomToolManagerContextIntegration:
|
|||
manager = CustomToolManager(mock_engine)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.get_organization_id_from_workflow_run"
|
||||
) as mock_get_org:
|
||||
mock_get_org.return_value = 1
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(
|
||||
return_value=[sample_tools[0]]
|
||||
) # Just weather
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(
|
||||
return_value=[sample_tools[0]]
|
||||
) # Just weather
|
||||
# Get custom tool schemas - returns FunctionSchema objects
|
||||
custom_schemas = await manager.get_tool_schemas(["weather-uuid-123"])
|
||||
|
||||
# Get custom tool schemas - returns FunctionSchema objects
|
||||
custom_schemas = await manager.get_tool_schemas(["weather-uuid-123"])
|
||||
# Create built-in function schemas (like calculator, timezone)
|
||||
builtin_functions = [
|
||||
get_function_schema(
|
||||
"safe_calculator",
|
||||
"Evaluate a mathematical expression safely",
|
||||
properties={
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Mathematical expression to evaluate",
|
||||
}
|
||||
},
|
||||
required=["expression"],
|
||||
),
|
||||
get_function_schema(
|
||||
"get_current_time",
|
||||
"Get the current time in a timezone",
|
||||
properties={
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Timezone name (e.g., America/New_York)",
|
||||
}
|
||||
},
|
||||
required=["timezone"],
|
||||
),
|
||||
]
|
||||
|
||||
# Create built-in function schemas (like calculator, timezone)
|
||||
builtin_functions = [
|
||||
get_function_schema(
|
||||
"safe_calculator",
|
||||
"Evaluate a mathematical expression safely",
|
||||
properties={
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "Mathematical expression to evaluate",
|
||||
}
|
||||
},
|
||||
required=["expression"],
|
||||
),
|
||||
get_function_schema(
|
||||
"get_current_time",
|
||||
"Get the current time in a timezone",
|
||||
properties={
|
||||
"timezone": {
|
||||
"type": "string",
|
||||
"description": "Timezone name (e.g., America/New_York)",
|
||||
}
|
||||
},
|
||||
required=["timezone"],
|
||||
),
|
||||
]
|
||||
# Combine built-in and custom functions - both are FunctionSchema objects
|
||||
all_functions = builtin_functions + custom_schemas
|
||||
|
||||
# Combine built-in and custom functions - both are FunctionSchema objects
|
||||
all_functions = builtin_functions + custom_schemas
|
||||
# Update context
|
||||
context = LLMContext()
|
||||
context.set_messages([{"role": "system", "content": "Old prompt"}])
|
||||
|
||||
# Update context
|
||||
context = LLMContext()
|
||||
context.set_messages([{"role": "system", "content": "Old prompt"}])
|
||||
new_system = {
|
||||
"role": "system",
|
||||
"content": "Assistant with calculator and weather tools",
|
||||
}
|
||||
_update_llm_context(context, new_system, all_functions)
|
||||
|
||||
new_system = {
|
||||
"role": "system",
|
||||
"content": "Assistant with calculator and weather tools",
|
||||
}
|
||||
_update_llm_context(context, new_system, all_functions)
|
||||
# Verify all tools are present
|
||||
tools = context.tools
|
||||
assert len(tools.standard_tools) == 3
|
||||
|
||||
# Verify all tools are present
|
||||
tools = context.tools
|
||||
assert len(tools.standard_tools) == 3
|
||||
|
||||
tool_names = {t.name for t in tools.standard_tools}
|
||||
assert "safe_calculator" in tool_names
|
||||
assert "get_current_time" in tool_names
|
||||
assert "get_weather" in tool_names
|
||||
tool_names = {t.name for t in tools.standard_tools}
|
||||
assert "safe_calculator" in tool_names
|
||||
assert "get_current_time" in tool_names
|
||||
assert "get_weather" in tool_names
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_preserves_function_call_history(
|
||||
|
|
@ -230,65 +213,60 @@ class TestCustomToolManagerContextIntegration:
|
|||
manager = CustomToolManager(mock_engine)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.get_organization_id_from_workflow_run"
|
||||
) as mock_get_org:
|
||||
mock_get_org.return_value = 1
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=[sample_tools[0]])
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=[sample_tools[0]])
|
||||
# Get schemas - returns FunctionSchema objects
|
||||
schemas = await manager.get_tool_schemas(["weather-uuid-123"])
|
||||
|
||||
# Get schemas - returns FunctionSchema objects
|
||||
schemas = await manager.get_tool_schemas(["weather-uuid-123"])
|
||||
# Create context with function call history
|
||||
context = LLMContext()
|
||||
context.set_messages(
|
||||
[
|
||||
{"role": "system", "content": "Old system prompt"},
|
||||
{"role": "user", "content": "What's the weather in NYC?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "New York, NY"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_123",
|
||||
"content": '{"temperature": 72, "condition": "sunny"}',
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The weather in NYC is 72°F and sunny!",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Create context with function call history
|
||||
context = LLMContext()
|
||||
context.set_messages(
|
||||
[
|
||||
{"role": "system", "content": "Old system prompt"},
|
||||
{"role": "user", "content": "What's the weather in NYC?"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"arguments": '{"location": "New York, NY"}',
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_123",
|
||||
"content": '{"temperature": 72, "condition": "sunny"}',
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The weather in NYC is 72°F and sunny!",
|
||||
},
|
||||
]
|
||||
)
|
||||
new_system = {"role": "system", "content": "Updated weather assistant"}
|
||||
_update_llm_context(context, new_system, schemas)
|
||||
|
||||
new_system = {"role": "system", "content": "Updated weather assistant"}
|
||||
_update_llm_context(context, new_system, schemas)
|
||||
messages = context.messages
|
||||
# System + user + assistant(tool_call) + tool + assistant = 5
|
||||
assert len(messages) == 5
|
||||
|
||||
messages = context.messages
|
||||
# System + user + assistant(tool_call) + tool + assistant = 5
|
||||
assert len(messages) == 5
|
||||
# Verify function call messages are preserved
|
||||
tool_call_msg = messages[2]
|
||||
assert tool_call_msg["role"] == "assistant"
|
||||
assert "tool_calls" in tool_call_msg
|
||||
|
||||
# Verify function call messages are preserved
|
||||
tool_call_msg = messages[2]
|
||||
assert tool_call_msg["role"] == "assistant"
|
||||
assert "tool_calls" in tool_call_msg
|
||||
|
||||
tool_result_msg = messages[3]
|
||||
assert tool_result_msg["role"] == "tool"
|
||||
assert tool_result_msg["tool_call_id"] == "call_123"
|
||||
tool_result_msg = messages[3]
|
||||
assert tool_result_msg["role"] == "tool"
|
||||
assert tool_result_msg["tool_call_id"] == "call_123"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_tool_list_does_not_set_tools(self, mock_engine):
|
||||
|
|
@ -296,26 +274,21 @@ class TestCustomToolManagerContextIntegration:
|
|||
manager = CustomToolManager(mock_engine)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.get_organization_id_from_workflow_run"
|
||||
) as mock_get_org:
|
||||
mock_get_org.return_value = 1
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=[])
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=[])
|
||||
schemas = await manager.get_tool_schemas([])
|
||||
assert schemas == []
|
||||
|
||||
schemas = await manager.get_tool_schemas([])
|
||||
assert schemas == []
|
||||
context = LLMContext()
|
||||
context.set_messages([{"role": "system", "content": "Old"}])
|
||||
|
||||
context = LLMContext()
|
||||
context.set_messages([{"role": "system", "content": "Old"}])
|
||||
new_system = {"role": "system", "content": "No tools available"}
|
||||
_update_llm_context(context, new_system, [])
|
||||
|
||||
new_system = {"role": "system", "content": "No tools available"}
|
||||
_update_llm_context(context, new_system, [])
|
||||
|
||||
# Context should have updated message but no tools set
|
||||
assert context.messages[0]["content"] == "No tools available"
|
||||
# Context should have updated message but no tools set
|
||||
assert context.messages[0]["content"] == "No tools available"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_numeric_and_boolean_parameter_types(self, mock_engine):
|
||||
|
|
@ -357,33 +330,28 @@ class TestCustomToolManagerContextIntegration:
|
|||
manager = CustomToolManager(mock_engine)
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.get_organization_id_from_workflow_run"
|
||||
) as mock_get_org:
|
||||
mock_get_org.return_value = 1
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=[tool_with_types])
|
||||
|
||||
with patch(
|
||||
"api.services.workflow.pipecat_engine_custom_tools.db_client"
|
||||
) as mock_db:
|
||||
mock_db.get_tools_by_uuids = AsyncMock(return_value=[tool_with_types])
|
||||
# Get schemas - returns FunctionSchema objects
|
||||
schemas = await manager.get_tool_schemas(["order-uuid"])
|
||||
schema = schemas[0]
|
||||
|
||||
# Get schemas - returns FunctionSchema objects
|
||||
schemas = await manager.get_tool_schemas(["order-uuid"])
|
||||
schema = schemas[0]
|
||||
# Verify types using FunctionSchema attributes
|
||||
assert schema.properties["item_id"]["type"] == "string"
|
||||
assert schema.properties["quantity"]["type"] == "number"
|
||||
assert schema.properties["express_shipping"]["type"] == "boolean"
|
||||
|
||||
# Verify types using FunctionSchema attributes
|
||||
assert schema.properties["item_id"]["type"] == "string"
|
||||
assert schema.properties["quantity"]["type"] == "number"
|
||||
assert schema.properties["express_shipping"]["type"] == "boolean"
|
||||
# Update context - pass schema directly
|
||||
context = LLMContext()
|
||||
context.set_messages([{"role": "system", "content": "Old"}])
|
||||
_update_llm_context(
|
||||
context, {"role": "system", "content": "Order assistant"}, schemas
|
||||
)
|
||||
|
||||
# Update context - pass schema directly
|
||||
context = LLMContext()
|
||||
context.set_messages([{"role": "system", "content": "Old"}])
|
||||
_update_llm_context(
|
||||
context, {"role": "system", "content": "Order assistant"}, schemas
|
||||
)
|
||||
|
||||
# Verify tool was set with correct types
|
||||
tool = context.tools.standard_tools[0]
|
||||
assert tool.name == "place_order"
|
||||
assert tool.properties["quantity"]["type"] == "number"
|
||||
assert tool.properties["express_shipping"]["type"] == "boolean"
|
||||
# Verify tool was set with correct types
|
||||
tool = context.tools.standard_tools[0]
|
||||
assert tool.name == "place_order"
|
||||
assert tool.properties["quantity"]["type"] == "number"
|
||||
assert tool.properties["express_shipping"]["type"] == "boolean"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue