From ba35ea8e60836fa9df11456c123e09316b373314 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Thu, 25 Dec 2025 01:41:55 +0530 Subject: [PATCH 01/30] feat: Agent Config and API Interoperability --- autogen/agentchat/conversable_agent.py | 5 ++++- autogen/oai/agent_config_handler.py | 16 ++++++++++++++++ autogen/oai/anthropic.py | 5 +++++ autogen/oai/bedrock.py | 17 +++++++++++++++-- autogen/oai/cerebras.py | 7 +++++++ autogen/oai/client.py | 1 + autogen/oai/cohere.py | 5 ++++- autogen/oai/gemini.py | 5 +++++ autogen/oai/groq.py | 7 +++++++ autogen/oai/mistral.py | 8 ++++++++ autogen/oai/ollama.py | 9 +++++++++ autogen/oai/openai_responses.py | 4 ++++ autogen/oai/together.py | 6 ++++++ 13 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 autogen/oai/agent_config_handler.py diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 9e8d84282ff..761f37b6ea6 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -27,6 +27,8 @@ Union, ) +from pydantic import BaseModel + from ..cache.cache import AbstractCache, Cache from ..code_utils import ( PYTHON_VARIANTS, @@ -162,6 +164,7 @@ def __init__( silent: bool | None = None, context_variables: Optional["ContextVariables"] = None, functions: list[Callable[..., Any]] | Callable[..., Any] = None, + response_format: str | dict[str, Any] | BaseModel | type[BaseModel] | None = None, update_agent_state_before_reply: list[Callable | UpdateSystemMessage] | Callable | UpdateSystemMessage @@ -223,10 +226,10 @@ def __init__( 15) update_agent_state_before_reply (List[Callable[..., Any]]): A list of functions, including UpdateSystemMessage's, called to update the agent before it replies.\n 16) handoffs (Handoffs): Handoffs object containing all handoff transition conditions.\n """ + self.response_format = response_format if response_format is not None else None self.handoffs = handoffs if handoffs is not None else Handoffs() self.input_guardrails: list[Guardrail] = [] self.output_guardrails: list[Guardrail] = [] - # we change code_execution_config below and we have to make sure we don't change the input # in case of UserProxyAgent, without this we could even change the default value {} code_execution_config = ( diff --git a/autogen/oai/agent_config_handler.py b/autogen/oai/agent_config_handler.py new file mode 100644 index 00000000000..c20923815fe --- /dev/null +++ b/autogen/oai/agent_config_handler.py @@ -0,0 +1,16 @@ +# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors +# +# SPDX-License-Identifier: Apache-2.0 + +from typing import Any + +from autogen.agentchat.conversable_agent import ConversableAgent + + +def agent_config_parser(agent: ConversableAgent) -> dict[str, Any]: + agent_config = [] + if agent.response_format is not None: + agent_config.append({ + "response_format": agent.pop("response_format"), + }) + return agent_config diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index f125cfd17ac..1a097f844d4 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -82,6 +82,8 @@ from pydantic import BaseModel, Field from typing_extensions import Unpack +from autogen.oai.agent_config_handler import agent_config_parser + from ..code_utils import content_str from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict @@ -762,7 +764,10 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: Returns: ChatCompletion object compatible with OpenAI format """ + agent = params.pop("agent", None) model = params.get("model") + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") response_format = params.get("response_format") or self._response_format # Route to appropriate implementation based on model and response_format diff --git a/autogen/oai/bedrock.py b/autogen/oai/bedrock.py index 030b4d0e5fc..66f690170ce 100644 --- a/autogen/oai/bedrock.py +++ b/autogen/oai/bedrock.py @@ -32,6 +32,7 @@ import base64 import json +import logging import os import re import time @@ -44,6 +45,7 @@ from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .client_utils import validate_parameter from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage @@ -51,6 +53,8 @@ import boto3 from botocore.config import Config +logger = logging.getLogger(__name__) + class BedrockEntryDict(LLMConfigEntryDict, total=False): api_type: Literal["bedrock"] @@ -421,15 +425,24 @@ def parse_params(self, params: BedrockEntryDict | dict[str, Any]) -> tuple[dict[ def create(self, params) -> ChatCompletion: """Run Amazon Bedrock inference and return AG2 response""" # Set custom client class settings + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") self.parse_custom_params(params) # Parse the inference parameters base_params, additional_params = self.parse_params(params) # Handle response_format for structured outputs - has_response_format = self._response_format is not None + has_response_format = ( + agent_config["response_format"] + if "response_format" in agent_config and agent_config["response_format"] is not None + else self._response_format + if self._response_format is not None + else None + ) if has_response_format: - structured_output_tool = self._create_structured_output_tool(self._response_format) + structured_output_tool = self._create_structured_output_tool(has_response_format) # Merge with user tools if any user_tools = params.get("tools", []) tool_config = self._merge_tools_with_structured_output(user_tools, structured_output_tool) diff --git a/autogen/oai/cerebras.py b/autogen/oai/cerebras.py index 7313b6e1653..c397004d0f6 100644 --- a/autogen/oai/cerebras.py +++ b/autogen/oai/cerebras.py @@ -24,6 +24,7 @@ from __future__ import annotations import copy +import logging import math import os import time @@ -33,11 +34,14 @@ from pydantic import Field from typing_extensions import Unpack +from autogen.oai.agent_config_handler import agent_config_parser + from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict from .client_utils import should_hide_tools, validate_parameter from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage +logger = logging.getLogger(__name__) with optional_import_block(): from cerebras.cloud.sdk import Cerebras, Stream @@ -146,6 +150,9 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("cerebras", "cerebras") def create(self, params: dict) -> ChatCompletion: + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) # Convert AG2 messages to Cerebras messages diff --git a/autogen/oai/client.py b/autogen/oai/client.py index 3562fd4b2ed..89747785961 100644 --- a/autogen/oai/client.py +++ b/autogen/oai/client.py @@ -1259,6 +1259,7 @@ def create(self, **config: Any) -> ModelClient.ModelClientResponseProtocol: return response continue # filter is not passed; try the next config try: + params = params["agent"] = self.agent request_ts = get_current_ts() response = client.create(params) except Exception as e: diff --git a/autogen/oai/cohere.py b/autogen/oai/cohere.py index 813f86f67f6..6af1775b067 100644 --- a/autogen/oai/cohere.py +++ b/autogen/oai/cohere.py @@ -39,6 +39,7 @@ from pydantic import BaseModel, Field from typing_extensions import Unpack +from autogen.oai.agent_config_handler import agent_config_parser from autogen.oai.client_utils import FormatterProtocol, logging_formatter, validate_parameter from ..import_utils import optional_import_block, require_optional_import @@ -241,11 +242,13 @@ def ensure_type_fields(obj: dict, defs: dict) -> dict: @require_optional_import("cohere", "cohere") def create(self, params: dict) -> ChatCompletion: + agent = params.pop("agent", None) messages = params.get("messages", []) client_name = params.get("client_name") or "AG2" cohere_tool_names = set() tool_calls_modified_ids = set() - + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") # Parse parameters to the Cohere API's parameters cohere_params = self.parse_params(params) diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index f41c4be14f5..e604082266a 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -57,6 +57,8 @@ from pydantic import BaseModel, Field from typing_extensions import Unpack +from autogen.oai.agent_config_handler import agent_config_parser + from ..import_utils import optional_import_block, require_optional_import from ..json_utils import resolve_json_references from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict @@ -244,6 +246,9 @@ def get_usage(response: ChatCompletion) -> dict[str, Any]: } def create(self, params: dict[str, Any]) -> ChatCompletion: + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") # When running in async context via run_in_executor from ConversableAgent.a_generate_oai_reply, # this method runs in a new thread that doesn't have an event loop by default. The Google Genai # client requires an event loop even for synchronous operations, so we need to ensure one exists. diff --git a/autogen/oai/groq.py b/autogen/oai/groq.py index 7980544dc06..f5bf2e78e49 100644 --- a/autogen/oai/groq.py +++ b/autogen/oai/groq.py @@ -24,6 +24,7 @@ from __future__ import annotations import copy +import logging import os import time import warnings @@ -32,6 +33,8 @@ from pydantic import Field from typing_extensions import Unpack +from autogen.oai.agent_config_handler import agent_config_parser + from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict from .client_utils import should_hide_tools, validate_parameter @@ -40,6 +43,7 @@ with optional_import_block(): from groq import Groq, Stream +logger = logging.getLogger(__name__) # Cost per thousand tokens - Input / Output (NOTE: Convert $/Million to $/K) GROQ_PRICING_1K = { "llama3-70b-8192": (0.00059, 0.00079), @@ -166,6 +170,9 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("groq", "groq") def create(self, params: dict) -> ChatCompletion: + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) # Convert AG2 messages to Groq messages diff --git a/autogen/oai/mistral.py b/autogen/oai/mistral.py index cba7473dcba..86432fc8cfd 100644 --- a/autogen/oai/mistral.py +++ b/autogen/oai/mistral.py @@ -26,6 +26,7 @@ """ import json +import logging import os import time import warnings @@ -33,6 +34,8 @@ from typing_extensions import Unpack +from autogen.oai.agent_config_handler import agent_config_parser + from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict from .client_utils import should_hide_tools, validate_parameter @@ -52,6 +55,8 @@ UserMessage, ) +logger = logging.getLogger(__name__) + class MistralEntryDict(LLMConfigEntryDict, total=False): api_type: Literal["mistral"] @@ -203,6 +208,9 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("mistralai", "mistral") def create(self, params: dict[str, Any]) -> ChatCompletion: + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") # 1. Parse parameters to Mistral.AI API's parameters mistral_params = self.parse_params(params) diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index a20e173c275..e73d8ddaeec 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -25,6 +25,7 @@ import ast import copy import json +import logging import random import re import time @@ -33,6 +34,8 @@ from pydantic import BaseModel, Field, HttpUrl +from autogen.oai.agent_config_handler import agent_config_parser + from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict from .client_utils import FormatterProtocol, should_hide_tools, validate_parameter @@ -44,6 +47,9 @@ from ollama import Client +logger = logging.getLogger(__name__) + + class OllamaEntryDict(LLMConfigEntryDict, total=False): api_type: Literal["ollama"] client_host: HttpUrl | None @@ -227,6 +233,9 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import(["ollama", "fix_busted_json"], "ollama") def create(self, params: dict) -> ChatCompletion: + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) # Are tools involved in this conversation? diff --git a/autogen/oai/openai_responses.py b/autogen/oai/openai_responses.py index 3c2db89f039..69bf9534b26 100644 --- a/autogen/oai/openai_responses.py +++ b/autogen/oai/openai_responses.py @@ -14,6 +14,7 @@ from autogen.code_utils import content_str from autogen.import_utils import optional_import_block, require_optional_import +from autogen.oai.agent_config_handler import agent_config_parser if TYPE_CHECKING: from autogen.oai.client import ModelClient, OpenAI, OpenAILLMConfigEntry @@ -555,6 +556,9 @@ def create(self, params: dict[str, Any]) -> "Response": workspace_dir = params.pop("workspace_dir", os.getcwd()) allowed_paths = params.pop("allowed_paths", ["**"]) built_in_tools = params.pop("built_in_tools", []) + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") if self.previous_response_id is not None and "previous_response_id" not in params: params["previous_response_id"] = self.previous_response_id diff --git a/autogen/oai/together.py b/autogen/oai/together.py index 9e7234b4ec2..080364318dd 100644 --- a/autogen/oai/together.py +++ b/autogen/oai/together.py @@ -35,9 +35,12 @@ import warnings from typing import Any, Literal +from PIL.Image import logger from pydantic import Field from typing_extensions import Unpack +from autogen.oai.agent_config_handler import agent_config_parser + from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict from .client_utils import should_hide_tools, validate_parameter @@ -177,6 +180,9 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("together", "together") def create(self, params: dict) -> ChatCompletion: + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) # Convert AG2 messages to Together.AI messages From ccfa8156f36b41d4bf46fb43695b0898d74969ce Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Thu, 25 Dec 2025 23:54:34 +0530 Subject: [PATCH 02/30] fix: core tests --- autogen/oai/agent_config_handler.py | 13 +++++++------ autogen/oai/anthropic.py | 3 +-- autogen/oai/cerebras.py | 3 +-- autogen/oai/client.py | 15 +++++++++++++-- autogen/oai/cohere.py | 2 +- autogen/oai/gemini.py | 3 +-- autogen/oai/groq.py | 3 +-- autogen/oai/mistral.py | 3 +-- autogen/oai/ollama.py | 3 +-- autogen/oai/openai_responses.py | 3 ++- autogen/oai/together.py | 3 +-- 11 files changed, 30 insertions(+), 24 deletions(-) diff --git a/autogen/oai/agent_config_handler.py b/autogen/oai/agent_config_handler.py index c20923815fe..7aa2bb5377b 100644 --- a/autogen/oai/agent_config_handler.py +++ b/autogen/oai/agent_config_handler.py @@ -2,15 +2,16 @@ # # SPDX-License-Identifier: Apache-2.0 -from typing import Any +from typing import TYPE_CHECKING, Any -from autogen.agentchat.conversable_agent import ConversableAgent +if TYPE_CHECKING: + from autogen.agentchat.conversable_agent import ConversableAgent -def agent_config_parser(agent: ConversableAgent) -> dict[str, Any]: - agent_config = [] - if agent.response_format is not None: +def agent_config_parser(agent: "ConversableAgent") -> dict[str, Any]: + agent_config: dict[str, Any] = [] + if agent is not None and hasattr(agent, "response_format") and agent.response_format is not None: agent_config.append({ - "response_format": agent.pop("response_format"), + "response_format": agent.response_format, }) return agent_config diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index 1a097f844d4..9dd61d49487 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -82,11 +82,10 @@ from pydantic import BaseModel, Field from typing_extensions import Unpack -from autogen.oai.agent_config_handler import agent_config_parser - from ..code_utils import content_str from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser logger = logging.getLogger(__name__) from .client_utils import FormatterProtocol, validate_parameter diff --git a/autogen/oai/cerebras.py b/autogen/oai/cerebras.py index c397004d0f6..3fabbeae800 100644 --- a/autogen/oai/cerebras.py +++ b/autogen/oai/cerebras.py @@ -34,10 +34,9 @@ from pydantic import Field from typing_extensions import Unpack -from autogen.oai.agent_config_handler import agent_config_parser - from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .client_utils import should_hide_tools, validate_parameter from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage diff --git a/autogen/oai/client.py b/autogen/oai/client.py index 89747785961..ee59908fb2d 100644 --- a/autogen/oai/client.py +++ b/autogen/oai/client.py @@ -33,6 +33,7 @@ from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict from ..logger.logger_utils import get_current_ts from ..runtime_logging import log_chat_completion, log_new_client, log_new_wrapper, logging_enabled +from .agent_config_handler import agent_config_parser from .client_utils import FormatterProtocol, logging_formatter, merge_config_with_tools from .openai_utils import OAI_PRICE1K, get_key, is_valid_api_key @@ -200,6 +201,10 @@ _ch.setFormatter(logging_formatter) logger.addHandler(_ch) +import logging + +logger = logging.getLogger("ag2.event.processor") + LEGACY_DEFAULT_CACHE_SEED = 41 LEGACY_CACHE_DIR = ".cache" OPEN_API_BASE_URL_PREFIX = "https://api.openai.com" @@ -493,6 +498,9 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: Returns: The completion. """ + agent = params.pop("agent", None) + agent_config = agent_config_parser(agent) if agent is not None else None + logger.info(f"Agent config: {agent_config}") iostream = IOStream.get_default() is_structured_output = self.response_format is not None or "response_format" in params @@ -1176,7 +1184,8 @@ def create(self, **config: Any) -> ModelClient.ModelClientResponseProtocol: cache = extra_kwargs.get("cache") filter_func = extra_kwargs.get("filter_func") context = extra_kwargs.get("context") - agent = extra_kwargs.get("agent") + agent = extra_kwargs.get("agent", None) + price = extra_kwargs.get("price", None) if isinstance(price, list): price = tuple(price) @@ -1259,7 +1268,9 @@ def create(self, **config: Any) -> ModelClient.ModelClientResponseProtocol: return response continue # filter is not passed; try the next config try: - params = params["agent"] = self.agent + # Add agent to params if provided (for downstream use) + if agent is not None: + params["agent"] = agent request_ts = get_current_ts() response = client.create(params) except Exception as e: diff --git a/autogen/oai/cohere.py b/autogen/oai/cohere.py index 6af1775b067..01b2feb935f 100644 --- a/autogen/oai/cohere.py +++ b/autogen/oai/cohere.py @@ -39,11 +39,11 @@ from pydantic import BaseModel, Field from typing_extensions import Unpack -from autogen.oai.agent_config_handler import agent_config_parser from autogen.oai.client_utils import FormatterProtocol, logging_formatter, validate_parameter from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage with optional_import_block(): diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index e604082266a..f792f8aff5a 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -57,11 +57,10 @@ from pydantic import BaseModel, Field from typing_extensions import Unpack -from autogen.oai.agent_config_handler import agent_config_parser - from ..import_utils import optional_import_block, require_optional_import from ..json_utils import resolve_json_references from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .client_utils import FormatterProtocol from .gemini_types import ToolConfig from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage diff --git a/autogen/oai/groq.py b/autogen/oai/groq.py index f5bf2e78e49..f4a922dcdbb 100644 --- a/autogen/oai/groq.py +++ b/autogen/oai/groq.py @@ -33,10 +33,9 @@ from pydantic import Field from typing_extensions import Unpack -from autogen.oai.agent_config_handler import agent_config_parser - from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .client_utils import should_hide_tools, validate_parameter from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage diff --git a/autogen/oai/mistral.py b/autogen/oai/mistral.py index 86432fc8cfd..15bf8aeb723 100644 --- a/autogen/oai/mistral.py +++ b/autogen/oai/mistral.py @@ -34,10 +34,9 @@ from typing_extensions import Unpack -from autogen.oai.agent_config_handler import agent_config_parser - from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .client_utils import should_hide_tools, validate_parameter from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index e73d8ddaeec..2b9c05e21f1 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -34,10 +34,9 @@ from pydantic import BaseModel, Field, HttpUrl -from autogen.oai.agent_config_handler import agent_config_parser - from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .client_utils import FormatterProtocol, should_hide_tools, validate_parameter from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage diff --git a/autogen/oai/openai_responses.py b/autogen/oai/openai_responses.py index 69bf9534b26..925186731e9 100644 --- a/autogen/oai/openai_responses.py +++ b/autogen/oai/openai_responses.py @@ -14,7 +14,8 @@ from autogen.code_utils import content_str from autogen.import_utils import optional_import_block, require_optional_import -from autogen.oai.agent_config_handler import agent_config_parser + +from .agent_config_handler import agent_config_parser if TYPE_CHECKING: from autogen.oai.client import ModelClient, OpenAI, OpenAILLMConfigEntry diff --git a/autogen/oai/together.py b/autogen/oai/together.py index 080364318dd..c62350b7707 100644 --- a/autogen/oai/together.py +++ b/autogen/oai/together.py @@ -39,10 +39,9 @@ from pydantic import Field from typing_extensions import Unpack -from autogen.oai.agent_config_handler import agent_config_parser - from ..import_utils import optional_import_block, require_optional_import from ..llm_config.entry import LLMConfigEntry, LLMConfigEntryDict +from .agent_config_handler import agent_config_parser from .client_utils import should_hide_tools, validate_parameter from .oai_models import ChatCompletion, ChatCompletionMessage, ChatCompletionMessageToolCall, Choice, CompletionUsage From 2bdfe121b84c6f113b2a7afd62fc5c567a1e8edc Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:01:00 +0530 Subject: [PATCH 03/30] feat: update ollama client with agent response format --- autogen/oai/bedrock.py | 2 +- autogen/oai/ollama.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/autogen/oai/bedrock.py b/autogen/oai/bedrock.py index 66f690170ce..814c6948839 100644 --- a/autogen/oai/bedrock.py +++ b/autogen/oai/bedrock.py @@ -433,7 +433,7 @@ def create(self, params) -> ChatCompletion: # Parse the inference parameters base_params, additional_params = self.parse_params(params) - # Handle response_format for structured outputs + # Handle response_format for structured outputs, check if agent_config has a response_format else fallback to self._response_format has_response_format = ( agent_config["response_format"] if "response_format" in agent_config and agent_config["response_format"] is not None diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index 2b9c05e21f1..dc0f47c9848 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -237,6 +237,16 @@ def create(self, params: dict) -> ChatCompletion: logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) + self._response_format = ( + agent_config["response_format"] + if "response_format" in agent_config and agent_config["response_format"] is not None + else self._response_format + if self._response_format is not None + else params.get("response_format") + if params.get("response_format") is not None + else None + ) + # Are tools involved in this conversation? self._tools_in_conversation = "tools" in params From 1ccf034dabd9af4c62ee6efd2baff0ed913fc547 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:08:18 +0530 Subject: [PATCH 04/30] feat: update resposnes with agent config --- autogen/oai/openai_responses.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/autogen/oai/openai_responses.py b/autogen/oai/openai_responses.py index 925186731e9..32c1b014b64 100644 --- a/autogen/oai/openai_responses.py +++ b/autogen/oai/openai_responses.py @@ -561,6 +561,16 @@ def create(self, params: dict[str, Any]) -> "Response": agent_config = agent_config_parser(agent) if agent is not None else None logger.info(f"Agent config: {agent_config}") + self.response_format = ( + agent_config["response_format"] + if "response_format" in agent_config and agent_config["response_format"] is not None + else self.response_format + if self.response_format is not None + else params.get("response_format") + if params.get("response_format") is not None + else None + ) + if self.previous_response_id is not None and "previous_response_id" not in params: params["previous_response_id"] = self.previous_response_id From 2d777eee5fc9f2f3b10c37acdad6ece43ac8018b Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:16:37 +0530 Subject: [PATCH 05/30] feat: update gemini with agent config --- autogen/oai/gemini.py | 14 ++++++++++---- autogen/oai/ollama.py | 2 -- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index f792f8aff5a..257118590e9 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -301,6 +301,13 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: thinking_budget=thinking_budget, thinking_level=thinking_level, ) + self._response_format = ( + agent_config["response_format"] + if "response_format" in agent_config and agent_config["response_format"] is not None + else params.get("response_format") + if params.get("response_format") is not None + else None + ) generation_config = { gemini_term: params[autogen_term] for autogen_term, gemini_term in self.PARAMS_MAPPING.items() @@ -326,16 +333,15 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: # If response_format exists, we want structured outputs # Based on # https://ai.google.dev/gemini-api/docs/structured-output?lang=python#supply-schema-in-config - if params.get("response_format"): - self._response_format = params.get("response_format") + if self._response_format: generation_config["response_mime_type"] = "application/json" - response_format_schema_raw = params.get("response_format") + response_format_schema_raw = self._response_format if isinstance(response_format_schema_raw, dict): response_schema = resolve_json_references(response_format_schema_raw) else: - response_schema = resolve_json_references(params.get("response_format").model_json_schema()) + response_schema = resolve_json_references(self._response_format.model_json_schema()) if "$defs" in response_schema: response_schema.pop("$defs") generation_config["response_schema"] = response_schema diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index dc0f47c9848..20b980af8ea 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -240,8 +240,6 @@ def create(self, params: dict) -> ChatCompletion: self._response_format = ( agent_config["response_format"] if "response_format" in agent_config and agent_config["response_format"] is not None - else self._response_format - if self._response_format is not None else params.get("response_format") if params.get("response_format") is not None else None From e2f712b47cd928df7640968350df7800aaaa4606 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:27:03 +0530 Subject: [PATCH 06/30] feat: update cohere client with agent config --- autogen/oai/cohere.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/autogen/oai/cohere.py b/autogen/oai/cohere.py index 01b2feb935f..a6f2b1c1dc3 100644 --- a/autogen/oai/cohere.py +++ b/autogen/oai/cohere.py @@ -155,10 +155,8 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: ) # Handle structured output response format from Pydantic model - if "response_format" in params and params["response_format"] is not None: - self._response_format = params.get("response_format") - - response_format = params["response_format"] + if self._response_format: + response_format = self._response_format # Check if it's a Pydantic model if hasattr(response_format, "model_json_schema"): @@ -251,7 +249,13 @@ def create(self, params: dict) -> ChatCompletion: logger.info(f"Agent config: {agent_config}") # Parse parameters to the Cohere API's parameters cohere_params = self.parse_params(params) - + self._response_format = ( + agent_config["response_format"] + if "response_format" in agent_config and agent_config["response_format"] is not None + else params.get("response_format") + if params.get("response_format") is not None + else None + ) cohere_params["messages"] = messages if "tools" in params: From ce1683c7fa746e13fe2353dbed369346a33f34c9 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:39:05 +0530 Subject: [PATCH 07/30] fix: coditional logic --- autogen/oai/agent_config_handler.py | 2 +- autogen/oai/bedrock.py | 2 +- autogen/oai/cohere.py | 4 ++-- autogen/oai/gemini.py | 8 +++++--- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/autogen/oai/agent_config_handler.py b/autogen/oai/agent_config_handler.py index 7aa2bb5377b..68a00cdb7af 100644 --- a/autogen/oai/agent_config_handler.py +++ b/autogen/oai/agent_config_handler.py @@ -10,7 +10,7 @@ def agent_config_parser(agent: "ConversableAgent") -> dict[str, Any]: agent_config: dict[str, Any] = [] - if agent is not None and hasattr(agent, "response_format") and agent.response_format is not None: + if hasattr(agent, "response_format") and agent.response_format is not None: agent_config.append({ "response_format": agent.response_format, }) diff --git a/autogen/oai/bedrock.py b/autogen/oai/bedrock.py index 814c6948839..a0c2a981335 100644 --- a/autogen/oai/bedrock.py +++ b/autogen/oai/bedrock.py @@ -433,7 +433,7 @@ def create(self, params) -> ChatCompletion: # Parse the inference parameters base_params, additional_params = self.parse_params(params) - # Handle response_format for structured outputs, check if agent_config has a response_format else fallback to self._response_format + # Handle response_format for structured outputs, check if agent_config has a response_format else has_response_format = ( agent_config["response_format"] if "response_format" in agent_config and agent_config["response_format"] is not None diff --git a/autogen/oai/cohere.py b/autogen/oai/cohere.py index a6f2b1c1dc3..114fede4ab9 100644 --- a/autogen/oai/cohere.py +++ b/autogen/oai/cohere.py @@ -155,7 +155,7 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: ) # Handle structured output response format from Pydantic model - if self._response_format: + if self._response_format is not None: response_format = self._response_format # Check if it's a Pydantic model @@ -254,7 +254,7 @@ def create(self, params: dict) -> ChatCompletion: if "response_format" in agent_config and agent_config["response_format"] is not None else params.get("response_format") if params.get("response_format") is not None - else None + else self._response_format ) cohere_params["messages"] = messages diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index 257118590e9..49ed61ad06e 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -303,10 +303,12 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: ) self._response_format = ( agent_config["response_format"] - if "response_format" in agent_config and agent_config["response_format"] is not None + if agent_config is not None + and "response_format" in agent_config + and agent_config["response_format"] is not None else params.get("response_format") if params.get("response_format") is not None - else None + else self._response_format ) generation_config = { gemini_term: params[autogen_term] @@ -333,7 +335,7 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: # If response_format exists, we want structured outputs # Based on # https://ai.google.dev/gemini-api/docs/structured-output?lang=python#supply-schema-in-config - if self._response_format: + if self._response_format is not None: generation_config["response_mime_type"] = "application/json" response_format_schema_raw = self._response_format From b207974a3f2c2509739c13c3a5381167f8876aa5 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 22:27:59 +0530 Subject: [PATCH 08/30] feat: update agent config on anthropic --- autogen/oai/anthropic.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index 9dd61d49487..6d97696393e 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -767,8 +767,11 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: model = params.get("model") agent_config = agent_config_parser(agent) if agent is not None else None logger.info(f"Agent config: {agent_config}") - response_format = params.get("response_format") or self._response_format - + response_format = ( + agent_config["response_format"] + if "response_format" in agent_config and agent_config["response_format"] is not None + else params.get("response_format", self._response_format if self.response_format is not None else None) + ) # Route to appropriate implementation based on model and response_format if response_format: self._response_format = response_format From 5a7e1774603f4709b7bf6f696233c2541ca8feed Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:12:56 +0530 Subject: [PATCH 09/30] fix: param.get(,) --- autogen/oai/bedrock.py | 8 ++++---- autogen/oai/cohere.py | 8 ++++---- autogen/oai/gemini.py | 4 +--- autogen/oai/ollama.py | 8 ++++---- autogen/oai/openai_responses.py | 4 +++- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/autogen/oai/bedrock.py b/autogen/oai/bedrock.py index a0c2a981335..cb0c353f7b1 100644 --- a/autogen/oai/bedrock.py +++ b/autogen/oai/bedrock.py @@ -436,10 +436,10 @@ def create(self, params) -> ChatCompletion: # Handle response_format for structured outputs, check if agent_config has a response_format else has_response_format = ( agent_config["response_format"] - if "response_format" in agent_config and agent_config["response_format"] is not None - else self._response_format - if self._response_format is not None - else None + if agent_config is not None + and "response_format" in agent_config + and agent_config["response_format"] is not None + else params.get("response_format", self._response_format if self._response_format is not None else None) ) if has_response_format: structured_output_tool = self._create_structured_output_tool(has_response_format) diff --git a/autogen/oai/cohere.py b/autogen/oai/cohere.py index 114fede4ab9..61a72a5e934 100644 --- a/autogen/oai/cohere.py +++ b/autogen/oai/cohere.py @@ -251,10 +251,10 @@ def create(self, params: dict) -> ChatCompletion: cohere_params = self.parse_params(params) self._response_format = ( agent_config["response_format"] - if "response_format" in agent_config and agent_config["response_format"] is not None - else params.get("response_format") - if params.get("response_format") is not None - else self._response_format + if agent_config is not None + and "response_format" in agent_config + and agent_config["response_format"] is not None + else params.get("response_format", self._response_format if self._response_format is not None else None) ) cohere_params["messages"] = messages diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index 49ed61ad06e..fa0737812e5 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -306,9 +306,7 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: if agent_config is not None and "response_format" in agent_config and agent_config["response_format"] is not None - else params.get("response_format") - if params.get("response_format") is not None - else self._response_format + else params.get("response_format", self._response_format if self._response_format is not None else None) ) generation_config = { gemini_term: params[autogen_term] diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index 20b980af8ea..ed9a7013a1c 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -239,10 +239,10 @@ def create(self, params: dict) -> ChatCompletion: self._response_format = ( agent_config["response_format"] - if "response_format" in agent_config and agent_config["response_format"] is not None - else params.get("response_format") - if params.get("response_format") is not None - else None + if agent_config is not None + and "response_format" in agent_config + and agent_config["response_format"] is not None + else params.get("response_format", self._response_format if self._response_format is not None else None) ) # Are tools involved in this conversation? diff --git a/autogen/oai/openai_responses.py b/autogen/oai/openai_responses.py index 32c1b014b64..925c6a53df8 100644 --- a/autogen/oai/openai_responses.py +++ b/autogen/oai/openai_responses.py @@ -563,7 +563,9 @@ def create(self, params: dict[str, Any]) -> "Response": self.response_format = ( agent_config["response_format"] - if "response_format" in agent_config and agent_config["response_format"] is not None + if agent_config is not None + and "response_format" in agent_config + and agent_config["response_format"] is not None else self.response_format if self.response_format is not None else params.get("response_format") From 751fb04c3bf3190114de32a3d7fa57bd90e58a8a Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:15:16 +0530 Subject: [PATCH 10/30] fix: anthropic agent_config --- autogen/oai/anthropic.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index 6d97696393e..2d8a548cb2d 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -769,8 +769,10 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: logger.info(f"Agent config: {agent_config}") response_format = ( agent_config["response_format"] - if "response_format" in agent_config and agent_config["response_format"] is not None - else params.get("response_format", self._response_format if self.response_format is not None else None) + if agent_config is not None + and "response_format" in agent_config + and agent_config["response_format"] is not None + else params.get("response_format", self._response_format if self._response_format is not None else None) ) # Route to appropriate implementation based on model and response_format if response_format: From 96ac71f9c3d915c643621fe08058e141207f6949 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sat, 27 Dec 2025 01:17:47 +0530 Subject: [PATCH 11/30] test: bedrock agent config --- autogen/oai/agent_config_handler.py | 6 +- autogen/oai/bedrock.py | 5 +- test/oai/test_agent_config_handler.py | 166 +++++++ test/oai/test_bedrock.py | 620 ++++++++++++++++++++++++++ 4 files changed, 791 insertions(+), 6 deletions(-) create mode 100644 test/oai/test_agent_config_handler.py diff --git a/autogen/oai/agent_config_handler.py b/autogen/oai/agent_config_handler.py index 68a00cdb7af..4298c6aa4c7 100644 --- a/autogen/oai/agent_config_handler.py +++ b/autogen/oai/agent_config_handler.py @@ -9,9 +9,7 @@ def agent_config_parser(agent: "ConversableAgent") -> dict[str, Any]: - agent_config: dict[str, Any] = [] + agent_config: dict[str, Any] = {} if hasattr(agent, "response_format") and agent.response_format is not None: - agent_config.append({ - "response_format": agent.response_format, - }) + agent_config["response_format"] = agent.response_format return agent_config diff --git a/autogen/oai/bedrock.py b/autogen/oai/bedrock.py index cb0c353f7b1..47810661b2c 100644 --- a/autogen/oai/bedrock.py +++ b/autogen/oai/bedrock.py @@ -435,13 +435,14 @@ def create(self, params) -> ChatCompletion: # Handle response_format for structured outputs, check if agent_config has a response_format else has_response_format = ( - agent_config["response_format"] + agent_config.get("response_format") if agent_config is not None and "response_format" in agent_config - and agent_config["response_format"] is not None + and agent_config.get("response_format") is not None else params.get("response_format", self._response_format if self._response_format is not None else None) ) if has_response_format: + self._response_format = has_response_format structured_output_tool = self._create_structured_output_tool(has_response_format) # Merge with user tools if any user_tools = params.get("tools", []) diff --git a/test/oai/test_agent_config_handler.py b/test/oai/test_agent_config_handler.py new file mode 100644 index 00000000000..d639f609a80 --- /dev/null +++ b/test/oai/test_agent_config_handler.py @@ -0,0 +1,166 @@ +# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors +# +# SPDX-License-Identifier: Apache-2.0 +# +# Portions derived from https://github.com/microsoft/autogen are under the MIT License. +# SPDX-License-Identifier: MIT +# !/usr/bin/env python3 -m pytest + +from unittest.mock import MagicMock + +from pydantic import BaseModel + +from autogen.oai.agent_config_handler import agent_config_parser + + +class TestAgentConfigParser: + """Test suite for agent_config_parser function.""" + + def test_agent_without_response_format_attribute(self): + """Test agent that doesn't have response_format attribute.""" + agent = MagicMock() + # Remove response_format if it exists + if hasattr(agent, "response_format"): + delattr(agent, "response_format") + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result == {} + + def test_agent_with_response_format_none(self): + """Test agent with response_format set to None.""" + agent = MagicMock() + agent.response_format = None + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result == {} + + def test_agent_with_response_format_dict(self): + """Test agent with response_format as a dictionary.""" + agent = MagicMock() + response_format = {"type": "json_object"} + agent.response_format = response_format + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result == {"response_format": response_format} + + def test_agent_with_response_format_pydantic_model(self): + """Test agent with response_format as a Pydantic BaseModel.""" + + class TestModel(BaseModel): + name: str + age: int + + agent = MagicMock() + agent.response_format = TestModel + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result["response_format"] == TestModel + + def test_agent_with_response_format_string(self): + """Test agent with response_format as a string.""" + agent = MagicMock() + response_format = "json_object" + agent.response_format = response_format + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result == {"response_format": response_format} + + def test_agent_with_response_format_complex_dict(self): + """Test agent with response_format as a complex dictionary.""" + agent = MagicMock() + response_format = { + "type": "json_schema", + "json_schema": { + "name": "test_schema", + "schema": {"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}}, + }, + } + agent.response_format = response_format + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result == {"response_format": response_format} + + def test_agent_with_response_format_empty_dict(self): + """Test agent with response_format as an empty dictionary.""" + agent = MagicMock() + agent.response_format = {} + + result = agent_config_parser(agent) + # Empty dict is not None, so it should be included + assert isinstance(result, dict) + assert result == {"response_format": {}} + + def test_agent_with_response_format_false(self): + """Test agent with response_format set to False (falsy but not None).""" + agent = MagicMock() + agent.response_format = False + + result = agent_config_parser(agent) + # False is not None, so it should be included + assert isinstance(result, dict) + assert result == {"response_format": False} + + def test_agent_with_response_format_zero(self): + """Test agent with response_format set to 0 (falsy but not None).""" + agent = MagicMock() + agent.response_format = 0 + + result = agent_config_parser(agent) + # 0 is not None, so it should be included + assert isinstance(result, dict) + assert result == {"response_format": 0} + + def test_real_conversable_agent_without_response_format(self): + """Test with a real ConversableAgent instance without response_format.""" + from autogen.agentchat.conversable_agent import ConversableAgent + + agent = ConversableAgent(name="test_agent", llm_config=False) + # Ensure response_format doesn't exist or is None + if hasattr(agent, "response_format"): + agent.response_format = None + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result == {} + + def test_real_conversable_agent_with_response_format(self): + """Test with a real ConversableAgent instance with response_format.""" + from autogen.agentchat.conversable_agent import ConversableAgent + + agent = ConversableAgent(name="test_agent", llm_config=False) + response_format = {"type": "json_object"} + agent.response_format = response_format + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result == {"response_format": response_format} + + def test_real_conversable_agent_with_pydantic_response_format(self): + """Test with a real ConversableAgent instance with Pydantic BaseModel response_format.""" + from autogen.agentchat.conversable_agent import ConversableAgent + + # Define a nested Pydantic model similar to what's used in other tests + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + result = agent_config_parser(agent) + assert isinstance(result, dict) + assert result["response_format"] == MathReasoning + # Verify it's the correct class + assert issubclass(result["response_format"], BaseModel) + # Verify the model structure + assert hasattr(result["response_format"], "model_json_schema") diff --git a/test/oai/test_bedrock.py b/test/oai/test_bedrock.py index 18538835938..4c1a1f26000 100644 --- a/test/oai/test_bedrock.py +++ b/test/oai/test_bedrock.py @@ -9,6 +9,7 @@ import pytest from pydantic import BaseModel, ValidationError +from autogen.agentchat.conversable_agent import ConversableAgent from autogen.import_utils import run_for_optional_imports from autogen.llm_config import LLMConfig from autogen.oai.bedrock import BedrockClient, BedrockLLMConfigEntry, oai_messages_to_bedrock_messages @@ -1505,3 +1506,622 @@ def test_agent_with_dict_schema_structured_output(self): tc for tc in tool_calls if tc.get("function", {}).get("name") == "__structured_output" ] assert len(structured_output_tools) > 0, "Should have __structured_output tool call" + + +# Test agent_config response_format functionality +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_with_agent_config_response_format(bedrock_client: BedrockClient): + """Test that agent_config response_format takes precedence over params response_format.""" + # Create real agent with response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": { + "steps": [{"explanation": "Step 1", "output": "Result"}], + "final_answer": "Answer", + }, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + params = { + "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify agent_config response_format was used (MathReasoning, not the dict in params) + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + assert "tools" in tool_config + + # Check that the tool is for MathReasoning (should have steps and final_answer) + tools = tool_config["tools"] + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + # Verify it's MathReasoning schema, not the simple dict from params + tool_spec = structured_output_tool["toolSpec"] + input_schema = tool_spec.get("inputSchema", {}) + json_schema = input_schema.get("json", {}) + properties = json_schema.get("properties", {}) + assert "steps" in properties # MathReasoning has steps + assert "final_answer" in properties # MathReasoning has final_answer + assert "name" not in properties # Not the simple dict from params + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_with_agent_config_response_format_overrides_client_format(bedrock_client: BedrockClient): + """Test that agent_config response_format takes precedence over client._response_format (from llm_config).""" + # Set client response_format (simulating it being set from llm_config) + bedrock_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": { + "steps": [{"explanation": "Step 1", "output": "Result"}], + "final_answer": "Answer", + }, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + params = { + "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify agent_config response_format was used (MathReasoning), not client._response_format + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + tools = tool_config["tools"] + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + # Verify it's MathReasoning (has steps and final_answer), not the age schema + tool_spec = structured_output_tool["toolSpec"] + input_schema = tool_spec.get("inputSchema", {}) + json_schema = input_schema.get("json", {}) + properties = json_schema.get("properties", {}) + assert "steps" in properties + assert "final_answer" in properties + assert "age" not in properties # Not the client._response_format schema + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_with_agent_config_response_format_overrides_params_and_client_format(bedrock_client: BedrockClient): + """Test that agent_config response_format takes precedence over both params and client._response_format.""" + # Set client response_format (from llm_config) + bedrock_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": { + "steps": [{"explanation": "Step 1", "output": "Result"}], + "final_answer": "Answer", + }, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + # Both params and client have response_format, but agent should override both + params = { + "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify agent_config response_format was used (MathReasoning), not params or client._response_format + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + tools = tool_config["tools"] + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + # Verify it's MathReasoning + tool_spec = structured_output_tool["toolSpec"] + input_schema = tool_spec.get("inputSchema", {}) + json_schema = input_schema.get("json", {}) + properties = json_schema.get("properties", {}) + assert "steps" in properties + assert "final_answer" in properties + assert "age" not in properties # Not client._response_format + assert "name" not in properties # Not params response_format + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_with_agent_config_response_format_and_user_tools(bedrock_client: BedrockClient): + """Test that agent_config response_format works correctly with user-provided tools.""" + # Create real agent with response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": { + "steps": [{"explanation": "Step 1", "output": "Result"}], + "final_answer": "Answer", + }, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + user_tools = [ + { + "type": "function", + "function": { + "name": "get_weather", + "description": "Get weather information", + "parameters": { + "type": "object", + "properties": {"location": {"type": "string"}}, + "required": ["location"], + }, + }, + } + ] + + params = { + "messages": [{"role": "user", "content": "Get weather and format response"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "tools": user_tools, + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify both structured output tool and user tool are present + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + tools = tool_config["tools"] + + # Should have both tools + assert len(tools) == 2, f"Expected 2 tools, got {len(tools)}" + + # Check for structured output tool + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + + # Check for user tool + user_tool = next((t for t in tools if t.get("toolSpec", {}).get("name") == "get_weather"), None) + assert user_tool is not None + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_with_agent_no_response_format_falls_back_to_params(bedrock_client: BedrockClient): + """Test that when agent has no response_format, it falls back to params response_format.""" + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": {"name": "John", "age": 30}, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + "required": ["name"], + } + + params = { + "messages": [{"role": "user", "content": "Create a profile"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "response_format": dict_schema, + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify params response_format was used (dict_schema, not MathReasoning) + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + tools = tool_config["tools"] + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + # Verify it's the dict_schema (has name and age), not MathReasoning + tool_spec = structured_output_tool["toolSpec"] + input_schema = tool_spec.get("inputSchema", {}) + json_schema = input_schema.get("json", {}) + properties = json_schema.get("properties", {}) + assert "name" in properties + assert "age" in properties + assert "steps" not in properties # Not MathReasoning + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_with_agent_no_response_format_falls_back_to_client_format(bedrock_client: BedrockClient): + """Test that when agent has no response_format and params has none, it falls back to client._response_format.""" + # Set client response_format (from llm_config) + bedrock_client._response_format = MathReasoning + + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": { + "steps": [{"explanation": "Step 1", "output": "Result"}], + "final_answer": "Answer", + }, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + params = { + "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify client._response_format was used (MathReasoning) + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + tools = tool_config["tools"] + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + # Verify it's MathReasoning + tool_spec = structured_output_tool["toolSpec"] + input_schema = tool_spec.get("inputSchema", {}) + json_schema = input_schema.get("json", {}) + properties = json_schema.get("properties", {}) + assert "steps" in properties + assert "final_answer" in properties + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_with_agent_response_format_none_ignores_agent(bedrock_client: BedrockClient): + """Test that when agent.response_format is None, it's ignored and falls back to params.""" + # Create real agent with response_format=None + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = None + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": {"name": "John"}, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + } + + params = { + "messages": [{"role": "user", "content": "Create a profile"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "response_format": dict_schema, + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify params response_format was used (agent.response_format=None should be ignored) + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + tools = tool_config["tools"] + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + # Verify it's the dict_schema from params + tool_spec = structured_output_tool["toolSpec"] + input_schema = tool_spec.get("inputSchema", {}) + json_schema = input_schema.get("json", {}) + properties = json_schema.get("properties", {}) + assert "name" in properties + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +def test_create_without_agent_uses_params_response_format(bedrock_client: BedrockClient): + """Test that when no agent is provided, params response_format is used (existing behavior).""" + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + # Mock Bedrock response + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": {"email": "test@example.com"}, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + dict_schema = { + "type": "object", + "properties": {"email": {"type": "string"}}, + "required": ["email"], + } + + params = { + "messages": [{"role": "user", "content": "Create contact"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "response_format": dict_schema, + # No agent parameter + } + + response = bedrock_client.create(params) + + # Verify params response_format was used + call_args = mock_bedrock_runtime.converse.call_args + assert call_args is not None, "converse should have been called" + assert "toolConfig" in call_args.kwargs + tool_config = call_args.kwargs["toolConfig"] + tools = tool_config["tools"] + structured_output_tool = next( + (t for t in tools if t.get("toolSpec", {}).get("name") == "__structured_output"), None + ) + assert structured_output_tool is not None + # Verify it's the dict_schema from params + tool_spec = structured_output_tool["toolSpec"] + input_schema = tool_spec.get("inputSchema", {}) + json_schema = input_schema.get("json", {}) + properties = json_schema.get("properties", {}) + assert "email" in properties + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +@patch("autogen.oai.bedrock.agent_config_parser") +def test_agent_config_parser_called_with_agent(mock_parser, bedrock_client: BedrockClient): + """Test that agent_config_parser is called when agent is provided.""" + # Create real agent + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Mock agent_config_parser to return expected format + mock_parser.return_value = {"response_format": MathReasoning} + + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + mock_response = { + "stopReason": "tool_use", + "output": { + "message": { + "content": [ + { + "toolUse": { + "toolUseId": "tool_123", + "name": "__structured_output", + "input": { + "steps": [{"explanation": "Step 1", "output": "Result"}], + "final_answer": "Answer", + }, + } + } + ] + } + }, + "usage": {"inputTokens": 50, "outputTokens": 30, "totalTokens": 80}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + params = { + "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + "agent": agent, + } + + response = bedrock_client.create(params) + + # Verify agent_config_parser was called with the agent + mock_parser.assert_called_once_with(agent) + + +@run_for_optional_imports(["boto3", "botocore"], "bedrock") +@patch("autogen.oai.bedrock.agent_config_parser") +def test_agent_config_parser_not_called_without_agent(mock_parser, bedrock_client: BedrockClient): + """Test that agent_config_parser is not called when agent is not provided.""" + # Mock bedrock_runtime + mock_bedrock_runtime = MagicMock() + bedrock_client.bedrock_runtime = mock_bedrock_runtime + + mock_response = { + "stopReason": "finished", + "output": {"message": {"content": [{"text": "Response"}]}}, + "usage": {"inputTokens": 50, "outputTokens": 20, "totalTokens": 70}, + "ResponseMetadata": {"RequestId": "test-request-id"}, + } + mock_bedrock_runtime.converse.return_value = mock_response + + params = { + "messages": [{"role": "user", "content": "Hello"}], + "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", + # No agent parameter + } + + response = bedrock_client.create(params) + + # Verify agent_config_parser was not called + mock_parser.assert_not_called() From fa2f448401ba208382af8e70d95562201bfd3c87 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sat, 27 Dec 2025 03:03:20 +0530 Subject: [PATCH 12/30] test: ollama agent config --- autogen/oai/ollama.py | 20 ++- test/oai/test_ollama.py | 296 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 4 deletions(-) diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index ed9a7013a1c..049fb4f662f 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -238,10 +238,10 @@ def create(self, params: dict) -> ChatCompletion: messages = params.get("messages", []) self._response_format = ( - agent_config["response_format"] + agent_config.get("response_format") if agent_config is not None and "response_format" in agent_config - and agent_config["response_format"] is not None + and agent_config.get("response_format") is not None else params.get("response_format", self._response_format if self._response_format is not None else None) ) @@ -525,7 +525,14 @@ def oai_messages_to_ollama_messages(self, messages: list[dict[str, Any]], tools: del ollama_messages[-1] # Ensure the last message is a user / system message, if not, add a user message - if ollama_messages[-1]["role"] != "user" and ollama_messages[-1]["role"] != "system": + if ( + len(ollama_messages) > 0 + and ollama_messages[-1]["role"] != "user" + and ollama_messages[-1]["role"] != "system" + ): + ollama_messages.append({"role": "user", "content": "Please continue."}) + elif len(ollama_messages) == 0: + # If no messages, add a default user message ollama_messages.append({"role": "user", "content": "Please continue."}) return ollama_messages @@ -554,7 +561,12 @@ def _convert_json_response(self, response: str) -> Any: def _format_json_response(response: Any, original_answer: str) -> str: """Formats the JSON response for structured outputs using the format method if it exists.""" - return response.format() if isinstance(response, FormatterProtocol) else original_answer + if isinstance(response, str): + return response + elif isinstance(response, FormatterProtocol): + return response.format() + else: + return original_answer @require_optional_import("fix_busted_json", "ollama") diff --git a/test/oai/test_ollama.py b/test/oai/test_ollama.py index 89fe6c064da..5eebf1b75e8 100644 --- a/test/oai/test_ollama.py +++ b/test/oai/test_ollama.py @@ -489,3 +489,299 @@ def test_extract_json_response_params(ollama_client): assert isinstance(ollama_params["format"], dict) assert ollama_params["format"] == converted_dict + + +# Add this import at the top with other imports +from autogen.agentchat.conversable_agent import ConversableAgent + +# Add these tests after the existing response_format tests (around line 492) + + +# Test agent_config response_format functionality +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +def test_create_with_agent_config_response_format(ollama_client): + """Test that agent_config response_format takes precedence over params response_format.""" + # Create real agent with response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + } + + # Call create to set _response_format from agent_config + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"steps": [], "final_answer": "test"}'}, + "created_at": "mock_response_id_123", # Add this + "prompt_eval_count": 0, # Add this + "eval_count": 0, # Add this + } + # Remove "done": True - it's not used + ollama_client.create(params) + + # Verify agent_config response_format was used (MathReasoning, not the dict in params) + ollama_params = ollama_client.parse_params({"model": "llama3.1:8b"}) + assert "format" in ollama_params + # MathReasoning schema should have steps and final_answer properties + format_schema = ollama_params["format"] + assert "properties" in format_schema + assert "steps" in format_schema["properties"] + assert "final_answer" in format_schema["properties"] + assert "name" not in format_schema["properties"] # Not the simple dict from params + + +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +def test_create_with_agent_config_response_format_overrides_client_format(ollama_client): + """Test that agent_config response_format takes precedence over client._response_format (from llm_config).""" + # Set client response_format (simulating it being set from llm_config) + ollama_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "agent": agent, + } + + # Call create to set _response_format from agent_config + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"steps": [], "final_answer": "test"}'}, + "created_at": "mock_response_id_123", + "prompt_eval_count": 0, + "eval_count": 0, + } + ollama_client.create(params) + + # Verify agent_config response_format was used (MathReasoning), not client._response_format + ollama_params = ollama_client.parse_params({"model": "llama3.1:8b"}) + assert "format" in ollama_params + format_schema = ollama_params["format"] + assert "properties" in format_schema + assert "steps" in format_schema["properties"] + assert "final_answer" in format_schema["properties"] + assert "age" not in format_schema["properties"] # Not the client._response_format schema + + +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +def test_create_with_agent_config_response_format_overrides_params_and_client_format(ollama_client): + """Test that agent_config response_format takes precedence over both params and client._response_format.""" + # Set client response_format (from llm_config) + ollama_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Both params and client have response_format, but agent should override both + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + } + + # Call create to set _response_format from agent_config + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"steps": [], "final_answer": "test"}'}, + "created_at": "mock_response_id_123", + "prompt_eval_count": 0, + "eval_count": 0, + } + ollama_client.create(params) + + # Verify agent_config response_format was used (MathReasoning), not params or client._response_format + ollama_params = ollama_client.parse_params({"model": "llama3.1:8b"}) + assert "format" in ollama_params + format_schema = ollama_params["format"] + assert "properties" in format_schema + assert "steps" in format_schema["properties"] + assert "final_answer" in format_schema["properties"] + assert "age" not in format_schema["properties"] # Not client._response_format + assert "name" not in format_schema["properties"] # Not params response_format + + +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +def test_create_with_agent_no_response_format_falls_back_to_params(ollama_client): + """Test that when agent has no response_format, it falls back to params response_format.""" + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + "required": ["name"], + } + + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "response_format": dict_schema, + "agent": agent, + } + + # Call create to set _response_format from params + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"name": "John", "age": 30}'}, + "created_at": "mock_response_id_123", + "prompt_eval_count": 0, + "eval_count": 0, + } + ollama_client.create(params) + + # Verify params response_format was used (dict_schema, not MathReasoning) + ollama_params = ollama_client.parse_params({"model": "llama3.1:8b"}) + assert "format" in ollama_params + format_schema = ollama_params["format"] + assert format_schema == dict_schema + assert "name" in format_schema["properties"] + assert "age" in format_schema["properties"] + assert "steps" not in format_schema["properties"] # Not MathReasoning + + +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +def test_create_with_agent_no_response_format_falls_back_to_client_format(ollama_client): + """Test that when agent has no response_format and params has none, it falls back to client._response_format.""" + # Set client response_format (from llm_config) + ollama_client._response_format = MathReasoning + + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "agent": agent, + } + + # Call create to set _response_format from client._response_format + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"steps": [], "final_answer": "test"}'}, + "created_at": "mock_response_id_123", + "prompt_eval_count": 0, + "eval_count": 0, + } + ollama_client.create(params) + + # Verify client._response_format was used (MathReasoning) + ollama_params = ollama_client.parse_params({"model": "llama3.1:8b"}) + assert "format" in ollama_params + format_schema = ollama_params["format"] + assert "properties" in format_schema + assert "steps" in format_schema["properties"] + assert "final_answer" in format_schema["properties"] + + +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +def test_create_with_agent_response_format_none_ignores_agent(ollama_client): + """Test that when agent.response_format is None, it's ignored and falls back to params.""" + # Create real agent with response_format=None + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = None + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + } + + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "response_format": dict_schema, + "agent": agent, + } + + # Call create to set _response_format from params (agent.response_format=None should be ignored) + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"name": "John"}'}, + "created_at": "mock_response_id_123", + "prompt_eval_count": 0, + "eval_count": 0, + } + ollama_client.create(params) + + # Verify params response_format was used (agent.response_format=None should be ignored) + ollama_params = ollama_client.parse_params({"model": "llama3.1:8b"}) + assert "format" in ollama_params + format_schema = ollama_params["format"] + assert format_schema == dict_schema + assert "name" in format_schema["properties"] + + +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +def test_create_without_agent_uses_params_response_format(ollama_client): + """Test that when no agent is provided, params response_format is used (existing behavior).""" + dict_schema = { + "type": "object", + "properties": {"email": {"type": "string"}}, + "required": ["email"], + } + + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "response_format": dict_schema, + # No agent parameter + } + + # Call create to set _response_format from params + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"email": "test@example.com"}'}, + "created_at": "mock_response_id_123", + "prompt_eval_count": 0, + "eval_count": 0, + } + ollama_client.create(params) + + # Verify params response_format was used + ollama_params = ollama_client.parse_params({"model": "llama3.1:8b"}) + assert "format" in ollama_params + format_schema = ollama_params["format"] + assert format_schema == dict_schema + assert "email" in format_schema["properties"] + + +@run_for_optional_imports(["ollama", "fix_busted_json"], "ollama") +@patch("autogen.oai.ollama.agent_config_parser") +def test_agent_config_parser_called_with_agent(mock_parser, ollama_client): + """Test that agent_config_parser is called when agent is provided.""" + # Create real agent + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Mock agent_config_parser to return expected format + mock_parser.return_value = {"response_format": MathReasoning} + + params = { + "model": "llama3.1:8b", + "messages": [{"role": "user", "content": "Test message"}], # Add this + "agent": agent, + } + + # Call create + with patch("autogen.oai.ollama.ollama") as mock_ollama: + mock_ollama.chat.return_value = { + "message": {"content": '{"steps": [], "final_answer": "test"}'}, + "created_at": "mock_response_id_123", + "prompt_eval_count": 0, + "eval_count": 0, + } + ollama_client.create(params) + + # Verify agent_config_parser was called with the agent + mock_parser.assert_called_once_with(agent) From f35207598b1988f28fcf01348bf79c474fc002a8 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sat, 27 Dec 2025 04:05:28 +0530 Subject: [PATCH 13/30] test: gemini agent config --- autogen/oai/gemini.py | 4 +- test/oai/test_gemini.py | 705 ++++++++++++++++++++++++++-------------- 2 files changed, 467 insertions(+), 242 deletions(-) diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index fa0737812e5..c5a3731680b 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -302,10 +302,10 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: thinking_level=thinking_level, ) self._response_format = ( - agent_config["response_format"] + agent_config.get("response_format") if agent_config is not None and "response_format" in agent_config - and agent_config["response_format"] is not None + and agent_config.get("response_format") is not None else params.get("response_format", self._response_format if self._response_format is not None else None) ) generation_config = { diff --git a/test/oai/test_gemini.py b/test/oai/test_gemini.py index 003878e730d..11f068730d7 100644 --- a/test/oai/test_gemini.py +++ b/test/oai/test_gemini.py @@ -13,6 +13,7 @@ import pytest from pydantic import BaseModel +from autogen.agentchat.conversable_agent import ConversableAgent from autogen.import_utils import optional_import_block, run_for_optional_imports from autogen.llm_config import LLMConfig from autogen.oai.gemini import GeminiClient, GeminiLLMConfigEntry @@ -480,6 +481,470 @@ class MathReasoning(BaseModel): ): gemini_client._convert_json_response(no_json_response) + +# Test agent_config response_format functionality (outside TestGeminiClient class) +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +def test_create_with_agent_config_response_format(): + """Test that agent_config response_format takes precedence over params response_format.""" + # Create client directly (not using fixture) + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Create real agent with response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + } + + # Call create to set _response_format from agent_config + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"steps": [], "final_answer": "test"}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify agent_config response_format was used (MathReasoning, not the dict in params) + assert gemini_client._response_format == MathReasoning + # Verify it's MathReasoning schema, not the simple dict from params + assert hasattr(gemini_client._response_format, "model_json_schema") + schema = gemini_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + assert "name" not in schema["properties"] # Not the simple dict from params + + +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +def test_create_with_agent_config_response_format_overrides_client_format(): + """Test that agent_config response_format takes precedence over client._response_format (from llm_config).""" + # Create client directly + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Set client response_format (simulating it being set from llm_config) + gemini_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "agent": agent, + } + + # Call create to set _response_format from agent_config + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"steps": [], "final_answer": "test"}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify agent_config response_format was used (MathReasoning), not client._response_format + assert gemini_client._response_format == MathReasoning + assert hasattr(gemini_client._response_format, "model_json_schema") + schema = gemini_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + assert "age" not in schema["properties"] # Not the client._response_format schema + + +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +def test_create_with_agent_config_response_format_overrides_params_and_client_format(): + """Test that agent_config response_format takes precedence over both params and client._response_format.""" + # Create client directly + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Set client response_format (from llm_config) + gemini_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Both params and client have response_format, but agent should override both + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + } + + # Call create to set _response_format from agent_config + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"steps": [], "final_answer": "test"}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify it's MathReasoning + assert gemini_client._response_format == MathReasoning + schema = gemini_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + assert "age" not in schema["properties"] # Not client._response_format + assert "name" not in schema["properties"] # Not params response_format + + +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +def test_create_with_agent_no_response_format_falls_back_to_params(): + """Test that when agent has no response_format, it falls back to params response_format.""" + # Create client directly + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + "required": ["name"], + } + + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": dict_schema, + "agent": agent, + } + + # Call create to set _response_format from params + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"name": "John", "age": 30}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify it's the dict_schema from params + assert gemini_client._response_format == dict_schema + assert isinstance(gemini_client._response_format, dict) + assert "name" in gemini_client._response_format["properties"] + + +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +def test_create_with_agent_no_response_format_falls_back_to_client_format(): + """Test that when agent has no response_format and params has none, it falls back to client._response_format.""" + # Create client directly + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Set client response_format (from llm_config) + gemini_client._response_format = MathReasoning + + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "agent": agent, + } + + # Call create to set _response_format from client._response_format + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"steps": [], "final_answer": "test"}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify it's MathReasoning (has steps and final_answer), not the age schema + assert gemini_client._response_format == MathReasoning + schema = gemini_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + + +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +def test_create_with_agent_response_format_none_ignores_agent(): + """Test that when agent.response_format is None, it's ignored and falls back to params.""" + # Create client directly + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + # Create real agent with response_format=None + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = None + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + } + + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": dict_schema, + "agent": agent, + } + + # Call create to set _response_format from params (agent.response_format=None should be ignored) + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"name": "John"}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify it's the dict_schema from params + assert gemini_client._response_format == dict_schema + assert isinstance(gemini_client._response_format, dict) + assert "name" in gemini_client._response_format["properties"] + + +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +def test_create_without_agent_uses_params_response_format(): + """Test that when no agent is provided, params response_format is used (existing behavior).""" + # Create client directly + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + dict_schema = { + "type": "object", + "properties": {"email": {"type": "string"}}, + "required": ["email"], + } + + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": dict_schema, + # No agent parameter + } + + # Call create to set _response_format from params + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"email": "test@example.com"}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify it's the dict_schema from params + assert gemini_client._response_format == dict_schema + assert isinstance(gemini_client._response_format, dict) + assert "email" in gemini_client._response_format["properties"] + + +@run_for_optional_imports(["vertexai", "PIL", "google.auth", "google.api", "google.cloud", "google.genai"], "gemini") +@patch("autogen.oai.gemini.agent_config_parser") +def test_agent_config_parser_called_with_agent(mock_parser): + """Test that agent_config_parser is called when agent is provided.""" + # Create client directly + system_message = ["You are a helpful AI assistant."] + gemini_client = GeminiClient(api_key="fake_api_key", system_message=system_message) + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Create real agent + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Mock agent_config_parser to return expected format + mock_parser.return_value = {"response_format": MathReasoning} + + params = { + "model": "gemini-pro", + "messages": [{"role": "user", "content": "Test message"}], + "agent": agent, + } + + # Call create + with patch("autogen.oai.gemini.genai.Client") as mock_generative_client: + mock_chat = MagicMock() + mock_generative_client.return_value.chats.create.return_value = mock_chat + + mock_text_part = MagicMock() + mock_text_part.text = '{"steps": [], "final_answer": "test"}' + mock_text_part.function_call = None + + mock_usage_metadata = MagicMock() + mock_usage_metadata.prompt_token_count = 10 + mock_usage_metadata.candidates_token_count = 5 + + mock_candidate = MagicMock() + mock_candidate.content.parts = [mock_text_part] + + mock_response = MagicMock(spec=GenerateContentResponse) + mock_response.usage_metadata = mock_usage_metadata + mock_response.candidates = [mock_candidate] + + mock_chat.send_message.return_value = mock_response + + gemini_client.create(params) + + # Verify agent_config_parser was called with the agent + mock_parser.assert_called_once_with(agent) + @pytest.fixture def nested_function_parameters(self) -> dict[str, Any]: return { @@ -745,246 +1210,6 @@ def test_generation_config_with_seed(self, mock_generate_content_config, mock_ge """Test that seed parameter is properly passed to generation config""" # Mock setup mock_chat = MagicMock() - mock_generative_client.return_value.chats.create.return_value = mock_chat - - mock_text_part = MagicMock() - mock_text_part.text = "Test response" - mock_text_part.function_call = None - - mock_usage_metadata = MagicMock() - mock_usage_metadata.prompt_token_count = 10 - mock_usage_metadata.candidates_token_count = 5 - - mock_candidate = MagicMock() - mock_candidate.content.parts = [mock_text_part] - - mock_response = MagicMock(spec=GenerateContentResponse) - mock_response.usage_metadata = mock_usage_metadata - mock_response.candidates = [mock_candidate] - - mock_chat.send_message.return_value = mock_response - - # Call create with seed parameter - gemini_client.create({ - "model": "gemini-pro", - "messages": [{"content": "Hello", "role": "user"}], - "seed": 42, - "temperature": 0.7, - "max_tokens": 100, - "top_p": 0.9, - "top_k": 5, - }) - - # Verify GenerateContentConfig was called with correct parameters - mock_generate_content_config.assert_called_once() - call_kwargs = mock_generate_content_config.call_args.kwargs - - # Check that generation config parameters are correctly mapped - assert call_kwargs["seed"] == 42, "Seed parameter should be passed to generation config" - assert call_kwargs["temperature"] == 0.7, "Temperature parameter should be passed to generation config" - assert call_kwargs["max_output_tokens"] == 100, "max_tokens should be mapped to max_output_tokens" - assert call_kwargs["top_p"] == 0.9, "top_p parameter should be passed to generation config" - assert call_kwargs["top_k"] == 5, "top_k parameter should be passed to generation config" - - @patch("autogen.oai.gemini.genai.Client") - @patch("autogen.oai.gemini.GenerateContentConfig") - @patch("autogen.oai.gemini.ThinkingConfig") - def test_generation_config_with_thinking_config( - self, mock_thinking_config, mock_generate_content_config, mock_generative_client, gemini_client - ): - """Test that thinking parameters are properly passed to generation config""" - mock_chat = MagicMock() - mock_generative_client.return_value.chats.create.return_value = mock_chat - - mock_text_part = MagicMock() - mock_text_part.text = "Thoughtful response" - mock_text_part.function_call = None - - mock_usage_metadata = MagicMock() - mock_usage_metadata.prompt_token_count = 12 - mock_usage_metadata.candidates_token_count = 6 - - mock_candidate = MagicMock() - mock_candidate.content.parts = [mock_text_part] - - mock_response = MagicMock(spec=GenerateContentResponse) - mock_response.usage_metadata = mock_usage_metadata - mock_response.candidates = [mock_candidate] - - mock_chat.send_message.return_value = mock_response - - gemini_client.create({ - "model": "gemini-pro", - "messages": [{"content": "Hello", "role": "user"}], - "include_thoughts": True, - "thinking_budget": 1024, - "thinking_level": "High", - }) - - mock_thinking_config.assert_called_once_with( - include_thoughts=True, - thinking_budget=1024, - thinking_level="High", - ) - - config_kwargs = mock_generate_content_config.call_args.kwargs - assert config_kwargs["thinking_config"] == mock_thinking_config.return_value, ( - "thinking_config should be passed to GenerateContentConfig" - ) - - @patch("autogen.oai.gemini.genai.Client") - @patch("autogen.oai.gemini.GenerateContentConfig") - @patch("autogen.oai.gemini.ThinkingConfig") - def test_generation_config_with_default_thinking_config( - self, mock_thinking_config, mock_generate_content_config, mock_generative_client, gemini_client - ): - """Test that a default ThinkingConfig is created and passed when no thinking params are provided""" - mock_chat = MagicMock() - mock_generative_client.return_value.chats.create.return_value = mock_chat - - mock_text_part = MagicMock() - mock_text_part.text = "Response" - mock_text_part.function_call = None - - mock_usage_metadata = MagicMock() - mock_usage_metadata.prompt_token_count = 5 - mock_usage_metadata.candidates_token_count = 3 - - mock_candidate = MagicMock() - mock_candidate.content.parts = [mock_text_part] - - mock_response = MagicMock(spec=GenerateContentResponse) - mock_response.usage_metadata = mock_usage_metadata - mock_response.candidates = [mock_candidate] - - mock_chat.send_message.return_value = mock_response - - # Call create without thinking params - gemini_client.create({ - "model": "gemini-pro", - "messages": [{"content": "Hello", "role": "user"}], - }) - - mock_thinking_config.assert_called_once_with( - include_thoughts=None, - thinking_budget=None, - thinking_level=None, - ) - - config_kwargs = mock_generate_content_config.call_args.kwargs - assert config_kwargs["thinking_config"] == mock_thinking_config.return_value, ( - "default thinking_config should still be passed to GenerateContentConfig" - ) - - @pytest.mark.parametrize( - "kwargs,expected", - [ - ({"include_thoughts": True}, {"include_thoughts": True, "thinking_budget": None, "thinking_level": None}), - ({"thinking_budget": 256}, {"include_thoughts": None, "thinking_budget": 256, "thinking_level": None}), - ({"thinking_level": "High"}, {"include_thoughts": None, "thinking_budget": None, "thinking_level": "High"}), - ( - {"include_thoughts": False, "thinking_budget": 512}, - {"include_thoughts": False, "thinking_budget": 512, "thinking_level": None}, - ), - ( - {"include_thoughts": True, "thinking_level": "Low"}, - {"include_thoughts": True, "thinking_budget": None, "thinking_level": "Low"}, - ), - ( - {"thinking_budget": 1024, "thinking_level": "High"}, - {"include_thoughts": None, "thinking_budget": 1024, "thinking_level": "High"}, - ), - ( - {"include_thoughts": True, "thinking_budget": 2048, "thinking_level": "High"}, - {"include_thoughts": True, "thinking_budget": 2048, "thinking_level": "High"}, - ), - # Test "Medium" thinking level - ( - {"thinking_level": "Medium"}, - {"include_thoughts": None, "thinking_budget": None, "thinking_level": "Medium"}, - ), - ( - {"include_thoughts": True, "thinking_level": "Medium"}, - {"include_thoughts": True, "thinking_budget": None, "thinking_level": "Medium"}, - ), - ( - {"thinking_budget": 512, "thinking_level": "Medium"}, - {"include_thoughts": None, "thinking_budget": 512, "thinking_level": "Medium"}, - ), - # Test "Minimal" thinking level - ( - {"thinking_level": "Minimal"}, - {"include_thoughts": None, "thinking_budget": None, "thinking_level": "Minimal"}, - ), - ( - {"include_thoughts": True, "thinking_level": "Minimal"}, - {"include_thoughts": True, "thinking_budget": None, "thinking_level": "Minimal"}, - ), - ( - {"thinking_budget": 128, "thinking_level": "Minimal"}, - {"include_thoughts": None, "thinking_budget": 128, "thinking_level": "Minimal"}, - ), - ], - ) - @patch("autogen.oai.gemini.genai.Client") - @patch("autogen.oai.gemini.GenerateContentConfig") - @patch("autogen.oai.gemini.ThinkingConfig") - def test_generation_config_thinking_param_variants( - self, - mock_thinking_config, - mock_generate_content_config, - mock_generative_client, - gemini_client, - kwargs, - expected, - ): - """Test individual and combined thinking params are passed through to ThinkingConfig and GenerateContentConfig""" - mock_chat = MagicMock() - mock_generative_client.return_value.chats.create.return_value = mock_chat - - mock_text_part = MagicMock() - mock_text_part.text = "Response" - mock_text_part.function_call = None - - mock_usage_metadata = MagicMock() - mock_usage_metadata.prompt_token_count = 5 - mock_usage_metadata.candidates_token_count = 3 - - mock_candidate = MagicMock() - mock_candidate.content.parts = [mock_text_part] - - mock_response = MagicMock(spec=GenerateContentResponse) - mock_response.usage_metadata = mock_usage_metadata - mock_response.candidates = [mock_candidate] - - mock_chat.send_message.return_value = mock_response - - params = { - "model": "gemini-pro", - "messages": [{"content": "Hello", "role": "user"}], - **kwargs, - } - gemini_client.create(params) - - mock_thinking_config.assert_called_once_with( - include_thoughts=expected["include_thoughts"], - thinking_budget=expected["thinking_budget"], - thinking_level=expected["thinking_level"], - ) - - config_kwargs = mock_generate_content_config.call_args.kwargs - assert config_kwargs["thinking_config"] == mock_thinking_config.return_value, ( - "thinking_config should be passed to GenerateContentConfig" - ) - - @patch("autogen.oai.gemini.GenerativeModel") - @patch("autogen.oai.gemini.GenerationConfig") - def test_vertexai_generation_config_with_seed( - self, mock_generation_config, mock_generative_model, gemini_client_with_credentials - ): - """Test that seed parameter is properly passed to VertexAI generation config""" - # Mock setup - mock_chat = MagicMock() mock_model = MagicMock() mock_generative_model.return_value = mock_model mock_model.start_chat.return_value = mock_chat From 4b99101fab12ad9442f32e049552b8ec774773a4 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sat, 27 Dec 2025 04:17:10 +0530 Subject: [PATCH 14/30] test: anthropic agent config --- autogen/oai/anthropic.py | 4 +- test/oai/test_anthropic.py | 436 ++++++++++++++++++++++++++++++++----- 2 files changed, 386 insertions(+), 54 deletions(-) diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index 2d8a548cb2d..ef1b6bc59c2 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -768,10 +768,10 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: agent_config = agent_config_parser(agent) if agent is not None else None logger.info(f"Agent config: {agent_config}") response_format = ( - agent_config["response_format"] + agent_config.get("response_format") if agent_config is not None and "response_format" in agent_config - and agent_config["response_format"] is not None + and agent_config.get("response_format") is not None else params.get("response_format", self._response_format if self._response_format is not None else None) ) # Route to appropriate implementation based on model and response_format diff --git a/test/oai/test_anthropic.py b/test/oai/test_anthropic.py index a0ac33ace35..c718298326e 100644 --- a/test/oai/test_anthropic.py +++ b/test/oai/test_anthropic.py @@ -22,6 +22,8 @@ from pydantic import BaseModel +from autogen.agentchat.conversable_agent import ConversableAgent + logger = logging.getLogger(__name__) @@ -604,74 +606,401 @@ def test_transform_schema_preserves_nested_structures(): assert transformed["additionalProperties"] is True +def create_mock_anthropic_response(): + """Helper function to create a mock Anthropic Message response.""" + return Message( + id="msg_123", + content=[ + TextBlock( + text='{"steps": [], "final_answer": "test"}', + type="text", + ) + ], + model="claude-sonnet-4-5", + role="assistant", + stop_reason="end_turn", + type="message", + usage={"input_tokens": 10, "output_tokens": 25}, + ) + + +# ============================================================================== +# Unit Tests for Agent Config Response Format +# ============================================================================== + + @run_for_optional_imports(["anthropic"], "anthropic") -def test_create_routes_to_native_or_json_mode(anthropic_client, monkeypatch): - """Test that create() method routes to correct implementation.""" +def test_create_with_agent_config_response_format(anthropic_client, monkeypatch): + """Test that agent_config response_format takes precedence over params response_format.""" + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str - native_called = False - json_mode_called = False - standard_called = False + # Create real agent with response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + "max_tokens": 100, + } + + # Mock both native and JSON mode methods to prevent real API calls def mock_create_with_native(params): - nonlocal native_called - native_called = True return create_mock_anthropic_response() def mock_create_with_json_mode(params): - nonlocal json_mode_called - json_mode_called = True return create_mock_anthropic_response() - def mock_create_standard(params): - nonlocal standard_called - standard_called = True + monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) + monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) + # Ensure we take the native path + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) + + # Call create to set _response_format from agent_config + anthropic_client.create(params) + + # Verify agent_config response_format was used (MathReasoning, not the dict in params) + assert anthropic_client._response_format == MathReasoning + # Verify it's MathReasoning schema, not the simple dict from params + assert hasattr(anthropic_client._response_format, "model_json_schema") + schema = anthropic_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + assert "name" not in schema["properties"] # Not the simple dict from params + + +@run_for_optional_imports(["anthropic"], "anthropic") +def test_create_with_agent_config_response_format_overrides_client_format(anthropic_client, monkeypatch): + """Test that agent_config response_format takes precedence over client._response_format (from llm_config).""" + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Set client response_format (simulating it being set from llm_config) + anthropic_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "agent": agent, + "max_tokens": 100, + } + + # Mock both native and JSON mode methods + def mock_create_with_native(params): + return create_mock_anthropic_response() + + def mock_create_with_json_mode(params): return create_mock_anthropic_response() - # Mock the internal methods monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) - monkeypatch.setattr(anthropic_client, "_create_standard", mock_create_standard) + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) - # Test 1: Sonnet 4.5 with response_format -> native - anthropic_client._response_format = BaseModel - params = {"model": "claude-sonnet-4-5", "messages": [], "max_tokens": 100} + # Call create to set _response_format from agent_config anthropic_client.create(params) - assert native_called, "Should use native structured output for Sonnet 4.5" - # Reset flags - native_called = json_mode_called = standard_called = False + # Verify agent_config response_format was used (MathReasoning), not client._response_format + assert anthropic_client._response_format == MathReasoning + assert hasattr(anthropic_client._response_format, "model_json_schema") + schema = anthropic_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + assert "age" not in schema["properties"] # Not the client._response_format schema + + +@run_for_optional_imports(["anthropic"], "anthropic") +def test_create_with_agent_config_response_format_overrides_params_and_client_format(anthropic_client, monkeypatch): + """Test that agent_config response_format takes precedence over both params and client._response_format.""" + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Set client response_format (from llm_config) + anthropic_client._response_format = {"type": "object", "properties": {"age": {"type": "integer"}}} + + # Create real agent with different response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + # Both params and client have response_format, but agent should override both + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent": agent, + "max_tokens": 100, + } + + # Mock both native and JSON mode methods + def mock_create_with_native(params): + return create_mock_anthropic_response() + + def mock_create_with_json_mode(params): + return create_mock_anthropic_response() + + monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) + monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) - # Test 2: Haiku with response_format -> JSON Mode - params = {"model": "claude-3-haiku-20240307", "messages": [], "max_tokens": 100} + # Call create to set _response_format from agent_config anthropic_client.create(params) - assert json_mode_called, "Should use JSON Mode for older models" - # Reset flags - native_called = json_mode_called = standard_called = False + # Verify it's MathReasoning + assert anthropic_client._response_format == MathReasoning + schema = anthropic_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + assert "age" not in schema["properties"] # Not client._response_format + assert "name" not in schema["properties"] # Not params response_format - # Test 3: No response_format -> standard - anthropic_client._response_format = None - params = {"model": "claude-sonnet-4-5", "messages": [], "max_tokens": 100} + +@run_for_optional_imports(["anthropic"], "anthropic") +def test_create_with_agent_no_response_format_falls_back_to_params(anthropic_client, monkeypatch): + """Test that when agent has no response_format, it falls back to params response_format.""" + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + "required": ["name"], + } + + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": dict_schema, + "agent": agent, + "max_tokens": 100, + } + + # Mock both native and JSON mode methods + def mock_create_with_native(params): + return create_mock_anthropic_response() + + def mock_create_with_json_mode(params): + return create_mock_anthropic_response() + + monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) + monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) + + # Call create to set _response_format from params anthropic_client.create(params) - assert standard_called, "Should use standard create without response_format" + # Verify it's the dict_schema from params + assert anthropic_client._response_format == dict_schema + assert isinstance(anthropic_client._response_format, dict) + assert "name" in anthropic_client._response_format["properties"] -def create_mock_anthropic_response(): - """Helper to create mock Anthropic response.""" - with optional_import_block() as result: - from anthropic.types import Message, TextBlock - - if result.is_successful: - return Message( - id="msg_test123", - content=[TextBlock(text='{"test": "response"}', type="text")], - model="claude-sonnet-4-5", - role="assistant", - stop_reason="end_turn", - type="message", - usage={"input_tokens": 10, "output_tokens": 20}, - ) - return None + +@run_for_optional_imports(["anthropic"], "anthropic") +def test_create_with_agent_no_response_format_falls_back_to_client_format(anthropic_client, monkeypatch): + """Test that when agent has no response_format and params has none, it falls back to client._response_format.""" + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Set client response_format (from llm_config) + anthropic_client._response_format = MathReasoning + + # Create real agent without response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + # Don't set response_format - it should not exist or be None + + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "agent": agent, + "max_tokens": 100, + } + + # Mock both native and JSON mode methods + def mock_create_with_native(params): + return create_mock_anthropic_response() + + def mock_create_with_json_mode(params): + return create_mock_anthropic_response() + + monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) + monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) + + # Call create to set _response_format from client._response_format + anthropic_client.create(params) + + # Verify it's MathReasoning (has steps and final_answer), not the age schema + assert anthropic_client._response_format == MathReasoning + schema = anthropic_client._response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + + +@run_for_optional_imports(["anthropic"], "anthropic") +def test_create_with_agent_response_format_none_ignores_agent(anthropic_client, monkeypatch): + """Test that when agent.response_format is None, it's ignored and falls back to params.""" + # Create real agent with response_format=None + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = None + + dict_schema = { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + } + + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": dict_schema, + "agent": agent, + "max_tokens": 100, + } + + # Mock both native and JSON mode methods + def mock_create_with_native(params): + return create_mock_anthropic_response() + + def mock_create_with_json_mode(params): + return create_mock_anthropic_response() + + monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) + monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) + + # Call create to set _response_format from params (agent.response_format=None should be ignored) + anthropic_client.create(params) + + # Verify it's the dict_schema from params + assert anthropic_client._response_format == dict_schema + assert isinstance(anthropic_client._response_format, dict) + assert "name" in anthropic_client._response_format["properties"] + + +@run_for_optional_imports(["anthropic"], "anthropic") +def test_create_without_agent_uses_params_response_format(anthropic_client, monkeypatch): + """Test that when no agent is provided, params response_format is used (existing behavior).""" + dict_schema = { + "type": "object", + "properties": {"email": {"type": "string"}}, + "required": ["email"], + } + + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "response_format": dict_schema, + # No agent parameter + "max_tokens": 100, + } + + # Mock both native and JSON mode methods + def mock_create_with_native(params): + return create_mock_anthropic_response() + + def mock_create_with_json_mode(params): + return create_mock_anthropic_response() + + monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) + monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) + + # Call create to set _response_format from params + anthropic_client.create(params) + + # Verify it's the dict_schema from params + assert anthropic_client._response_format == dict_schema + assert isinstance(anthropic_client._response_format, dict) + assert "email" in anthropic_client._response_format["properties"] + + +@run_for_optional_imports(["anthropic"], "anthropic") +def test_agent_config_parser_called_with_agent(anthropic_client, monkeypatch): + """Test that agent_config_parser is called when agent is provided.""" + from unittest.mock import patch + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Create real agent + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.response_format = MathReasoning + + params = { + "model": "claude-sonnet-4-5", + "messages": [{"role": "user", "content": "Test message"}], + "agent": agent, + "max_tokens": 100, + } + + # Mock agent_config_parser + with patch("autogen.oai.anthropic.agent_config_parser") as mock_parser: + mock_parser.return_value = {"response_format": MathReasoning} + + # Mock both native and JSON mode methods + def mock_create_with_native(params): + return create_mock_anthropic_response() + + def mock_create_with_json_mode(params): + return create_mock_anthropic_response() + + monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_create_with_native) + monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_create_with_json_mode) + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) + + # Call create + anthropic_client.create(params) + + # Verify agent_config_parser was called with the agent + mock_parser.assert_called_once_with(agent) @run_for_optional_imports(["anthropic"], "anthropic") @@ -741,24 +1070,27 @@ def test_json_mode_fallback_on_native_failure(anthropic_client, monkeypatch): """Test graceful fallback to JSON Mode if native fails.""" def mock_native_failure(params): - raise Exception("Beta API not available") + from anthropic import BadRequestError + + raise BadRequestError(message="Beta API not available", response=None, body=None) def mock_json_mode_success(params): return create_mock_anthropic_response() monkeypatch.setattr(anthropic_client, "_create_with_native_structured_output", mock_native_failure) monkeypatch.setattr(anthropic_client, "_create_with_json_mode", mock_json_mode_success) + # Ensure we take the native path + monkeypatch.setattr("autogen.oai.anthropic.has_beta_messages_api", lambda: True) + monkeypatch.setattr("autogen.oai.anthropic.supports_native_structured_outputs", lambda model: True) anthropic_client._response_format = BaseModel - # Should fallback gracefully + # Should fallback gracefully - should NOT raise, should return JSON mode response params = {"model": "claude-sonnet-4-5", "messages": [], "max_tokens": 100} - # Note: This test verifies the fallback logic exists in the implementation - # The actual implementation should catch exceptions and fallback - with pytest.raises(Exception): - # Currently will raise; implementation should add fallback logic - anthropic_client.create(params) + # Should fallback gracefully without raising + result = anthropic_client.create(params) + assert result is not None # Should return the JSON mode response @run_for_optional_imports(["anthropic"], "anthropic") From 48bfc82c709812b2f8099eac64d24d8c4589eedd Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sat, 27 Dec 2025 20:36:09 +0530 Subject: [PATCH 15/30] fix: scaling and memory writes --- autogen/agentchat/conversable_agent.py | 9 ++++----- autogen/llm_config/__init__.py | 3 ++- autogen/llm_config/config.py | 5 +++++ autogen/oai/agent_config_handler.py | 12 ++++++------ autogen/oai/anthropic.py | 4 ++-- autogen/oai/bedrock.py | 4 ++-- autogen/oai/cerebras.py | 4 ++-- autogen/oai/client.py | 6 +++--- autogen/oai/cohere.py | 4 ++-- autogen/oai/gemini.py | 4 ++-- autogen/oai/groq.py | 4 ++-- autogen/oai/mistral.py | 4 ++-- autogen/oai/ollama.py | 4 ++-- autogen/oai/openai_responses.py | 4 ++-- autogen/oai/together.py | 4 ++-- 15 files changed, 40 insertions(+), 35 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 761f37b6ea6..e14deff7a6d 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -27,8 +27,6 @@ Union, ) -from pydantic import BaseModel - from ..cache.cache import AbstractCache, Cache from ..code_utils import ( PYTHON_VARIANTS, @@ -65,7 +63,7 @@ from ..io.base import AsyncIOStreamProtocol, AsyncInputStream, IOStream, IOStreamProtocol, InputStream from ..io.run_response import AsyncRunResponse, AsyncRunResponseProtocol, RunResponse, RunResponseProtocol from ..io.thread_io_stream import AsyncThreadIOStream, ThreadIOStream -from ..llm_config import LLMConfig +from ..llm_config import AgentConfig, LLMConfig from ..llm_config.client import ModelClient from ..oai.client import OpenAIWrapper from ..runtime_logging import log_event, log_function_use, log_new_agent, logging_enabled @@ -164,7 +162,7 @@ def __init__( silent: bool | None = None, context_variables: Optional["ContextVariables"] = None, functions: list[Callable[..., Any]] | Callable[..., Any] = None, - response_format: str | dict[str, Any] | BaseModel | type[BaseModel] | None = None, + agent_config: AgentConfig | None = None, update_agent_state_before_reply: list[Callable | UpdateSystemMessage] | Callable | UpdateSystemMessage @@ -226,7 +224,8 @@ def __init__( 15) update_agent_state_before_reply (List[Callable[..., Any]]): A list of functions, including UpdateSystemMessage's, called to update the agent before it replies.\n 16) handoffs (Handoffs): Handoffs object containing all handoff transition conditions.\n """ - self.response_format = response_format if response_format is not None else None + # self.response_format = response_format if response_format is not None else None + self.agent_config = agent_config if agent_config is not None else None self.handoffs = handoffs if handoffs is not None else Handoffs() self.input_guardrails: list[Guardrail] = [] self.output_guardrails: list[Guardrail] = [] diff --git a/autogen/llm_config/__init__.py b/autogen/llm_config/__init__.py index ec36bdc6e43..1b89ddb5f53 100644 --- a/autogen/llm_config/__init__.py +++ b/autogen/llm_config/__init__.py @@ -3,9 +3,10 @@ # SPDX-License-Identifier: Apache-2.0 from .client import ModelClient -from .config import LLMConfig +from .config import AgentConfig, LLMConfig __all__ = ( + "AgentConfig", "LLMConfig", "ModelClient", ) diff --git a/autogen/llm_config/config.py b/autogen/llm_config/config.py index ebc44eeff82..f31d82d9361 100644 --- a/autogen/llm_config/config.py +++ b/autogen/llm_config/config.py @@ -51,6 +51,11 @@ def default(cls) -> "LLMConfig": ConfigItem: TypeAlias = LLMConfigEntry | ConfigEntries | dict[str, Any] +@export_module("autogen") +class AgentConfig(BaseModel): + response_format: str | dict[str, Any] | BaseModel | type[BaseModel] | None = None + + @export_module("autogen") class LLMConfig(metaclass=MetaLLMConfig): _current_llm_config: ContextVar["LLMConfig"] = ContextVar("current_llm_config") diff --git a/autogen/oai/agent_config_handler.py b/autogen/oai/agent_config_handler.py index 4298c6aa4c7..c6c4084186b 100644 --- a/autogen/oai/agent_config_handler.py +++ b/autogen/oai/agent_config_handler.py @@ -5,11 +5,11 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from autogen.agentchat.conversable_agent import ConversableAgent + from autogen.llm_config import AgentConfig -def agent_config_parser(agent: "ConversableAgent") -> dict[str, Any]: - agent_config: dict[str, Any] = {} - if hasattr(agent, "response_format") and agent.response_format is not None: - agent_config["response_format"] = agent.response_format - return agent_config +def agent_config_parser(agent_config: AgentConfig) -> dict[str, Any]: + _agent_config: dict[str, Any] = {} + if hasattr(agent_config, "response_format") and agent_config.response_format is not None: + _agent_config["response_format"] = agent_config.response_format + return _agent_config diff --git a/autogen/oai/anthropic.py b/autogen/oai/anthropic.py index ef1b6bc59c2..77652254da8 100644 --- a/autogen/oai/anthropic.py +++ b/autogen/oai/anthropic.py @@ -763,9 +763,9 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: Returns: ChatCompletion object compatible with OpenAI format """ - agent = params.pop("agent", None) + agent_config = params.pop("agent_config", None) model = params.get("model") - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") response_format = ( agent_config.get("response_format") diff --git a/autogen/oai/bedrock.py b/autogen/oai/bedrock.py index 47810661b2c..8d6f4299387 100644 --- a/autogen/oai/bedrock.py +++ b/autogen/oai/bedrock.py @@ -425,8 +425,8 @@ def parse_params(self, params: BedrockEntryDict | dict[str, Any]) -> tuple[dict[ def create(self, params) -> ChatCompletion: """Run Amazon Bedrock inference and return AG2 response""" # Set custom client class settings - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") self.parse_custom_params(params) diff --git a/autogen/oai/cerebras.py b/autogen/oai/cerebras.py index 3fabbeae800..bb34131c0ff 100644 --- a/autogen/oai/cerebras.py +++ b/autogen/oai/cerebras.py @@ -149,8 +149,8 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("cerebras", "cerebras") def create(self, params: dict) -> ChatCompletion: - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) diff --git a/autogen/oai/client.py b/autogen/oai/client.py index ee59908fb2d..2f036ebf959 100644 --- a/autogen/oai/client.py +++ b/autogen/oai/client.py @@ -498,8 +498,8 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: Returns: The completion. """ - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") iostream = IOStream.get_default() @@ -1270,7 +1270,7 @@ def create(self, **config: Any) -> ModelClient.ModelClientResponseProtocol: try: # Add agent to params if provided (for downstream use) if agent is not None: - params["agent"] = agent + params["agent_config"] = agent.agent_config request_ts = get_current_ts() response = client.create(params) except Exception as e: diff --git a/autogen/oai/cohere.py b/autogen/oai/cohere.py index 61a72a5e934..c7eb9abb907 100644 --- a/autogen/oai/cohere.py +++ b/autogen/oai/cohere.py @@ -240,12 +240,12 @@ def ensure_type_fields(obj: dict, defs: dict) -> dict: @require_optional_import("cohere", "cohere") def create(self, params: dict) -> ChatCompletion: - agent = params.pop("agent", None) + agent_config = params.pop("agent_config", None) messages = params.get("messages", []) client_name = params.get("client_name") or "AG2" cohere_tool_names = set() tool_calls_modified_ids = set() - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") # Parse parameters to the Cohere API's parameters cohere_params = self.parse_params(params) diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index c5a3731680b..f53578573ae 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -245,8 +245,8 @@ def get_usage(response: ChatCompletion) -> dict[str, Any]: } def create(self, params: dict[str, Any]) -> ChatCompletion: - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") # When running in async context via run_in_executor from ConversableAgent.a_generate_oai_reply, # this method runs in a new thread that doesn't have an event loop by default. The Google Genai diff --git a/autogen/oai/groq.py b/autogen/oai/groq.py index f4a922dcdbb..1339b38ee10 100644 --- a/autogen/oai/groq.py +++ b/autogen/oai/groq.py @@ -169,8 +169,8 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("groq", "groq") def create(self, params: dict) -> ChatCompletion: - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) diff --git a/autogen/oai/mistral.py b/autogen/oai/mistral.py index 15bf8aeb723..8364a0855c3 100644 --- a/autogen/oai/mistral.py +++ b/autogen/oai/mistral.py @@ -207,8 +207,8 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("mistralai", "mistral") def create(self, params: dict[str, Any]) -> ChatCompletion: - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") # 1. Parse parameters to Mistral.AI API's parameters mistral_params = self.parse_params(params) diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index 049fb4f662f..fe1ce08e34f 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -232,8 +232,8 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import(["ollama", "fix_busted_json"], "ollama") def create(self, params: dict) -> ChatCompletion: - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) diff --git a/autogen/oai/openai_responses.py b/autogen/oai/openai_responses.py index 925c6a53df8..f0f07b83c4e 100644 --- a/autogen/oai/openai_responses.py +++ b/autogen/oai/openai_responses.py @@ -557,8 +557,8 @@ def create(self, params: dict[str, Any]) -> "Response": workspace_dir = params.pop("workspace_dir", os.getcwd()) allowed_paths = params.pop("allowed_paths", ["**"]) built_in_tools = params.pop("built_in_tools", []) - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") self.response_format = ( diff --git a/autogen/oai/together.py b/autogen/oai/together.py index c62350b7707..1ad7d4cbff5 100644 --- a/autogen/oai/together.py +++ b/autogen/oai/together.py @@ -179,8 +179,8 @@ def parse_params(self, params: dict[str, Any]) -> dict[str, Any]: @require_optional_import("together", "together") def create(self, params: dict) -> ChatCompletion: - agent = params.pop("agent", None) - agent_config = agent_config_parser(agent) if agent is not None else None + agent_config = params.pop("agent_config", None) + agent_config = agent_config_parser(agent_config) if agent_config is not None else None logger.info(f"Agent config: {agent_config}") messages = params.get("messages", []) From b130ce810cd464add3b6429d2895b0acc6daae2a Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sun, 28 Dec 2025 01:20:06 +0530 Subject: [PATCH 16/30] fix: anthropic test --- autogen/oai/agent_config_handler.py | 2 +- test/oai/test_anthropic.py | 16 ++++++++-------- test/oai/test_bedrock.py | 28 ++++++++++++++-------------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/autogen/oai/agent_config_handler.py b/autogen/oai/agent_config_handler.py index c6c4084186b..54f745fcfaa 100644 --- a/autogen/oai/agent_config_handler.py +++ b/autogen/oai/agent_config_handler.py @@ -8,7 +8,7 @@ from autogen.llm_config import AgentConfig -def agent_config_parser(agent_config: AgentConfig) -> dict[str, Any]: +def agent_config_parser(agent_config: "AgentConfig") -> dict[str, Any]: _agent_config: dict[str, Any] = {} if hasattr(agent_config, "response_format") and agent_config.response_format is not None: _agent_config["response_format"] = agent_config.response_format diff --git a/test/oai/test_anthropic.py b/test/oai/test_anthropic.py index c718298326e..5e724b3af2b 100644 --- a/test/oai/test_anthropic.py +++ b/test/oai/test_anthropic.py @@ -11,7 +11,7 @@ import pytest from autogen.import_utils import optional_import_block, run_for_optional_imports -from autogen.llm_config import LLMConfig +from autogen.llm_config import AgentConfig, LLMConfig from autogen.oai.anthropic import AnthropicClient, AnthropicLLMConfigEntry, _calculate_cost with optional_import_block() as result: @@ -644,13 +644,13 @@ class MathReasoning(BaseModel): # Create real agent with response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) params = { "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "Test message"}], "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, "max_tokens": 100, } @@ -698,12 +698,12 @@ class MathReasoning(BaseModel): # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) params = { "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "Test message"}], - "agent": agent, + "agent_config": agent.agent_config, "max_tokens": 100, } @@ -749,14 +749,14 @@ class MathReasoning(BaseModel): # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Both params and client have response_format, but agent should override both params = { "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "Test message"}], "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, "max_tokens": 100, } @@ -971,7 +971,7 @@ class MathReasoning(BaseModel): # Create real agent agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) params = { "model": "claude-sonnet-4-5", diff --git a/test/oai/test_bedrock.py b/test/oai/test_bedrock.py index 4c1a1f26000..7d841283013 100644 --- a/test/oai/test_bedrock.py +++ b/test/oai/test_bedrock.py @@ -11,7 +11,7 @@ from autogen.agentchat.conversable_agent import ConversableAgent from autogen.import_utils import run_for_optional_imports -from autogen.llm_config import LLMConfig +from autogen.llm_config import AgentConfig, LLMConfig from autogen.oai.bedrock import BedrockClient, BedrockLLMConfigEntry, oai_messages_to_bedrock_messages from autogen.oai.oai_models import ChatCompletionMessageToolCall @@ -1514,7 +1514,7 @@ def test_create_with_agent_config_response_format(bedrock_client: BedrockClient) """Test that agent_config response_format takes precedence over params response_format.""" # Create real agent with response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Mock bedrock_runtime mock_bedrock_runtime = MagicMock() @@ -1548,7 +1548,7 @@ def test_create_with_agent_config_response_format(bedrock_client: BedrockClient) "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, } response = bedrock_client.create(params) @@ -1584,7 +1584,7 @@ def test_create_with_agent_config_response_format_overrides_client_format(bedroc # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Mock bedrock_runtime mock_bedrock_runtime = MagicMock() @@ -1617,7 +1617,7 @@ def test_create_with_agent_config_response_format_overrides_client_format(bedroc params = { "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", - "agent": agent, + "agent_config": agent.agent_config, } response = bedrock_client.create(params) @@ -1650,7 +1650,7 @@ def test_create_with_agent_config_response_format_overrides_params_and_client_fo # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Mock bedrock_runtime mock_bedrock_runtime = MagicMock() @@ -1685,7 +1685,7 @@ def test_create_with_agent_config_response_format_overrides_params_and_client_fo "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, } response = bedrock_client.create(params) @@ -1716,7 +1716,7 @@ def test_create_with_agent_config_response_format_and_user_tools(bedrock_client: """Test that agent_config response_format works correctly with user-provided tools.""" # Create real agent with response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Mock bedrock_runtime mock_bedrock_runtime = MagicMock() @@ -1765,7 +1765,7 @@ def test_create_with_agent_config_response_format_and_user_tools(bedrock_client: "messages": [{"role": "user", "content": "Get weather and format response"}], "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", "tools": user_tools, - "agent": agent, + "agent_config": agent.agent_config, } response = bedrock_client.create(params) @@ -1899,7 +1899,7 @@ def test_create_with_agent_no_response_format_falls_back_to_client_format(bedroc params = { "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", - "agent": agent, + "agent_config": agent.agent_config, } response = bedrock_client.create(params) @@ -2055,7 +2055,7 @@ def test_agent_config_parser_called_with_agent(mock_parser, bedrock_client: Bedr """Test that agent_config_parser is called when agent is provided.""" # Create real agent agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Mock agent_config_parser to return expected format mock_parser.return_value = {"response_format": MathReasoning} @@ -2090,13 +2090,13 @@ def test_agent_config_parser_called_with_agent(mock_parser, bedrock_client: Bedr params = { "messages": [{"role": "user", "content": "Solve 2x + 5 = -25"}], "model": "anthropic.claude-3-5-sonnet-20241022-v2:0", - "agent": agent, + "agent_config": agent.agent_config, } response = bedrock_client.create(params) - # Verify agent_config_parser was called with the agent - mock_parser.assert_called_once_with(agent) + # Verify agent_config_parser was called with agent.agent_config + mock_parser.assert_called_once_with(agent.agent_config) @run_for_optional_imports(["boto3", "botocore"], "bedrock") From b8a62c3ffe06bd0ba332235550ca0bb9d61ec7dc Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sun, 28 Dec 2025 01:24:16 +0530 Subject: [PATCH 17/30] fix: gemini tests --- test/oai/test_gemini.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/oai/test_gemini.py b/test/oai/test_gemini.py index 11f068730d7..f33870f9734 100644 --- a/test/oai/test_gemini.py +++ b/test/oai/test_gemini.py @@ -15,7 +15,7 @@ from autogen.agentchat.conversable_agent import ConversableAgent from autogen.import_utils import optional_import_block, run_for_optional_imports -from autogen.llm_config import LLMConfig +from autogen.llm_config import AgentConfig, LLMConfig from autogen.oai.gemini import GeminiClient, GeminiLLMConfigEntry with optional_import_block() as result: @@ -501,13 +501,13 @@ class MathReasoning(BaseModel): # Create real agent with response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) params = { "model": "gemini-pro", "messages": [{"role": "user", "content": "Test message"}], "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from agent_config @@ -565,12 +565,12 @@ class MathReasoning(BaseModel): # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) params = { "model": "gemini-pro", "messages": [{"role": "user", "content": "Test message"}], - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from agent_config @@ -627,14 +627,14 @@ class MathReasoning(BaseModel): # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Both params and client have response_format, but agent should override both params = { "model": "gemini-pro", "messages": [{"role": "user", "content": "Test message"}], "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from agent_config @@ -691,7 +691,7 @@ def test_create_with_agent_no_response_format_falls_back_to_params(): "model": "gemini-pro", "messages": [{"role": "user", "content": "Test message"}], "response_format": dict_schema, - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from params @@ -750,7 +750,7 @@ class MathReasoning(BaseModel): params = { "model": "gemini-pro", "messages": [{"role": "user", "content": "Test message"}], - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from client._response_format @@ -805,7 +805,7 @@ def test_create_with_agent_response_format_none_ignores_agent(): "model": "gemini-pro", "messages": [{"role": "user", "content": "Test message"}], "response_format": dict_schema, - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from params (agent.response_format=None should be ignored) @@ -907,7 +907,7 @@ class MathReasoning(BaseModel): # Create real agent agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Mock agent_config_parser to return expected format mock_parser.return_value = {"response_format": MathReasoning} @@ -915,7 +915,7 @@ class MathReasoning(BaseModel): params = { "model": "gemini-pro", "messages": [{"role": "user", "content": "Test message"}], - "agent": agent, + "agent_config": agent.agent_config, } # Call create @@ -942,8 +942,8 @@ class MathReasoning(BaseModel): gemini_client.create(params) - # Verify agent_config_parser was called with the agent - mock_parser.assert_called_once_with(agent) + # Verify agent_config_parser was called with agent.agent_config + mock_parser.assert_called_once_with(agent.agent_config) @pytest.fixture def nested_function_parameters(self) -> dict[str, Any]: From ca2db1fdd612445a0d48e479a72ba761994f1f27 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sun, 28 Dec 2025 01:37:47 +0530 Subject: [PATCH 18/30] fix: ollama tests --- test/oai/test_ollama.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/oai/test_ollama.py b/test/oai/test_ollama.py index 5eebf1b75e8..e99372ee249 100644 --- a/test/oai/test_ollama.py +++ b/test/oai/test_ollama.py @@ -11,7 +11,7 @@ from pydantic import BaseModel from autogen.import_utils import run_for_optional_imports -from autogen.llm_config import LLMConfig +from autogen.llm_config import AgentConfig, LLMConfig from autogen.oai.ollama import OllamaClient, OllamaLLMConfigEntry, response_to_tool_call @@ -503,13 +503,13 @@ def test_create_with_agent_config_response_format(ollama_client): """Test that agent_config response_format takes precedence over params response_format.""" # Create real agent with response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) params = { "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Test message"}], # Add this "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from agent_config @@ -542,12 +542,12 @@ def test_create_with_agent_config_response_format_overrides_client_format(ollama # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) params = { "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Test message"}], # Add this - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from agent_config @@ -578,14 +578,14 @@ def test_create_with_agent_config_response_format_overrides_params_and_client_fo # Create real agent with different response_format agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Both params and client have response_format, but agent should override both params = { "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Test message"}], # Add this "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from agent_config @@ -626,7 +626,7 @@ def test_create_with_agent_no_response_format_falls_back_to_params(ollama_client "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Test message"}], # Add this "response_format": dict_schema, - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from params @@ -662,7 +662,7 @@ def test_create_with_agent_no_response_format_falls_back_to_client_format(ollama params = { "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Test message"}], # Add this - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from client._response_format @@ -701,7 +701,7 @@ def test_create_with_agent_response_format_none_ignores_agent(ollama_client): "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Test message"}], # Add this "response_format": dict_schema, - "agent": agent, + "agent_config": agent.agent_config, } # Call create to set _response_format from params (agent.response_format=None should be ignored) @@ -762,7 +762,7 @@ def test_agent_config_parser_called_with_agent(mock_parser, ollama_client): """Test that agent_config_parser is called when agent is provided.""" # Create real agent agent = ConversableAgent(name="test_agent", llm_config=False) - agent.response_format = MathReasoning + agent.agent_config = AgentConfig(response_format=MathReasoning) # Mock agent_config_parser to return expected format mock_parser.return_value = {"response_format": MathReasoning} @@ -770,7 +770,7 @@ def test_agent_config_parser_called_with_agent(mock_parser, ollama_client): params = { "model": "llama3.1:8b", "messages": [{"role": "user", "content": "Test message"}], # Add this - "agent": agent, + "agent_config": agent.agent_config, } # Call create @@ -783,5 +783,5 @@ def test_agent_config_parser_called_with_agent(mock_parser, ollama_client): } ollama_client.create(params) - # Verify agent_config_parser was called with the agent - mock_parser.assert_called_once_with(agent) + # Verify agent_config_parser was called with agent.agent_config + mock_parser.assert_called_once_with(agent.agent_config) From 8335c2ed4f0c00398b95e28935952492cd70056d Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sun, 28 Dec 2025 01:52:50 +0530 Subject: [PATCH 19/30] test: agent config in OAI client --- test/oai/test_client.py | 126 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/test/oai/test_client.py b/test/oai/test_client.py index 2abc2b8c2ad..bca55f24973 100755 --- a/test/oai/test_client.py +++ b/test/oai/test_client.py @@ -16,11 +16,13 @@ from unittest.mock import MagicMock import pytest +from pydantic import BaseModel from autogen import OpenAIWrapper +from autogen.agentchat.conversable_agent import ConversableAgent from autogen.cache.cache import Cache from autogen.import_utils import optional_import_block, run_for_optional_imports -from autogen.llm_config import LLMConfig +from autogen.llm_config import AgentConfig, LLMConfig from autogen.oai.client import ( AOPENAI_FALLBACK_KWARGS, LEGACY_CACHE_DIR, @@ -994,3 +996,125 @@ def test_completion_o1_mini(self, o1_mini_client: OpenAIWrapper, messages: list[ @pytest.mark.skip(reason="Wait for o1 to be available in CI") def test_completion_o1(self, o1_client: OpenAIWrapper, messages: list[dict[str, str]]) -> None: self._test_completion(o1_client, messages) + + +# Test agent_config response_format functionality +@run_for_optional_imports(["openai"], "openai") +def test_create_with_agent_config_response_format(mock_credentials: Credentials, monkeypatch): + """Test that agent_config response_format takes precedence over params response_format.""" + from unittest.mock import patch + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Create real agent with response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.agent_config = AgentConfig(response_format=MathReasoning) + + config_list = mock_credentials.config_list + client = OpenAIWrapper(config_list=config_list) + + params = { + "model": config_list[0]["model"], + "messages": [{"role": "user", "content": "Test message"}], + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent_config": agent.agent_config, + } + + # Mock the OpenAI client's create method + with patch.object(client._clients[0], "create") as mock_create: + mock_response = ChatCompletion( + id="chatcmpl-test", + choices=[ + Choice( + finish_reason="stop", + index=0, + message=ChatCompletionMessage(content='{"steps": [], "final_answer": "test"}', role="assistant"), + ) + ], + created=1677652288, + model=params["model"], + object="chat.completion", + usage=CompletionUsage(completion_tokens=10, prompt_tokens=10, total_tokens=20), + ) + mock_create.return_value = mock_response + + # Call create + client.create(**params) + + # Verify agent_config was passed to the client + call_args = mock_create.call_args + assert call_args is not None + call_params = call_args[0][0] if call_args[0] else {} + # The agent_config should have been processed and response_format should be MathReasoning + # Check that response_format in params was overridden by agent_config + assert "agent_config" in call_params or "response_format" in call_params + + +@run_for_optional_imports(["openai"], "openai") +def test_agent_config_parser_called_with_agent_config(mock_credentials: Credentials, monkeypatch): + """Test that agent_config_parser is called when agent_config is provided.""" + from unittest.mock import MagicMock, patch + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Create real agent + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.agent_config = AgentConfig(response_format=MathReasoning) + + config_list = mock_credentials.config_list + client = OpenAIWrapper(config_list=config_list) + + params = { + "model": config_list[0]["model"], + "messages": [{"role": "user", "content": "Test message"}], + "agent_config": agent.agent_config, + } + + # Mock agent_config_parser + with patch("autogen.oai.client.agent_config_parser") as mock_parser: + mock_parser.return_value = {"response_format": MathReasoning} + + # Mock the underlying OpenAI client's chat.completions.create method + openai_client = client._clients[0]._oai_client + with patch.object(openai_client.chat.completions, "create") as mock_openai_create: + mock_openai_response = MagicMock() + mock_openai_response.model = params["model"] + mock_openai_response.id = "chatcmpl-test" + mock_openai_response.created = 1677652288 + mock_openai_response.object = "chat.completion" + mock_openai_response.choices = [ + MagicMock( + finish_reason="stop", + index=0, + message=MagicMock( + content='{"steps": [], "final_answer": "test"}', + role="assistant", + ), + ) + ] + mock_openai_response.usage = MagicMock( + completion_tokens=10, + prompt_tokens=10, + total_tokens=20, + ) + mock_openai_create.return_value = mock_openai_response + + # Call create - this will call OpenAIClient.create which calls agent_config_parser + client.create(**params) + + # Verify agent_config_parser was called with agent.agent_config + mock_parser.assert_called_once_with(agent.agent_config) From 7ae4baa0712d8f5e1074d0936bafe72b3da38f99 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sun, 28 Dec 2025 01:57:16 +0530 Subject: [PATCH 20/30] fix: pre-commit --- test/oai/test_responses_client.py | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/test/oai/test_responses_client.py b/test/oai/test_responses_client.py index 2e182523e9d..573f85f1e02 100644 --- a/test/oai/test_responses_client.py +++ b/test/oai/test_responses_client.py @@ -18,6 +18,8 @@ import pytest +from autogen.agentchat.conversable_agent import ConversableAgent +from autogen.llm_config import AgentConfig from autogen.oai.openai_responses import OpenAIResponsesClient, calculate_openai_image_cost # Try to import ImageGenerationCall for proper mocking @@ -1884,3 +1886,83 @@ def test_convert_messages_to_input_preserves_order_in_reverse(mocked_openai_clie assert input_items[0]["content"][0]["text"] == "Third" assert input_items[1]["content"][0]["text"] == "Second" assert input_items[2]["content"][0]["text"] == "First" + + +# ----------------------------------------------------------------------------- +# Agent Config Tests +# ----------------------------------------------------------------------------- + + +def test_create_with_agent_config_response_format(mocked_openai_client): + """Test that agent_config response_format takes precedence over params response_format.""" + from pydantic import BaseModel + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Create real agent with response_format + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.agent_config = AgentConfig(response_format=MathReasoning) + + client = OpenAIResponsesClient(mocked_openai_client) + + params = { + "messages": [{"role": "user", "content": "Test message"}], + "response_format": {"type": "object", "properties": {"name": {"type": "string"}}}, # Different from agent + "agent_config": agent.agent_config, + } + + # Call create - this will set response_format from agent_config + client.create(params) + + # Verify agent_config response_format was used (MathReasoning, not the dict in params) + assert client.response_format == MathReasoning + # Verify it's MathReasoning schema, not the simple dict from params + assert hasattr(client.response_format, "model_json_schema") + schema = client.response_format.model_json_schema() + assert "steps" in schema["properties"] + assert "final_answer" in schema["properties"] + assert "name" not in schema["properties"] # Not the simple dict from params + + +def test_agent_config_parser_called_with_agent_config(mocked_openai_client): + """Test that agent_config_parser is called when agent_config is provided.""" + from unittest.mock import patch + + from pydantic import BaseModel + + # Define test Pydantic model + class Step(BaseModel): + explanation: str + output: str + + class MathReasoning(BaseModel): + steps: list[Step] + final_answer: str + + # Create real agent + agent = ConversableAgent(name="test_agent", llm_config=False) + agent.agent_config = AgentConfig(response_format=MathReasoning) + + client = OpenAIResponsesClient(mocked_openai_client) + + params = { + "messages": [{"role": "user", "content": "Test message"}], + "agent_config": agent.agent_config, + } + + # Mock agent_config_parser + with patch("autogen.oai.openai_responses.agent_config_parser") as mock_parser: + mock_parser.return_value = {"response_format": MathReasoning} + + # Call create - this will call OpenAIResponsesClient.create which calls agent_config_parser + client.create(params) + + # Verify agent_config_parser was called with agent.agent_config + mock_parser.assert_called_once_with(agent.agent_config) From fefbe4cb4d30ca4ffad757aa6badbcca504034c7 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Sun, 28 Dec 2025 21:01:54 +0530 Subject: [PATCH 21/30] fix: update param --- autogen/oai/client.py | 14 +++++++++++--- autogen/oai/gemini.py | 1 + autogen/oai/ollama.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/autogen/oai/client.py b/autogen/oai/client.py index 2f036ebf959..d91d82911fb 100644 --- a/autogen/oai/client.py +++ b/autogen/oai/client.py @@ -500,10 +500,18 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: """ agent_config = params.pop("agent_config", None) agent_config = agent_config_parser(agent_config) if agent_config is not None else None - logger.info(f"Agent config: {agent_config}") iostream = IOStream.get_default() - - is_structured_output = self.response_format is not None or "response_format" in params + self.response_format = ( + agent_config.get("response_format") + if agent_config is not None + and "response_format" in agent_config + and agent_config.get("response_format") is not None + else params.get("response_format") + ) + params["response_format"] = self.response_format + is_structured_output = ( + self.response_format is not None or "response_format" in params or agent_config is not None + ) if is_structured_output: diff --git a/autogen/oai/gemini.py b/autogen/oai/gemini.py index f53578573ae..77d26cbb760 100644 --- a/autogen/oai/gemini.py +++ b/autogen/oai/gemini.py @@ -308,6 +308,7 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: and agent_config.get("response_format") is not None else params.get("response_format", self._response_format if self._response_format is not None else None) ) + params["response_format"] = self._response_format generation_config = { gemini_term: params[autogen_term] for autogen_term, gemini_term in self.PARAMS_MAPPING.items() diff --git a/autogen/oai/ollama.py b/autogen/oai/ollama.py index fe1ce08e34f..0894d2f8aa2 100644 --- a/autogen/oai/ollama.py +++ b/autogen/oai/ollama.py @@ -244,7 +244,7 @@ def create(self, params: dict) -> ChatCompletion: and agent_config.get("response_format") is not None else params.get("response_format", self._response_format if self._response_format is not None else None) ) - + params["response_format"] = self._response_format # Are tools involved in this conversation? self._tools_in_conversation = "tools" in params From aa42c019964e5c5a4f962ef9c49d4e86aee9ac89 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Mon, 29 Dec 2025 01:45:17 +0530 Subject: [PATCH 22/30] feat: add basic interop --- autogen/agentchat/conversable_agent.py | 13 ++++++++++++- autogen/llm_config/config.py | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index e14deff7a6d..2117301b71e 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -260,7 +260,7 @@ def __init__( "Please implement __deepcopy__ method for each value class in llm_config to support deepcopy." " Refer to the docs for more details: https://docs.ag2.ai/docs/user-guide/advanced-concepts/llm-configuration-deep-dive/#adding-http-client-in-llm_config-for-proxy" ) from e - + llm_config = self._interoperate_llm_config(llm_config) if agent_config.api_type is not None else llm_config self.llm_config = self._validate_llm_config(llm_config) self.client = self._create_client(self.llm_config) self._validate_name(name) @@ -439,6 +439,15 @@ def _add_single_function(self, func: Callable, name: str | None = None, descript # Register the function self.register_for_llm(name=name, description=description, silent_override=True)(func) + def _interoperate_llm_config(self, llm_config: LLMConfig | Literal[False]) -> LLMConfig | Literal[False]: + if self.agent_config is not None and self.agent_config.api_type is not None: + for config in llm_config.config_list: + if isinstance(config, dict): + config["api_type"] = self.agent_config.api_type + elif hasattr(config, "api_type"): + config.api_type = self.agent_config.api_type + return llm_config + def _register_update_agent_state_before_reply( self, functions: list[Callable[..., Any]] | Callable[..., Any] | None ): @@ -503,6 +512,7 @@ def _validate_llm_config( @classmethod def _create_client(cls, llm_config: LLMConfig | Literal[False]) -> OpenAIWrapper | None: + logger.info(f"LLM Config: {llm_config.config_list[0].api_type}") return None if llm_config is False else OpenAIWrapper(**llm_config) @staticmethod @@ -2150,6 +2160,7 @@ def generate_oai_reply( **kwargs: Any, ) -> tuple[bool, str | dict[str, Any] | None]: """Generate a reply using autogen.oai.""" + logger.info(f"Messages: {self.client}") client = self.client if config is None else config if client is None: return False, None diff --git a/autogen/llm_config/config.py b/autogen/llm_config/config.py index f31d82d9361..504ca8c5de2 100644 --- a/autogen/llm_config/config.py +++ b/autogen/llm_config/config.py @@ -54,6 +54,12 @@ def default(cls) -> "LLMConfig": @export_module("autogen") class AgentConfig(BaseModel): response_format: str | dict[str, Any] | BaseModel | type[BaseModel] | None = None + api_type: str | None = None + + model_config = ConfigDict(extra="allow") + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) @export_module("autogen") From 1f51d350d63b95c1e0cc708fc9050b0dcedef04e Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:14:28 +0530 Subject: [PATCH 23/30] fix: agent event test --- autogen/agentchat/conversable_agent.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 2117301b71e..6c731c6d342 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -86,7 +86,7 @@ from .group.on_context_condition import OnContextCondition __all__ = ("ConversableAgent",) -logger = logging.getLogger(__name__) +logger = logging.getLogger("ag2.event.processor") F = TypeVar("F", bound=Callable[..., Any]) @@ -260,7 +260,11 @@ def __init__( "Please implement __deepcopy__ method for each value class in llm_config to support deepcopy." " Refer to the docs for more details: https://docs.ag2.ai/docs/user-guide/advanced-concepts/llm-configuration-deep-dive/#adding-http-client-in-llm_config-for-proxy" ) from e - llm_config = self._interoperate_llm_config(llm_config) if agent_config.api_type is not None else llm_config + llm_config = ( + self._interoperate_llm_config(llm_config if isinstance(llm_config, LLMConfig) else None) + if agent_config is not None and agent_config.api_type is not None + else llm_config + ) self.llm_config = self._validate_llm_config(llm_config) self.client = self._create_client(self.llm_config) self._validate_name(name) @@ -439,7 +443,7 @@ def _add_single_function(self, func: Callable, name: str | None = None, descript # Register the function self.register_for_llm(name=name, description=description, silent_override=True)(func) - def _interoperate_llm_config(self, llm_config: LLMConfig | Literal[False]) -> LLMConfig | Literal[False]: + def _interoperate_llm_config(self, llm_config: LLMConfig) -> LLMConfig | None: if self.agent_config is not None and self.agent_config.api_type is not None: for config in llm_config.config_list: if isinstance(config, dict): @@ -512,7 +516,6 @@ def _validate_llm_config( @classmethod def _create_client(cls, llm_config: LLMConfig | Literal[False]) -> OpenAIWrapper | None: - logger.info(f"LLM Config: {llm_config.config_list[0].api_type}") return None if llm_config is False else OpenAIWrapper(**llm_config) @staticmethod From dbb482ff6e696d1f6ac226b426fe98d3fcb625aa Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:32:29 +0530 Subject: [PATCH 24/30] ducumentation: add agent config guide --- .../python-examples/structured_output.mdx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/website/snippets/python-examples/structured_output.mdx b/website/snippets/python-examples/structured_output.mdx index 3f49637c86b..cb79234b88b 100644 --- a/website/snippets/python-examples/structured_output.mdx +++ b/website/snippets/python-examples/structured_output.mdx @@ -47,3 +47,34 @@ response.process() lesson_plan_json = json.loads(response.messages[-1]["content"]) print(json.dumps(lesson_plan_json, indent=2)) ``` + +#### Using AgentConfig for Agent-Specific Configuration (New) + +AG2 now supports an official `AgentConfig` class (from `autogen.llm_config`) for advanced per-agent configuration, such as specifying structured output schemas or agent-level API settings. `AgentConfig` can be passed to agents via the `agent_config` argument to enable schema-based results and other features, and is often preferred for more complex use cases, including setting up structured outputs. + +```python +import os +from autogen import AssistantAgent, UserProxyAgent, AgentConfig +from pydantic import BaseModel + +# Example: Define a structured output schema for the assistant reply +class JokeOutputSchema(BaseModel): + joke: str + explanation: str + +assistant_config = AgentConfig( + response_format=JokeOutputSchema, + api_type="openai", +) + +assistant = AssistantAgent("assistant", agent_config=assistant_config) +user_proxy = UserProxyAgent("user_proxy", code_execution_config=False) + +# Start the chat +user_proxy.initiate_chat( + assistant, + message="Tell me a joke about NVDA and TESLA stock prices.", +) +``` + +You can pass either `llm_config` (using `LLMConfig`) or `agent_config` (using `AgentConfig`) to agents, depending on your needs. Use `AgentConfig` when you want per-agent structured output or need to control how a particular agent interacts with the LLM API. From 9b8a5edcf4e8fd535ab2d295febb3cd006125a04 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh <69320370+priyansh4320@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:34:57 +0530 Subject: [PATCH 25/30] fix: add docstrings --- autogen/agentchat/conversable_agent.py | 1 + autogen/oai/agent_config_handler.py | 1 + 2 files changed, 2 insertions(+) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 6c731c6d342..950093f2c3f 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -444,6 +444,7 @@ def _add_single_function(self, func: Callable, name: str | None = None, descript self.register_for_llm(name=name, description=description, silent_override=True)(func) def _interoperate_llm_config(self, llm_config: LLMConfig) -> LLMConfig | None: + """Interoperate the llm_config with the agent_config""" if self.agent_config is not None and self.agent_config.api_type is not None: for config in llm_config.config_list: if isinstance(config, dict): diff --git a/autogen/oai/agent_config_handler.py b/autogen/oai/agent_config_handler.py index 54f745fcfaa..62786cb5cb3 100644 --- a/autogen/oai/agent_config_handler.py +++ b/autogen/oai/agent_config_handler.py @@ -9,6 +9,7 @@ def agent_config_parser(agent_config: "AgentConfig") -> dict[str, Any]: + """Parse the agent_config to a dictionary""" _agent_config: dict[str, Any] = {} if hasattr(agent_config, "response_format") and agent_config.response_format is not None: _agent_config["response_format"] = agent_config.response_format From 992e7e5fb630664d4a23d024e338aac002766ab5 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh Date: Mon, 5 Jan 2026 10:03:26 +0530 Subject: [PATCH 26/30] test: inteoperate_llm_config --- autogen/llm_config/config.py | 2 +- test/agentchat/test_conversable_agent.py | 92 +++++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/autogen/llm_config/config.py b/autogen/llm_config/config.py index 504ca8c5de2..fea28aebbd4 100644 --- a/autogen/llm_config/config.py +++ b/autogen/llm_config/config.py @@ -54,7 +54,7 @@ def default(cls) -> "LLMConfig": @export_module("autogen") class AgentConfig(BaseModel): response_format: str | dict[str, Any] | BaseModel | type[BaseModel] | None = None - api_type: str | None = None + api_type: Literal["openai", "responses"] | None = None model_config = ConfigDict(extra="allow") diff --git a/test/agentchat/test_conversable_agent.py b/test/agentchat/test_conversable_agent.py index d1b041004a3..484c59727a9 100755 --- a/test/agentchat/test_conversable_agent.py +++ b/test/agentchat/test_conversable_agent.py @@ -31,7 +31,7 @@ from autogen.fast_depends.utils import is_coroutine_callable from autogen.import_utils import run_for_optional_imports, skip_on_missing_imports from autogen.llm_config import LLMConfig -from autogen.oai.client import OpenAILLMConfigEntry +from autogen.oai.client import OpenAILLMConfigEntry, OpenAIResponsesLLMConfigEntry from autogen.tools.tool import Tool from test.credentials import Credentials from test.marks import credentials_all_llms @@ -2245,3 +2245,93 @@ def runtime_tool(message: str) -> str: assert len(executor.function_map) == 2 assert "pre_tool" in executor.function_map assert "runtime_tool" in executor.function_map + + +def test_interoperate_llm_config(): + """Test _interoperate_llm_config method with various scenarios.""" + from autogen.llm_config import AgentConfig + + # Test 1: agent_config is None - should return llm_config unchanged + agent = ConversableAgent("agent1", llm_config=False) + agent.agent_config = None + llm_config = LLMConfig(OpenAILLMConfigEntry(model="gpt-3", api_key="test")) + original_api_type = llm_config.config_list[0].api_type + + result = agent._interoperate_llm_config(llm_config) + assert result is llm_config + assert llm_config.config_list[0].api_type == original_api_type + + # Test 2: agent_config.api_type is None - should return llm_config unchanged + agent.agent_config = AgentConfig(api_type=None) + llm_config = LLMConfig(OpenAILLMConfigEntry(model="gpt-3", api_key="test")) + original_api_type = llm_config.config_list[0].api_type + + result = agent._interoperate_llm_config(llm_config) + assert result is llm_config + assert llm_config.config_list[0].api_type == original_api_type + + # Test 3: agent_config exists with api_type="openai" and config_list contains dicts + agent.agent_config = AgentConfig(api_type="openai") + # Create LLMConfig with dict entries (legacy format) + llm_config_dict = LLMConfig({"model": "gpt-3", "api_key": "test"}) + + result = agent._interoperate_llm_config(llm_config_dict) + assert result is llm_config_dict + # Check that dict entries got updated + for config in llm_config_dict.config_list: + if isinstance(config, dict): + assert config["api_type"] == "openai" + + # Test 4: agent_config exists with api_type="openai" and config_list contains LLMConfigEntry objects + agent.agent_config = AgentConfig(api_type="openai") + llm_config = LLMConfig(OpenAILLMConfigEntry(model="gpt-3", api_key="test")) + original_api_type = llm_config.config_list[0].api_type + + result = agent._interoperate_llm_config(llm_config) + assert result is llm_config + # Check that LLMConfigEntry objects got updated (or remain correct if already matching) + assert llm_config.config_list[0].api_type == "openai" + # Note: original_api_type is already "openai" for OpenAILLMConfigEntry, so no change occurs + assert llm_config.config_list[0].api_type == original_api_type + + # Test 5: agent_config exists with api_type="responses" and config_list contains mixed types + agent.agent_config = AgentConfig(api_type="responses") + llm_config = LLMConfig(OpenAILLMConfigEntry(model="gpt-3", api_key="test"), {"model": "gpt-4", "api_key": "test2"}) + + result = agent._interoperate_llm_config(llm_config) + assert result is llm_config + # Check that all entries got updated + for config in llm_config.config_list: + if isinstance(config, dict): + assert config["api_type"] == "responses" + elif hasattr(config, "api_type"): + # For typed entries, api_type is fixed by the type, but interoperability sets it + # OpenAILLMConfigEntry has api_type="openai" which is fixed, so it won't change to "responses" + # But dict entries should be updated + pass + + # Test 6: agent_config exists with api_type="responses" and config_list contains dict entries + agent.agent_config = AgentConfig(api_type="responses") + # Create LLMConfig with dict entries to test api_type update + llm_config = LLMConfig({"model": "gpt-3", "api_key": "test"}) + original_api_type = llm_config.config_list[0].get("api_type") + + result = agent._interoperate_llm_config(llm_config) + assert result is llm_config + # Check that dict entries got updated to "responses" + assert llm_config.config_list[0]["api_type"] == "responses" + if original_api_type is not None: + assert llm_config.config_list[0]["api_type"] != original_api_type + + # Test 7: agent_config exists with api_type="openai" and config_list contains OpenAIResponsesLLMConfigEntry + agent.agent_config = AgentConfig(api_type="openai") + llm_config = LLMConfig(OpenAIResponsesLLMConfigEntry(model="gpt-3", api_key="test")) + original_api_type = llm_config.config_list[0].api_type + + result = agent._interoperate_llm_config(llm_config) + assert result is llm_config + # The interoperability method sets api_type from agent_config, even for typed entries + # Note: This changes the api_type from "responses" to "openai" despite the Literal type constraint + assert llm_config.config_list[0].api_type == "openai" # Changed by interoperability + assert original_api_type == "responses" # Was originally "responses" + assert llm_config.config_list[0].api_type != original_api_type # Confirms it changed From 55d5247f789be2bbc4717b1506b55c851c44f116 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh Date: Mon, 5 Jan 2026 13:25:36 +0530 Subject: [PATCH 27/30] documentation: notebook example --- autogen/__init__.py | 3 +- notebook/agnetchat_agentconfig_example.ipynb | 314 +++++++++++++++++++ 2 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 notebook/agnetchat_agentconfig_example.ipynb diff --git a/autogen/__init__.py b/autogen/__init__.py index 5fee8c8d1b5..4cb81d773f9 100644 --- a/autogen/__init__.py +++ b/autogen/__init__.py @@ -31,7 +31,7 @@ SenderRequiredError, UndefinedNextAgentError, ) -from .llm_config import LLMConfig, ModelClient +from .llm_config import AgentConfig, LLMConfig, ModelClient from .oai import ( Cache, OpenAIWrapper, @@ -54,6 +54,7 @@ "DEFAULT_MODEL", "FAST_MODEL", "Agent", + "AgentConfig", "AgentNameConflictError", "AssistantAgent", "Cache", diff --git a/notebook/agnetchat_agentconfig_example.ipynb b/notebook/agnetchat_agentconfig_example.ipynb new file mode 100644 index 00000000000..7b873b75459 --- /dev/null +++ b/notebook/agnetchat_agentconfig_example.ipynb @@ -0,0 +1,314 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# AgentConfig for Structured Outputs\n", + "\n", + "AG2 now supports an official `AgentConfig` class (from `autogen.llm_config`) for advanced per-agent configuration, such as specifying structured output schemas or agent-level API settings. This notebook demonstrates how to use `AgentConfig` to enable schema-based structured outputs in your agents.\n", + "\n", + "````{=mdx}\n", + ":::info Requirements\n", + "Install `ag2`:\n", + "\n", + "pip install -U ag2[openai]\n", + "```\n", + "\n", + "> **Note:** If you have been using `autogen` or `ag2`, all you need to do is upgrade it using: \n", + ">\n", + "> pip install -U autogen\n", + "> ```\n", + "> or \n", + ">\n", + "> pip install -U ag2\n", + "> ```\n", + "> as `autogen`, and `ag2` are aliases for the same PyPI package. \n", + "\n", + "\n", + "For more information, please refer to the [installation guide](https://docs.ag2.ai/latest/docs/user-guide/basic-concepts/installing-ag2).\n", + ":::\n", + "````" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What is AgentConfig?\n", + "\n", + "`AgentConfig` is a dedicated configuration class designed for per-agent settings in AG2. It provides a clean and explicit way to configure agent-specific features, particularly structured outputs through the `response_format` parameter.\n", + "\n", + "### Key Features:\n", + "\n", + "- **Per-Agent Configuration**: Configure settings specific to individual agents without affecting others\n", + "- **Structured Output Support**: Specify Pydantic models as `response_format` to enforce structured responses\n", + "- **API Type Control**: Set `api_type` directly at the agent level\n", + "- **Extensible Design**: Accepts additional configuration parameters through keyword arguments\n", + "\n", + "### Advantages over LLMConfig for Agent-Level Configuration:\n", + "\n", + "1. **Simplicity**: `AgentConfig` is designed specifically for agent-level configuration, making it more intuitive for per-agent settings\n", + "2. **Clarity**: Explicitly indicates that the configuration is agent-specific, improving code readability\n", + "3. **Flexibility**: Allows different agents in the same workflow to have different API types and response formats\n", + "4. **Preference for Structured Outputs**: `AgentConfig` is often preferred for setting up structured outputs, as it provides a clean interface for schema-based results\n", + "\n", + "You can pass either `llm_config` (using `LLMConfig`) or `agent_config` (using `AgentConfig`) to agents, depending on your needs. Use `AgentConfig` when you want per-agent structured output or need to control how a particular agent interacts with the LLM API." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 1: Two-Agent Chat with Structured Outputs\n", + "\n", + "This example demonstrates how to use `AgentConfig` with structured outputs in a simple two-agent conversation. We'll create an assistant agent that responds with structured joke data, and a user proxy agent to interact with it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from pydantic import BaseModel\n", + "\n", + "from autogen import AgentConfig, AssistantAgent, LLMConfig, UserProxyAgent\n", + "\n", + "llm_config = LLMConfig(\n", + " config_list=[\n", + " {\n", + " \"model\": \"gpt-4o-mini\",\n", + " \"api_key\": os.getenv(\"OPENAI_API_KEY\"),\n", + " \"api_type\": \"openai\",\n", + " }\n", + " ]\n", + ")\n", + "\n", + "\n", + "# Define a structured output schema for the assistant's responses\n", + "class JokeOutputSchema(BaseModel):\n", + " joke: str\n", + " explanation: str\n", + "\n", + "\n", + "# Create AgentConfig with structured output and API type\n", + "assistant_config = AgentConfig(\n", + " response_format=JokeOutputSchema,\n", + " api_type=\"openai\",\n", + ")\n", + "\n", + "# Create the assistant agent with AgentConfig\n", + "assistant = AssistantAgent(\n", + " \"assistant\",\n", + " agent_config=assistant_config,\n", + " system_message=\"You are a helpful assistant that tells jokes and explains them. Always structure your responses according to the JokeOutputSchema.\",\n", + " llm_config=llm_config,\n", + ")\n", + "\n", + "# Create the user proxy agent (no structured output needed)\n", + "user_proxy = UserProxyAgent(\n", + " \"user_proxy\",\n", + " code_execution_config=False,\n", + " human_input_mode=\"NEVER\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Start the Two-Agent Chat\n", + "\n", + "Now let's initiate a conversation between the user proxy and the assistant. The assistant will respond with structured data matching our `JokeOutputSchema`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Start the chat\n", + "\n", + "result = user_proxy.run(\n", + " assistant,\n", + " message=\"Tell me a joke about NVDA and TESLA stock prices.\",\n", + " max_turns=1,\n", + ")\n", + "result.process()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Example 2: GroupChat with Multiple Agents Using Structured Outputs\n", + "\n", + "In this example, we'll create a GroupChat with multiple agents, each potentially using `AgentConfig` for structured outputs. This demonstrates how different agents can have different configurations in a multi-agent conversation.\n", + "\n", + "We'll create a scenario where:\n", + "- A **Reviewer** agent provides structured feedback\n", + "- A **Summarizer** agent provides structured summaries\n", + "- A **User Proxy** agent coordinates the conversation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "from pydantic import BaseModel\n", + "\n", + "from autogen import AgentConfig, AssistantAgent, LLMConfig, UserProxyAgent\n", + "from autogen.agentchat import initiate_group_chat\n", + "from autogen.agentchat.group.patterns.auto import AutoPattern\n", + "\n", + "\n", + "class Answer(BaseModel):\n", + " answer: str\n", + "\n", + "\n", + "llm_config = LLMConfig(\n", + " config_list=[\n", + " {\n", + " \"model\": \"gpt-4o-mini\",\n", + " \"api_key\": os.getenv(\"OPENAI_API_KEY\"),\n", + " \"api_type\": \"openai\",\n", + " \"response_format\": Answer,\n", + " }\n", + " ]\n", + ")\n", + "\n", + "\n", + "# Define structured output schema for the Reviewer agent\n", + "class ReviewFeedback(BaseModel):\n", + " rating: int # 1-5 scale\n", + " strengths: list[str]\n", + " weaknesses: list[str]\n", + " recommendation: str\n", + "\n", + "\n", + "# Define structured output schema for the Summarizer agent\n", + "class SummaryOutput(BaseModel):\n", + " title: str\n", + " key_points: list[str]\n", + " conclusion: str\n", + "\n", + "\n", + "# Create AgentConfig for the Reviewer agent\n", + "reviewer_config = AgentConfig(\n", + " response_format=ReviewFeedback,\n", + ")\n", + "\n", + "# Create AgentConfig for the Summarizer agent\n", + "summarizer_config = AgentConfig(\n", + " response_format=SummaryOutput,\n", + ")\n", + "\n", + "# Create the Reviewer agent with structured output\n", + "reviewer = AssistantAgent(\n", + " name=\"reviewer\",\n", + " agent_config=reviewer_config,\n", + " system_message=\"You are a thorough reviewer. Analyze content and provide structured feedback with ratings, strengths, weaknesses, and recommendations.\",\n", + " llm_config=llm_config,\n", + ")\n", + "\n", + "# Create the Summarizer agent with structured output\n", + "summarizer = AssistantAgent(\n", + " name=\"summarizer\",\n", + " agent_config=summarizer_config,\n", + " system_message=\"You are a concise summarizer. Create structured summaries with titles, key points, and conclusions.\",\n", + " llm_config=llm_config,\n", + ")\n", + "\n", + "# Create the User Proxy agent (no structured output needed)\n", + "user_proxy = UserProxyAgent(\n", + " name=\"user_proxy\",\n", + " code_execution_config=False,\n", + " human_input_mode=\"NEVER\",\n", + " llm_config=llm_config,\n", + ")\n", + "\n", + "# Create GroupChat with all agents\n", + "pattern = AutoPattern(\n", + " initial_agent=user_proxy,\n", + " agents=[reviewer, summarizer, user_proxy],\n", + " group_manager_args={\n", + " \"llm_config\": llm_config, # Use same config for group manager\n", + " },\n", + ")\n", + "\n", + "print(\"AutoPattern created with structured output agent!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Start the GroupChat\n", + "\n", + "Now let's initiate the GroupChat. The reviewer and summarizer agents will provide structured responses according to their respective schemas." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Start the GroupChat conversation\n", + "result, context, last_agent = initiate_group_chat(\n", + " pattern=pattern,\n", + " messages=\"Please review and summarize this idea: Creating an AI-powered assistant for code review that provides structured feedback on code quality, security, and best practices.\",\n", + " max_rounds=5,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Key Takeaways\n", + "\n", + "1. **AgentConfig is Ideal for Structured Outputs**: When you need to enforce structured responses from agents, `AgentConfig` provides a clean and explicit way to specify the `response_format` parameter.\n", + "\n", + "2. **Per-Agent Flexibility**: Different agents in the same workflow can have different structured output schemas, allowing for specialized agent roles with specific response formats.\n", + "\n", + "3. **Simplicity**: `AgentConfig` simplifies agent-level configuration, making it easier to understand and maintain code that uses structured outputs.\n", + "\n", + "4. **Integration with GroupChat**: `AgentConfig` works seamlessly with GroupChat, allowing multiple agents with different structured output schemas to collaborate effectively.\n", + "\n", + "5. **Clean System Messages**: With structured outputs, you don't need to include formatting instructions in system messages - the schema enforces the structure automatically.\n", + "\n", + "Remember: You can use either `llm_config` (with `LLMConfig`) or `agent_config` (with `AgentConfig`) depending on your needs. For per-agent structured outputs and clearer agent-specific configuration, `AgentConfig` is the preferred choice." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.2" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 327cd85587947cba64de578c20a3a969804eaf4f Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh Date: Mon, 5 Jan 2026 18:07:17 +0530 Subject: [PATCH 28/30] feat: make agentconfig groupchat compatible --- autogen/agentchat/conversable_agent.py | 2 +- autogen/oai/client.py | 40 +++++++++++++------- notebook/agnetchat_agentconfig_example.ipynb | 2 +- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/autogen/agentchat/conversable_agent.py b/autogen/agentchat/conversable_agent.py index 950093f2c3f..e4f90081db3 100644 --- a/autogen/agentchat/conversable_agent.py +++ b/autogen/agentchat/conversable_agent.py @@ -2164,7 +2164,7 @@ def generate_oai_reply( **kwargs: Any, ) -> tuple[bool, str | dict[str, Any] | None]: """Generate a reply using autogen.oai.""" - logger.info(f"Messages: {self.client}") + # logger.info(f"Messages: {self.client}") client = self.client if config is None else config if client is None: return False, None diff --git a/autogen/oai/client.py b/autogen/oai/client.py index d91d82911fb..3198dcea992 100644 --- a/autogen/oai/client.py +++ b/autogen/oai/client.py @@ -501,17 +501,29 @@ def create(self, params: dict[str, Any]) -> ChatCompletion: agent_config = params.pop("agent_config", None) agent_config = agent_config_parser(agent_config) if agent_config is not None else None iostream = IOStream.get_default() - self.response_format = ( + + # Priority: agent_config.response_format > llm_config.response_format (params) + # This logic correctly implements: + # 1. If response_format in llm_config but NOT in agent_config → use llm_config + # 2. If response_format NOT in llm_config but IN agent_config → use agent_config + # 3. If in BOTH → use agent_config (agent_config takes priority) + agent_config_response_format = ( agent_config.get("response_format") if agent_config is not None and "response_format" in agent_config and agent_config.get("response_format") is not None - else params.get("response_format") + else None ) - params["response_format"] = self.response_format - is_structured_output = ( - self.response_format is not None or "response_format" in params or agent_config is not None + llm_config_response_format = params.get("response_format") + + # Set response_format with proper priority: agent_config > llm_config + self.response_format = ( + agent_config_response_format if agent_config_response_format is not None else llm_config_response_format ) + params["response_format"] = self.response_format + + # Fix: Only enable structured output when we actually have a response_format + is_structured_output = self.response_format is not None if is_structured_output: @@ -520,24 +532,26 @@ def _create_or_parse(*args, **kwargs): kwargs.pop("stream") kwargs.pop("stream_options", None) - if ( - isinstance(kwargs["response_format"], dict) - and kwargs["response_format"].get("type") != "json_object" - ): + # Get response_format from kwargs (which comes from params) + response_format_value = kwargs.get("response_format") + + if response_format_value is None: + # Should not happen if is_structured_output is True, but guard against it + kwargs.pop("response_format", None) + elif isinstance(response_format_value, dict) and response_format_value.get("type") != "json_object": kwargs["response_format"] = { "type": "json_schema", "json_schema": { "schema": _ensure_strict_json_schema( - kwargs["response_format"], path=(), root=kwargs["response_format"] + response_format_value, path=(), root=response_format_value ), "name": "response_format", "strict": True, }, } else: - kwargs["response_format"] = type_to_response_format_param( - self.response_format or params["response_format"] - ) + # Convert Pydantic model or other types to OpenAI's response_format format + kwargs["response_format"] = type_to_response_format_param(response_format_value) return self._oai_client.chat.completions.create(*args, **kwargs) diff --git a/notebook/agnetchat_agentconfig_example.ipynb b/notebook/agnetchat_agentconfig_example.ipynb index 7b873b75459..749f75adb10 100644 --- a/notebook/agnetchat_agentconfig_example.ipynb +++ b/notebook/agnetchat_agentconfig_example.ipynb @@ -180,7 +180,7 @@ " \"model\": \"gpt-4o-mini\",\n", " \"api_key\": os.getenv(\"OPENAI_API_KEY\"),\n", " \"api_type\": \"openai\",\n", - " \"response_format\": Answer,\n", + " # \"response_format\": Answer,\n", " }\n", " ]\n", ")\n", From 56c19ef06c36203ad40038e8826fb4df13439dc0 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh Date: Mon, 5 Jan 2026 18:15:56 +0530 Subject: [PATCH 29/30] fix: notebook content --- notebook/agnetchat_agentconfig_example.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/notebook/agnetchat_agentconfig_example.ipynb b/notebook/agnetchat_agentconfig_example.ipynb index 749f75adb10..d33a1535fe8 100644 --- a/notebook/agnetchat_agentconfig_example.ipynb +++ b/notebook/agnetchat_agentconfig_example.ipynb @@ -43,7 +43,7 @@ "\n", "- **Per-Agent Configuration**: Configure settings specific to individual agents without affecting others\n", "- **Structured Output Support**: Specify Pydantic models as `response_format` to enforce structured responses\n", - "- **API Type Control**: Set `api_type` directly at the agent level\n", + "- **API Type Control**: Set `api_type` directly at the agent level, Literal[\"openai\", \"responses\"]\n", "- **Extensible Design**: Accepts additional configuration parameters through keyword arguments\n", "\n", "### Advantages over LLMConfig for Agent-Level Configuration:\n", @@ -286,7 +286,7 @@ "\n", "5. **Clean System Messages**: With structured outputs, you don't need to include formatting instructions in system messages - the schema enforces the structure automatically.\n", "\n", - "Remember: You can use either `llm_config` (with `LLMConfig`) or `agent_config` (with `AgentConfig`) depending on your needs. For per-agent structured outputs and clearer agent-specific configuration, `AgentConfig` is the preferred choice." + "Remember: You can use `llm_config` (with `LLMConfig`) and `agent_config` (with `AgentConfig`) depending on your needs. For per-agent structured outputs and clearer agent-specific configuration, `AgentConfig` is the preferred choice." ] } ], From 24d0c9d24fde9cb408baa7cee1fe0465caebc197 Mon Sep 17 00:00:00 2001 From: Priyanshu Yashwant Deshmukh Date: Mon, 5 Jan 2026 22:30:51 +0530 Subject: [PATCH 30/30] test: agentconfig integration OAI --- test/agentchat/test_conversable_agent.py | 382 +++++++++++++++++++++++ 1 file changed, 382 insertions(+) diff --git a/test/agentchat/test_conversable_agent.py b/test/agentchat/test_conversable_agent.py index 484c59727a9..be5d8211d52 100755 --- a/test/agentchat/test_conversable_agent.py +++ b/test/agentchat/test_conversable_agent.py @@ -2335,3 +2335,385 @@ def test_interoperate_llm_config(): assert llm_config.config_list[0].api_type == "openai" # Changed by interoperability assert original_api_type == "responses" # Was originally "responses" assert llm_config.config_list[0].api_type != original_api_type # Confirms it changed + + +@pytest.mark.integration() +@run_for_optional_imports("openai", "openai") +def test_agent_config_response_format_priority_in_group_chat(credentials_gpt_4o_mini: Credentials) -> None: + """Test that agent_config.response_format takes priority over llm_config.response_format in group chat. + + This test verifies: + 1. agent_config.response_format is used when both agent_config and llm_config have response_format + 2. llm_config.response_format is used when agent_config doesn't have response_format + 3. Group chat works correctly with multiple agents using different structured output schemas + 4. Actual responses match the expected schemas + """ + import json + + from pydantic import BaseModel, ValidationError + + from autogen import AgentConfig, AssistantAgent, LLMConfig, UserProxyAgent + from autogen.agentchat import initiate_group_chat + from autogen.agentchat.group.patterns.auto import AutoPattern + + # Define structured output schemas + class ReviewFeedback(BaseModel): + rating: int # 1-5 scale + strengths: list[str] + weaknesses: list[str] + recommendation: str + + class SummaryOutput(BaseModel): + title: str + key_points: list[str] + conclusion: str + + class Answer(BaseModel): + answer: str + + # Create llm_config with response_format in config_list (but will be overridden by agent_config) + llm_config = LLMConfig( + config_list=[ + { + "model": credentials_gpt_4o_mini.config_list[0]["model"], + "api_key": credentials_gpt_4o_mini.config_list[0]["api_key"], + "api_type": "openai", + "response_format": Answer, # This should be overridden by agent_config + } + ] + ) + + # Create AgentConfig for Reviewer agent (has response_format - should take priority) + reviewer_config = AgentConfig( + response_format=ReviewFeedback, + ) + + # Create AgentConfig for Summarizer agent (has response_format - should take priority) + summarizer_config = AgentConfig( + response_format=SummaryOutput, + ) + + # Create Reviewer agent with agent_config (should use ReviewFeedback, not Answer from llm_config) + reviewer = AssistantAgent( + name="reviewer", + agent_config=reviewer_config, + system_message="You are a thorough reviewer. Analyze content and provide structured feedback with ratings, strengths, weaknesses, and recommendations. Always respond in valid JSON matching the ReviewFeedback schema.", + llm_config=llm_config, + human_input_mode="NEVER", + ) + + # Create Summarizer agent with agent_config (should use SummaryOutput, not Answer from llm_config) + summarizer = AssistantAgent( + name="summarizer", + agent_config=summarizer_config, + system_message="You are a concise summarizer. Create structured summaries with titles, key points, and conclusions. Always respond in valid JSON matching the SummaryOutput schema.", + llm_config=llm_config, + human_input_mode="NEVER", + ) + + # Create User Proxy agent without agent_config (should use Answer from llm_config) + user_proxy = UserProxyAgent( + name="user_proxy", + code_execution_config=False, + human_input_mode="NEVER", + llm_config=llm_config, + ) + + # Create GroupChat with all agents + pattern = AutoPattern( + initial_agent=user_proxy, + agents=[reviewer, summarizer, user_proxy], + group_manager_args={ + "llm_config": llm_config, + "human_input_mode": "NEVER", + }, + ) + + # Start the GroupChat conversation + result, context, last_agent = initiate_group_chat( + pattern=pattern, + messages="Please review and summarize this idea: Creating an AI-powered assistant for code review that provides structured feedback on code quality, security, and best practices.", + max_rounds=5, + ) + + # Verify the chat completed successfully + assert result is not None, "Group chat should return a result" + assert len(result.chat_history) > 0, "Group chat should have messages" + + # Verify that reviewer's response is in ReviewFeedback format (not Answer format) + reviewer_messages = [msg for msg in result.chat_history if msg.get("name") == "reviewer"] + if reviewer_messages: + reviewer_content = reviewer_messages[-1].get("content", "") + assert reviewer_content, "Reviewer should have responded with content" + + # Try to parse as JSON and validate against ReviewFeedback schema + try: + # Extract JSON from content (might be wrapped in markdown code blocks or plain JSON) + content_str = reviewer_content.strip() + # Remove markdown code blocks if present + if content_str.startswith("```") or content_str.startswith("```json"): + lines = content_str.split("\n") + content_str = "\n".join(lines[1:-1]) if len(lines) > 2 else content_str + + parsed_content = json.loads(content_str) + + # Validate against ReviewFeedback schema using Pydantic + try: + review_feedback = ReviewFeedback.model_validate(parsed_content) + # Verify it has the expected fields (not Answer format) + assert hasattr(review_feedback, "rating"), "ReviewFeedback should have 'rating' field" + assert hasattr(review_feedback, "strengths"), "ReviewFeedback should have 'strengths' field" + assert hasattr(review_feedback, "weaknesses"), "ReviewFeedback should have 'weaknesses' field" + assert hasattr(review_feedback, "recommendation"), "ReviewFeedback should have 'recommendation' field" + # Verify it does NOT have 'answer' field (which would indicate Answer format was used) + assert "answer" not in parsed_content, ( + "Reviewer should respond in ReviewFeedback format, not Answer format" + ) + except ValidationError as e: + pytest.fail(f"Reviewer response does not match ReviewFeedback schema: {e}") + except json.JSONDecodeError: + # If not JSON, at least verify it contains ReviewFeedback-specific keywords + assert any( + keyword in reviewer_content.lower() + for keyword in ["rating", "strengths", "weaknesses", "recommendation"] + ), "Reviewer should respond in ReviewFeedback format, not Answer format" + + # Verify that summarizer's response is in SummaryOutput format (not Answer format) + summarizer_messages = [msg for msg in result.chat_history if msg.get("name") == "summarizer"] + if summarizer_messages: + summarizer_content = summarizer_messages[-1].get("content", "") + assert summarizer_content, "Summarizer should have responded with content" + + # Try to parse as JSON and validate against SummaryOutput schema + try: + # Extract JSON from content + content_str = summarizer_content.strip() + if content_str.startswith("```") or content_str.startswith("```json"): + lines = content_str.split("\n") + content_str = "\n".join(lines[1:-1]) if len(lines) > 2 else content_str + + parsed_content = json.loads(content_str) + + # Validate against SummaryOutput schema using Pydantic + try: + summary_output = SummaryOutput.model_validate(parsed_content) + # Verify it has the expected fields (not Answer format) + assert hasattr(summary_output, "title"), "SummaryOutput should have 'title' field" + assert hasattr(summary_output, "key_points"), "SummaryOutput should have 'key_points' field" + assert hasattr(summary_output, "conclusion"), "SummaryOutput should have 'conclusion' field" + # Verify it does NOT have 'answer' field + assert "answer" not in parsed_content, ( + "Summarizer should respond in SummaryOutput format, not Answer format" + ) + except ValidationError as e: + pytest.fail(f"Summarizer response does not match SummaryOutput schema: {e}") + except json.JSONDecodeError: + # If not JSON, at least verify it contains SummaryOutput-specific keywords + assert any( + keyword in summarizer_content.lower() for keyword in ["title", "key_points", "conclusion", "key points"] + ), "Summarizer should respond in SummaryOutput format, not Answer format" + + # Verify that agents with agent_config use their own response_format, not llm_config's + assert reviewer.agent_config is not None + assert reviewer.agent_config.response_format == ReviewFeedback + assert summarizer.agent_config is not None + assert summarizer.agent_config.response_format == SummaryOutput + + +@run_for_optional_imports("openai", "openai") +def test_agent_config_response_format_fallback_to_llm_config(credentials_gpt_4o_mini: Credentials) -> None: + """Test that llm_config.response_format is used when agent_config doesn't have response_format.""" + import json + + from pydantic import BaseModel, ValidationError + + from autogen import AgentConfig, AssistantAgent, LLMConfig, UserProxyAgent + + class Answer(BaseModel): + answer: str + + # Create llm_config with response_format + llm_config = LLMConfig( + config_list=[ + { + "model": credentials_gpt_4o_mini.config_list[0]["model"], + "api_key": credentials_gpt_4o_mini.config_list[0]["api_key"], + "api_type": "openai", + "response_format": Answer, + } + ] + ) + + # Create AgentConfig without response_format (should fallback to llm_config) + agent_config_no_format = AgentConfig( + # No response_format specified + ) + + # Create agent with agent_config that has no response_format + agent = AssistantAgent( + name="test_agent", + agent_config=agent_config_no_format, + system_message="You are a helpful assistant. Always respond in the Answer format with a JSON object containing an 'answer' field.", + llm_config=llm_config, + ) + + # Verify that the agent will use llm_config's response_format + assert agent.agent_config is not None + assert agent.agent_config.response_format is None, "agent_config should not have response_format" + + # Test that the agent actually uses llm_config's response_format by running it + user_proxy = UserProxyAgent( + name="user_proxy", + code_execution_config=False, + human_input_mode="NEVER", + ) + + result = user_proxy.run( + agent, + message="What is 2+2?", + max_turns=1, + ) + + # Verify the response matches Answer schema + assert result is not None + messages = result.chat_history if hasattr(result, "chat_history") else [] + + assistant_messages = [msg for msg in messages if msg.get("name") == "test_agent" or msg.get("role") == "assistant"] + if assistant_messages: + content = assistant_messages[-1].get("content", "") + assert content, "Agent should have responded with content" + + # Try to parse and validate against Answer schema + try: + content_str = content.strip() + if content_str.startswith("```") or content_str.startswith("```json"): + lines = content_str.split("\n") + content_str = "\n".join(lines[1:-1]) if len(lines) > 2 else content_str + + parsed_content = json.loads(content_str) + + # Validate against Answer schema + try: + answer = Answer.model_validate(parsed_content) + assert hasattr(answer, "answer"), "Response should have 'answer' field matching Answer schema" + assert isinstance(answer.answer, str), "Answer field should be a string" + assert len(answer.answer) > 0, "Answer should not be empty" + except ValidationError as e: + pytest.fail(f"Agent response does not match Answer schema (from llm_config): {e}") + except json.JSONDecodeError: + # If not JSON, at least verify it contains answer-like content + assert "answer" in content.lower() or len(content) > 0, ( + "Agent should respond in Answer format from llm_config" + ) + + +@run_for_optional_imports("openai", "openai") +def test_agent_config_response_format_priority_single_agent(credentials_gpt_4o_mini: Credentials) -> None: + """Test that agent_config.response_format takes priority in a simple two-agent chat.""" + import json + + from pydantic import BaseModel, ValidationError + + from autogen import AgentConfig, AssistantAgent, LLMConfig, UserProxyAgent + + class Answer(BaseModel): + answer: str + + class JokeOutput(BaseModel): + joke: str + explanation: str + + # Create llm_config with Answer as response_format + llm_config = LLMConfig( + config_list=[ + { + "model": credentials_gpt_4o_mini.config_list[0]["model"], + "api_key": credentials_gpt_4o_mini.config_list[0]["api_key"], + "api_type": "openai", + "response_format": Answer, # This should be overridden + } + ] + ) + + # Create AgentConfig with JokeOutput (should take priority) + agent_config = AgentConfig( + response_format=JokeOutput, + ) + + # Create assistant with agent_config + assistant = AssistantAgent( + name="assistant", + agent_config=agent_config, + system_message="You are a helpful assistant that tells jokes. Always respond in valid JSON matching the JokeOutput schema with 'joke' and 'explanation' fields.", + llm_config=llm_config, + ) + + # Create user proxy + user_proxy = UserProxyAgent( + name="user_proxy", + code_execution_config=False, + human_input_mode="NEVER", + ) + + # Test the chat + result = user_proxy.run( + assistant, + message="Tell me a joke about AI.", + max_turns=1, + ) + + # Verify the response + assert result is not None + messages = result.chat_history if hasattr(result, "chat_history") else [] + + # Find assistant's message + assistant_messages = [msg for msg in messages if msg.get("name") == "assistant" or msg.get("role") == "assistant"] + if assistant_messages: + content = assistant_messages[-1].get("content", "") + assert content, "Assistant should have responded with content" + + # Try to parse and validate against JokeOutput schema (not Answer schema) + try: + content_str = content.strip() + if content_str.startswith("```") or content_str.startswith("```json"): + lines = content_str.split("\n") + content_str = "\n".join(lines[1:-1]) if len(lines) > 2 else content_str + + parsed_content = json.loads(content_str) + + # Validate against JokeOutput schema (should succeed) + try: + joke_output = JokeOutput.model_validate(parsed_content) + assert hasattr(joke_output, "joke"), "Response should have 'joke' field matching JokeOutput schema" + assert hasattr(joke_output, "explanation"), ( + "Response should have 'explanation' field matching JokeOutput schema" + ) + assert isinstance(joke_output.joke, str), "Joke field should be a string" + assert isinstance(joke_output.explanation, str), "Explanation field should be a string" + # Verify it does NOT have 'answer' field (which would indicate Answer format was used) + assert "answer" not in parsed_content, ( + "Assistant should respond in JokeOutput format (from agent_config), not Answer format (from llm_config)" + ) + except ValidationError as e: + pytest.fail(f"Assistant response does not match JokeOutput schema (from agent_config): {e}") + + # Verify it does NOT match Answer schema (should fail validation) + try: + Answer.model_validate(parsed_content) + pytest.fail("Response should NOT match Answer schema - agent_config should override llm_config") + except ValidationError: + # This is expected - Answer validation should fail + pass + + except json.JSONDecodeError: + # If not JSON, at least verify it contains JokeOutput-specific keywords + assert any(keyword in content.lower() for keyword in ["joke", "explanation"]), ( + "Assistant should respond in JokeOutput format, not Answer format" + ) + assert "answer" not in content.lower(), ( + "Assistant should not respond in Answer format - agent_config should override llm_config" + ) + + # Verify agent_config takes priority + assert assistant.agent_config is not None + assert assistant.agent_config.response_format == JokeOutput