Files
agent-desktop/tests/unit/providers/test_openai_stream_toolcall_compat.py
T

181 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import annotations
import json
from datetime import datetime
from types import SimpleNamespace
from typing import Any
from agentscope.credential import OpenAICredential
from pineagents.providers.openai_chat_model_compat import (
OpenAIChatModelCompat,
_sanitize_tool_call,
)
class CompatHarnessOpenAIChatModel(OpenAIChatModelCompat):
async def parse_stream_for_test(
self,
start_datetime: datetime,
stream: Any,
) -> list[Any]:
responses = []
async for response in self._parse_stream_response(
start_datetime,
stream,
):
responses.append(response)
return responses
class FakeAsyncStream:
def __init__(self, items: list[Any]):
self._items = items
self._iter = None
async def __aenter__(self) -> "FakeAsyncStream":
self._iter = iter(self._items)
return self
async def __aexit__(self, exc_type, exc, tb) -> bool:
return False
def __aiter__(self) -> "FakeAsyncStream":
return self
async def __anext__(self) -> Any:
assert self._iter is not None
try:
return next(self._iter)
except StopIteration as exc:
raise StopAsyncIteration from exc
def _make_chunk(tool_calls: list[Any]) -> Any:
delta = SimpleNamespace(
reasoning_content=None,
content=None,
tool_calls=tool_calls,
)
choice = SimpleNamespace(delta=delta)
return SimpleNamespace(usage=None, choices=[choice])
async def test_stream_parser_skips_tool_call_without_function() -> None:
model = CompatHarnessOpenAIChatModel(
credential=OpenAICredential(
api_key="sk-test",
base_url="https://api.openai.com/v1",
),
model="dummy",
stream=True,
)
malformed_tool_call = SimpleNamespace(
index=0,
id="call_bad",
function=None,
)
none_arguments_tool_call = SimpleNamespace(
index=1,
id="call_partial",
function=SimpleNamespace(name="ping", arguments=None),
)
valid_tool_call = SimpleNamespace(
index=0,
id="call_ok",
function=SimpleNamespace(name="ping", arguments='{"x":1}'),
)
stream = FakeAsyncStream(
[
_make_chunk([malformed_tool_call]),
_make_chunk([none_arguments_tool_call]),
_make_chunk([valid_tool_call]),
],
)
responses = await model.parse_stream_for_test(
datetime.now(),
stream,
)
assert responses
tool_blocks = [
block
for response in responses
for block in response.content
if getattr(block, "type", None) in ("tool_use", "tool_call")
]
assert tool_blocks
last = tool_blocks[-1]
assert getattr(last, "name", None) == "ping"
block_input = getattr(last, "input", None)
if isinstance(block_input, str):
block_input = json.loads(block_input)
assert block_input == {"x": 1}
def test_sanitize_tool_call_normalizes_non_string_arguments() -> None:
none_arguments_tool_call = SimpleNamespace(
index=0,
id="call_partial",
function=SimpleNamespace(name="ping", arguments=None),
)
non_string_arguments_tool_call = SimpleNamespace(
index=1,
id="call_dict",
function=SimpleNamespace(name="ping", arguments={"x": 2}),
)
missing_arguments_tool_call = SimpleNamespace(
index=2,
id="call_missing_args",
function=SimpleNamespace(name="ping"),
)
missing_name_tool_call = SimpleNamespace(
index=3,
id="call_missing_name",
function=SimpleNamespace(arguments={"x": 3}),
)
missing_name_and_arguments_tool_call = SimpleNamespace(
index=4,
id="call_missing_both",
function=SimpleNamespace(),
)
sanitized_none_arguments = _sanitize_tool_call(none_arguments_tool_call)
assert sanitized_none_arguments is not None
assert sanitized_none_arguments.function.name == "ping"
assert sanitized_none_arguments.function.arguments == ""
sanitized_non_string_arguments = _sanitize_tool_call(
non_string_arguments_tool_call,
)
assert sanitized_non_string_arguments is not None
assert sanitized_non_string_arguments.function.name == "ping"
assert isinstance(sanitized_non_string_arguments.function.arguments, str)
assert json.loads(sanitized_non_string_arguments.function.arguments) == {
"x": 2,
}
sanitized_missing_arguments = _sanitize_tool_call(
missing_arguments_tool_call,
)
assert sanitized_missing_arguments is not None
assert sanitized_missing_arguments.function.name == "ping"
assert sanitized_missing_arguments.function.arguments == ""
sanitized_missing_name = _sanitize_tool_call(missing_name_tool_call)
assert sanitized_missing_name is not None
assert sanitized_missing_name.function.name == ""
assert isinstance(sanitized_missing_name.function.arguments, str)
assert json.loads(sanitized_missing_name.function.arguments) == {"x": 3}
sanitized_missing_name_and_arguments = _sanitize_tool_call(
missing_name_and_arguments_tool_call,
)
assert sanitized_missing_name_and_arguments is not None
assert sanitized_missing_name_and_arguments.function.name == ""
assert sanitized_missing_name_and_arguments.function.arguments == ""