2026-05-04 21:35:37 +05:30
|
|
|
"""Tests for LLM behavior when calling an unregistered function."""
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from pipecat.frames.frames import (
|
|
|
|
|
FunctionCallInProgressFrame,
|
|
|
|
|
FunctionCallResultFrame,
|
|
|
|
|
FunctionCallsFromLLMInfoFrame,
|
|
|
|
|
FunctionCallsStartedFrame,
|
|
|
|
|
LLMContextFrame,
|
|
|
|
|
LLMFullResponseEndFrame,
|
|
|
|
|
LLMFullResponseStartFrame,
|
2026-05-29 16:19:42 +05:30
|
|
|
UserTurnInferenceCompletedFrame,
|
2026-05-04 21:35:37 +05:30
|
|
|
)
|
|
|
|
|
from pipecat.pipeline.pipeline import Pipeline
|
|
|
|
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
2026-05-07 12:23:41 +05:30
|
|
|
|
2026-05-04 21:35:37 +05:30
|
|
|
from pipecat.tests import MockLLMService, run_test
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestUnregisteredFunctionCall:
|
|
|
|
|
"""Tests for LLM behavior when generating a tool call for an unregistered function."""
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_unregistered_function_emits_error_result(self):
|
|
|
|
|
"""LLM calling an unregistered function should still terminate with a
|
|
|
|
|
FunctionCallResultFrame whose result is an error string, instead of
|
|
|
|
|
crashing the pipeline."""
|
|
|
|
|
chunks = MockLLMService.create_function_call_chunks(
|
|
|
|
|
function_name="nonexistent_tool",
|
|
|
|
|
arguments={"foo": "bar"},
|
|
|
|
|
tool_call_id="call_missing_1",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
llm = MockLLMService(mock_chunks=chunks, chunk_delay=0.001)
|
|
|
|
|
|
|
|
|
|
# Intentionally do NOT register any handler for "nonexistent_tool".
|
|
|
|
|
|
|
|
|
|
messages = [{"role": "user", "content": "Please use a tool I never registered"}]
|
|
|
|
|
context = LLMContext(messages)
|
|
|
|
|
|
|
|
|
|
pipeline = Pipeline([llm])
|
|
|
|
|
|
|
|
|
|
received_down_frames, _ = await run_test(
|
|
|
|
|
pipeline,
|
|
|
|
|
frames_to_send=[LLMContextFrame(context)],
|
|
|
|
|
expected_down_frames=[
|
|
|
|
|
LLMFullResponseStartFrame,
|
|
|
|
|
FunctionCallsFromLLMInfoFrame,
|
2026-05-29 16:19:42 +05:30
|
|
|
UserTurnInferenceCompletedFrame,
|
2026-05-04 21:35:37 +05:30
|
|
|
FunctionCallsStartedFrame,
|
|
|
|
|
LLMFullResponseEndFrame,
|
|
|
|
|
FunctionCallInProgressFrame,
|
|
|
|
|
FunctionCallResultFrame,
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result_frames = [
|
|
|
|
|
f for f in received_down_frames if isinstance(f, FunctionCallResultFrame)
|
|
|
|
|
]
|
|
|
|
|
assert len(result_frames) == 1, (
|
|
|
|
|
"Expected exactly one FunctionCallResultFrame for the unregistered call"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result_frame = result_frames[0]
|
|
|
|
|
assert result_frame.function_name == "nonexistent_tool"
|
|
|
|
|
assert result_frame.tool_call_id == "call_missing_1"
|
|
|
|
|
assert result_frame.arguments == {"foo": "bar"}
|
|
|
|
|
|
|
|
|
|
# Pipecat's missing-function handler returns a string error.
|
|
|
|
|
assert isinstance(result_frame.result, str)
|
2026-05-16 18:05:23 +05:30
|
|
|
assert "not currently available" in result_frame.result
|
2026-05-04 21:35:37 +05:30
|
|
|
assert "nonexistent_tool" in result_frame.result
|
|
|
|
|
|
|
|
|
|
# In-progress frame should also be emitted before the result so mute
|
|
|
|
|
# strategies can release the tool_call_id.
|
|
|
|
|
in_progress_frames = [
|
|
|
|
|
f
|
|
|
|
|
for f in received_down_frames
|
|
|
|
|
if isinstance(f, FunctionCallInProgressFrame)
|
|
|
|
|
]
|
|
|
|
|
assert len(in_progress_frames) == 1
|
|
|
|
|
assert in_progress_frames[0].function_name == "nonexistent_tool"
|
|
|
|
|
assert in_progress_frames[0].tool_call_id == "call_missing_1"
|