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 libs/partners/openai/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__pycache__
tiktoken_cache
14 changes: 13 additions & 1 deletion libs/partners/openai/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,19 @@ TEST_FILE ?= tests/unit_tests/

integration_test integration_tests: TEST_FILE=tests/integration_tests/

test tests integration_test integration_tests:
# unit tests are run with the --disable-socket flag to prevent network calls
# use tiktoken cache to enable token counting without socket (internet) access
test tests:
mkdir -p tiktoken_cache
@if [ ! -f tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 ]; then \
curl -o tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken; \
fi
@if [ ! -f tiktoken_cache/fb374d419588a4632f3f557e76b4b70aebbca790 ]; then \
curl -o tiktoken_cache/fb374d419588a4632f3f557e76b4b70aebbca790 https://openaipublic.blob.core.windows.net/encodings/o200k_base.tiktoken; \
fi
TIKTOKEN_CACHE_DIR=tiktoken_cache poetry run pytest --disable-socket --allow-unix-socket $(TEST_FILE)

integration_test integration_tests:
poetry run pytest $(TEST_FILE)

test_watch:
Expand Down
13 changes: 13 additions & 0 deletions libs/partners/openai/langchain_openai/chat_models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage:
invalid_tool_calls.append(
make_invalid_tool_call(raw_tool_call, str(e))
)
if audio := _dict.get("audio"):
additional_kwargs["audio"] = audio
return AIMessage(
content=content,
additional_kwargs=additional_kwargs,
Expand Down Expand Up @@ -219,6 +221,17 @@ def _convert_message_to_dict(message: BaseMessage) -> dict:
# If tool calls present, content null value should be None not empty string.
if "function_call" in message_dict or "tool_calls" in message_dict:
message_dict["content"] = message_dict["content"] or None

if "audio" in message.additional_kwargs:
# openai doesn't support passing the data back - only the id
# https://platform.openai.com/docs/guides/audio/multi-turn-conversations
raw_audio = message.additional_kwargs["audio"]
audio = (
{"id": message.additional_kwargs["audio"]["id"]}
if "id" in raw_audio
else raw_audio
)
message_dict["audio"] = audio
elif isinstance(message, SystemMessage):
message_dict["role"] = "system"
elif isinstance(message, FunctionMessage):
Expand Down
586 changes: 311 additions & 275 deletions libs/partners/openai/poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion libs/partners/openai/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ ignore_missing_imports = true
[tool.poetry.dependencies]
python = ">=3.9,<4.0"
langchain-core = "^0.3.9"
openai = "^1.40.0"
openai = "^1.52.0"
tiktoken = ">=0.7,<1"

[tool.ruff.lint]
Expand Down Expand Up @@ -72,6 +72,7 @@ syrupy = "^4.0.2"
pytest-watcher = "^0.3.4"
pytest-asyncio = "^0.21.1"
pytest-cov = "^4.1.0"
pytest-socket = "^0.6.0"
[[tool.poetry.group.test.dependencies.numpy]]
version = "^1"
python = "<3.12"
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import base64
import json
from pathlib import Path
from typing import Any, AsyncIterator, List, Literal, Optional, cast

import httpx
Expand Down Expand Up @@ -949,3 +950,71 @@ async def test_json_mode_async() -> None:
assert isinstance(full, AIMessageChunk)
assert isinstance(full.content, str)
assert json.loads(full.content) == {"a": 1}


def test_audio_output_modality() -> None:
llm = ChatOpenAI(
model="gpt-4o-audio-preview",
temperature=0,
model_kwargs={
"modalities": ["text", "audio"],
"audio": {"voice": "alloy", "format": "wav"},
},
)

history: List[BaseMessage] = [
HumanMessage("Make me a short audio clip of you yelling")
]

output = llm.invoke(history)

assert isinstance(output, AIMessage)
assert "audio" in output.additional_kwargs

history.append(output)
history.append(HumanMessage("Make me a short audio clip of you whispering"))

output = llm.invoke(history)

assert isinstance(output, AIMessage)
assert "audio" in output.additional_kwargs


def test_audio_input_modality() -> None:
llm = ChatOpenAI(
model="gpt-4o-audio-preview",
temperature=0,
model_kwargs={
"modalities": ["text", "audio"],
"audio": {"voice": "alloy", "format": "wav"},
},
)
filepath = Path(__file__).parent / "audio_input.wav"

audio_data = filepath.read_bytes()
b64_audio_data = base64.b64encode(audio_data).decode("utf-8")

history: list[BaseMessage] = [
HumanMessage(
[
{"type": "text", "text": "What is happening in this audio clip"},
{
"type": "input_audio",
"input_audio": {"data": b64_audio_data, "format": "wav"},
},
]
)
]

output = llm.invoke(history)

assert isinstance(output, AIMessage)
assert "audio" in output.additional_kwargs

history.append(output)
history.append(HumanMessage("Why?"))

output = llm.invoke(history)

assert isinstance(output, AIMessage)
assert "audio" in output.additional_kwargs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ def test__convert_dict_to_message_tool_call() -> None:
name="GenerateUsername",
args="oops",
id="call_wm0JY6CdwOMZ4eTxHWUThDNz",
error="Function GenerateUsername arguments:\n\noops\n\nare not valid JSON. Received JSONDecodeError Expecting value: line 1 column 1 (char 0)", # noqa: E501
error=(
"Function GenerateUsername arguments:\n\noops\n\nare not "
"valid JSON. Received JSONDecodeError Expecting value: line 1 "
"column 1 (char 0)\nFor troubleshooting, visit: https://python"
".langchain.com/docs/troubleshooting/errors/OUTPUT_PARSING_FAILURE"
),
type="invalid_tool_call",
)
],
Expand Down
2 changes: 1 addition & 1 deletion libs/partners/openai/tests/unit_tests/llms/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def mock_completion() -> dict:
}


@pytest.mark.parametrize("model", ["gpt-3.5-turbo-instruct", "text-davinci-003"])
@pytest.mark.parametrize("model", ["gpt-3.5-turbo-instruct"])
def test_get_token_ids(model: str) -> None:
OpenAI(model=model).get_token_ids("foo")
return
Expand Down
1 change: 1 addition & 0 deletions libs/partners/openai/tests/unit_tests/test_token_counts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
_CHAT_MODELS = ["gpt-4", "gpt-4-32k", "gpt-3.5-turbo"]


@pytest.mark.xfail(reason="Old models require different tiktoken cached file")
@pytest.mark.parametrize("model", _MODELS)
def test_openai_get_num_tokens(model: str) -> None:
"""Test get_tokens."""
Expand Down