Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/5855.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed a bug in the Perplexity adapter where merging consecutive assistant messages dropped `tool_calls` and collapsed consecutive tool messages onto the first call's id.
17 changes: 12 additions & 5 deletions src/pipecat/adapters/llm/services/perplexity_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,24 @@ def _transform_messages(
if current["role"] == next_msg["role"] == "system":
# Perplexity allows multiple initial system messages, don't merge
i += 1
elif current["role"] == next_msg["role"]:
# A tool message names the call its result answers in tool_call_id,
# so merging two of them would leave both results on the first
# call's id. Leave consecutive tool messages as they are.
elif current["role"] == next_msg["role"] and "tool_call_id" not in next_msg:
# Convert string content to list-of-dicts format for merging
if isinstance(current.get("content"), str):
current["content"] = [{"type": "text", "text": current["content"]}]
if isinstance(next_msg.get("content"), str):
next_msg["content"] = [{"type": "text", "text": next_msg["content"]}]
# Merge content from next message into current
if isinstance(current.get("content"), list) and isinstance(
next_msg.get("content"), list
):
current["content"].extend(next_msg["content"])
if isinstance(next_msg.get("content"), list):
if isinstance(current.get("content"), list):
current["content"].extend(next_msg["content"])
else:
current["content"] = next_msg["content"]
# tool_calls are carried outside "content", so merge those too
if next_msg.get("tool_calls"):
current.setdefault("tool_calls", []).extend(next_msg["tool_calls"])
msgs.pop(i + 1)
else:
i += 1
Expand Down
108 changes: 108 additions & 0 deletions tests/test_get_llm_invocation_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2906,6 +2906,114 @@ async def test_tool_messages_preserved(self):
self.assertEqual(params["messages"][2]["content"], "Sunny, 72F")
self.assertEqual(params["messages"][3]["role"], "user")

async def test_merging_assistants_keeps_tool_calls(self):
"""Merging a text assistant message with a tool-call-only one keeps the call.

Some conversation histories carry the assistant's text and its tool call
as two consecutive assistant messages; the tool message that follows
answers the call carried by the second one.
"""
messages: list[LLMStandardMessage] = [
{"role": "user", "content": "Weather in LA?"},
{"role": "assistant", "content": "Let me check on that."},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"location": "LA"}'},
}
],
},
{"role": "tool", "content": '{"temperature": "75"}', "tool_call_id": "call_1"},
{"role": "user", "content": "Thanks!"},
]

context = LLMContext(messages=messages)
params = await self.adapter.get_llm_invocation_params(
context, convert_developer_to_user=True
)

self.assertEqual(len(params["messages"]), 4)
merged = params["messages"][1]
self.assertEqual(merged["role"], "assistant")
self.assertEqual([part["text"] for part in merged["content"]], ["Let me check on that."])
self.assertEqual([call["id"] for call in merged["tool_calls"]], ["call_1"])
self.assertEqual(params["messages"][2]["role"], "tool")
self.assertEqual(params["messages"][2]["tool_call_id"], "call_1")
self.assertEqual(params["messages"][3]["role"], "user")

async def test_merging_tool_call_only_assistant_keeps_content(self):
"""A history can carry the call before the assistant's text.

Merging then has to move the text into the tool-call message rather
than drop it, since the message holding it is the one that is removed.
"""
messages: list[LLMStandardMessage] = [
{"role": "user", "content": "Weather in LA?"},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"location": "LA"}'},
}
],
},
{"role": "assistant", "content": "Looks sunny."},
{"role": "user", "content": "Thanks!"},
]

context = LLMContext(messages=messages)
params = await self.adapter.get_llm_invocation_params(
context, convert_developer_to_user=True
)

self.assertEqual(len(params["messages"]), 3)
merged = params["messages"][1]
self.assertEqual(merged["role"], "assistant")
self.assertEqual([call["id"] for call in merged["tool_calls"]], ["call_1"])
self.assertEqual([part["text"] for part in merged["content"]], ["Looks sunny."])
self.assertEqual(params["messages"][2]["role"], "user")

async def test_consecutive_tool_messages_keep_their_own_ids(self):
"""Tool results for different calls are left apart rather than merged."""
messages: list[LLMStandardMessage] = [
{"role": "user", "content": "Weather in LA and SF?"},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"location": "LA"}'},
},
{
"id": "call_2",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"location": "SF"}'},
},
],
},
{"role": "tool", "content": '{"temperature": "75"}', "tool_call_id": "call_1"},
{"role": "tool", "content": '{"temperature": "62"}', "tool_call_id": "call_2"},
{"role": "user", "content": "Thanks!"},
]

context = LLMContext(messages=messages)
params = await self.adapter.get_llm_invocation_params(
context, convert_developer_to_user=True
)

self.assertEqual(len(params["messages"]), 5)
self.assertEqual(params["messages"][2]["tool_call_id"], "call_1")
self.assertEqual(params["messages"][2]["content"], '{"temperature": "75"}')
self.assertEqual(params["messages"][3]["tool_call_id"], "call_2")
self.assertEqual(params["messages"][3]["content"], '{"temperature": "62"}')
self.assertEqual(params["messages"][4]["role"], "user")

async def test_developer_message_converted_to_user(self):
"""Developer messages are converted to user role."""
messages: list[LLMStandardMessage] = [
Expand Down