feat: add hybrid text + recording functionality in agents (#191)

* feat: add recording feature in agents

* chore: pin pipecat version

* feat: show usage in UI

* chore: update pipecat
This commit is contained in:
Abhishek 2026-03-16 15:04:08 +05:30 committed by GitHub
parent f075bcb623
commit 494c60d774
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 2865 additions and 397 deletions

View file

@ -13,14 +13,12 @@ from unittest.mock import AsyncMock, Mock, patch
import pytest
from api.services.workflow.pipecat_engine_utils import (
get_function_schema,
update_llm_context,
)
from api.services.workflow.pipecat_engine_custom_tools import get_function_schema
from api.services.workflow.tools.custom_tool import (
execute_http_tool,
tool_to_function_schema,
)
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.frames.frames import (
FunctionCallInProgressFrame,
FunctionCallResultFrame,
@ -862,11 +860,27 @@ class TestCustomToolManagerUnit:
assert result_received["status"] == "success"
def _update_llm_context(context, system_message, functions):
"""Inline helper replicating the old update_llm_context for tests."""
tools_schema = ToolsSchema(standard_tools=functions)
previous_interactions = context.messages
if previous_interactions and previous_interactions[0]["role"] == "system":
messages = [system_message] + previous_interactions[1:]
else:
messages = [system_message] + previous_interactions
context.set_messages(messages)
if functions:
context.set_tools(tools_schema)
class TestUpdateLLMContext:
"""Tests for update_llm_context function."""
"""Tests for _update_llm_context inline logic."""
def test_replaces_system_message(self):
"""Test that update_llm_context replaces existing system messages."""
"""Test that _update_llm_context replaces existing system messages."""
context = LLMContext()
context.set_messages(
[
@ -877,7 +891,7 @@ class TestUpdateLLMContext:
)
new_system = {"role": "system", "content": "New system message"}
update_llm_context(context, new_system, [])
_update_llm_context(context, new_system, [])
messages = context.messages
# Should have new system message at the start
@ -902,7 +916,7 @@ class TestUpdateLLMContext:
)
new_system = {"role": "system", "content": "New prompt"}
update_llm_context(context, new_system, [])
_update_llm_context(context, new_system, [])
messages = context.messages
assert len(messages) == 5
@ -923,7 +937,7 @@ class TestUpdateLLMContext:
]
new_system = {"role": "system", "content": "New prompt with tools"}
update_llm_context(context, new_system, functions)
_update_llm_context(context, new_system, functions)
# Verify tools were set
tools = context.tools
@ -936,7 +950,7 @@ class TestUpdateLLMContext:
context.set_messages([{"role": "system", "content": "Old"}])
new_system = {"role": "system", "content": "New prompt without tools"}
update_llm_context(context, new_system, [])
_update_llm_context(context, new_system, [])
# Tools should not be set (or remain None)
# Note: The function only calls set_tools if functions is truthy
@ -952,7 +966,7 @@ class TestUpdateLLMContext:
new_system = {"role": "system", "content": "Initial prompt"}
functions = [get_function_schema("test_func", "A test function")]
update_llm_context(context, new_system, functions)
_update_llm_context(context, new_system, functions)
messages = context.messages
assert len(messages) == 1

View file

@ -1,8 +1,8 @@
"""Integration tests for CustomToolManager with update_llm_context.
"""Integration tests for CustomToolManager with LLM context updates.
This module tests the full flow of:
1. CustomToolManager fetching and converting tool schemas
2. update_llm_context setting those tools on the LLM context
2. Setting those tools on the LLM context
3. Verifying the context is properly configured for LLM generation
"""
@ -10,16 +10,32 @@ from unittest.mock import AsyncMock, patch
import pytest
from api.services.workflow.pipecat_engine_custom_tools import CustomToolManager
from api.services.workflow.pipecat_engine_utils import (
from api.services.workflow.pipecat_engine_custom_tools import (
CustomToolManager,
get_function_schema,
update_llm_context,
)
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):
"""Inline helper replicating the update_llm_context logic for tests."""
tools_schema = ToolsSchema(standard_tools=functions)
previous_interactions = context.messages
if previous_interactions and previous_interactions[0]["role"] == "system":
messages = [system_message] + previous_interactions[1:]
else:
messages = [system_message] + previous_interactions
context.set_messages(messages)
if functions:
context.set_tools(tools_schema)
class TestCustomToolManagerContextIntegration:
"""Integration tests for CustomToolManager with LLMContext."""
@ -69,7 +85,7 @@ class TestCustomToolManagerContextIntegration:
"role": "system",
"content": "You are a scheduling assistant with access to weather and booking tools.",
}
update_llm_context(context, new_system, schemas)
_update_llm_context(context, new_system, schemas)
# Verify context was updated correctly
messages = context.messages
@ -195,7 +211,7 @@ class TestCustomToolManagerContextIntegration:
"role": "system",
"content": "Assistant with calculator and weather tools",
}
update_llm_context(context, new_system, all_functions)
_update_llm_context(context, new_system, all_functions)
# Verify all tools are present
tools = context.tools
@ -259,7 +275,7 @@ class TestCustomToolManagerContextIntegration:
)
new_system = {"role": "system", "content": "Updated weather assistant"}
update_llm_context(context, new_system, schemas)
_update_llm_context(context, new_system, schemas)
messages = context.messages
# System + user + assistant(tool_call) + tool + assistant = 5
@ -296,7 +312,7 @@ class TestCustomToolManagerContextIntegration:
context.set_messages([{"role": "system", "content": "Old"}])
new_system = {"role": "system", "content": "No tools available"}
update_llm_context(context, new_system, [])
_update_llm_context(context, new_system, [])
# Context should have updated message but no tools set
assert context.messages[0]["content"] == "No tools available"
@ -362,7 +378,7 @@ class TestCustomToolManagerContextIntegration:
# Update context - pass schema directly
context = LLMContext()
context.set_messages([{"role": "system", "content": "Old"}])
update_llm_context(
_update_llm_context(
context, {"role": "system", "content": "Order assistant"}, schemas
)

View file

@ -68,9 +68,7 @@ class ContextCapturingMockLLM(MockLLMService):
{
"step": self._current_step,
"messages": messages_snapshot,
"system_prompt": messages_snapshot[0]["content"]
if messages_snapshot
else None,
"system_prompt": self._settings.system_instruction,
}
)
@ -101,12 +99,10 @@ class ContextCapturingMockLLM(MockLLMService):
return False
def get_system_prompt_at_step(self, step: int) -> str:
"""Get the system prompt from context at a specific step."""
"""Get the system prompt from settings at a specific step."""
ctx = self.get_context_at_step(step)
if ctx and ctx["messages"]:
first_msg = ctx["messages"][0]
if first_msg.get("role") == "system":
return first_msg.get("content", "")
if ctx:
return ctx.get("system_prompt") or ""
return ""

View file

@ -186,7 +186,7 @@ class TestPipecatEngineToolCalls:
)
# Assert that the context was updated with END_CALL_SYSTEM_PROMPT
assert context.messages[0]["content"] == END_CALL_SYSTEM_PROMPT
assert llm._settings.system_instruction == END_CALL_SYSTEM_PROMPT
@pytest.mark.asyncio
async def test_parallel_builtin_and_transition_calls_through_engine_1(
@ -233,7 +233,7 @@ class TestPipecatEngineToolCalls:
)
# Assert that the context was updated with END_CALL_SYSTEM_PROMPT
assert context.messages[0]["content"] == END_CALL_SYSTEM_PROMPT
assert llm._settings.system_instruction == END_CALL_SYSTEM_PROMPT
@pytest.mark.asyncio
async def test_parallel_builtin_and_transition_calls_through_engine_with_text(
@ -281,7 +281,7 @@ class TestPipecatEngineToolCalls:
)
# Assert that the context was updated with END_CALL_SYSTEM_PROMPT
assert context.messages[0]["content"] == END_CALL_SYSTEM_PROMPT
assert llm._settings.system_instruction == END_CALL_SYSTEM_PROMPT
@pytest.mark.asyncio
async def test_single_transition_call_through_engine(
@ -315,4 +315,4 @@ class TestPipecatEngineToolCalls:
)
# Assert that the context was updated with END_CALL_SYSTEM_PROMPT
assert context.messages[0]["content"] == END_CALL_SYSTEM_PROMPT
assert llm._settings.system_instruction == END_CALL_SYSTEM_PROMPT