dv-pipecat-ai 0.0.85.dev5__py3-none-any.whl → 0.0.85.dev698__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dv-pipecat-ai might be problematic. Click here for more details.

Files changed (157) hide show
  1. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/METADATA +78 -117
  2. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/RECORD +157 -123
  3. pipecat/adapters/base_llm_adapter.py +38 -1
  4. pipecat/adapters/services/anthropic_adapter.py +9 -14
  5. pipecat/adapters/services/aws_nova_sonic_adapter.py +5 -0
  6. pipecat/adapters/services/bedrock_adapter.py +236 -13
  7. pipecat/adapters/services/gemini_adapter.py +12 -8
  8. pipecat/adapters/services/open_ai_adapter.py +19 -7
  9. pipecat/adapters/services/open_ai_realtime_adapter.py +5 -0
  10. pipecat/audio/filters/krisp_viva_filter.py +193 -0
  11. pipecat/audio/filters/noisereduce_filter.py +15 -0
  12. pipecat/audio/turn/base_turn_analyzer.py +9 -1
  13. pipecat/audio/turn/smart_turn/base_smart_turn.py +14 -8
  14. pipecat/audio/turn/smart_turn/data/__init__.py +0 -0
  15. pipecat/audio/turn/smart_turn/data/smart-turn-v3.0.onnx +0 -0
  16. pipecat/audio/turn/smart_turn/http_smart_turn.py +6 -2
  17. pipecat/audio/turn/smart_turn/local_smart_turn.py +1 -1
  18. pipecat/audio/turn/smart_turn/local_smart_turn_v2.py +1 -1
  19. pipecat/audio/turn/smart_turn/local_smart_turn_v3.py +124 -0
  20. pipecat/audio/vad/data/README.md +10 -0
  21. pipecat/audio/vad/vad_analyzer.py +13 -1
  22. pipecat/extensions/voicemail/voicemail_detector.py +5 -5
  23. pipecat/frames/frames.py +120 -87
  24. pipecat/observers/loggers/debug_log_observer.py +3 -3
  25. pipecat/observers/loggers/llm_log_observer.py +7 -3
  26. pipecat/observers/loggers/user_bot_latency_log_observer.py +22 -10
  27. pipecat/pipeline/runner.py +12 -4
  28. pipecat/pipeline/service_switcher.py +64 -36
  29. pipecat/pipeline/task.py +85 -24
  30. pipecat/processors/aggregators/dtmf_aggregator.py +28 -22
  31. pipecat/processors/aggregators/{gated_openai_llm_context.py → gated_llm_context.py} +9 -9
  32. pipecat/processors/aggregators/gated_open_ai_llm_context.py +12 -0
  33. pipecat/processors/aggregators/llm_response.py +6 -7
  34. pipecat/processors/aggregators/llm_response_universal.py +19 -15
  35. pipecat/processors/aggregators/user_response.py +6 -6
  36. pipecat/processors/aggregators/vision_image_frame.py +24 -2
  37. pipecat/processors/audio/audio_buffer_processor.py +43 -8
  38. pipecat/processors/filters/stt_mute_filter.py +2 -0
  39. pipecat/processors/frame_processor.py +103 -17
  40. pipecat/processors/frameworks/langchain.py +8 -2
  41. pipecat/processors/frameworks/rtvi.py +209 -68
  42. pipecat/processors/frameworks/strands_agents.py +170 -0
  43. pipecat/processors/logger.py +2 -2
  44. pipecat/processors/transcript_processor.py +4 -4
  45. pipecat/processors/user_idle_processor.py +3 -6
  46. pipecat/runner/run.py +270 -50
  47. pipecat/runner/types.py +2 -0
  48. pipecat/runner/utils.py +51 -10
  49. pipecat/serializers/exotel.py +5 -5
  50. pipecat/serializers/livekit.py +20 -0
  51. pipecat/serializers/plivo.py +6 -9
  52. pipecat/serializers/protobuf.py +6 -5
  53. pipecat/serializers/telnyx.py +2 -2
  54. pipecat/serializers/twilio.py +43 -23
  55. pipecat/services/ai_service.py +2 -6
  56. pipecat/services/anthropic/llm.py +2 -25
  57. pipecat/services/asyncai/tts.py +2 -3
  58. pipecat/services/aws/__init__.py +1 -0
  59. pipecat/services/aws/llm.py +122 -97
  60. pipecat/services/aws/nova_sonic/__init__.py +0 -0
  61. pipecat/services/aws/nova_sonic/context.py +367 -0
  62. pipecat/services/aws/nova_sonic/frames.py +25 -0
  63. pipecat/services/aws/nova_sonic/llm.py +1155 -0
  64. pipecat/services/aws/stt.py +1 -3
  65. pipecat/services/aws_nova_sonic/__init__.py +19 -1
  66. pipecat/services/aws_nova_sonic/aws.py +11 -1151
  67. pipecat/services/aws_nova_sonic/context.py +13 -355
  68. pipecat/services/aws_nova_sonic/frames.py +13 -17
  69. pipecat/services/azure/realtime/__init__.py +0 -0
  70. pipecat/services/azure/realtime/llm.py +65 -0
  71. pipecat/services/azure/stt.py +15 -0
  72. pipecat/services/cartesia/tts.py +2 -2
  73. pipecat/services/deepgram/__init__.py +1 -0
  74. pipecat/services/deepgram/flux/__init__.py +0 -0
  75. pipecat/services/deepgram/flux/stt.py +636 -0
  76. pipecat/services/elevenlabs/__init__.py +2 -1
  77. pipecat/services/elevenlabs/stt.py +254 -276
  78. pipecat/services/elevenlabs/tts.py +5 -5
  79. pipecat/services/fish/tts.py +2 -2
  80. pipecat/services/gemini_multimodal_live/events.py +38 -524
  81. pipecat/services/gemini_multimodal_live/file_api.py +23 -173
  82. pipecat/services/gemini_multimodal_live/gemini.py +41 -1403
  83. pipecat/services/gladia/stt.py +56 -72
  84. pipecat/services/google/__init__.py +1 -0
  85. pipecat/services/google/gemini_live/__init__.py +3 -0
  86. pipecat/services/google/gemini_live/file_api.py +189 -0
  87. pipecat/services/google/gemini_live/llm.py +1582 -0
  88. pipecat/services/google/gemini_live/llm_vertex.py +184 -0
  89. pipecat/services/google/llm.py +15 -11
  90. pipecat/services/google/llm_openai.py +3 -3
  91. pipecat/services/google/llm_vertex.py +86 -16
  92. pipecat/services/google/tts.py +7 -3
  93. pipecat/services/heygen/api.py +2 -0
  94. pipecat/services/heygen/client.py +8 -4
  95. pipecat/services/heygen/video.py +2 -0
  96. pipecat/services/hume/__init__.py +5 -0
  97. pipecat/services/hume/tts.py +220 -0
  98. pipecat/services/inworld/tts.py +6 -6
  99. pipecat/services/llm_service.py +15 -5
  100. pipecat/services/lmnt/tts.py +2 -2
  101. pipecat/services/mcp_service.py +4 -2
  102. pipecat/services/mem0/memory.py +6 -5
  103. pipecat/services/mistral/llm.py +29 -8
  104. pipecat/services/moondream/vision.py +42 -16
  105. pipecat/services/neuphonic/tts.py +2 -2
  106. pipecat/services/openai/__init__.py +1 -0
  107. pipecat/services/openai/base_llm.py +27 -20
  108. pipecat/services/openai/realtime/__init__.py +0 -0
  109. pipecat/services/openai/realtime/context.py +272 -0
  110. pipecat/services/openai/realtime/events.py +1106 -0
  111. pipecat/services/openai/realtime/frames.py +37 -0
  112. pipecat/services/openai/realtime/llm.py +829 -0
  113. pipecat/services/openai/tts.py +16 -8
  114. pipecat/services/openai_realtime/__init__.py +27 -0
  115. pipecat/services/openai_realtime/azure.py +21 -0
  116. pipecat/services/openai_realtime/context.py +21 -0
  117. pipecat/services/openai_realtime/events.py +21 -0
  118. pipecat/services/openai_realtime/frames.py +21 -0
  119. pipecat/services/openai_realtime_beta/azure.py +16 -0
  120. pipecat/services/openai_realtime_beta/openai.py +17 -5
  121. pipecat/services/playht/tts.py +31 -4
  122. pipecat/services/rime/tts.py +3 -4
  123. pipecat/services/sarvam/tts.py +2 -6
  124. pipecat/services/simli/video.py +2 -2
  125. pipecat/services/speechmatics/stt.py +1 -7
  126. pipecat/services/stt_service.py +34 -0
  127. pipecat/services/tavus/video.py +2 -2
  128. pipecat/services/tts_service.py +9 -9
  129. pipecat/services/vision_service.py +7 -6
  130. pipecat/services/vistaar/llm.py +4 -0
  131. pipecat/tests/utils.py +4 -4
  132. pipecat/transcriptions/language.py +41 -1
  133. pipecat/transports/base_input.py +17 -42
  134. pipecat/transports/base_output.py +42 -26
  135. pipecat/transports/daily/transport.py +199 -26
  136. pipecat/transports/heygen/__init__.py +0 -0
  137. pipecat/transports/heygen/transport.py +381 -0
  138. pipecat/transports/livekit/transport.py +228 -63
  139. pipecat/transports/local/audio.py +6 -1
  140. pipecat/transports/local/tk.py +11 -2
  141. pipecat/transports/network/fastapi_websocket.py +1 -1
  142. pipecat/transports/smallwebrtc/connection.py +98 -19
  143. pipecat/transports/smallwebrtc/request_handler.py +204 -0
  144. pipecat/transports/smallwebrtc/transport.py +65 -23
  145. pipecat/transports/tavus/transport.py +23 -12
  146. pipecat/transports/websocket/client.py +41 -5
  147. pipecat/transports/websocket/fastapi.py +21 -11
  148. pipecat/transports/websocket/server.py +14 -7
  149. pipecat/transports/whatsapp/api.py +8 -0
  150. pipecat/transports/whatsapp/client.py +47 -0
  151. pipecat/utils/base_object.py +54 -22
  152. pipecat/utils/string.py +12 -1
  153. pipecat/utils/tracing/service_decorators.py +21 -21
  154. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/WHEEL +0 -0
  155. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/licenses/LICENSE +0 -0
  156. {dv_pipecat_ai-0.0.85.dev5.dist-info → dv_pipecat_ai-0.0.85.dev698.dist-info}/top_level.txt +0 -0
  157. /pipecat/services/{aws_nova_sonic → aws/nova_sonic}/ready.wav +0 -0
@@ -9,7 +9,7 @@
9
9
  import copy
10
10
  import json
11
11
  from dataclasses import dataclass
12
- from typing import Any, Dict, List, Optional, TypedDict
12
+ from typing import Any, Dict, List, TypedDict
13
13
 
14
14
  from anthropic import NOT_GIVEN, NotGiven
15
15
  from anthropic.types.message_param import MessageParam
@@ -28,10 +28,7 @@ from pipecat.processors.aggregators.llm_context import (
28
28
 
29
29
 
30
30
  class AnthropicLLMInvocationParams(TypedDict):
31
- """Context-based parameters for invoking Anthropic's LLM API.
32
-
33
- This is a placeholder until support for universal LLMContext machinery is added for Anthropic.
34
- """
31
+ """Context-based parameters for invoking Anthropic's LLM API."""
35
32
 
36
33
  system: str | NotGiven
37
34
  messages: List[MessageParam]
@@ -45,13 +42,16 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
45
42
  to the specific format required by Anthropic's Claude models for function calling.
46
43
  """
47
44
 
45
+ @property
46
+ def id_for_llm_specific_messages(self) -> str:
47
+ """Get the identifier used in LLMSpecificMessage instances for Anthropic."""
48
+ return "anthropic"
49
+
48
50
  def get_llm_invocation_params(
49
51
  self, context: LLMContext, enable_prompt_caching: bool
50
52
  ) -> AnthropicLLMInvocationParams:
51
53
  """Get Anthropic-specific LLM invocation parameters from a universal LLM context.
52
54
 
53
- This is a placeholder until support for universal LLMContext machinery is added for Anthropic.
54
-
55
55
  Args:
56
56
  context: The LLM context containing messages, tools, etc.
57
57
  enable_prompt_caching: Whether prompt caching should be enabled.
@@ -59,7 +59,7 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
59
59
  Returns:
60
60
  Dictionary of parameters for invoking Anthropic's LLM API.
61
61
  """
62
- messages = self._from_universal_context_messages(self._get_messages(context))
62
+ messages = self._from_universal_context_messages(self.get_messages(context))
63
63
  return {
64
64
  "system": messages.system,
65
65
  "messages": (
@@ -76,8 +76,6 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
76
76
 
77
77
  Removes or truncates sensitive data like image content for safe logging.
78
78
 
79
- This is a placeholder until support for universal LLMContext machinery is added for Anthropic.
80
-
81
79
  Args:
82
80
  context: The LLM context containing messages.
83
81
 
@@ -85,7 +83,7 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
85
83
  List of messages in a format ready for logging about Anthropic.
86
84
  """
87
85
  # Get messages in Anthropic's format
88
- messages = self._from_universal_context_messages(self._get_messages(context)).messages
86
+ messages = self._from_universal_context_messages(self.get_messages(context)).messages
89
87
 
90
88
  # Sanitize messages for logging
91
89
  messages_for_logging = []
@@ -99,9 +97,6 @@ class AnthropicLLMAdapter(BaseLLMAdapter[AnthropicLLMInvocationParams]):
99
97
  messages_for_logging.append(msg)
100
98
  return messages_for_logging
101
99
 
102
- def _get_messages(self, context: LLMContext) -> List[LLMContextMessage]:
103
- return context.get_messages("anthropic")
104
-
105
100
  @dataclass
106
101
  class ConvertedMessages:
107
102
  """Container for Anthropic-formatted messages converted from universal context."""
@@ -31,6 +31,11 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
31
31
  specific function-calling format, enabling tool use with Nova Sonic models.
32
32
  """
33
33
 
34
+ @property
35
+ def id_for_llm_specific_messages(self) -> str:
36
+ """Get the identifier used in LLMSpecificMessage instances for AWS Nova Sonic."""
37
+ raise NotImplementedError("Universal LLMContext is not yet supported for AWS Nova Sonic.")
38
+
34
39
  def get_llm_invocation_params(self, context: LLMContext) -> AWSNovaSonicLLMInvocationParams:
35
40
  """Get AWS Nova Sonic-specific LLM invocation parameters from a universal LLM context.
36
41
 
@@ -6,21 +6,33 @@
6
6
 
7
7
  """AWS Bedrock LLM adapter for Pipecat."""
8
8
 
9
- from typing import Any, Dict, List, TypedDict
9
+ import base64
10
+ import copy
11
+ import json
12
+ from dataclasses import dataclass
13
+ from typing import Any, Dict, List, Literal, Optional, TypedDict
14
+
15
+ from loguru import logger
10
16
 
11
17
  from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
12
18
  from pipecat.adapters.schemas.function_schema import FunctionSchema
13
19
  from pipecat.adapters.schemas.tools_schema import ToolsSchema
14
- from pipecat.processors.aggregators.llm_context import LLMContext
20
+ from pipecat.processors.aggregators.llm_context import (
21
+ LLMContext,
22
+ LLMContextMessage,
23
+ LLMContextToolChoice,
24
+ LLMSpecificMessage,
25
+ LLMStandardMessage,
26
+ )
15
27
 
16
28
 
17
29
  class AWSBedrockLLMInvocationParams(TypedDict):
18
- """Context-based parameters for invoking AWS Bedrock's LLM API.
19
-
20
- This is a placeholder until support for universal LLMContext machinery is added for Bedrock.
21
- """
30
+ """Context-based parameters for invoking AWS Bedrock's LLM API."""
22
31
 
23
- pass
32
+ system: Optional[List[dict[str, Any]]] # [{"text": "system message"}]
33
+ messages: List[dict[str, Any]]
34
+ tools: List[dict[str, Any]]
35
+ tool_choice: LLMContextToolChoice
24
36
 
25
37
 
26
38
  class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]):
@@ -30,33 +42,244 @@ class AWSBedrockLLMAdapter(BaseLLMAdapter[AWSBedrockLLMInvocationParams]):
30
42
  into AWS Bedrock's expected tool format for function calling capabilities.
31
43
  """
32
44
 
45
+ @property
46
+ def id_for_llm_specific_messages(self) -> str:
47
+ """Get the identifier used in LLMSpecificMessage instances for AWS Bedrock."""
48
+ return "aws"
49
+
33
50
  def get_llm_invocation_params(self, context: LLMContext) -> AWSBedrockLLMInvocationParams:
34
51
  """Get AWS Bedrock-specific LLM invocation parameters from a universal LLM context.
35
52
 
36
- This is a placeholder until support for universal LLMContext machinery is added for Bedrock.
37
-
38
53
  Args:
39
54
  context: The LLM context containing messages, tools, etc.
40
55
 
41
56
  Returns:
42
57
  Dictionary of parameters for invoking AWS Bedrock's LLM API.
43
58
  """
44
- raise NotImplementedError("Universal LLMContext is not yet supported for AWS Bedrock.")
59
+ messages = self._from_universal_context_messages(self.get_messages(context))
60
+ return {
61
+ "system": messages.system,
62
+ "messages": messages.messages,
63
+ # NOTE: LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN)
64
+ "tools": self.from_standard_tools(context.tools) or [],
65
+ # To avoid refactoring in AWSBedrockLLMService, we just pass through tool_choice.
66
+ # Eventually (when we don't have to maintain the non-LLMContext code path) we should do
67
+ # the conversion to Bedrock's expected format here rather than in AWSBedrockLLMService.
68
+ "tool_choice": context.tool_choice,
69
+ }
45
70
 
46
71
  def get_messages_for_logging(self, context) -> List[Dict[str, Any]]:
47
72
  """Get messages from a universal LLM context in a format ready for logging about AWS Bedrock.
48
73
 
49
74
  Removes or truncates sensitive data like image content for safe logging.
50
75
 
51
- This is a placeholder until support for universal LLMContext machinery is added for Bedrock.
52
-
53
76
  Args:
54
77
  context: The LLM context containing messages.
55
78
 
56
79
  Returns:
57
80
  List of messages in a format ready for logging about AWS Bedrock.
58
81
  """
59
- raise NotImplementedError("Universal LLMContext is not yet supported for AWS Bedrock.")
82
+ # Get messages in Anthropic's format
83
+ messages = self._from_universal_context_messages(self.get_messages(context)).messages
84
+
85
+ # Sanitize messages for logging
86
+ messages_for_logging = []
87
+ for message in messages:
88
+ msg = copy.deepcopy(message)
89
+ if "content" in msg:
90
+ if isinstance(msg["content"], list):
91
+ for item in msg["content"]:
92
+ if item.get("image"):
93
+ item["image"]["source"]["bytes"] = "..."
94
+ messages_for_logging.append(msg)
95
+ return messages_for_logging
96
+
97
+ @dataclass
98
+ class ConvertedMessages:
99
+ """Container for Anthropic-formatted messages converted from universal context."""
100
+
101
+ messages: List[dict[str, Any]]
102
+ system: Optional[str]
103
+
104
+ def _from_universal_context_messages(
105
+ self, universal_context_messages: List[LLMContextMessage]
106
+ ) -> ConvertedMessages:
107
+ system = None
108
+ messages = []
109
+
110
+ # first, map messages using self._from_universal_context_message(m)
111
+ try:
112
+ messages = [self._from_universal_context_message(m) for m in universal_context_messages]
113
+ except Exception as e:
114
+ logger.error(f"Error mapping messages: {e}")
115
+
116
+ # See if we should pull the system message out of our messages list
117
+ if messages and messages[0]["role"] == "system":
118
+ system = messages[0]["content"]
119
+ messages.pop(0)
120
+
121
+ # Convert any subsequent "system"-role messages to "user"-role
122
+ # messages, as AWS Bedrock doesn't support system input messages.
123
+ for message in messages:
124
+ if message["role"] == "system":
125
+ message["role"] = "user"
126
+
127
+ # Merge consecutive messages with the same role.
128
+ i = 0
129
+ while i < len(messages) - 1:
130
+ current_message = messages[i]
131
+ next_message = messages[i + 1]
132
+ if current_message["role"] == next_message["role"]:
133
+ # Convert content to list of dictionaries if it's a string
134
+ if isinstance(current_message["content"], str):
135
+ current_message["content"] = [
136
+ {"type": "text", "text": current_message["content"]}
137
+ ]
138
+ if isinstance(next_message["content"], str):
139
+ next_message["content"] = [{"type": "text", "text": next_message["content"]}]
140
+ # Concatenate the content
141
+ current_message["content"].extend(next_message["content"])
142
+ # Remove the next message from the list
143
+ messages.pop(i + 1)
144
+ else:
145
+ i += 1
146
+
147
+ # Avoid empty content in messages
148
+ for message in messages:
149
+ if isinstance(message["content"], str) and message["content"] == "":
150
+ message["content"] = "(empty)"
151
+ elif isinstance(message["content"], list) and len(message["content"]) == 0:
152
+ message["content"] = [{"type": "text", "text": "(empty)"}]
153
+
154
+ return self.ConvertedMessages(messages=messages, system=system)
155
+
156
+ def _from_universal_context_message(self, message: LLMContextMessage) -> dict[str, Any]:
157
+ if isinstance(message, LLMSpecificMessage):
158
+ return copy.deepcopy(message.message)
159
+ return self._from_standard_message(message)
160
+
161
+ def _from_standard_message(self, message: LLMStandardMessage) -> dict[str, Any]:
162
+ """Convert standard format message to AWS Bedrock format.
163
+
164
+ Handles conversion of text content, tool calls, and tool results.
165
+ Empty text content is converted to "(empty)".
166
+
167
+ Args:
168
+ message: Message in standard format.
169
+
170
+ Returns:
171
+ Message in AWS Bedrock format.
172
+
173
+ Examples:
174
+ Standard format input::
175
+
176
+ {
177
+ "role": "assistant",
178
+ "tool_calls": [
179
+ {
180
+ "id": "123",
181
+ "function": {"name": "search", "arguments": '{"q": "test"}'}
182
+ }
183
+ ]
184
+ }
185
+
186
+ AWS Bedrock format output::
187
+
188
+ {
189
+ "role": "assistant",
190
+ "content": [
191
+ {
192
+ "toolUse": {
193
+ "toolUseId": "123",
194
+ "name": "search",
195
+ "input": {"q": "test"}
196
+ }
197
+ }
198
+ ]
199
+ }
200
+ """
201
+ message = copy.deepcopy(message)
202
+ if message["role"] == "tool":
203
+ # Try to parse the content as JSON if it looks like JSON
204
+ try:
205
+ if message["content"].strip().startswith("{") and message[
206
+ "content"
207
+ ].strip().endswith("}"):
208
+ content_json = json.loads(message["content"])
209
+ tool_result_content = [{"json": content_json}]
210
+ else:
211
+ tool_result_content = [{"text": message["content"]}]
212
+ except:
213
+ tool_result_content = [{"text": message["content"]}]
214
+
215
+ return {
216
+ "role": "user",
217
+ "content": [
218
+ {
219
+ "toolResult": {
220
+ "toolUseId": message["tool_call_id"],
221
+ "content": tool_result_content,
222
+ },
223
+ },
224
+ ],
225
+ }
226
+
227
+ if message.get("tool_calls"):
228
+ tc = message["tool_calls"]
229
+ ret = {"role": "assistant", "content": []}
230
+ for tool_call in tc:
231
+ function = tool_call["function"]
232
+ arguments = json.loads(function["arguments"])
233
+ new_tool_use = {
234
+ "toolUse": {
235
+ "toolUseId": tool_call["id"],
236
+ "name": function["name"],
237
+ "input": arguments,
238
+ }
239
+ }
240
+ ret["content"].append(new_tool_use)
241
+ return ret
242
+
243
+ # Handle text content
244
+ content = message.get("content")
245
+ if isinstance(content, str):
246
+ if content == "":
247
+ return {"role": message["role"], "content": [{"text": "(empty)"}]}
248
+ else:
249
+ return {"role": message["role"], "content": [{"text": content}]}
250
+ elif isinstance(content, list):
251
+ new_content = []
252
+ for item in content:
253
+ # fix empty text
254
+ if item.get("type", "") == "text":
255
+ text_content = item["text"] if item["text"] != "" else "(empty)"
256
+ new_content.append({"text": text_content})
257
+ # handle image_url -> image conversion
258
+ if item["type"] == "image_url":
259
+ new_item = {
260
+ "image": {
261
+ "format": "jpeg",
262
+ "source": {
263
+ "bytes": base64.b64decode(item["image_url"]["url"].split(",")[1])
264
+ },
265
+ }
266
+ }
267
+ new_content.append(new_item)
268
+ # In the case where there's a single image in the list (like what
269
+ # would result from a UserImageRawFrame), ensure that the image
270
+ # comes before text
271
+ image_indices = [i for i, item in enumerate(new_content) if "image" in item]
272
+ text_indices = [i for i, item in enumerate(new_content) if "text" in item]
273
+ if len(image_indices) == 1 and text_indices:
274
+ img_idx = image_indices[0]
275
+ first_txt_idx = text_indices[0]
276
+ if img_idx > first_txt_idx:
277
+ # Move image before the first text
278
+ image_item = new_content.pop(img_idx)
279
+ new_content.insert(first_txt_idx, image_item)
280
+ return {"role": message["role"], "content": new_content}
281
+
282
+ return message
60
283
 
61
284
  @staticmethod
62
285
  def _to_bedrock_function_format(function: FunctionSchema) -> Dict[str, Any]:
@@ -54,6 +54,11 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
54
54
  - Extracting and sanitizing messages from the LLM context for logging with Gemini.
55
55
  """
56
56
 
57
+ @property
58
+ def id_for_llm_specific_messages(self) -> str:
59
+ """Get the identifier used in LLMSpecificMessage instances for Google."""
60
+ return "google"
61
+
57
62
  def get_llm_invocation_params(self, context: LLMContext) -> GeminiLLMInvocationParams:
58
63
  """Get Gemini-specific LLM invocation parameters from a universal LLM context.
59
64
 
@@ -63,7 +68,7 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
63
68
  Returns:
64
69
  Dictionary of parameters for Gemini's API.
65
70
  """
66
- messages = self._from_universal_context_messages(self._get_messages(context))
71
+ messages = self._from_universal_context_messages(self.get_messages(context))
67
72
  return {
68
73
  "system_instruction": messages.system_instruction,
69
74
  "messages": messages.messages,
@@ -82,9 +87,11 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
82
87
  Includes both converted standard tools and any custom Gemini-specific tools.
83
88
  """
84
89
  functions_schema = tools_schema.standard_tools
85
- formatted_standard_tools = [
86
- {"function_declarations": [func.to_default_dict() for func in functions_schema]}
87
- ]
90
+ formatted_standard_tools = (
91
+ [{"function_declarations": [func.to_default_dict() for func in functions_schema]}]
92
+ if functions_schema
93
+ else []
94
+ )
88
95
  custom_gemini_tools = []
89
96
  if tools_schema.custom_tools:
90
97
  custom_gemini_tools = tools_schema.custom_tools.get(AdapterType.GEMINI, [])
@@ -103,7 +110,7 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
103
110
  List of messages in a format ready for logging about Gemini.
104
111
  """
105
112
  # Get messages in Gemini's format
106
- messages = self._from_universal_context_messages(self._get_messages(context)).messages
113
+ messages = self._from_universal_context_messages(self.get_messages(context)).messages
107
114
 
108
115
  # Sanitize messages for logging
109
116
  messages_for_logging = []
@@ -119,9 +126,6 @@ class GeminiLLMAdapter(BaseLLMAdapter[GeminiLLMInvocationParams]):
119
126
  messages_for_logging.append(obj)
120
127
  return messages_for_logging
121
128
 
122
- def _get_messages(self, context: LLMContext) -> List[LLMContextMessage]:
123
- return context.get_messages("google")
124
-
125
129
  @dataclass
126
130
  class ConvertedMessages:
127
131
  """Container for Google-formatted messages converted from universal context."""
@@ -24,6 +24,7 @@ from pipecat.processors.aggregators.llm_context import (
24
24
  LLMContext,
25
25
  LLMContextMessage,
26
26
  LLMContextToolChoice,
27
+ LLMSpecificMessage,
27
28
  NotGiven,
28
29
  )
29
30
 
@@ -47,6 +48,11 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
47
48
  - Extracting and sanitizing messages from the LLM context for logging about OpenAI.
48
49
  """
49
50
 
51
+ @property
52
+ def id_for_llm_specific_messages(self) -> str:
53
+ """Get the identifier used in LLMSpecificMessage instances for OpenAI."""
54
+ return "openai"
55
+
50
56
  def get_llm_invocation_params(self, context: LLMContext) -> OpenAILLMInvocationParams:
51
57
  """Get OpenAI-specific LLM invocation parameters from a universal LLM context.
52
58
 
@@ -57,7 +63,7 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
57
63
  Dictionary of parameters for OpenAI's ChatCompletion API.
58
64
  """
59
65
  return {
60
- "messages": self._from_universal_context_messages(self._get_messages(context)),
66
+ "messages": self._from_universal_context_messages(self.get_messages(context)),
61
67
  # NOTE; LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN)
62
68
  "tools": self.from_standard_tools(context.tools),
63
69
  "tool_choice": context.tool_choice,
@@ -91,7 +97,7 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
91
97
  List of messages in a format ready for logging about OpenAI.
92
98
  """
93
99
  msgs = []
94
- for message in self._get_messages(context):
100
+ for message in self.get_messages(context):
95
101
  msg = copy.deepcopy(message)
96
102
  if "content" in msg:
97
103
  if isinstance(msg["content"], list):
@@ -99,19 +105,25 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
99
105
  if item["type"] == "image_url":
100
106
  if item["image_url"]["url"].startswith("data:image/"):
101
107
  item["image_url"]["url"] = "data:image/..."
108
+ if item["type"] == "input_audio":
109
+ item["input_audio"]["data"] = "..."
102
110
  if "mime_type" in msg and msg["mime_type"].startswith("image/"):
103
111
  msg["data"] = "..."
104
112
  msgs.append(msg)
105
113
  return msgs
106
114
 
107
- def _get_messages(self, context: LLMContext) -> List[LLMContextMessage]:
108
- return context.get_messages("openai")
109
-
110
115
  def _from_universal_context_messages(
111
116
  self, messages: List[LLMContextMessage]
112
117
  ) -> List[ChatCompletionMessageParam]:
113
- # Just a pass-through: messages are already the right type
114
- return messages
118
+ result = []
119
+ for message in messages:
120
+ if isinstance(message, LLMSpecificMessage):
121
+ # Extract the actual message content from LLMSpecificMessage
122
+ result.append(message.message)
123
+ else:
124
+ # Standard message, pass through unchanged
125
+ result.append(message)
126
+ return result
115
127
 
116
128
  def _from_standard_tool_choice(
117
129
  self, tool_choice: LLMContextToolChoice | NotGiven
@@ -30,6 +30,11 @@ class OpenAIRealtimeLLMAdapter(BaseLLMAdapter):
30
30
  OpenAI's Realtime API for function calling capabilities.
31
31
  """
32
32
 
33
+ @property
34
+ def id_for_llm_specific_messages(self) -> str:
35
+ """Get the identifier used in LLMSpecificMessage instances for OpenAI Realtime."""
36
+ raise NotImplementedError("Universal LLMContext is not yet supported for OpenAI Realtime.")
37
+
33
38
  def get_llm_invocation_params(self, context: LLMContext) -> OpenAIRealtimeLLMInvocationParams:
34
39
  """Get OpenAI Realtime-specific LLM invocation parameters from a universal LLM context.
35
40