fix: clean up TypedDict / Optional patterns in 6 more LLM adapters
Same approach as the previous round — apply boundary casts where the code does dict-style mutation on TypedDict-typed values, narrow at return sites, and document the LLMSpecificMessage limitation in realtime adapters that pack history into a single text message. aws_nova_sonic_adapter.py — pure typing + small narrowing fixes: - Filter LLMSpecific items in `_from_universal_context_messages` (documented). - `_from_universal_context_message` now declared `-> AWSNovaSonicConversationHistoryMessage | None` (it already had paths returning None implicitly). - `get_messages_for_logging` returns `dict[str, Any]` per element via `dataclasses.asdict`, matching the declared return type. - Use a local `role` variable so pyright keeps the narrowing across the truthy-content guard. grok_realtime_adapter.py / inworld_realtime_adapter.py — same shape of fix as `open_ai_realtime_adapter.py` from the previous batch. The two files are essentially copies of the OpenAI Realtime adapter, so the same template applies: cast at the boundary, filter LLMSpecificMessage with a documented note, replace the implicit-None fallthrough with `raise ValueError`, and switch the `text_content +=` pattern (which fails when one of the parts is None) to a `text_parts.append(...)` + `" ".join(...)` pattern. open_ai_adapter.py — pure typing. Cast at the `OpenAILLMInvocationParams` return, narrow the system-instruction warning's `initial_content` to `str | None`, and cast the custom-tools list to `list[ChatCompletionToolParam]`. open_ai_responses_adapter.py — pure typing. Same shape: narrow `first_content` to `str | None` for the warning resolver, cast the constructed dict literals at append sites where the target is `ResponseInputItemParam`, and cast `get_messages_for_logging`'s return to the declared `list[dict[str, Any]]`. processors/aggregators/llm_context.py — pure typing. Cast the deepcopied message in the redaction loop in `get_messages` to `dict[str, Any]` and the create_image/audio_message return-dict literals to `LLMContextMessage`. Removes 6 newly-clean files from the pyright ignore list. Net: -77 pyright errors (full-config: 680 -> 603).
This commit is contained in:
@@ -6,11 +6,6 @@
|
|||||||
"exclude": ["**/*_pb2.py", "**/__pycache__"],
|
"exclude": ["**/*_pb2.py", "**/__pycache__"],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"tests",
|
"tests",
|
||||||
"src/pipecat/adapters/services/aws_nova_sonic_adapter.py",
|
|
||||||
"src/pipecat/adapters/services/grok_realtime_adapter.py",
|
|
||||||
"src/pipecat/adapters/services/inworld_realtime_adapter.py",
|
|
||||||
"src/pipecat/adapters/services/open_ai_adapter.py",
|
|
||||||
"src/pipecat/adapters/services/open_ai_responses_adapter.py",
|
|
||||||
"src/pipecat/audio/dtmf/utils.py",
|
"src/pipecat/audio/dtmf/utils.py",
|
||||||
"src/pipecat/audio/filters/aic_filter.py",
|
"src/pipecat/audio/filters/aic_filter.py",
|
||||||
"src/pipecat/audio/filters/krisp_viva_filter.py",
|
"src/pipecat/audio/filters/krisp_viva_filter.py",
|
||||||
@@ -18,7 +13,6 @@
|
|||||||
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v2.py",
|
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v2.py",
|
||||||
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v3.py",
|
"src/pipecat/audio/turn/smart_turn/local_smart_turn_v3.py",
|
||||||
"src/pipecat/audio/vad/silero.py",
|
"src/pipecat/audio/vad/silero.py",
|
||||||
"src/pipecat/processors/aggregators/llm_context.py",
|
|
||||||
"src/pipecat/processors/aggregators/llm_response_universal.py",
|
"src/pipecat/processors/aggregators/llm_response_universal.py",
|
||||||
"src/pipecat/processors/frame_processor.py",
|
"src/pipecat/processors/frame_processor.py",
|
||||||
"src/pipecat/processors/frameworks/langchain.py",
|
"src/pipecat/processors/frameworks/langchain.py",
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import asdict, dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict, cast
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -110,7 +110,10 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
|||||||
Returns:
|
Returns:
|
||||||
List of messages in a format ready for logging about AWS Nova Sonic.
|
List of messages in a format ready for logging about AWS Nova Sonic.
|
||||||
"""
|
"""
|
||||||
return self._from_universal_context_messages(self.get_messages(context)).messages
|
return [
|
||||||
|
asdict(m)
|
||||||
|
for m in self._from_universal_context_messages(self.get_messages(context)).messages
|
||||||
|
]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConvertedMessages:
|
class ConvertedMessages:
|
||||||
@@ -123,18 +126,27 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
|||||||
self, universal_context_messages: list[LLMContextMessage]
|
self, universal_context_messages: list[LLMContextMessage]
|
||||||
) -> ConvertedMessages:
|
) -> ConvertedMessages:
|
||||||
system_instruction = None
|
system_instruction = None
|
||||||
messages = []
|
messages: list[AWSNovaSonicConversationHistoryMessage] = []
|
||||||
|
|
||||||
# Bail if there are no messages
|
# Bail if there are no messages
|
||||||
if not universal_context_messages:
|
if not universal_context_messages:
|
||||||
return self.ConvertedMessages()
|
return self.ConvertedMessages(messages=[])
|
||||||
|
|
||||||
universal_context_messages = copy.deepcopy(universal_context_messages)
|
# NOTE: This adapter does not yet handle ``LLMSpecificMessage`` —
|
||||||
|
# those are filtered out below (the role-extraction and conversion
|
||||||
|
# logic only applies to standard message dicts). If/when this
|
||||||
|
# adapter grows a per-provider passthrough like the Anthropic
|
||||||
|
# adapter has, LLMSpecific items can flow through.
|
||||||
|
ucm: list[dict[str, Any]] = [
|
||||||
|
cast(dict[str, Any], m)
|
||||||
|
for m in copy.deepcopy(universal_context_messages)
|
||||||
|
if isinstance(m, dict)
|
||||||
|
]
|
||||||
|
|
||||||
# If we have a "system" message as our first message,
|
# If we have a "system" message as our first message,
|
||||||
# pull that out into "instruction"
|
# pull that out into "instruction"
|
||||||
if universal_context_messages[0].get("role") == "system":
|
if ucm and ucm[0].get("role") == "system":
|
||||||
system = universal_context_messages.pop(0)
|
system = ucm.pop(0)
|
||||||
content = system.get("content")
|
content = system.get("content")
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
system_instruction = content
|
system_instruction = content
|
||||||
@@ -145,19 +157,21 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
|||||||
|
|
||||||
# Convert any remaining "system"/"developer" messages to "user",
|
# Convert any remaining "system"/"developer" messages to "user",
|
||||||
# as Nova Sonic only supports "user" and "assistant" in history.
|
# as Nova Sonic only supports "user" and "assistant" in history.
|
||||||
for msg in universal_context_messages:
|
for msg in ucm:
|
||||||
if msg.get("role") in ("system", "developer"):
|
if msg.get("role") in ("system", "developer"):
|
||||||
msg["role"] = "user"
|
msg["role"] = "user"
|
||||||
|
|
||||||
# Process remaining messages to fill out conversation history.
|
# Process remaining messages to fill out conversation history.
|
||||||
for universal_context_message in universal_context_messages:
|
for universal_context_message in ucm:
|
||||||
message = self._from_universal_context_message(universal_context_message)
|
message = self._from_universal_context_message(universal_context_message)
|
||||||
if message:
|
if message:
|
||||||
messages.append(message)
|
messages.append(message)
|
||||||
|
|
||||||
return self.ConvertedMessages(messages=messages, system_instruction=system_instruction)
|
return self.ConvertedMessages(messages=messages, system_instruction=system_instruction)
|
||||||
|
|
||||||
def _from_universal_context_message(self, message) -> AWSNovaSonicConversationHistoryMessage:
|
def _from_universal_context_message(
|
||||||
|
self, message: dict[str, Any]
|
||||||
|
) -> AWSNovaSonicConversationHistoryMessage | None:
|
||||||
"""Convert standard message format to Nova Sonic format.
|
"""Convert standard message format to Nova Sonic format.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -167,17 +181,18 @@ class AWSNovaSonicLLMAdapter(BaseLLMAdapter[AWSNovaSonicLLMInvocationParams]):
|
|||||||
Nova Sonic conversation history message, or None if not convertible.
|
Nova Sonic conversation history message, or None if not convertible.
|
||||||
"""
|
"""
|
||||||
role = message.get("role")
|
role = message.get("role")
|
||||||
if message.get("role") == "user" or message.get("role") == "assistant":
|
if role == "user" or role == "assistant":
|
||||||
content = message.get("content")
|
content = message.get("content")
|
||||||
if isinstance(message.get("content"), list):
|
if isinstance(content, list):
|
||||||
content = ""
|
text_parts = []
|
||||||
for c in message.get("content"):
|
for c in content:
|
||||||
if c.get("type") == "text":
|
if c.get("type") == "text":
|
||||||
content += " " + c.get("text")
|
text_parts.append(c.get("text"))
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Unhandled content type in context message: {c.get('type')} - {message}"
|
f"Unhandled content type in context message: {c.get('type')} - {message}"
|
||||||
)
|
)
|
||||||
|
content = " ".join(t for t in text_parts if t)
|
||||||
# There won't be content if this is an assistant tool call entry.
|
# There won't be content if this is an assistant tool call entry.
|
||||||
# We're ignoring those since they can't be loaded into AWS Nova Sonic conversation
|
# We're ignoring those since they can't be loaded into AWS Nova Sonic conversation
|
||||||
# history
|
# history
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Grok's Voice Agent API.
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict, cast
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -85,7 +85,10 @@ class GrokRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
Returns:
|
Returns:
|
||||||
List of messages with sensitive data redacted.
|
List of messages with sensitive data redacted.
|
||||||
"""
|
"""
|
||||||
return self.get_messages(context, truncate_large_values=True)
|
return cast(
|
||||||
|
list[dict[str, Any]],
|
||||||
|
self.get_messages(context, truncate_large_values=True),
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConvertedMessages:
|
class ConvertedMessages:
|
||||||
@@ -111,11 +114,20 @@ class GrokRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
if not universal_context_messages:
|
if not universal_context_messages:
|
||||||
return self.ConvertedMessages(messages=[])
|
return self.ConvertedMessages(messages=[])
|
||||||
|
|
||||||
messages = copy.deepcopy(universal_context_messages)
|
# NOTE: This adapter does not yet handle ``LLMSpecificMessage`` —
|
||||||
|
# those are filtered out below. Other adapters (e.g. Anthropic)
|
||||||
|
# dispatch LLMSpecific items through a per-provider passthrough.
|
||||||
|
# The pack-into-single-text-message strategy here doesn't compose
|
||||||
|
# with opaque per-provider payloads.
|
||||||
|
messages: list[dict[str, Any]] = [
|
||||||
|
cast(dict[str, Any], m)
|
||||||
|
for m in copy.deepcopy(universal_context_messages)
|
||||||
|
if isinstance(m, dict)
|
||||||
|
]
|
||||||
system_instruction = None
|
system_instruction = None
|
||||||
|
|
||||||
# Extract system message as session instructions
|
# Extract system message as session instructions
|
||||||
if messages[0].get("role") == "system":
|
if messages and messages[0].get("role") == "system":
|
||||||
system = messages.pop(0)
|
system = messages.pop(0)
|
||||||
content = system.get("content")
|
content = system.get("content")
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
@@ -133,7 +145,9 @@ class GrokRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
# Single user message can be sent normally
|
# Single user message can be sent normally
|
||||||
if len(messages) == 1 and messages[0].get("role") == "user":
|
if len(messages) == 1 and messages[0].get("role") == "user":
|
||||||
return self.ConvertedMessages(
|
return self.ConvertedMessages(
|
||||||
messages=[self._from_universal_context_message(messages[0])],
|
messages=[
|
||||||
|
self._from_universal_context_message(cast(LLMContextMessage, messages[0]))
|
||||||
|
],
|
||||||
system_instruction=system_instruction,
|
system_instruction=system_instruction,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,26 +195,29 @@ class GrokRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
Returns:
|
Returns:
|
||||||
ConversationItem formatted for Grok Realtime API.
|
ConversationItem formatted for Grok Realtime API.
|
||||||
"""
|
"""
|
||||||
if message.get("role") == "user":
|
# NOTE: ``LLMSpecificMessage`` is not yet handled here — see the
|
||||||
content = message.get("content")
|
# corresponding note in `_from_universal_context_messages`.
|
||||||
|
msg = cast(dict[str, Any], message)
|
||||||
|
if msg.get("role") == "user":
|
||||||
|
content = msg.get("content")
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
text_content = ""
|
text_parts = []
|
||||||
for c in content:
|
for c in content:
|
||||||
if c.get("type") == "text":
|
if c.get("type") == "text":
|
||||||
text_content += " " + c.get("text")
|
text_parts.append(c.get("text"))
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Unhandled content type in context message: {c.get('type')} - {message}"
|
f"Unhandled content type in context message: {c.get('type')} - {msg}"
|
||||||
)
|
)
|
||||||
content = text_content.strip()
|
content = " ".join(t for t in text_parts if t).strip()
|
||||||
return events.ConversationItem(
|
return events.ConversationItem(
|
||||||
role="user",
|
role="user",
|
||||||
type="message",
|
type="message",
|
||||||
content=[events.ItemContent(type="input_text", text=content)],
|
content=[events.ItemContent(type="input_text", text=content)],
|
||||||
)
|
)
|
||||||
|
|
||||||
if message.get("role") == "assistant" and message.get("tool_calls"):
|
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
||||||
tc = message.get("tool_calls")[0]
|
tc = msg["tool_calls"][0]
|
||||||
return events.ConversationItem(
|
return events.ConversationItem(
|
||||||
type="function_call",
|
type="function_call",
|
||||||
call_id=tc["id"],
|
call_id=tc["id"],
|
||||||
@@ -208,7 +225,7 @@ class GrokRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
arguments=tc["function"]["arguments"],
|
arguments=tc["function"]["arguments"],
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error(f"Unhandled message type in _from_universal_context_message: {message}")
|
raise ValueError(f"Unhandled message type in _from_universal_context_message: {msg}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_grok_function_format(function: FunctionSchema) -> dict[str, Any]:
|
def _to_grok_function_format(function: FunctionSchema) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Inworld's Realtime API.
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict, cast
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -85,7 +85,10 @@ class InworldRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
Returns:
|
Returns:
|
||||||
List of messages with sensitive data redacted.
|
List of messages with sensitive data redacted.
|
||||||
"""
|
"""
|
||||||
return self.get_messages(context, truncate_large_values=True)
|
return cast(
|
||||||
|
list[dict[str, Any]],
|
||||||
|
self.get_messages(context, truncate_large_values=True),
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConvertedMessages:
|
class ConvertedMessages:
|
||||||
@@ -111,11 +114,20 @@ class InworldRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
if not universal_context_messages:
|
if not universal_context_messages:
|
||||||
return self.ConvertedMessages(messages=[])
|
return self.ConvertedMessages(messages=[])
|
||||||
|
|
||||||
messages = copy.deepcopy(universal_context_messages)
|
# NOTE: This adapter does not yet handle ``LLMSpecificMessage`` —
|
||||||
|
# those are filtered out below. Other adapters (e.g. Anthropic)
|
||||||
|
# dispatch LLMSpecific items through a per-provider passthrough.
|
||||||
|
# The pack-into-single-text-message strategy here doesn't compose
|
||||||
|
# with opaque per-provider payloads.
|
||||||
|
messages: list[dict[str, Any]] = [
|
||||||
|
cast(dict[str, Any], m)
|
||||||
|
for m in copy.deepcopy(universal_context_messages)
|
||||||
|
if isinstance(m, dict)
|
||||||
|
]
|
||||||
system_instruction = None
|
system_instruction = None
|
||||||
|
|
||||||
# Extract system message as session instructions
|
# Extract system message as session instructions
|
||||||
if messages[0].get("role") == "system":
|
if messages and messages[0].get("role") == "system":
|
||||||
system = messages.pop(0)
|
system = messages.pop(0)
|
||||||
content = system.get("content")
|
content = system.get("content")
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
@@ -133,7 +145,9 @@ class InworldRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
# Single user message can be sent normally
|
# Single user message can be sent normally
|
||||||
if len(messages) == 1 and messages[0].get("role") == "user":
|
if len(messages) == 1 and messages[0].get("role") == "user":
|
||||||
return self.ConvertedMessages(
|
return self.ConvertedMessages(
|
||||||
messages=[self._from_universal_context_message(messages[0])],
|
messages=[
|
||||||
|
self._from_universal_context_message(cast(LLMContextMessage, messages[0]))
|
||||||
|
],
|
||||||
system_instruction=system_instruction,
|
system_instruction=system_instruction,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,26 +195,29 @@ class InworldRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
Returns:
|
Returns:
|
||||||
ConversationItem formatted for Inworld Realtime API.
|
ConversationItem formatted for Inworld Realtime API.
|
||||||
"""
|
"""
|
||||||
if message.get("role") == "user":
|
# NOTE: ``LLMSpecificMessage`` is not yet handled here — see the
|
||||||
content = message.get("content")
|
# corresponding note in `_from_universal_context_messages`.
|
||||||
|
msg = cast(dict[str, Any], message)
|
||||||
|
if msg.get("role") == "user":
|
||||||
|
content = msg.get("content")
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
text_content = ""
|
text_parts = []
|
||||||
for c in content:
|
for c in content:
|
||||||
if c.get("type") == "text":
|
if c.get("type") == "text":
|
||||||
text_content += " " + c.get("text")
|
text_parts.append(c.get("text"))
|
||||||
else:
|
else:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Unhandled content type in context message: {c.get('type')} - {message}"
|
f"Unhandled content type in context message: {c.get('type')} - {msg}"
|
||||||
)
|
)
|
||||||
content = text_content.strip()
|
content = " ".join(t for t in text_parts if t).strip()
|
||||||
return events.ConversationItem(
|
return events.ConversationItem(
|
||||||
role="user",
|
role="user",
|
||||||
type="message",
|
type="message",
|
||||||
content=[events.ItemContent(type="input_text", text=content)],
|
content=[events.ItemContent(type="input_text", text=content)],
|
||||||
)
|
)
|
||||||
|
|
||||||
if message.get("role") == "assistant" and message.get("tool_calls"):
|
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
||||||
tc = message.get("tool_calls")[0]
|
tc = msg["tool_calls"][0]
|
||||||
return events.ConversationItem(
|
return events.ConversationItem(
|
||||||
type="function_call",
|
type="function_call",
|
||||||
call_id=tc["id"],
|
call_id=tc["id"],
|
||||||
@@ -208,7 +225,7 @@ class InworldRealtimeLLMAdapter(BaseLLMAdapter):
|
|||||||
arguments=tc["function"]["arguments"],
|
arguments=tc["function"]["arguments"],
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error(f"Unhandled message type in _from_universal_context_message: {message}")
|
raise ValueError(f"Unhandled message type in _from_universal_context_message: {msg}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_inworld_function_format(function: FunctionSchema) -> dict[str, Any]:
|
def _to_inworld_function_format(function: FunctionSchema) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -127,12 +127,15 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if system_instruction:
|
if system_instruction:
|
||||||
# Detect initial system message for warning purposes (don't extract)
|
# Detect initial system message for warning purposes (don't extract).
|
||||||
initial_content = (
|
# ChatCompletionMessageParam.content is `str | Iterable[...]`; we
|
||||||
messages[0].get("content", "")
|
# only forward it for warning purposes, so coerce non-strings to
|
||||||
if messages and messages[0].get("role") == "system"
|
# None — the resolver handles None.
|
||||||
else None
|
initial_content: str | None = None
|
||||||
)
|
if messages and messages[0].get("role") == "system":
|
||||||
|
raw_content = messages[0].get("content", "")
|
||||||
|
if isinstance(raw_content, str):
|
||||||
|
initial_content = raw_content
|
||||||
self._resolve_system_instruction(
|
self._resolve_system_instruction(
|
||||||
initial_content,
|
initial_content,
|
||||||
system_instruction,
|
system_instruction,
|
||||||
@@ -140,12 +143,15 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
|
|||||||
)
|
)
|
||||||
messages = [{"role": "system", "content": system_instruction}] + messages
|
messages = [{"role": "system", "content": system_instruction}] + messages
|
||||||
|
|
||||||
return {
|
return cast(
|
||||||
"messages": messages,
|
OpenAILLMInvocationParams,
|
||||||
# NOTE; LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN)
|
{
|
||||||
"tools": self.from_standard_tools(context.tools),
|
"messages": messages,
|
||||||
"tool_choice": _openai_from_llm_context_tool_choice(context.tool_choice),
|
# NOTE; LLMContext's tools are guaranteed to be a ToolsSchema (or NOT_GIVEN)
|
||||||
}
|
"tools": self.from_standard_tools(context.tools),
|
||||||
|
"tool_choice": _openai_from_llm_context_tool_choice(context.tool_choice),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> list[ChatCompletionToolParam]:
|
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> list[ChatCompletionToolParam]:
|
||||||
"""Convert function schemas to OpenAI's function-calling format.
|
"""Convert function schemas to OpenAI's function-calling format.
|
||||||
@@ -158,13 +164,19 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
|
|||||||
with ChatCompletion API.
|
with ChatCompletion API.
|
||||||
"""
|
"""
|
||||||
functions_schema = tools_schema.standard_tools
|
functions_schema = tools_schema.standard_tools
|
||||||
formatted_standard_tools = [
|
# `function=...` expects a `FunctionDefinition` TypedDict; the dict
|
||||||
ChatCompletionToolParam(type="function", function=func.to_default_dict())
|
# produced by `to_default_dict()` is structurally compatible. Cast at
|
||||||
|
# the boundary.
|
||||||
|
formatted_standard_tools: list[ChatCompletionToolParam] = [
|
||||||
|
ChatCompletionToolParam(type="function", function=cast(Any, func.to_default_dict()))
|
||||||
for func in functions_schema
|
for func in functions_schema
|
||||||
]
|
]
|
||||||
custom_openai_tools = []
|
custom_openai_tools: list[ChatCompletionToolParam] = []
|
||||||
if tools_schema.custom_tools:
|
if tools_schema.custom_tools:
|
||||||
custom_openai_tools = tools_schema.custom_tools.get(AdapterType.OPENAI, [])
|
custom_openai_tools = cast(
|
||||||
|
list[ChatCompletionToolParam],
|
||||||
|
tools_schema.custom_tools.get(AdapterType.OPENAI, []),
|
||||||
|
)
|
||||||
return formatted_standard_tools + custom_openai_tools
|
return formatted_standard_tools + custom_openai_tools
|
||||||
|
|
||||||
def get_messages_for_logging(self, context: LLMContext) -> list[dict[str, Any]]:
|
def get_messages_for_logging(self, context: LLMContext) -> list[dict[str, Any]]:
|
||||||
@@ -178,7 +190,10 @@ class OpenAILLMAdapter(BaseLLMAdapter[OpenAILLMInvocationParams]):
|
|||||||
Returns:
|
Returns:
|
||||||
List of messages in a format ready for logging about OpenAI.
|
List of messages in a format ready for logging about OpenAI.
|
||||||
"""
|
"""
|
||||||
return self.get_messages(context, truncate_large_values=True)
|
return cast(
|
||||||
|
list[dict[str, Any]],
|
||||||
|
self.get_messages(context, truncate_large_values=True),
|
||||||
|
)
|
||||||
|
|
||||||
def _from_universal_context_messages(
|
def _from_universal_context_messages(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
"""OpenAI Responses API adapter for Pipecat."""
|
"""OpenAI Responses API adapter for Pipecat."""
|
||||||
|
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict, cast
|
||||||
|
|
||||||
from openai._types import NotGiven as OpenAINotGiven
|
from openai._types import NotGiven as OpenAINotGiven
|
||||||
from openai.types.responses import FunctionToolParam, ResponseInputItemParam, ToolParam
|
from openai.types.responses import FunctionToolParam, ResponseInputItemParam, ToolParam
|
||||||
@@ -64,8 +64,11 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam
|
|||||||
if system_instruction and messages:
|
if system_instruction and messages:
|
||||||
first_msg = messages[0] if not isinstance(messages[0], LLMSpecificMessage) else None
|
first_msg = messages[0] if not isinstance(messages[0], LLMSpecificMessage) else None
|
||||||
if first_msg and first_msg.get("role") == "system":
|
if first_msg and first_msg.get("role") == "system":
|
||||||
|
# `content` is `str | Iterable[...]`; we only forward it for
|
||||||
|
# warning purposes. Coerce non-strings to None.
|
||||||
|
first_content = first_msg.get("content", "")
|
||||||
self._resolve_system_instruction(
|
self._resolve_system_instruction(
|
||||||
first_msg.get("content", ""),
|
first_content if isinstance(first_content, str) else None,
|
||||||
system_instruction,
|
system_instruction,
|
||||||
discard_context_system=False,
|
discard_context_system=False,
|
||||||
)
|
)
|
||||||
@@ -143,7 +146,10 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam
|
|||||||
Returns:
|
Returns:
|
||||||
List of messages in a format ready for logging.
|
List of messages in a format ready for logging.
|
||||||
"""
|
"""
|
||||||
return self.get_messages(context, truncate_large_values=True)
|
return cast(
|
||||||
|
list[dict[str, Any]],
|
||||||
|
self.get_messages(context, truncate_large_values=True),
|
||||||
|
)
|
||||||
|
|
||||||
def _convert_messages_to_input(
|
def _convert_messages_to_input(
|
||||||
self, messages: list[LLMContextMessage]
|
self, messages: list[LLMContextMessage]
|
||||||
@@ -169,13 +175,15 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam
|
|||||||
content = message.get("content", "")
|
content = message.get("content", "")
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
content = self._convert_multimodal_content(content)
|
content = self._convert_multimodal_content(content)
|
||||||
result.append({"role": "developer", "content": content})
|
result.append(
|
||||||
|
cast(ResponseInputItemParam, {"role": "developer", "content": content})
|
||||||
|
)
|
||||||
|
|
||||||
elif role == "user":
|
elif role == "user":
|
||||||
content = message.get("content", "")
|
content = message.get("content", "")
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
content = self._convert_multimodal_content(content)
|
content = self._convert_multimodal_content(content)
|
||||||
result.append({"role": "user", "content": content})
|
result.append(cast(ResponseInputItemParam, {"role": "user", "content": content}))
|
||||||
|
|
||||||
elif role == "assistant":
|
elif role == "assistant":
|
||||||
tool_calls = message.get("tool_calls")
|
tool_calls = message.get("tool_calls")
|
||||||
@@ -194,7 +202,9 @@ class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParam
|
|||||||
content = message.get("content", "")
|
content = message.get("content", "")
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
content = self._convert_multimodal_content(content)
|
content = self._convert_multimodal_content(content)
|
||||||
result.append({"role": "assistant", "content": content})
|
result.append(
|
||||||
|
cast(ResponseInputItemParam, {"role": "assistant", "content": content})
|
||||||
|
)
|
||||||
|
|
||||||
elif role == "tool":
|
elif role == "tool":
|
||||||
content = message.get("content", "")
|
content = message.get("content", "")
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import io
|
|||||||
import wave
|
import wave
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, TypeAlias, TypeGuard, TypeVar
|
from typing import Any, TypeAlias, TypeGuard, TypeVar, cast
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from openai._types import NOT_GIVEN as OPEN_AI_NOT_GIVEN
|
from openai._types import NOT_GIVEN as OPEN_AI_NOT_GIVEN
|
||||||
@@ -129,13 +129,13 @@ class LLMContext:
|
|||||||
url: The URL of the image.
|
url: The URL of the image.
|
||||||
text: Optional text to include with the image.
|
text: Optional text to include with the image.
|
||||||
"""
|
"""
|
||||||
content = []
|
content: list[dict[str, Any]] = []
|
||||||
if text:
|
if text:
|
||||||
content.append({"type": "text", "text": text})
|
content.append({"type": "text", "text": text})
|
||||||
|
|
||||||
content.append({"type": "image_url", "image_url": {"url": url}})
|
content.append({"type": "image_url", "image_url": {"url": url}})
|
||||||
|
|
||||||
return {"role": role, "content": content}
|
return cast(LLMContextMessage, {"role": role, "content": content})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create_image_message(
|
async def create_image_message(
|
||||||
@@ -187,7 +187,7 @@ class LLMContext:
|
|||||||
audio_frames: List of audio frame objects to include.
|
audio_frames: List of audio frame objects to include.
|
||||||
text: Optional text to include with the audio.
|
text: Optional text to include with the audio.
|
||||||
"""
|
"""
|
||||||
content = [{"type": "text", "text": text}]
|
content: list[dict[str, Any]] = [{"type": "text", "text": text}]
|
||||||
|
|
||||||
def encode_audio():
|
def encode_audio():
|
||||||
sample_rate = audio_frames[0].sample_rate
|
sample_rate = audio_frames[0].sample_rate
|
||||||
@@ -214,7 +214,7 @@ class LLMContext:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"role": role, "content": content}
|
return cast(LLMContextMessage, {"role": role, "content": content})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def messages(self) -> list[LLMContextMessage]:
|
def messages(self) -> list[LLMContextMessage]:
|
||||||
@@ -295,7 +295,10 @@ class LLMContext:
|
|||||||
result.append(msg_copy)
|
result.append(msg_copy)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg = copy.deepcopy(message)
|
# The standard message variant is a union of TypedDicts; the
|
||||||
|
# mutations below operate on plain dicts at runtime. Treat as
|
||||||
|
# such for the duration of the redaction loop.
|
||||||
|
msg: dict[str, Any] = cast(dict[str, Any], copy.deepcopy(message))
|
||||||
content = msg.get("content")
|
content = msg.get("content")
|
||||||
if isinstance(content, list):
|
if isinstance(content, list):
|
||||||
for item in content:
|
for item in content:
|
||||||
|
|||||||
Reference in New Issue
Block a user