mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: OpenAI responses client (#239)
* Responses client WIP * add responses class * fix typing errors * move test * streaming responses, structured outputs * tests * Update python/packages/main/tests/openai/test_openai_responses_client.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * pr comments * fix override import * fix mypy * add missing function override * PR comments * add docstrings --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
233c557173
commit
3ee9dddfa2
@@ -78,6 +78,7 @@ KNOWN_MEDIA_TYPES = [
|
||||
__all__ = [
|
||||
"AIContent",
|
||||
"AIContents",
|
||||
"AITool",
|
||||
"AgentRunResponse",
|
||||
"AgentRunResponseUpdate",
|
||||
"ChatFinishReason",
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
|
||||
from ._chat_client import * # noqa: F403
|
||||
from ._exceptions import * # noqa: F403
|
||||
from ._responses_client import * # noqa: F403
|
||||
from ._shared import * # noqa: F403
|
||||
|
||||
@@ -4,7 +4,7 @@ import json
|
||||
from collections.abc import AsyncIterable, Mapping, MutableSequence, Sequence
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from typing import Any, ClassVar, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from openai import AsyncOpenAI, AsyncStream
|
||||
from openai.types import CompletionUsage
|
||||
@@ -34,16 +34,16 @@ from ._shared import OpenAIConfigBase, OpenAIHandler, OpenAIModelTypes, OpenAISe
|
||||
__all__ = ["OpenAIChatClient"]
|
||||
|
||||
|
||||
# region OpenAIChatClientBase
|
||||
|
||||
|
||||
# Implements agent_framework.ChatClient protocol, through ChatClientBase
|
||||
@use_tool_calling
|
||||
class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
|
||||
"""OpenAI Chat completion class."""
|
||||
|
||||
MODEL_PROVIDER_NAME: ClassVar[str] = "openai"
|
||||
SUPPORTS_FUNCTION_CALLING: ClassVar[bool] = True
|
||||
|
||||
# region Overriding base class methods
|
||||
# most of the methods are overridden from the ChatCompletionClientBase class, otherwise it is mentioned
|
||||
# most of the methods are overridden from the ChatClientBase class, otherwise it is mentioned
|
||||
|
||||
async def _inner_get_response(
|
||||
self,
|
||||
@@ -63,7 +63,6 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
|
||||
self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices
|
||||
)
|
||||
|
||||
# @trace_streaming_chat_completion(MODEL_PROVIDER_NAME)
|
||||
async def _inner_get_streaming_response(
|
||||
self,
|
||||
*,
|
||||
@@ -79,6 +78,7 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
|
||||
if not isinstance(response, AsyncStream):
|
||||
raise ServiceInvalidResponseError("Expected an AsyncStream[ChatCompletionChunk] response.")
|
||||
async for chunk in response:
|
||||
assert isinstance(chunk, ChatCompletionChunk) # nosec # noqa: S101
|
||||
if len(chunk.choices) == 0 and chunk.usage is None:
|
||||
continue
|
||||
|
||||
@@ -269,6 +269,11 @@ class OpenAIChatClientBase(OpenAIHandler, ChatClientBase):
|
||||
return content.model_dump(exclude_none=True)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region OpenAIChatClient
|
||||
|
||||
|
||||
class OpenAIChatClient(OpenAIConfigBase, OpenAIChatClientBase):
|
||||
"""OpenAI Chat completion class."""
|
||||
|
||||
@@ -301,21 +306,13 @@ class OpenAIChatClient(OpenAIConfigBase, OpenAIChatClientBase):
|
||||
instruction_role (str | None): The role to use for 'instruction' messages, for example,
|
||||
"""
|
||||
try:
|
||||
if api_key:
|
||||
openai_settings = OpenAISettings(
|
||||
api_key=SecretStr(api_key),
|
||||
org_id=org_id,
|
||||
chat_model_id=ai_model_id,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
else:
|
||||
openai_settings = OpenAISettings(
|
||||
org_id=org_id,
|
||||
chat_model_id=ai_model_id,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
openai_settings = OpenAISettings(
|
||||
api_key=SecretStr(api_key) if api_key else None,
|
||||
org_id=org_id,
|
||||
chat_model_id=ai_model_id,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
except ValidationError as ex:
|
||||
raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex
|
||||
|
||||
@@ -345,3 +342,6 @@ class OpenAIChatClient(OpenAIConfigBase, OpenAIChatClientBase):
|
||||
ai_model_id=settings["ai_model_id"],
|
||||
default_headers=settings.get("default_headers"),
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -0,0 +1,558 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Callable, Mapping, MutableMapping, MutableSequence, Sequence
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
from typing import Any, Literal
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # type: ignore
|
||||
else:
|
||||
from typing_extensions import override # type: ignore[import]
|
||||
|
||||
from openai import AsyncOpenAI, AsyncStream
|
||||
from openai.types.responses.response import Response as OpenAIResponse
|
||||
from openai.types.responses.response_completed_event import ResponseCompletedEvent
|
||||
from openai.types.responses.response_content_part_added_event import ResponseContentPartAddedEvent
|
||||
from openai.types.responses.response_function_tool_call import ResponseFunctionToolCall
|
||||
from openai.types.responses.response_includable import ResponseIncludable
|
||||
from openai.types.responses.response_output_item import ResponseOutputItem
|
||||
from openai.types.responses.response_output_message import ResponseOutputMessage
|
||||
from openai.types.responses.response_output_refusal import ResponseOutputRefusal
|
||||
from openai.types.responses.response_output_text import ResponseOutputText
|
||||
from openai.types.responses.response_stream_event import ResponseStreamEvent as OpenAIResponseStreamEvent
|
||||
from openai.types.responses.response_text_delta_event import ResponseTextDeltaEvent
|
||||
from openai.types.responses.response_usage import ResponseUsage
|
||||
from pydantic import BaseModel, SecretStr, ValidationError
|
||||
|
||||
from .._clients import ChatClientBase, use_tool_calling
|
||||
from .._types import (
|
||||
AIContents,
|
||||
AITool,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
ChatRole,
|
||||
ChatToolMode,
|
||||
FunctionCallContent,
|
||||
FunctionResultContent,
|
||||
TextContent,
|
||||
UsageDetails,
|
||||
)
|
||||
from ..exceptions import ServiceInitializationError, ServiceInvalidResponseError
|
||||
from ._shared import OpenAIConfigBase, OpenAIHandler, OpenAIModelTypes, OpenAISettings
|
||||
|
||||
__all__ = ["OpenAIResponsesClient"]
|
||||
|
||||
# region ResponsesClient
|
||||
|
||||
|
||||
@use_tool_calling
|
||||
class OpenAIResponsesClient(OpenAIConfigBase, ChatClientBase, OpenAIHandler):
|
||||
"""OpenAI Responses client class."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ai_model_id: str | None = None,
|
||||
api_key: str | None = None,
|
||||
org_id: str | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncOpenAI | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
instruction_role: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize an OpenAIChatCompletion service.
|
||||
|
||||
Args:
|
||||
ai_model_id (str): OpenAI model name, see
|
||||
https://platform.openai.com/docs/models
|
||||
api_key (str | None): The optional API key to use. If provided will override,
|
||||
the env vars or .env file value.
|
||||
org_id (str | None): The optional org ID to use. If provided will override,
|
||||
the env vars or .env file value.
|
||||
default_headers: The default headers mapping of string keys to
|
||||
string values for HTTP requests. (Optional)
|
||||
async_client (Optional[AsyncOpenAI]): An existing client to use. (Optional)
|
||||
env_file_path (str | None): Use the environment settings file as a fallback
|
||||
to environment variables. (Optional)
|
||||
env_file_encoding (str | None): The encoding of the environment settings file. (Optional)
|
||||
instruction_role (str | None): The role to use for 'instruction' messages, for example,
|
||||
"""
|
||||
try:
|
||||
openai_settings = OpenAISettings(
|
||||
api_key=SecretStr(api_key) if api_key else None,
|
||||
org_id=org_id,
|
||||
responses_model_id=ai_model_id,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
except ValidationError as ex:
|
||||
raise ServiceInitializationError("Failed to create OpenAI settings.", ex) from ex
|
||||
|
||||
if not async_client and not openai_settings.api_key:
|
||||
raise ServiceInitializationError("The OpenAI API key is required.")
|
||||
if not openai_settings.responses_model_id:
|
||||
raise ServiceInitializationError("The OpenAI model ID is required.")
|
||||
|
||||
super().__init__(
|
||||
ai_model_id=openai_settings.responses_model_id,
|
||||
api_key=openai_settings.api_key.get_secret_value() if openai_settings.api_key else None,
|
||||
org_id=openai_settings.org_id,
|
||||
ai_model_type=OpenAIModelTypes.RESPONSE,
|
||||
default_headers=default_headers,
|
||||
client=async_client,
|
||||
instruction_role=instruction_role,
|
||||
)
|
||||
|
||||
def _filter_options(self, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Filter options for the responses call."""
|
||||
# The responses call does not support all the options that the chat completion call does.
|
||||
# We filter out the unsupported options.
|
||||
return {key: value for key, value in kwargs.items() if value is not None}
|
||||
|
||||
# The responses create call takes very different parameters than the chat completion call,
|
||||
# so we override the get_response method to handle the specific parameters for responses.
|
||||
@override
|
||||
async def get_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
*,
|
||||
# TODO(peterychang): enable this option. background: bool | None = None,
|
||||
include: list[ResponseIncludable] | None = None,
|
||||
instruction: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
parallel_tool_calls: bool | None = None,
|
||||
model: str | None = None,
|
||||
previous_response_id: str | None = None,
|
||||
reasoning: dict[str, str] | None = None,
|
||||
service_tier: str | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
seed: int | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ChatToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: AITool
|
||||
| list[AITool]
|
||||
| Callable[..., Any]
|
||||
| list[Callable[..., Any]]
|
||||
| MutableMapping[str, Any]
|
||||
| list[MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
truncation: str | None = None,
|
||||
timeout: float | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
"""Get a response from the OpenAI API.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
include: additional output data to include in the model response.
|
||||
instruction: a system (or developer) message inserted into the model's context.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
parallel_tool_calls: Whether to enable parallel tool calls.
|
||||
model: The model to use for the agent.
|
||||
previous_response_id: The ID of the previous response.
|
||||
reasoning: The reasoning to use for the response.
|
||||
service_tier: The service tier to use for the response.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use for the response.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
truncation: the truncation strategy to use.
|
||||
timeout: the timeout for the request.
|
||||
additional_properties: additional properties to include in the request.
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
|
||||
Returns:
|
||||
A chat response from the model.
|
||||
"""
|
||||
filtered_options = self._filter_options(
|
||||
background=False,
|
||||
include=include,
|
||||
instruction=instruction,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
previous_response_id=previous_response_id,
|
||||
reasoning=reasoning,
|
||||
service_tier=service_tier,
|
||||
truncation=truncation,
|
||||
timeout=timeout,
|
||||
)
|
||||
filtered_options.update(additional_properties or {})
|
||||
chat_options = ChatOptions(
|
||||
ai_model_id=model,
|
||||
max_tokens=max_tokens,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools, # type: ignore
|
||||
user=user,
|
||||
additional_properties=filtered_options,
|
||||
)
|
||||
return await super().get_response(
|
||||
messages=messages,
|
||||
chat_options=chat_options,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@override
|
||||
async def get_streaming_response(
|
||||
self,
|
||||
messages: str | ChatMessage | list[str] | list[ChatMessage],
|
||||
*,
|
||||
# TODO(peterychang): enable this option. background: bool | None = None,
|
||||
include: list[ResponseIncludable] | None = None,
|
||||
instruction: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
parallel_tool_calls: bool | None = None,
|
||||
model: str | None = None,
|
||||
previous_response_id: str | None = None,
|
||||
reasoning: dict[str, str] | None = None,
|
||||
service_tier: str | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
seed: int | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ChatToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: AITool
|
||||
| list[AITool]
|
||||
| Callable[..., Any]
|
||||
| list[Callable[..., Any]]
|
||||
| MutableMapping[str, Any]
|
||||
| list[MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
top_p: float | None = None,
|
||||
user: str | None = None,
|
||||
truncation: str | None = None,
|
||||
timeout: float | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
"""Get a streaming response from the OpenAI API.
|
||||
|
||||
Args:
|
||||
messages: the message or messages to send to the model
|
||||
include: additional output data to include in the model response.
|
||||
instruction: a system (or developer) message inserted into the model's context.
|
||||
max_tokens: The maximum number of tokens to generate.
|
||||
parallel_tool_calls: Whether to enable parallel tool calls.
|
||||
model: The model to use for the agent.
|
||||
previous_response_id: The ID of the previous response.
|
||||
reasoning: The reasoning to use for the response.
|
||||
service_tier: The service tier to use for the response.
|
||||
response_format: The format of the response.
|
||||
seed: The random seed to use for the response.
|
||||
store: whether to store the response.
|
||||
temperature: the sampling temperature to use.
|
||||
tool_choice: the tool choice for the request.
|
||||
tools: the tools to use for the request.
|
||||
top_p: the nucleus sampling probability to use.
|
||||
user: the user to associate with the request.
|
||||
truncation: the truncation strategy to use.
|
||||
timeout: the timeout for the request.
|
||||
additional_properties: additional properties to include in the request.
|
||||
kwargs: any additional keyword arguments,
|
||||
will only be passed to functions that are called.
|
||||
|
||||
Returns:
|
||||
A stream representing the response(s) from the LLM.
|
||||
"""
|
||||
filtered_options = self._filter_options(
|
||||
background=False,
|
||||
include=include,
|
||||
instruction=instruction,
|
||||
parallel_tool_calls=parallel_tool_calls,
|
||||
previous_response_id=previous_response_id,
|
||||
reasoning=reasoning,
|
||||
service_tier=service_tier,
|
||||
truncation=truncation,
|
||||
timeout=timeout,
|
||||
)
|
||||
filtered_options.update(additional_properties or {})
|
||||
chat_options = ChatOptions(
|
||||
ai_model_id=model,
|
||||
max_tokens=max_tokens,
|
||||
response_format=response_format,
|
||||
seed=seed,
|
||||
store=store,
|
||||
temperature=temperature,
|
||||
top_p=top_p,
|
||||
tool_choice=tool_choice,
|
||||
tools=tools, # type: ignore
|
||||
user=user,
|
||||
additional_properties=filtered_options,
|
||||
)
|
||||
async for update in super().get_streaming_response(
|
||||
messages=messages,
|
||||
chat_options=chat_options,
|
||||
**kwargs,
|
||||
):
|
||||
yield update
|
||||
|
||||
def _chat_to_response_tool_spec(self, tools: list[AITool | MutableMapping[str, Any]]) -> list[dict[str, Any]]:
|
||||
response_tools: list[dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, AITool):
|
||||
# TODO(peterychang): Support AITools
|
||||
continue
|
||||
if "function" not in tool:
|
||||
response_tools.append(tool if isinstance(tool, dict) else dict(tool))
|
||||
parameters = {"additionalProperties": False}
|
||||
parameters.update(tool.get("function", {}).get("parameters", {}))
|
||||
response_tools.append({
|
||||
"type": "function",
|
||||
"name": tool.get("function", {}).get("name", ""),
|
||||
"strict": True,
|
||||
"description": tool.get("function", {}).get("description", None),
|
||||
"parameters": parameters,
|
||||
})
|
||||
return response_tools
|
||||
|
||||
async def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
chat_options.additional_properties["stream"] = False
|
||||
if not chat_options.ai_model_id:
|
||||
chat_options.ai_model_id = self.ai_model_id
|
||||
if chat_options.tools:
|
||||
chat_options.additional_properties.update({
|
||||
"response_tools": self._chat_to_response_tool_spec(chat_options.tools)
|
||||
})
|
||||
response = await self._send_request(chat_options, messages=self._prepare_chat_history_for_request(messages))
|
||||
assert isinstance(response, OpenAIResponse) # nosec # noqa: S101
|
||||
return next(self._create_response_content(response, item) for item in response.output)
|
||||
|
||||
async def _inner_get_streaming_response(
|
||||
self,
|
||||
*,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
chat_options.additional_properties["stream"] = True
|
||||
chat_options.ai_model_id = chat_options.ai_model_id or self.ai_model_id
|
||||
|
||||
if chat_options.tools:
|
||||
chat_options.additional_properties.update({
|
||||
"response_tools": self._chat_to_response_tool_spec(chat_options.tools)
|
||||
})
|
||||
response = await self._send_request(chat_options, messages=self._prepare_chat_history_for_request(messages))
|
||||
if not isinstance(response, AsyncStream):
|
||||
raise ServiceInvalidResponseError("Expected an AsyncStream[ResponseStreamEvent] response.")
|
||||
async for chunk in response:
|
||||
update = self._create_streaming_response_content(chunk) # type: ignore
|
||||
if not update:
|
||||
continue
|
||||
yield update
|
||||
|
||||
def _create_response_content(self, response: OpenAIResponse, item: ResponseOutputItem) -> "ChatResponse":
|
||||
"""Create a chat message content object from a choice."""
|
||||
items: MutableSequence[ChatMessage] = []
|
||||
metadata: dict[str, Any] = response.metadata or {}
|
||||
# TODO(peterychang): Add support for other content types
|
||||
if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_response(response)]:
|
||||
items.append(ChatMessage(role="assistant", contents=parsed_tool_calls))
|
||||
if isinstance(item, ResponseOutputMessage):
|
||||
for content in item.content:
|
||||
# TODO(peterychang): Add annotations when available
|
||||
if isinstance(content, ResponseOutputText):
|
||||
items.append(ChatMessage(role=item.role, text=content.text))
|
||||
metadata.update(self._get_metadata_from_response(content))
|
||||
elif isinstance(content, ResponseOutputRefusal):
|
||||
items.append(ChatMessage(role=item.role, text=content.refusal))
|
||||
return ChatResponse(
|
||||
response_id=response.id,
|
||||
created_at=datetime.fromtimestamp(response.created_at).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
usage_details=self._usage_details_from_openai(response.usage) if response.usage else None,
|
||||
messages=items,
|
||||
model_id=self.ai_model_id,
|
||||
additional_properties=metadata,
|
||||
raw_representation=response,
|
||||
)
|
||||
|
||||
def _create_streaming_response_content(
|
||||
self,
|
||||
event: OpenAIResponseStreamEvent,
|
||||
) -> ChatResponseUpdate | None:
|
||||
"""Create a streaming chat message content object from a choice."""
|
||||
metadata: dict[str, Any] = {}
|
||||
items: list[AIContents] = []
|
||||
# TODO(peterychang): Add support for other content types
|
||||
if isinstance(event, ResponseContentPartAddedEvent):
|
||||
if isinstance(event.part, ResponseOutputText):
|
||||
items.append(TextContent(text=event.part.text))
|
||||
metadata.update(self._get_metadata_from_response(event.part))
|
||||
elif isinstance(event.part, ResponseOutputRefusal):
|
||||
items.append(TextContent(text=event.part.refusal))
|
||||
elif isinstance(event, ResponseTextDeltaEvent):
|
||||
items.append(TextContent(text=event.delta))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
elif isinstance(event, ResponseCompletedEvent):
|
||||
# Tool calls are available in the completed event
|
||||
if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_response(event.response)]:
|
||||
items.extend(parsed_tool_calls)
|
||||
else:
|
||||
return None
|
||||
return ChatResponseUpdate(
|
||||
contents=items,
|
||||
role=ChatRole.ASSISTANT,
|
||||
ai_model_id=self.ai_model_id,
|
||||
additional_properties=metadata,
|
||||
raw_representation=event,
|
||||
)
|
||||
|
||||
def _get_tool_calls_from_response(self, response: OpenAIResponse) -> list[AIContents]:
|
||||
resp: list[AIContents] = []
|
||||
# TODO(peterychang): Support the other tool calls
|
||||
for item in (i for i in response.output if isinstance(i, ResponseFunctionToolCall)):
|
||||
fcc = FunctionCallContent(
|
||||
call_id=item.id if item.id else "",
|
||||
name=item.name,
|
||||
arguments=item.arguments,
|
||||
additional_properties={"call_id": item.call_id},
|
||||
)
|
||||
resp.append(fcc)
|
||||
|
||||
return resp
|
||||
|
||||
def _usage_details_from_openai(self, usage: ResponseUsage) -> UsageDetails | None:
|
||||
return UsageDetails(
|
||||
prompt_tokens=usage.input_tokens,
|
||||
completion_tokens=usage.output_tokens,
|
||||
total_tokens=usage.total_tokens,
|
||||
)
|
||||
|
||||
def _openai_chat_message_parser(
|
||||
self,
|
||||
message: ChatMessage,
|
||||
tool_id_to_call_id: dict[str, str],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Parse a chat message into the openai format."""
|
||||
all_messages: list[dict[str, Any]] = []
|
||||
args: dict[str, Any] = {
|
||||
"role": message.role.value if isinstance(message.role, ChatRole) else message.role,
|
||||
}
|
||||
if message.additional_properties:
|
||||
args["metadata"] = message.additional_properties
|
||||
for content in message.contents:
|
||||
match content:
|
||||
case FunctionResultContent():
|
||||
new_args: dict[str, Any] = {}
|
||||
new_args.update(self._openai_content_parser(content, tool_id_to_call_id))
|
||||
all_messages.append(new_args)
|
||||
case FunctionCallContent():
|
||||
function_call = self._openai_content_parser(content, tool_id_to_call_id)
|
||||
all_messages.append(function_call) # type: ignore
|
||||
case _:
|
||||
if "content" not in args:
|
||||
args["content"] = []
|
||||
args["content"].append(self._openai_content_parser(content, tool_id_to_call_id)) # type: ignore
|
||||
if "content" in args or "tool_calls" in args:
|
||||
all_messages.append(args)
|
||||
return all_messages
|
||||
|
||||
def _openai_content_parser(
|
||||
self,
|
||||
content: AIContents,
|
||||
tool_id_to_call_id: dict[str, str],
|
||||
) -> dict[str, Any]:
|
||||
"""Parse contents into the openai format."""
|
||||
match content:
|
||||
case FunctionCallContent():
|
||||
return {
|
||||
"id": content.call_id,
|
||||
"call_id": tool_id_to_call_id[content.call_id],
|
||||
"type": "function_call",
|
||||
"name": content.name,
|
||||
"arguments": content.arguments,
|
||||
}
|
||||
case FunctionResultContent():
|
||||
# call_id for the result needs to be the same as the call_id for the function call
|
||||
return {
|
||||
"call_id": tool_id_to_call_id[content.call_id],
|
||||
"type": "function_call_output",
|
||||
"output": content.result,
|
||||
}
|
||||
case TextContent():
|
||||
return {
|
||||
"type": "input_text",
|
||||
"text": content.text,
|
||||
}
|
||||
# TODO(peterychang): We'll probably need to specialize the other content types as well
|
||||
case _:
|
||||
return content.model_dump(exclude_none=True)
|
||||
|
||||
def _prepare_chat_history_for_request(
|
||||
self,
|
||||
chat_messages: Sequence[ChatMessage],
|
||||
role_key: str = "role",
|
||||
content_key: str = "content",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Prepare the chat history for a request.
|
||||
|
||||
Allowing customization of the key names for role/author, and optionally overriding the role.
|
||||
|
||||
ChatRole.TOOL messages need to be formatted different than system/user/assistant messages:
|
||||
They require a "tool_call_id" and (function) "name" key, and the "metadata" key should
|
||||
be removed. The "encoding" key should also be removed.
|
||||
|
||||
Override this method to customize the formatting of the chat history for a request.
|
||||
|
||||
Args:
|
||||
chat_messages: The chat history to prepare.
|
||||
role_key: The key name for the role/author.
|
||||
content_key: The key name for the content/message.
|
||||
|
||||
Returns:
|
||||
prepared_chat_history (Any): The prepared chat history for a request.
|
||||
"""
|
||||
tool_id_to_call_id: dict[str, str] = {}
|
||||
for message in chat_messages:
|
||||
for content in message.contents:
|
||||
if isinstance(content, FunctionCallContent):
|
||||
assert content.additional_properties and "call_id" in content.additional_properties # nosec # noqa: S101
|
||||
call_id = content.additional_properties["call_id"]
|
||||
tool_id_to_call_id[content.call_id] = call_id
|
||||
list_of_list = [self._openai_chat_message_parser(message, tool_id_to_call_id) for message in chat_messages]
|
||||
# Flatten the list of lists into a single list
|
||||
return list(chain.from_iterable(list_of_list))
|
||||
|
||||
def _get_metadata_from_response(self, output: Any) -> dict[str, Any]:
|
||||
"""Get metadata from a chat choice."""
|
||||
return {
|
||||
"logprobs": getattr(output, "logprobs", None),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, settings: dict[str, Any]) -> "OpenAIResponsesClient":
|
||||
"""Initialize an Open AI service from a dictionary of settings.
|
||||
|
||||
Args:
|
||||
settings: A dictionary of settings for the service.
|
||||
"""
|
||||
return OpenAIResponsesClient(
|
||||
ai_model_id=settings["ai_model_id"],
|
||||
default_headers=settings.get("default_headers"),
|
||||
api_key=settings.get("api_key"),
|
||||
org_id=settings.get("org_id"),
|
||||
)
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -18,6 +18,8 @@ from openai.types import Completion
|
||||
from openai.types.audio import Transcription
|
||||
from openai.types.chat import ChatCompletion, ChatCompletionChunk
|
||||
from openai.types.images_response import ImagesResponse
|
||||
from openai.types.responses.response import Response
|
||||
from openai.types.responses.response_stream_event import ResponseStreamEvent
|
||||
from pydantic import BaseModel, ConfigDict, Field, SecretStr, validate_call
|
||||
from pydantic.types import StringConstraints
|
||||
|
||||
@@ -38,6 +40,8 @@ RESPONSE_TYPE = Union[
|
||||
AsyncStream[Completion],
|
||||
list[Any],
|
||||
ImagesResponse,
|
||||
Response,
|
||||
AsyncStream[ResponseStreamEvent],
|
||||
Transcription,
|
||||
_legacy_response.HttpxBinaryResponseContent,
|
||||
]
|
||||
@@ -138,6 +142,9 @@ class OpenAIHandler(AFBaseModel, ABC):
|
||||
if self.ai_model_type == OpenAIModelTypes.TEXT_TO_SPEECH:
|
||||
assert isinstance(options, TextToSpeechOptions) # nosec # noqa: S101
|
||||
return await self._send_text_to_audio_request(options)
|
||||
if self.ai_model_type == OpenAIModelTypes.RESPONSE:
|
||||
assert isinstance(options, ChatOptions) # nosec # noqa: S101
|
||||
return await self._send_response_request(options, messages)
|
||||
|
||||
raise NotImplementedError(f"Model type {self.ai_model_type} is not supported")
|
||||
|
||||
@@ -208,6 +215,46 @@ class OpenAIHandler(AFBaseModel, ABC):
|
||||
ex,
|
||||
) from ex
|
||||
|
||||
async def _send_response_request(
|
||||
self,
|
||||
chat_options: "ChatOptions",
|
||||
messages: list[dict[str, Any]] | None = None,
|
||||
) -> Response | AsyncStream[ResponseStreamEvent]:
|
||||
try:
|
||||
options_dict = chat_options.to_provider_settings()
|
||||
if messages and "input" not in options_dict:
|
||||
options_dict["input"] = messages
|
||||
if "input" not in options_dict:
|
||||
raise ServiceInvalidRequestError("Messages are required for chat completions")
|
||||
if chat_options.tools is None:
|
||||
options_dict.pop("parallel_tool_calls", None)
|
||||
else:
|
||||
options_dict["tools"] = options_dict["response_tools"]
|
||||
options_dict.pop("response_tools", None)
|
||||
if chat_options.response_format:
|
||||
# create call does not support response_format, so we need to handle it via parse call
|
||||
resp_format = options_dict.pop("response_format", None)
|
||||
return await self.client.responses.parse(
|
||||
**options_dict,
|
||||
text_format=resp_format,
|
||||
)
|
||||
return await self.client.responses.create(**options_dict) # type: ignore
|
||||
except BadRequestError as ex:
|
||||
if ex.code == "content_filter":
|
||||
raise OpenAIContentFilterException(
|
||||
f"{type(self)} service encountered a content error",
|
||||
ex,
|
||||
) from ex
|
||||
raise ServiceResponseException(
|
||||
f"{type(self)} service failed to complete the prompt",
|
||||
ex,
|
||||
) from ex
|
||||
except Exception as ex:
|
||||
raise ServiceResponseException(
|
||||
f"{type(self)} service failed to complete the prompt",
|
||||
ex,
|
||||
) from ex
|
||||
|
||||
def _handle_structured_outputs(self, chat_options: "ChatOptions", options_dict: dict[str, Any]) -> None:
|
||||
if (
|
||||
chat_options.response_format
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
from typing import Annotated
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agent_framework import ChatClient, ChatMessage, ChatResponse, ChatResponseUpdate, TextContent, ai_function
|
||||
from agent_framework.exceptions import ServiceInitializationError, ServiceResponseException
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
skip_if_openai_integration_tests_disabled = pytest.mark.skipif(
|
||||
os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true"
|
||||
or os.getenv("OPENAI_API_KEY", "") in ("", "test-dummy-key"),
|
||||
reason="No real OPENAI_API_KEY provided; skipping integration tests."
|
||||
if os.getenv("RUN_INTEGRATION_TESTS", "false").lower() == "true"
|
||||
else "Integration tests are disabled.",
|
||||
)
|
||||
|
||||
|
||||
class OutputStruct(BaseModel):
|
||||
"""A structured output for testing purposes."""
|
||||
|
||||
location: str
|
||||
weather: str
|
||||
|
||||
|
||||
@ai_function
|
||||
async def get_weather(location: Annotated[str, "The location as a city name"]) -> str:
|
||||
"""Get the current weather in a given location."""
|
||||
# Implementation of the tool to get weather
|
||||
return f"The current weather in {location} is sunny."
|
||||
|
||||
|
||||
def test_init(openai_unit_test_env: dict[str, str]) -> None:
|
||||
# Test successful initialization
|
||||
openai_responses_client = OpenAIResponsesClient()
|
||||
|
||||
assert openai_responses_client.ai_model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
|
||||
assert isinstance(openai_responses_client, ChatClient)
|
||||
|
||||
|
||||
def test_init_validation_fail() -> None:
|
||||
# Test successful initialization
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIResponsesClient(api_key="34523", ai_model_id={"test": "dict"}) # type: ignore
|
||||
|
||||
|
||||
def test_init_ai_model_id_constructor(openai_unit_test_env: dict[str, str]) -> None:
|
||||
# Test successful initialization
|
||||
ai_model_id = "test_model_id"
|
||||
openai_responses_client = OpenAIResponsesClient(ai_model_id=ai_model_id)
|
||||
|
||||
assert openai_responses_client.ai_model_id == ai_model_id
|
||||
assert isinstance(openai_responses_client, ChatClient)
|
||||
|
||||
|
||||
def test_init_with_default_header(openai_unit_test_env: dict[str, str]) -> None:
|
||||
default_headers = {"X-Unit-Test": "test-guid"}
|
||||
|
||||
# Test successful initialization
|
||||
openai_responses_client = OpenAIResponsesClient(
|
||||
default_headers=default_headers,
|
||||
)
|
||||
|
||||
assert openai_responses_client.ai_model_id == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
|
||||
assert isinstance(openai_responses_client, ChatClient)
|
||||
|
||||
# Assert that the default header we added is present in the client's default headers
|
||||
for key, value in default_headers.items():
|
||||
assert key in openai_responses_client.client.default_headers
|
||||
assert openai_responses_client.client.default_headers[key] == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exclude_list", [["OPENAI_RESPONSES_MODEL_ID"]], indirect=True)
|
||||
def test_init_with_empty_model_id(openai_unit_test_env: dict[str, str]) -> None:
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIResponsesClient(
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exclude_list", [["OPENAI_API_KEY"]], indirect=True)
|
||||
def test_init_with_empty_api_key(openai_unit_test_env: dict[str, str]) -> None:
|
||||
ai_model_id = "test_model_id"
|
||||
|
||||
with pytest.raises(ServiceInitializationError):
|
||||
OpenAIResponsesClient(
|
||||
ai_model_id=ai_model_id,
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
def test_serialize(openai_unit_test_env: dict[str, str]) -> None:
|
||||
default_headers = {"X-Unit-Test": "test-guid"}
|
||||
|
||||
settings = {
|
||||
"ai_model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"],
|
||||
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
|
||||
"default_headers": default_headers,
|
||||
}
|
||||
|
||||
openai_responses_client = OpenAIResponsesClient.from_dict(settings)
|
||||
dumped_settings = openai_responses_client.to_dict()
|
||||
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
|
||||
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
|
||||
# Assert that the default header we added is present in the dumped_settings default headers
|
||||
for key, value in default_headers.items():
|
||||
assert key in dumped_settings["default_headers"]
|
||||
assert dumped_settings["default_headers"][key] == value
|
||||
# Assert that the 'User-Agent' header is not present in the dumped_settings default headers
|
||||
assert "User-Agent" not in dumped_settings["default_headers"]
|
||||
|
||||
|
||||
def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None:
|
||||
settings = {
|
||||
"ai_model_id": openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"],
|
||||
"api_key": openai_unit_test_env["OPENAI_API_KEY"],
|
||||
"org_id": openai_unit_test_env["OPENAI_ORG_ID"],
|
||||
}
|
||||
|
||||
openai_responses_client = OpenAIResponsesClient.from_dict(settings)
|
||||
dumped_settings = openai_responses_client.to_dict()
|
||||
assert dumped_settings["ai_model_id"] == openai_unit_test_env["OPENAI_RESPONSES_MODEL_ID"]
|
||||
assert dumped_settings["api_key"] == openai_unit_test_env["OPENAI_API_KEY"]
|
||||
assert dumped_settings["org_id"] == openai_unit_test_env["OPENAI_ORG_ID"]
|
||||
# Assert that the 'User-Agent' header is not present in the dumped_settings default headers
|
||||
assert "User-Agent" not in dumped_settings["default_headers"]
|
||||
|
||||
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_response() -> None:
|
||||
"""Test OpenAI chat completion responses."""
|
||||
openai_responses_client = OpenAIResponsesClient(ai_model_id="gpt-4.1-mini")
|
||||
|
||||
assert isinstance(openai_responses_client, ChatClient)
|
||||
|
||||
messages: list[ChatMessage] = []
|
||||
messages.append(
|
||||
ChatMessage(
|
||||
role="user",
|
||||
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
|
||||
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
|
||||
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
|
||||
"of climate change.",
|
||||
)
|
||||
)
|
||||
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
|
||||
|
||||
# Test that the client can be used to get a response
|
||||
response = await openai_responses_client.get_response(messages=messages)
|
||||
|
||||
assert response is not None
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert "scientists" in response.text
|
||||
|
||||
messages.clear()
|
||||
messages.append(ChatMessage(role="user", text="The weather in New York is sunny"))
|
||||
messages.append(ChatMessage(role="user", text="What is the weather in New York?"))
|
||||
|
||||
# Test that the client can be used to get a response
|
||||
response = await openai_responses_client.get_response(
|
||||
messages=messages,
|
||||
response_format=OutputStruct,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert isinstance(response, ChatResponse)
|
||||
output = OutputStruct.model_validate_json(response.text)
|
||||
assert output.location == "New York"
|
||||
assert "sunny" in output.weather
|
||||
|
||||
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_response_tools() -> None:
|
||||
"""Test OpenAI chat completion responses."""
|
||||
openai_responses_client = OpenAIResponsesClient(ai_model_id="gpt-4o-mini")
|
||||
|
||||
assert isinstance(openai_responses_client, ChatClient)
|
||||
|
||||
messages: list[ChatMessage] = []
|
||||
messages.append(ChatMessage(role="user", text="What is the weather in New York?"))
|
||||
|
||||
# Test that the client can be used to get a response
|
||||
response = await openai_responses_client.get_response(
|
||||
messages=messages,
|
||||
tools=[get_weather],
|
||||
tool_choice="auto",
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert isinstance(response, ChatResponse)
|
||||
assert "sunny" in response.text
|
||||
|
||||
messages.clear()
|
||||
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
|
||||
|
||||
# Test that the client can be used to get a response
|
||||
response = await openai_responses_client.get_response(
|
||||
messages=messages,
|
||||
tools=[get_weather],
|
||||
tool_choice="auto",
|
||||
response_format=OutputStruct,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert isinstance(response, ChatResponse)
|
||||
output = OutputStruct.model_validate_json(response.text)
|
||||
assert "Seattle" in output.location
|
||||
assert "sunny" in output.weather
|
||||
|
||||
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_streaming() -> None:
|
||||
"""Test Azure OpenAI chat completion responses."""
|
||||
openai_responses_client = OpenAIResponsesClient(ai_model_id="gpt-4.1-mini")
|
||||
|
||||
assert isinstance(openai_responses_client, ChatClient)
|
||||
|
||||
messages: list[ChatMessage] = []
|
||||
messages.append(
|
||||
ChatMessage(
|
||||
role="user",
|
||||
text="Emily and David, two passionate scientists, met during a research expedition to Antarctica. "
|
||||
"Bonded by their love for the natural world and shared curiosity, they uncovered a "
|
||||
"groundbreaking phenomenon in glaciology that could potentially reshape our understanding "
|
||||
"of climate change.",
|
||||
)
|
||||
)
|
||||
messages.append(ChatMessage(role="user", text="who are Emily and David?"))
|
||||
|
||||
# Test that the client can be used to get a response
|
||||
response = openai_responses_client.get_streaming_response(messages=messages)
|
||||
|
||||
full_message: str = ""
|
||||
async for chunk in response:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert "scientists" in full_message
|
||||
|
||||
messages.clear()
|
||||
messages.append(ChatMessage(role="user", text="The weather in Seattle is sunny"))
|
||||
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
|
||||
|
||||
# This is currently broken. See https://github.com/openai/openai-python/issues/2305
|
||||
with pytest.raises(ServiceResponseException):
|
||||
response = openai_responses_client.get_streaming_response(
|
||||
messages=messages,
|
||||
response_format=OutputStruct,
|
||||
)
|
||||
full_message = ""
|
||||
async for chunk in response:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
full_message += content.text
|
||||
|
||||
output = OutputStruct.model_validate_json(full_message)
|
||||
assert "Seattle" in output.location
|
||||
assert "sunny" in output.weather
|
||||
|
||||
|
||||
@skip_if_openai_integration_tests_disabled
|
||||
async def test_openai_responses_client_streaming_tools() -> None:
|
||||
"""Test OpenAI chat completion responses."""
|
||||
openai_responses_client = OpenAIResponsesClient(ai_model_id="gpt-4o-mini")
|
||||
|
||||
assert isinstance(openai_responses_client, ChatClient)
|
||||
|
||||
messages: list[ChatMessage] = [ChatMessage(role="user", text="What is the weather in Seattle?")]
|
||||
|
||||
# Test that the client can be used to get a response
|
||||
response = openai_responses_client.get_streaming_response(
|
||||
messages=messages,
|
||||
tools=[get_weather],
|
||||
tool_choice="auto",
|
||||
)
|
||||
full_message: str = ""
|
||||
async for chunk in response:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
full_message += content.text
|
||||
|
||||
assert "sunny" in full_message
|
||||
|
||||
messages.clear()
|
||||
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
|
||||
|
||||
# This is currently broken. See https://github.com/openai/openai-python/issues/2305
|
||||
with pytest.raises(ServiceResponseException):
|
||||
response = openai_responses_client.get_streaming_response(
|
||||
messages=messages,
|
||||
tools=[get_weather],
|
||||
tool_choice="auto",
|
||||
response_format=OutputStruct,
|
||||
)
|
||||
full_message = ""
|
||||
async for chunk in response:
|
||||
assert chunk is not None
|
||||
assert isinstance(chunk, ChatResponseUpdate)
|
||||
for content in chunk.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
full_message += content.text
|
||||
|
||||
output = OutputStruct.model_validate_json(full_message)
|
||||
assert "Seattle" in output.location
|
||||
assert "sunny" in output.weather
|
||||
@@ -0,0 +1,36 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from pydantic import Field
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = OpenAIResponsesClient(ai_model_id="gpt-4o-mini")
|
||||
message = "What's the weather in Amsterdam and in Paris?"
|
||||
stream = False
|
||||
print(f"User: {message}")
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_streaming_response(message, tools=get_weather):
|
||||
if str(chunk):
|
||||
print(str(chunk), end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(message, tools=get_weather)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user