Python: [BREAKING] parameter naming and other fixes (#1255)

* parameter naming and other fixes

* fix test

* fix azure openai responses decorator ordering

* fix test

* fix mypy

* fixes in options handling

* fix tests

* final fixes

* exclude macos tests

* fix model param
This commit is contained in:
Eduard van Valkenburg
2025-10-07 23:18:48 +02:00
committed by GitHub
Unverified
parent 1a81ed202e
commit 76900f0eab
22 changed files with 327 additions and 378 deletions
@@ -351,22 +351,28 @@ class AzureAIAgentClient(BaseChatClient):
Returns:
str: The agent_id to use
"""
run_options = run_options or {}
# If no agent_id is provided, create a temporary agent
if self.agent_id is None:
if not self.model_id:
raise ServiceInitializationError("Model deployment name is required for agent creation.")
if "model" not in run_options or not run_options["model"]:
raise ServiceInitializationError(
"Model deployment name is required for agent creation, "
"can also be passed to the get_response methods."
)
agent_name: str = self.agent_name or "UnnamedAgent"
args: dict[str, Any] = {"model": self.model_id, "name": agent_name}
if run_options:
if "tools" in run_options:
args["tools"] = run_options["tools"]
if "tool_resources" in run_options:
args["tool_resources"] = run_options["tool_resources"]
if "instructions" in run_options:
args["instructions"] = run_options["instructions"]
if "response_format" in run_options:
args["response_format"] = run_options["response_format"]
args: dict[str, Any] = {
"model": run_options["model"],
"name": agent_name,
}
if "tools" in run_options:
args["tools"] = run_options["tools"]
if "tool_resources" in run_options:
args["tool_resources"] = run_options["tool_resources"]
if "instructions" in run_options:
args["instructions"] = run_options["instructions"]
if "response_format" in run_options:
args["response_format"] = run_options["response_format"]
created_agent = await self.project_client.agents.create_agent(**args)
self.agent_id = str(created_agent.id)
self._should_delete_agent = True
@@ -673,7 +679,10 @@ class AzureAIAgentClient(BaseChatClient):
if chat_options is not None:
run_options["max_completion_tokens"] = chat_options.max_tokens
run_options["model"] = chat_options.model_id
if chat_options.model_id is not None:
run_options["model"] = chat_options.model_id
else:
run_options["model"] = self.model_id
run_options["top_p"] = chat_options.top_p
run_options["temperature"] = chat_options.temperature
run_options["parallel_tool_calls"] = chat_options.allow_multiple_tool_calls
@@ -285,7 +285,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_create_new(
azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"])
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, azure_ai_settings=azure_ai_settings)
agent_id = await chat_client._get_agent_id_or_create() # type: ignore
agent_id = await chat_client._get_agent_id_or_create(run_options={"model": azure_ai_settings.model_deployment_name}) # type: ignore
assert agent_id == "test-agent-id"
assert chat_client._should_delete_agent # type: ignore
@@ -577,6 +577,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_run_options(
"tools": [{"type": "function", "function": {"name": "test_tool"}}],
"instructions": "Test instructions",
"response_format": {"type": "json_object"},
"model": azure_ai_settings.model_deployment_name,
}
agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore
@@ -1277,7 +1278,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_agent_name(
# Ensure agent_name is None to test the default
chat_client.agent_name = None # type: ignore
agent_id = await chat_client._get_agent_id_or_create() # type: ignore
agent_id = await chat_client._get_agent_id_or_create(run_options={"model": azure_ai_settings.model_deployment_name}) # type: ignore
assert agent_id == "test-agent-id"
# Verify create_agent was called with default "UnnamedAgent"
@@ -1294,7 +1295,7 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_response_format(
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, azure_ai_settings=azure_ai_settings)
# Test with response_format in run_options
run_options = {"response_format": {"type": "json_object"}}
run_options = {"response_format": {"type": "json_object"}, "model": azure_ai_settings.model_deployment_name}
agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore
@@ -1313,7 +1314,10 @@ async def test_azure_ai_chat_client_get_agent_id_or_create_with_tool_resources(
chat_client = create_test_azure_ai_chat_client(mock_ai_project_client, azure_ai_settings=azure_ai_settings)
# Test with tool_resources in run_options
run_options = {"tool_resources": {"vector_store_ids": ["vs-123"]}}
run_options = {
"tool_resources": {"vector_store_ids": ["vs-123"]},
"model": azure_ai_settings.model_deployment_name,
}
agent_id = await chat_client._get_agent_id_or_create(run_options) # type: ignore
+81 -63
View File
@@ -486,7 +486,7 @@ class ChatAgent(BaseAgent):
from agent_framework.clients import OpenAIChatClient
# Create a basic chat agent
client = OpenAIChatClient(model="gpt-4")
client = OpenAIChatClient(model_id="gpt-4")
agent = ChatAgent(chat_client=client, name="assistant", description="A helpful assistant")
# Run the agent with a simple message
@@ -514,6 +514,26 @@ class ChatAgent(BaseAgent):
# Use streaming responses
async for update in agent.run_stream("What's the weather in Paris?"):
print(update.text, end="")
With additional provider specific options:
.. code-block:: python
agent = ChatAgent(
chat_client=client,
name="reasoning-agent",
instructions="You are a reasoning assistant.",
model_id="gpt-5",
temperature=0.7,
max_tokens=500,
additional_chat_options={
"reasoning": {"effort": "high", "summary": "concise"}
}, # OpenAI Responses specific.
)
# Use streaming responses
async for update in agent.run_stream("How do you prove the pythagorean theorem?"):
print(update.text, end="")
"""
AGENT_SYSTEM_NAME: ClassVar[str] = "microsoft.agent_framework"
@@ -534,7 +554,7 @@ class ChatAgent(BaseAgent):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -549,7 +569,7 @@ class ChatAgent(BaseAgent):
| None = None,
top_p: float | None = None,
user: str | None = None,
request_kwargs: dict[str, Any] | None = None,
additional_chat_options: dict[str, Any] | None = None,
**kwargs: Any,
) -> None:
"""Initialize a ChatAgent instance.
@@ -578,7 +598,7 @@ class ChatAgent(BaseAgent):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -589,8 +609,9 @@ class ChatAgent(BaseAgent):
tools: The tools to use for the request.
top_p: The nucleus sampling probability to use.
user: The user to associate with the request.
request_kwargs: A dictionary of other values that will be passed through
additional_chat_options: A dictionary of other values that will be passed through
to the chat_client ``get_response`` and ``get_streaming_response`` methods.
This can be used to pass provider specific parameters.
kwargs: Any additional keyword arguments. Will be stored as ``additional_properties``.
Raises:
@@ -626,7 +647,7 @@ class ChatAgent(BaseAgent):
self._local_mcp_tools = [tool for tool in normalized_tools if isinstance(tool, MCPTool)]
agent_tools = [tool for tool in normalized_tools if not isinstance(tool, MCPTool)]
self.chat_options = ChatOptions(
model_id=model,
model_id=model_id,
conversation_id=conversation_id,
frequency_penalty=frequency_penalty,
instructions=instructions,
@@ -643,7 +664,7 @@ class ChatAgent(BaseAgent):
tools=agent_tools,
top_p=top_p,
user=user,
additional_properties=request_kwargs or {}, # type: ignore
additional_properties=additional_chat_options or {}, # type: ignore
)
self._async_exit_stack = AsyncExitStack()
self._update_agent_name()
@@ -701,7 +722,7 @@ class ChatAgent(BaseAgent):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -716,7 +737,7 @@ class ChatAgent(BaseAgent):
| None = None,
top_p: float | None = None,
user: str | None = None,
additional_properties: dict[str, Any] | None = None,
additional_chat_options: dict[str, Any] | None = None,
**kwargs: Any,
) -> AgentRunResponse:
"""Run the agent with the given messages and options.
@@ -736,7 +757,7 @@ class ChatAgent(BaseAgent):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -747,7 +768,8 @@ class ChatAgent(BaseAgent):
tools: The tools to use for the request.
top_p: The nucleus sampling probability to use.
user: The user to associate with the request.
additional_properties: Additional properties to include in the request.
additional_chat_options: Additional properties to include in the request.
Use this field for provider-specific parameters.
kwargs: Additional keyword arguments for the agent.
Will only be passed to functions that are called.
@@ -778,30 +800,27 @@ class ChatAgent(BaseAgent):
if not mcp_server.is_connected:
await self._async_exit_stack.enter_async_context(mcp_server)
final_tools.extend(mcp_server.functions)
response = await self.chat_client.get_response(
messages=thread_messages,
chat_options=run_chat_options
& ChatOptions(
model_id=model,
conversation_id=thread.service_thread_id,
frequency_penalty=frequency_penalty,
logit_bias=logit_bias,
max_tokens=max_tokens,
metadata=metadata,
presence_penalty=presence_penalty,
response_format=response_format,
seed=seed,
stop=stop,
store=store,
temperature=temperature,
tool_choice=tool_choice,
tools=final_tools,
top_p=top_p,
user=user,
additional_properties=additional_properties or {},
),
**kwargs,
co = run_chat_options & ChatOptions(
model_id=model_id,
conversation_id=thread.service_thread_id,
frequency_penalty=frequency_penalty,
logit_bias=logit_bias,
max_tokens=max_tokens,
metadata=metadata,
presence_penalty=presence_penalty,
response_format=response_format,
seed=seed,
stop=stop,
store=store,
temperature=temperature,
tool_choice=tool_choice,
tools=final_tools,
top_p=top_p,
user=user,
**(additional_chat_options or {}),
)
response = await self.chat_client.get_response(messages=thread_messages, chat_options=co, **kwargs)
await self._update_thread_with_type_and_conversation_id(thread, response.conversation_id)
@@ -832,7 +851,7 @@ class ChatAgent(BaseAgent):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -847,7 +866,7 @@ class ChatAgent(BaseAgent):
| None = None,
top_p: float | None = None,
user: str | None = None,
additional_properties: dict[str, Any] | None = None,
additional_chat_options: dict[str, Any] | None = None,
**kwargs: Any,
) -> AsyncIterable[AgentRunResponseUpdate]:
"""Stream the agent with the given messages and options.
@@ -867,7 +886,7 @@ class ChatAgent(BaseAgent):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -878,7 +897,8 @@ class ChatAgent(BaseAgent):
tools: The tools to use for the request.
top_p: The nucleus sampling probability to use.
user: The user to associate with the request.
additional_properties: Additional properties to include in the request.
additional_chat_options: Additional properties to include in the request.
Use this field for provider-specific parameters.
kwargs: Any additional keyword arguments.
Will only be passed to functions that are called.
@@ -890,8 +910,6 @@ class ChatAgent(BaseAgent):
thread=thread, input_messages=input_messages
)
agent_name = self._get_agent_name()
response_updates: list[ChatResponseUpdate] = []
# Resolve final tool list (runtime provided tools + local MCP server tools)
final_tools: list[ToolProtocol | MutableMapping[str, Any] | Callable[..., Any]] = []
normalized_tools: list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]] = ( # type: ignore[reportUnknownVariableType]
@@ -911,29 +929,29 @@ class ChatAgent(BaseAgent):
await self._async_exit_stack.enter_async_context(mcp_server)
final_tools.extend(mcp_server.functions)
co = run_chat_options & ChatOptions(
conversation_id=thread.service_thread_id,
frequency_penalty=frequency_penalty,
logit_bias=logit_bias,
max_tokens=max_tokens,
metadata=metadata,
model_id=model_id,
presence_penalty=presence_penalty,
response_format=response_format,
seed=seed,
stop=stop,
store=store,
temperature=temperature,
tool_choice=tool_choice,
tools=final_tools,
top_p=top_p,
user=user,
**(additional_chat_options or {}),
)
response_updates: list[ChatResponseUpdate] = []
async for update in self.chat_client.get_streaming_response(
messages=thread_messages,
chat_options=run_chat_options
& ChatOptions(
conversation_id=thread.service_thread_id,
frequency_penalty=frequency_penalty,
logit_bias=logit_bias,
max_tokens=max_tokens,
metadata=metadata,
model_id=model,
presence_penalty=presence_penalty,
response_format=response_format,
seed=seed,
stop=stop,
store=store,
temperature=temperature,
tool_choice=tool_choice,
tools=final_tools,
top_p=top_p,
user=user,
additional_properties=additional_properties or {},
),
**kwargs,
messages=thread_messages, chat_options=co, **kwargs
):
response_updates.append(update)
@@ -951,7 +969,7 @@ class ChatAgent(BaseAgent):
raw_representation=update,
)
response = ChatResponse.from_chat_response_updates(response_updates)
response = ChatResponse.from_chat_response_updates(response_updates, output_format_type=co.response_format)
await self._update_thread_with_type_and_conversation_id(thread, response.conversation_id)
await self._notify_thread_of_new_messages(thread, input_messages, response.messages)
@@ -101,7 +101,7 @@ class ChatClientProtocol(Protocol):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -129,7 +129,7 @@ class ChatClientProtocol(Protocol):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -160,7 +160,7 @@ class ChatClientProtocol(Protocol):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -188,7 +188,7 @@ class ChatClientProtocol(Protocol):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -240,7 +240,7 @@ def prepare_messages(messages: str | ChatMessage | list[str] | list[ChatMessage]
def merge_chat_options(
*,
base_chat_options: ChatOptions | Any | None,
model: str | None = None,
model_id: str | None = None,
frequency_penalty: float | None = None,
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
@@ -265,7 +265,7 @@ def merge_chat_options(
Keyword Args:
base_chat_options: Optional base ChatOptions to merge with direct parameters.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
frequency_penalty: The frequency penalty to use.
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
@@ -292,40 +292,11 @@ def merge_chat_options(
if base_chat_options is not None and not isinstance(base_chat_options, ChatOptions):
raise TypeError("chat_options must be an instance of ChatOptions")
if base_chat_options is not None:
# Combine tools from both sources
base_tools = base_chat_options.tools or []
combined_tools = [*base_tools, *(tools or [])] if tools else base_tools
if base_chat_options is None:
base_chat_options = ChatOptions()
# Create new chat_options, using direct parameters when provided, otherwise fall back to base
return ChatOptions(
model_id=model if model is not None else base_chat_options.model_id,
frequency_penalty=(
frequency_penalty if frequency_penalty is not None else base_chat_options.frequency_penalty
),
logit_bias=logit_bias if logit_bias is not None else base_chat_options.logit_bias,
max_tokens=max_tokens if max_tokens is not None else base_chat_options.max_tokens,
metadata=metadata if metadata is not None else base_chat_options.metadata,
presence_penalty=(presence_penalty if presence_penalty is not None else base_chat_options.presence_penalty),
response_format=(response_format if response_format is not None else base_chat_options.response_format),
seed=seed if seed is not None else base_chat_options.seed,
stop=stop if stop is not None else base_chat_options.stop,
store=store if store is not None else base_chat_options.store,
temperature=temperature if temperature is not None else base_chat_options.temperature,
top_p=top_p if top_p is not None else base_chat_options.top_p,
tool_choice=(
tool_choice if (tool_choice is not None and tool_choice != "auto") else base_chat_options.tool_choice # type: ignore[arg-type]
),
tools=combined_tools or None,
user=user if user is not None else base_chat_options.user,
additional_properties=(
additional_properties if additional_properties is not None else base_chat_options.additional_properties
),
conversation_id=base_chat_options.conversation_id,
)
# No base options, create from direct parameters only
return ChatOptions(
model_id=model,
return base_chat_options & ChatOptions(
model_id=model_id,
frequency_penalty=frequency_penalty,
logit_bias=logit_bias,
max_tokens=max_tokens,
@@ -340,7 +311,7 @@ def merge_chat_options(
tool_choice=tool_choice,
tools=tools,
user=user,
additional_properties=additional_properties or {},
additional_properties=additional_properties,
)
@@ -560,7 +531,7 @@ class BaseChatClient(SerializationMixin, ABC):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -592,7 +563,7 @@ class BaseChatClient(SerializationMixin, ABC):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -604,17 +575,18 @@ class BaseChatClient(SerializationMixin, ABC):
top_p: The nucleus sampling probability to use.
user: The user to associate with the request.
additional_properties: Additional properties to include in the request.
Can be used for provider-specific parameters.
kwargs: Any additional keyword arguments.
May include ``chat_options`` which provides base values that can be overridden by direct parameters.
Returns:
A chat response from the model.
A chat response from the model_id.
"""
# Normalize tools and merge with base chat_options
normalized_tools = await self._normalize_tools(tools)
chat_options = merge_chat_options(
base_chat_options=kwargs.pop("chat_options", None),
model=model,
model_id=model_id,
frequency_penalty=frequency_penalty,
logit_bias=logit_bias,
max_tokens=max_tokens,
@@ -654,7 +626,7 @@ class BaseChatClient(SerializationMixin, ABC):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -686,7 +658,7 @@ class BaseChatClient(SerializationMixin, ABC):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -698,6 +670,7 @@ class BaseChatClient(SerializationMixin, ABC):
top_p: The nucleus sampling probability to use.
user: The user to associate with the request.
additional_properties: Additional properties to include in the request.
Can be used for provider-specific parameters.
kwargs: Any additional keyword arguments.
May include ``chat_options`` which provides base values that can be overridden by direct parameters.
@@ -708,7 +681,7 @@ class BaseChatClient(SerializationMixin, ABC):
normalized_tools = await self._normalize_tools(tools)
chat_options = merge_chat_options(
base_chat_options=kwargs.pop("chat_options", None),
model=model,
model_id=model_id,
frequency_penalty=frequency_penalty,
logit_bias=logit_bias,
max_tokens=max_tokens,
@@ -787,7 +760,7 @@ class BaseChatClient(SerializationMixin, ABC):
logit_bias: dict[str | int, float] | None = None,
max_tokens: int | None = None,
metadata: dict[str, Any] | None = None,
model: str | None = None,
model_id: str | None = None,
presence_penalty: float | None = None,
response_format: type[BaseModel] | None = None,
seed: int | None = None,
@@ -802,7 +775,7 @@ class BaseChatClient(SerializationMixin, ABC):
| None = None,
top_p: float | None = None,
user: str | None = None,
request_kwargs: dict[str, Any] | None = None,
additional_chat_options: dict[str, Any] | None = None,
**kwargs: Any,
) -> "ChatAgent":
"""Create a ChatAgent with this client.
@@ -824,7 +797,7 @@ class BaseChatClient(SerializationMixin, ABC):
logit_bias: The logit bias to use.
max_tokens: The maximum number of tokens to generate.
metadata: Additional metadata to include in the request.
model: The model to use for the agent.
model_id: The model_id to use for the agent.
presence_penalty: The presence penalty to use.
response_format: The format of the response.
seed: The random seed to use.
@@ -835,8 +808,9 @@ class BaseChatClient(SerializationMixin, ABC):
tools: The tools to use for the request.
top_p: The nucleus sampling probability to use.
user: The user to associate with the request.
request_kwargs: A dictionary of other values that will be passed through
additional_chat_options: A dictionary of other values that will be passed through
to the chat_client ``get_response`` and ``get_streaming_response`` methods.
This can be used to pass provider specific parameters.
kwargs: Any additional keyword arguments. Will be stored as ``additional_properties``.
Returns:
@@ -848,7 +822,7 @@ class BaseChatClient(SerializationMixin, ABC):
from agent_framework.clients import OpenAIChatClient
# Create a client
client = OpenAIChatClient(model="gpt-4")
client = OpenAIChatClient(model_id="gpt-4")
# Create an agent using the convenience method
agent = client.create_agent(
@@ -873,7 +847,7 @@ class BaseChatClient(SerializationMixin, ABC):
logit_bias=logit_bias,
max_tokens=max_tokens,
metadata=metadata,
model=model,
model_id=model_id,
presence_penalty=presence_penalty,
response_format=response_format,
seed=seed,
@@ -884,6 +858,6 @@ class BaseChatClient(SerializationMixin, ABC):
tools=tools,
top_p=top_p,
user=user,
request_kwargs=request_kwargs,
additional_chat_options=additional_chat_options,
**kwargs,
)
@@ -221,7 +221,7 @@ class ChatContext(SerializationMixin):
async def process(self, context: ChatContext, next):
print(f"Chat client: {context.chat_client.__class__.__name__}")
print(f"Messages: {len(context.messages)}")
print(f"Model: {context.chat_options.model}")
print(f"Model: {context.chat_options.model_id}")
# Store metadata
context.metadata["input_tokens"] = self.count_tokens(context.messages)
@@ -163,7 +163,7 @@ class SerializationMixin:
combined_exclude.update(self.INJECTABLE)
# Get all instance attributes
result: dict[str, Any] = {"type": self._get_type_identifier()}
result: dict[str, Any] = {} if "type" in combined_exclude else {"type": self._get_type_identifier()}
for key, value in self.__dict__.items():
if key not in combined_exclude and not key.startswith("_"):
if exclude_none and value is None:
+37 -57
View File
@@ -13,14 +13,14 @@ from collections.abc import (
Sequence,
)
from copy import deepcopy
from typing import Any, ClassVar, Literal, TypeVar, overload
from typing import Any, ClassVar, Literal, TypeVar, cast, overload
from pydantic import BaseModel, ValidationError
from ._logging import get_logger
from ._serialization import SerializationMixin
from ._tools import ToolProtocol, ai_function
from .exceptions import AdditionItemMismatch
from .exceptions import AdditionItemMismatch, ContentError
if sys.version_info >= (3, 11):
from typing import Self # pragma: no cover
@@ -96,7 +96,10 @@ def _parse_content(content_data: MutableMapping[str, Any]) -> "Contents":
content_data: Content data (dict)
Returns:
Content object or raises ValidationError if parsing fails
Content object
Raises:
ContentError if parsing fails
"""
content_type = str(content_data.get("type"))
match content_type:
@@ -125,7 +128,7 @@ def _parse_content(content_data: MutableMapping[str, Any]) -> "Contents":
case "text_reasoning":
return TextReasoningContent.from_dict(content_data)
case _:
raise ValidationError([f"Unknown content type '{content_type}'"], model=Contents) # type: ignore
raise ContentError(f"Unknown content type '{content_type}'")
def _parse_content_list(contents_data: Sequence[Any]) -> list["Contents"]:
@@ -143,8 +146,8 @@ def _parse_content_list(contents_data: Sequence[Any]) -> list["Contents"]:
try:
content = _parse_content(content_data)
contents.append(content)
except ValidationError as ve:
logger.warning(f"Skipping unknown content type or invalid content: {ve}")
except ContentError as exc:
logger.warning(f"Skipping unknown content type or invalid content: {exc}")
else:
# If it's already a content object, keep it as is
contents.append(content_data)
@@ -2098,8 +2101,8 @@ def _process_update(
try:
cont = _parse_content(content)
message.contents.append(cont)
except ValidationError as ve:
logger.warning(f"Skipping unknown content type or invalid content: {ve}")
except ContentError as exc:
logger.warning(f"Skipping unknown content type or invalid content: {exc}")
else:
message.contents.append(content)
# Incorporate the update's properties into the response.
@@ -2816,12 +2819,12 @@ class AgentRunResponseUpdate(SerializationMixin):
kwargs: will be combined with additional_properties if provided.
"""
contents = [] if contents is None else _parse_content_list(contents)
parsed_contents: list[Contents] = [] if contents is None else _parse_content_list(contents)
if text is not None:
if isinstance(text, str):
text = TextContent(text=text)
contents.append(text)
parsed_contents.append(text)
# Convert role from dict if needed (for SerializationMixin support)
if isinstance(role, MutableMapping):
@@ -2829,7 +2832,7 @@ class AgentRunResponseUpdate(SerializationMixin):
elif isinstance(role, str):
role = Role(value=role)
self.contents = contents
self.contents = parsed_contents
self.role = role
self.author_name = author_name
self.response_id = response_id
@@ -3011,11 +3014,11 @@ class ChatOptions(SerializationMixin):
top_p: float | None = None,
user: str | None = None,
additional_properties: MutableMapping[str, Any] | None = None,
**kwargs: Any,
):
"""Initialize ChatOptions.
Keyword Args:
additional_properties: Provider-specific additional properties.
model_id: The AI model ID to use.
allow_multiple_tool_calls: Whether to allow multiple tool calls.
conversation_id: The conversation ID.
@@ -3034,6 +3037,8 @@ class ChatOptions(SerializationMixin):
tools: List of available tools.
top_p: The top-p value (must be between 0.0 and 1.0).
user: The user ID.
additional_properties: Provider-specific additional properties, can also be passed as kwargs.
**kwargs: Additional properties to include in additional_properties.
"""
# Validate numeric constraints and convert types as needed
if frequency_penalty is not None:
@@ -3055,7 +3060,12 @@ class ChatOptions(SerializationMixin):
if max_tokens is not None and max_tokens <= 0:
raise ValueError("max_tokens must be greater than 0")
self.additional_properties = additional_properties or {}
if additional_properties is None:
additional_properties = {}
if kwargs:
additional_properties.update(kwargs)
self.additional_properties = cast(dict[str, Any], additional_properties)
self.model_id = model_id
self.allow_multiple_tool_calls = allow_multiple_tool_calls
self.conversation_id = conversation_id
@@ -3128,48 +3138,11 @@ class ChatOptions(SerializationMixin):
case "none":
return ToolMode.NONE
case _:
raise ValidationError(f"Invalid tool choice: {tool_choice}")
raise ContentError(f"Invalid tool choice: {tool_choice}")
if isinstance(tool_choice, (dict, Mapping)):
return ToolMode.from_dict(tool_choice) # type: ignore
return tool_choice
def to_provider_settings(self, *, by_alias: bool = True, exclude: set[str] | None = None) -> dict[str, Any]:
"""Convert the ChatOptions to a dictionary suitable for provider requests.
Keyword Args:
by_alias: Use alias names for fields if True.
exclude: Additional keys to exclude from the output.
Returns:
Dictionary of settings for provider.
"""
default_exclude = {"additional_properties", "type"} # 'type' is for serialization, not API calls
# No tool choice if no tools are defined
if self.tools is None or len(self.tools) == 0:
default_exclude.add("tool_choice")
# No metadata and logit bias if they are empty
# Prevents 400 error
if not self.logit_bias:
default_exclude.add("logit_bias")
if not self.metadata:
default_exclude.add("metadata")
merged_exclude = default_exclude if exclude is None else default_exclude | set(exclude)
settings = self.to_dict(exclude_none=True, exclude=merged_exclude)
if by_alias and self.model_id is not None:
settings["model"] = settings.pop("model_id", None)
# Serialize tool_choice to its string representation for provider settings
if "tool_choice" in settings and isinstance(self.tool_choice, ToolMode):
settings["tool_choice"] = self.tool_choice.serialize_model()
settings = {k: v for k, v in settings.items() if v is not None}
settings.update(self.additional_properties)
for key in merged_exclude:
settings.pop(key, None)
return settings
def __and__(self, other: object) -> "ChatOptions":
"""Combines two ChatOptions instances.
@@ -3189,14 +3162,13 @@ class ChatOptions(SerializationMixin):
combined.tools = list(self.tools) if self.tools else None
combined.logit_bias = dict(self.logit_bias) if self.logit_bias else None
combined.metadata = dict(self.metadata) if self.metadata else None
combined.additional_properties = dict(self.additional_properties)
combined.response_format = response_format
# Apply scalar and mapping updates from the other options
updated_data = other.to_dict(exclude_none=True, exclude={"tools"})
logit_bias = updated_data.pop("logit_bias", {})
metadata = updated_data.pop("metadata", {})
additional_properties = updated_data.pop("additional_properties", {})
additional_properties: dict[str, Any] = updated_data.pop("additional_properties", {})
for key, value in updated_data.items():
setattr(combined, key, value)
@@ -3205,10 +3177,18 @@ class ChatOptions(SerializationMixin):
# Preserve response_format from other if it exists, otherwise keep self's
if other.response_format is not None:
combined.response_format = other.response_format
combined.instructions = "\n".join([combined.instructions or "", other.instructions or ""])
combined.logit_bias = {**(combined.logit_bias or {}), **logit_bias}
combined.metadata = {**(combined.metadata or {}), **metadata}
combined.additional_properties = {**(combined.additional_properties or {}), **additional_properties}
if other.instructions:
combined.instructions = "\n".join([combined.instructions or "", other.instructions or ""])
combined.logit_bias = (
{**(combined.logit_bias or {}), **logit_bias} if logit_bias or combined.logit_bias else None
)
combined.metadata = {**(combined.metadata or {}), **metadata} if metadata or combined.metadata else None
if combined.additional_properties and additional_properties:
combined.additional_properties.update(additional_properties)
else:
if additional_properties:
combined.additional_properties = additional_properties
if other_tools:
if combined.tools is None:
combined.tools = list(other_tools)
@@ -21,8 +21,8 @@ from ._shared import (
TAzureOpenAIResponsesClient = TypeVar("TAzureOpenAIResponsesClient", bound="AzureOpenAIResponsesClient")
@use_observability
@use_function_invocation
@use_observability
@use_chat_middleware
class AzureOpenAIResponsesClient(AzureOpenAIConfigMixin, OpenAIBaseResponsesClient):
"""Azure Responses completion class."""
@@ -171,7 +171,6 @@ class AzureOpenAIConfigMixin(OpenAIBase):
Args:
deployment_name: Name of the deployment.
ai_model_type: The type of OpenAI model to deploy.
endpoint: The specific endpoint URL for the deployment.
base_url: The base URL for Azure services.
api_version: Azure API version. Defaults to the defined DEFAULT_AZURE_API_VERSION.
@@ -140,3 +140,9 @@ class MiddlewareException(AgentFrameworkException):
"""An error occurred during middleware execution."""
pass
class ContentError(AgentFrameworkException):
"""An error occurred while processing content."""
pass
@@ -825,7 +825,7 @@ def _trace_get_response(
) -> "ChatResponse":
global OBSERVABILITY_SETTINGS
if not OBSERVABILITY_SETTINGS.ENABLED:
# If model diagnostics are not enabled, just return the completion
# If model_id diagnostics are not enabled, just return the completion
return await func(
self,
messages=messages,
@@ -836,7 +836,7 @@ def _trace_get_response(
if "operation_duration_histogram" not in self.additional_properties:
self.additional_properties["operation_duration_histogram"] = _get_duration_histogram()
model_id = (
kwargs.get("model")
kwargs.get("model_id")
or (chat_options.model_id if (chat_options := kwargs.get("chat_options")) else None)
or getattr(self, "model_id", None)
)
@@ -848,7 +848,7 @@ def _trace_get_response(
attributes = _get_span_attributes(
operation_name=OtelAttr.CHAT_COMPLETION_OPERATION,
provider_name=provider_name,
model_id=model_id,
model=model_id,
service_url=service_url,
**kwargs,
)
@@ -923,7 +923,7 @@ def _trace_get_streaming_response(
self.additional_properties["operation_duration_histogram"] = _get_duration_histogram()
model_id = (
kwargs.get("model")
kwargs.get("model_id")
or (chat_options.model_id if (chat_options := kwargs.get("chat_options")) else None)
or getattr(self, "model_id", None)
)
@@ -935,7 +935,7 @@ def _trace_get_streaming_response(
attributes = _get_span_attributes(
operation_name=OtelAttr.CHAT_COMPLETION_OPERATION,
provider_name=provider_name,
model_id=model_id,
model=model_id,
service_url=service_url,
**kwargs,
)
@@ -1346,7 +1346,7 @@ def _get_span_attributes(**kwargs: Any) -> dict[str, Any]:
attributes[SpanAttributes.LLM_SYSTEM] = system_name
if provider_name := kwargs.get("provider_name"):
attributes[OtelAttr.PROVIDER_NAME] = provider_name
attributes[SpanAttributes.LLM_REQUEST_MODEL] = kwargs.get("model_id", "unknown")
attributes[SpanAttributes.LLM_REQUEST_MODEL] = kwargs.get("model", "unknown")
if service_url := kwargs.get("service_url"):
attributes[OtelAttr.ADDRESS] = service_url
if conversation_id := kwargs.get("conversation_id", chat_options.conversation_id):
@@ -154,7 +154,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]:
# Preprocess web search tool if it exists
options_dict = chat_options.to_provider_settings()
options_dict = chat_options.to_dict(exclude={"type"})
instructions = options_dict.pop("instructions", None)
if instructions:
messages = [ChatMessage(role="system", text=instructions), *messages]
@@ -172,14 +172,20 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
options_dict.pop("parallel_tool_calls", None)
options_dict.pop("tool_choice", None)
if "model" not in options_dict:
if "model_id" not in options_dict:
options_dict["model"] = self.model_id
else:
options_dict["model"] = options_dict.pop("model_id")
if (
chat_options.response_format
and isinstance(chat_options.response_format, type)
and issubclass(chat_options.response_format, BaseModel)
):
options_dict["response_format"] = type_to_response_format_param(chat_options.response_format)
if additional_properties := options_dict.pop("additional_properties", None):
for key, value in additional_properties.items():
if value is not None:
options_dict[key] = value
return options_dict
def _create_chat_response(self, response: ChatCompletion, chat_options: ChatOptions) -> "ChatResponse":
@@ -300,27 +300,26 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]:
"""Take ChatOptions and create the specific options for Responses API."""
options_dict: dict[str, Any] = {}
if chat_options.max_tokens is not None:
options_dict["max_output_tokens"] = chat_options.max_tokens
if chat_options.temperature is not None:
options_dict["temperature"] = chat_options.temperature
if chat_options.top_p is not None:
options_dict["top_p"] = chat_options.top_p
if chat_options.user is not None:
options_dict["user"] = chat_options.user
# messages
if instructions := options_dict.pop("instructions", None):
messages = [ChatMessage(role="system", text=instructions), *messages]
request_input = self._prepare_chat_messages_for_request(messages)
if not request_input:
raise ServiceInvalidRequestError("Messages are required for chat completions")
options_dict["input"] = request_input
options_dict: dict[str, Any] = chat_options.to_dict(
exclude={
"type",
"response_format", # handled in inner get methods
"presence_penalty", # not supported
"frequency_penalty", # not supported
"logit_bias", # not supported
"seed", # not supported
"stop", # not supported
}
)
translations = {
"model_id": "model",
"allow_multiple_tool_calls": "parallel_tool_calls",
"conversation_id": "previous_response_id",
"max_tokens": "max_output_tokens",
}
for old_key, new_key in translations.items():
if old_key in options_dict and old_key != new_key:
options_dict[new_key] = options_dict.pop(old_key)
# tools
if chat_options.tools is None:
@@ -328,13 +327,23 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
else:
options_dict["tools"] = self._tools_to_response_tools(chat_options.tools)
# other settings
options_dict["store"] = chat_options.store is True
if chat_options.conversation_id:
options_dict["previous_response_id"] = chat_options.conversation_id
if "model" not in options_dict:
# model id
if not options_dict.get("model"):
options_dict["model"] = self.model_id
# messages
request_input = self._prepare_chat_messages_for_request(messages)
if not request_input:
raise ServiceInvalidRequestError("Messages are required for chat completions")
options_dict["input"] = request_input
# additional provider specific settings
if additional_properties := options_dict.pop("additional_properties", None):
for key, value in additional_properties.items():
if value is not None:
options_dict[key] = value
if "store" not in options_dict:
options_dict["store"] = False
return options_dict
def _prepare_chat_messages_for_request(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]:
@@ -630,6 +639,11 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
additional_properties=additional_properties,
)
)
if hasattr(item, "summary") and item.summary:
for summary in item.summary:
contents.append(
TextReasoningContent(text=summary.text, raw_representation=summary) # type: ignore[arg-type]
)
case "code_interpreter_call": # ResponseOutputCodeInterpreterCall
if hasattr(item, "outputs") and item.outputs:
for code_output in item.outputs:
@@ -238,7 +238,7 @@ async def test_chat_client_observability(mock_chat_client, span_exporter: InMemo
messages = [ChatMessage(role=Role.USER, text="Test message")]
span_exporter.clear()
response = await client.get_response(messages=messages, model="Test")
response = await client.get_response(messages=messages, model_id="Test")
assert response is not None
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
@@ -263,7 +263,7 @@ async def test_chat_client_streaming_observability(
span_exporter.clear()
# Collect all yielded updates
updates = []
async for update in client.get_streaming_response(messages=messages, model="Test"):
async for update in client.get_streaming_response(messages=messages, model_id="Test"):
updates.append(update)
# Verify we got the expected updates, this shouldn't be dependent on otel
+7 -111
View File
@@ -4,13 +4,12 @@ from collections.abc import AsyncIterable
from typing import Any
import pytest
from pydantic import BaseModel, ValidationError
from pydantic import BaseModel
from pytest import fixture, mark, raises
from agent_framework import (
AgentRunResponse,
AgentRunResponseUpdate,
AIFunction,
BaseContent,
ChatMessage,
ChatOptions,
@@ -37,7 +36,7 @@ from agent_framework import (
UsageDetails,
ai_function,
)
from agent_framework.exceptions import AdditionItemMismatch
from agent_framework.exceptions import AdditionItemMismatch, ContentError
@fixture
@@ -451,7 +450,8 @@ def test_ai_content_serialization(content_type: type[BaseContent], args: dict):
else:
# Normal attribute checking for other content types
for key, value in args.items():
assert getattr(deserialized, key) == value
if value:
assert getattr(deserialized, key) == value
# For now, skip the TestModel validation since it still uses Pydantic
# This would need to be updated when we migrate more classes
@@ -772,53 +772,11 @@ def test_chat_options_init() -> None:
assert options.model_id is None
def test_chat_options_init_with_args(ai_function_tool, ai_tool) -> None:
options = ChatOptions(
model_id="gpt-4",
max_tokens=1024,
temperature=0.7,
top_p=0.9,
presence_penalty=0.0,
frequency_penalty=0.0,
user="user-123",
tools=[ai_function_tool, ai_tool],
tool_choice="required",
additional_properties={"custom": True},
logit_bias={"a": 1},
metadata={"m": "v"},
)
assert options.model_id == "gpt-4"
assert options.max_tokens == 1024
assert options.temperature == 0.7
assert options.top_p == 0.9
assert options.presence_penalty == 0.0
assert options.frequency_penalty == 0.0
assert options.user == "user-123"
for tool in options.tools:
assert isinstance(tool, ToolProtocol)
assert tool.name is not None
assert tool.description is not None
if isinstance(tool, AIFunction):
assert tool.parameters() is not None
settings = options.to_provider_settings()
assert settings["model"] == "gpt-4" # uses alias
assert settings["tool_choice"] == "required" # serialized via model_serializer
assert settings["custom"] is True # from additional_properties
assert "additional_properties" not in settings
def test_chat_options_tool_choice_validation_errors():
with raises((ValidationError, TypeError)):
with raises((ContentError, TypeError)):
ChatOptions(tool_choice="invalid-choice")
def test_chat_options_tool_choice_excluded_when_no_tools():
options = ChatOptions(tool_choice="auto")
settings = options.to_provider_settings()
assert "tool_choice" not in settings
def test_chat_options_and(ai_function_tool, ai_tool) -> None:
options1 = ChatOptions(model_id="gpt-4o", tools=[ai_function_tool], logit_bias={"x": 1}, metadata={"a": "b"})
options2 = ChatOptions(model_id="gpt-4.1", tools=[ai_tool], additional_properties={"p": 1})
@@ -1059,69 +1017,6 @@ def test_chat_tool_mode_eq_with_string():
assert ToolMode.AUTO == "auto"
def test_chat_options_tool_choice_dict_mapping(ai_tool):
opts = ChatOptions(tool_choice={"mode": "required", "required_function_name": "fn"}, tools=[ai_tool])
assert isinstance(opts.tool_choice, ToolMode)
assert opts.tool_choice.mode == "required"
assert opts.tool_choice.required_function_name == "fn"
# provider settings serialize to just the mode
settings = opts.to_provider_settings()
assert settings["tool_choice"] == "required"
def test_chat_options_to_provider_settings_with_falsy_values():
"""Test that falsy values (except None) are included in provider settings."""
options = ChatOptions(
temperature=0.0, # falsy but not None
top_p=0.0, # falsy but not None
presence_penalty=False, # falsy but not None
frequency_penalty=None, # None - should be excluded
additional_properties={"empty_string": "", "zero": 0, "false_flag": False, "none_value": None},
)
settings = options.to_provider_settings()
# Falsy values that are not None should be included
assert "temperature" in settings
assert isinstance(settings["temperature"], float)
assert settings["temperature"] == 0.0
assert "top_p" in settings
assert isinstance(settings["top_p"], float)
assert settings["top_p"] == 0.0
assert "presence_penalty" in settings
assert isinstance(settings["presence_penalty"], float) # converted to float
assert settings["presence_penalty"] == 0.0
# None values should be excluded
assert "frequency_penalty" not in settings
# Additional properties - falsy values should always be included
assert "empty_string" in settings
assert settings["empty_string"] == ""
assert "zero" in settings
assert settings["zero"] == 0
assert "false_flag" in settings
assert settings["false_flag"] is False
assert "none_value" in settings
assert settings["none_value"] is None
def test_chat_options_empty_logit_bias_and_metadata_excluded():
"""Test that empty logit_bias and metadata are excluded from provider settings."""
options = ChatOptions(
model_id="gpt-4o",
logit_bias={}, # empty dict should be excluded
metadata={}, # empty dict should be excluded
)
settings = options.to_provider_settings()
# Empty logit_bias and metadata should be excluded
assert "logit_bias" not in settings
assert "metadata" not in settings
assert settings["model"] == "gpt-4o"
# region AgentRunResponse
@@ -1905,7 +1800,8 @@ def test_content_roundtrip_serialization(content_class: type[BaseContent], init_
elif isinstance(value, dict) and hasattr(reconstructed_value, "to_dict"):
# Compare the dict with the serialized form of the object, excluding 'type' key
reconstructed_dict = reconstructed_value.to_dict()
assert len(reconstructed_dict) == len(value)
if value:
assert len(reconstructed_dict) == len(value)
else:
assert reconstructed_value == value
@@ -71,9 +71,7 @@ async def test_cmc(
chat_history.append(ChatMessage(role="user", text="hello world"))
openai_chat_completion = OpenAIChatClient()
await openai_chat_completion.get_response(
messages=chat_history,
)
await openai_chat_completion.get_response(messages=chat_history)
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
stream=False,
@@ -189,6 +187,26 @@ async def test_cmc_general_exception(
)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_additional_properties(
mock_create: AsyncMock,
chat_history: list[ChatMessage],
mock_chat_completion_response: ChatCompletion,
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
openai_chat_completion = OpenAIChatClient()
await openai_chat_completion.get_response(messages=chat_history, additional_properties={"reasoning_effort": "low"})
mock_create.assert_awaited_once_with(
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
stream=False,
messages=openai_chat_completion._prepare_chat_history_for_request(chat_history), # type: ignore
reasoning_effort="low",
)
# region Streaming
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch
import pytest
from openai import BadRequestError
from openai.types.responses.response_reasoning_item import Summary
from openai.types.responses.response_reasoning_summary_text_delta_event import ResponseReasoningSummaryTextDeltaEvent
from openai.types.responses.response_reasoning_summary_text_done_event import ResponseReasoningSummaryTextDoneEvent
from openai.types.responses.response_reasoning_text_delta_event import ResponseReasoningTextDeltaEvent
@@ -209,7 +210,7 @@ def test_get_response_with_all_parameters() -> None:
instructions="You are a helpful assistant",
max_tokens=100,
parallel_tool_calls=True,
model="gpt-4",
model_id="gpt-4",
previous_response_id="prev-123",
reasoning={"chain_of_thought": "enabled"},
service_tier="auto",
@@ -535,13 +536,13 @@ def test_response_content_creation_with_reasoning() -> None:
mock_reasoning_item = MagicMock()
mock_reasoning_item.type = "reasoning"
mock_reasoning_item.content = [mock_reasoning_content]
mock_reasoning_item.summary = ["Summary"]
mock_reasoning_item.summary = [Summary(text="Summary", type="summary_text")]
mock_response.output = [mock_reasoning_item]
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
assert len(response.messages[0].contents) == 1
assert len(response.messages[0].contents) == 2
assert isinstance(response.messages[0].contents[0], TextReasoningContent)
assert response.messages[0].contents[0].text == "Reasoning step"
@@ -1536,11 +1537,9 @@ async def test_openai_responses_client_agent_chat_options_run_level() -> None:
instructions="You are a helpful assistant.",
) as agent:
response = await agent.run(
"Provide a brief, helpful response.",
max_tokens=100,
temperature=0.7,
top_p=0.9,
seed=123,
"Provide a brief, helpful response about why the sky blue is.",
max_tokens=600,
model_id="gpt-4o",
user="comprehensive-test-user",
tools=[get_weather],
tool_choice="auto",
@@ -2077,7 +2076,6 @@ def test_prepare_options_store_parameter_handling() -> None:
chat_options = ChatOptions(store=False, conversation_id="")
options = client._prepare_options(messages, chat_options) # type: ignore
assert options["store"] is False
assert "previous_response_id" not in options
chat_options = ChatOptions(store=None, conversation_id=None)
options = client._prepare_options(messages, chat_options) # type: ignore
@@ -189,7 +189,7 @@ class EntityDiscovery:
framework="agent_framework",
tools=[str(tool) for tool in (tools_list or [])],
instructions=instructions,
model=model,
model_id=model,
chat_client_type=chat_client_type,
context_providers=context_providers_list,
middleware=middleware_list,
@@ -547,7 +547,7 @@ class EntityDiscovery:
description=description,
tools=tools_union,
instructions=instructions,
model=model,
model_id=model,
chat_client_type=chat_client_type,
context_providers=context_providers_list,
middleware=middleware_list,
@@ -67,7 +67,7 @@ class SessionManager:
logger.debug(f"Closed session: {session_id}")
def add_request_record(
self, session_id: str, entity_id: str, executor_name: str, request_input: Any, model: str
self, session_id: str, entity_id: str, executor_name: str, request_input: Any, model_id: str
) -> str:
"""Add a request record to a session.
@@ -76,7 +76,7 @@ class SessionManager:
entity_id: ID of the entity being executed
executor_name: Name of the executor
request_input: Input for the request
model: Model name
model_id: Model name
Returns:
Request ID
@@ -91,7 +91,7 @@ class SessionManager:
"entity_id": entity_id,
"executor": executor_name,
"input": request_input,
"model": model,
"model_id": model_id,
"stream": True,
}
session["requests"].append(request_record)
@@ -39,7 +39,7 @@ class EntityInfo(BaseModel):
# Agent-specific fields (optional, populated when available)
instructions: str | None = None
model: str | None = None
model_id: str | None = None
chat_client_type: str | None = None
context_providers: list[str] | None = None
middleware: list[str] | None = None
@@ -2,41 +2,66 @@
import asyncio
from agent_framework import HostedCodeInterpreterTool, TextContent, TextReasoningContent, UsageContent
from agent_framework.openai import OpenAIResponsesClient
"""
OpenAI Responses Client Reasoning Example
This sample demonstrates advanced reasoning capabilities using OpenAI's o1 models,
This sample demonstrates advanced reasoning capabilities using OpenAI's gpt-5 models,
showing step-by-step reasoning process visualization and complex problem-solving.
This uses the additional_chat_options parameter to enable reasoning with high effort and detailed summaries.
You can also set these options at the run level, since they are api and/or provider specific, you will need to lookup
the correct values for your provider, since these are passed through as-is.
In this case they are here: https://platform.openai.com/docs/api-reference/responses/create#responses-create-reasoning
"""
agent = OpenAIResponsesClient(model_id="gpt-5").create_agent(
name="MathHelper",
instructions="You are a personal math tutor. When asked a math question, "
"reason over how best to approach the problem and share your thought process.",
additional_chat_options={"reasoning": {"effort": "high", "summary": "detailed"}},
)
async def reasoning_example() -> None:
"""Example of reasoning response (get results as they are generated)."""
print("=== Reasoning Example ===")
print("\033[92m=== Reasoning Example ===\033[0m")
agent = OpenAIResponsesClient(model_id="gpt-5").create_agent(
name="MathHelper",
instructions="You are a personal math tutor. When asked a math question, "
"write and run code using the python tool to answer the question.",
tools=HostedCodeInterpreterTool(),
reasoning={"effort": "high", "summary": "detailed"},
)
query = "I need to solve the equation 3x + 11 = 14 and I need to prove the pythagorean theorem. Can you help me?"
print(f"User: {query}")
print(f"{agent.name}: ", end="", flush=True)
response = await agent.run(query)
for msg in response.messages:
if msg.contents:
for content in msg.contents:
if content.type == "text_reasoning":
print(f"\033[94m{content.text}\033[0m", end="", flush=True)
elif content.type == "text":
print(content.text, end="", flush=True)
print("\n")
if response.usage_details:
print(f"Usage: {response.usage_details}")
query = "I need to solve the equation 3x + 11 = 14. Can you help me?"
async def streaming_reasoning_example() -> None:
"""Example of reasoning response (get results as they are generated)."""
print("\033[92m=== Streaming Reasoning Example ===\033[0m")
query = "I need to solve the equation 3x + 11 = 14 and I need to prove the pythagorean theorem. Can you help me?"
print(f"User: {query}")
print(f"{agent.name}: ", end="", flush=True)
usage = None
async for chunk in agent.run_stream(query):
if chunk.contents:
for content in chunk.contents:
if isinstance(content, TextReasoningContent):
print(f"\033[97m{content.text}\033[0m", end="", flush=True)
elif isinstance(content, TextContent):
if content.type == "text_reasoning":
print(f"\033[94m{content.text}\033[0m", end="", flush=True)
elif content.type == "text":
print(content.text, end="", flush=True)
elif isinstance(content, UsageContent):
elif content.type == "usage":
usage = content
print("\n")
if usage:
@@ -44,9 +69,10 @@ async def reasoning_example() -> None:
async def main() -> None:
print("=== Basic OpenAI Responses Reasoning Agent Example ===")
print("\033[92m=== Basic OpenAI Responses Reasoning Agent Example ===\033[0m")
await reasoning_example()
await streaming_reasoning_example()
if __name__ == "__main__":