You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_openai_agent.py 20 kB

Feature: Add OpenAIAgent backed by OpenAI Response API (#6418) ## Why are these changes needed? This PR introduces a new `OpenAIAgent` implementation that uses the [OpenAI Response API](https://platform.openai.com/docs/guides/responses-vs-chat-completions) as its backend. The OpenAI Assistant API will be deprecated in 2026, and the Response API is its successor. This change ensures our codebase is future-proof and aligned with OpenAI’s latest platform direction. ### Motivation - **Deprecation Notice:** The OpenAI Assistant API will be deprecated in 2026. - **Future-Proofing:** The Response API is the recommended replacement and offers improved capabilities for stateful, multi-turn, and tool-augmented conversations. - **AgentChat Compatibility:** The new agent is designed to conform to the behavior and expectations of `AssistantAgent` in AgentChat, but is implemented directly on top of the OpenAI Response API. ### Key Changes - **New Agent:** Adds `OpenAIAgent`, a stateful agent that interacts with the OpenAI Response API. - **Stateful Design:** The agent maintains conversation state, tool usage, and other metadata as required by the Response API. - **AssistantAgent Parity:** The new agent matches the interface and behavior of `AssistantAgent` in AgentChat, ensuring a smooth migration path. - **Direct OpenAI Integration:** Uses the official `openai` Python library for all API interactions. - **Extensible:** Designed to support future enhancements, such as advanced tool use, function calling, and multi-modal capabilities. ### Migration Path - Existing users of the Assistant API should migrate to the new `OpenAIAgent` to ensure long-term compatibility. - Documentation and examples will be updated to reflect the new agent and its usage patterns. ### References - [OpenAI: Responses vs. Chat Completions](https://platform.openai.com/docs/guides/responses-vs-chat-completions) - [OpenAI Deprecation Notice](https://platform.openai.com/docs/guides/responses-vs-chat-completions#deprecation-timeline) --- ## Related issue number Closes #6032 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [X] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [X] I've made sure all auto checks have passed. Co-authored-by: Griffin Bassman <griffinbassman@gmail.com>
1 year ago
Feature: Add OpenAIAgent backed by OpenAI Response API (#6418) ## Why are these changes needed? This PR introduces a new `OpenAIAgent` implementation that uses the [OpenAI Response API](https://platform.openai.com/docs/guides/responses-vs-chat-completions) as its backend. The OpenAI Assistant API will be deprecated in 2026, and the Response API is its successor. This change ensures our codebase is future-proof and aligned with OpenAI’s latest platform direction. ### Motivation - **Deprecation Notice:** The OpenAI Assistant API will be deprecated in 2026. - **Future-Proofing:** The Response API is the recommended replacement and offers improved capabilities for stateful, multi-turn, and tool-augmented conversations. - **AgentChat Compatibility:** The new agent is designed to conform to the behavior and expectations of `AssistantAgent` in AgentChat, but is implemented directly on top of the OpenAI Response API. ### Key Changes - **New Agent:** Adds `OpenAIAgent`, a stateful agent that interacts with the OpenAI Response API. - **Stateful Design:** The agent maintains conversation state, tool usage, and other metadata as required by the Response API. - **AssistantAgent Parity:** The new agent matches the interface and behavior of `AssistantAgent` in AgentChat, ensuring a smooth migration path. - **Direct OpenAI Integration:** Uses the official `openai` Python library for all API interactions. - **Extensible:** Designed to support future enhancements, such as advanced tool use, function calling, and multi-modal capabilities. ### Migration Path - Existing users of the Assistant API should migrate to the new `OpenAIAgent` to ensure long-term compatibility. - Documentation and examples will be updated to reflect the new agent and its usage patterns. ### References - [OpenAI: Responses vs. Chat Completions](https://platform.openai.com/docs/guides/responses-vs-chat-completions) - [OpenAI Deprecation Notice](https://platform.openai.com/docs/guides/responses-vs-chat-completions#deprecation-timeline) --- ## Related issue number Closes #6032 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [X] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [X] I've made sure all auto checks have passed. Co-authored-by: Griffin Bassman <griffinbassman@gmail.com>
1 year ago
Feature: Add OpenAIAgent backed by OpenAI Response API (#6418) ## Why are these changes needed? This PR introduces a new `OpenAIAgent` implementation that uses the [OpenAI Response API](https://platform.openai.com/docs/guides/responses-vs-chat-completions) as its backend. The OpenAI Assistant API will be deprecated in 2026, and the Response API is its successor. This change ensures our codebase is future-proof and aligned with OpenAI’s latest platform direction. ### Motivation - **Deprecation Notice:** The OpenAI Assistant API will be deprecated in 2026. - **Future-Proofing:** The Response API is the recommended replacement and offers improved capabilities for stateful, multi-turn, and tool-augmented conversations. - **AgentChat Compatibility:** The new agent is designed to conform to the behavior and expectations of `AssistantAgent` in AgentChat, but is implemented directly on top of the OpenAI Response API. ### Key Changes - **New Agent:** Adds `OpenAIAgent`, a stateful agent that interacts with the OpenAI Response API. - **Stateful Design:** The agent maintains conversation state, tool usage, and other metadata as required by the Response API. - **AssistantAgent Parity:** The new agent matches the interface and behavior of `AssistantAgent` in AgentChat, ensuring a smooth migration path. - **Direct OpenAI Integration:** Uses the official `openai` Python library for all API interactions. - **Extensible:** Designed to support future enhancements, such as advanced tool use, function calling, and multi-modal capabilities. ### Migration Path - Existing users of the Assistant API should migrate to the new `OpenAIAgent` to ensure long-term compatibility. - Documentation and examples will be updated to reflect the new agent and its usage patterns. ### References - [OpenAI: Responses vs. Chat Completions](https://platform.openai.com/docs/guides/responses-vs-chat-completions) - [OpenAI Deprecation Notice](https://platform.openai.com/docs/guides/responses-vs-chat-completions#deprecation-timeline) --- ## Related issue number Closes #6032 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [X] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [X] I've made sure all auto checks have passed. Co-authored-by: Griffin Bassman <griffinbassman@gmail.com>
1 year ago
Feature: Add OpenAIAgent backed by OpenAI Response API (#6418) ## Why are these changes needed? This PR introduces a new `OpenAIAgent` implementation that uses the [OpenAI Response API](https://platform.openai.com/docs/guides/responses-vs-chat-completions) as its backend. The OpenAI Assistant API will be deprecated in 2026, and the Response API is its successor. This change ensures our codebase is future-proof and aligned with OpenAI’s latest platform direction. ### Motivation - **Deprecation Notice:** The OpenAI Assistant API will be deprecated in 2026. - **Future-Proofing:** The Response API is the recommended replacement and offers improved capabilities for stateful, multi-turn, and tool-augmented conversations. - **AgentChat Compatibility:** The new agent is designed to conform to the behavior and expectations of `AssistantAgent` in AgentChat, but is implemented directly on top of the OpenAI Response API. ### Key Changes - **New Agent:** Adds `OpenAIAgent`, a stateful agent that interacts with the OpenAI Response API. - **Stateful Design:** The agent maintains conversation state, tool usage, and other metadata as required by the Response API. - **AssistantAgent Parity:** The new agent matches the interface and behavior of `AssistantAgent` in AgentChat, ensuring a smooth migration path. - **Direct OpenAI Integration:** Uses the official `openai` Python library for all API interactions. - **Extensible:** Designed to support future enhancements, such as advanced tool use, function calling, and multi-modal capabilities. ### Migration Path - Existing users of the Assistant API should migrate to the new `OpenAIAgent` to ensure long-term compatibility. - Documentation and examples will be updated to reflect the new agent and its usage patterns. ### References - [OpenAI: Responses vs. Chat Completions](https://platform.openai.com/docs/guides/responses-vs-chat-completions) - [OpenAI Deprecation Notice](https://platform.openai.com/docs/guides/responses-vs-chat-completions#deprecation-timeline) --- ## Related issue number Closes #6032 ## Checks - [ ] I've included any doc changes needed for <https://microsoft.github.io/autogen/>. See <https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to build and test documentation locally. - [X] I've added tests (if relevant) corresponding to the changes introduced in this PR. - [X] I've made sure all auto checks have passed. Co-authored-by: Griffin Bassman <griffinbassman@gmail.com>
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. import json
  2. from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional, Type, Union, cast
  3. from unittest.mock import AsyncMock, MagicMock, patch
  4. import pytest
  5. from autogen_agentchat.base import Response
  6. from autogen_agentchat.messages import BaseChatMessage, MultiModalMessage, TextMessage
  7. from autogen_core import CancellationToken, Image
  8. from autogen_core.models import UserMessage
  9. from autogen_core.tools import Tool, ToolSchema
  10. from autogen_ext.agents.openai import OpenAIAgent
  11. from openai import AsyncOpenAI
  12. from pydantic import BaseModel
  13. class FakeChunkDelta:
  14. def __init__(self, content: Optional[str] = None, tool_calls: Optional[List[Any]] = None) -> None:
  15. self.content = content
  16. self.tool_calls = tool_calls
  17. class FakeChunkChoice:
  18. def __init__(self, delta: Optional[FakeChunkDelta] = None, finish_reason: Optional[str] = None) -> None:
  19. self.delta = delta
  20. self.finish_reason = finish_reason
  21. self.index = 0
  22. class FakeChunk:
  23. def __init__(self, id: str = "chunk-1", choices: Optional[List[FakeChunkChoice]] = None) -> None:
  24. self.id = id
  25. self.choices = choices or []
  26. class FakeToolCallFunction:
  27. def __init__(self, name: str = "", arguments: str = "") -> None:
  28. self.name = name
  29. self.arguments = arguments
  30. class FakeToolCall:
  31. def __init__(self, id: str = "call-1", function: Optional[FakeToolCallFunction] = None) -> None:
  32. self.id = id
  33. self.type = "function"
  34. self.function = function or FakeToolCallFunction()
  35. def create_mock_openai_client() -> AsyncOpenAI:
  36. """Create a mock OpenAI client for the Responses API."""
  37. client = AsyncMock(spec=AsyncOpenAI)
  38. async def mock_responses_create(**kwargs: Any) -> Any:
  39. class MockResponse:
  40. def __init__(self, output_text: str, id: str) -> None:
  41. self.output_text = output_text
  42. self.id = id
  43. if "tools" in kwargs and kwargs["tools"]:
  44. return MockResponse(output_text='{"temperature": 72.5, "conditions": "sunny"}', id="resp-123")
  45. return MockResponse(output_text="Hello world!", id="resp-abc")
  46. responses = MagicMock()
  47. responses.create = AsyncMock(side_effect=mock_responses_create)
  48. client.responses = responses
  49. return client
  50. @pytest.fixture
  51. def mock_openai_client() -> AsyncOpenAI:
  52. return create_mock_openai_client()
  53. @pytest.fixture
  54. def mock_error_client() -> AsyncOpenAI:
  55. client = AsyncMock(spec=AsyncOpenAI)
  56. beta = MagicMock()
  57. client.beta = beta
  58. beta.chat = MagicMock()
  59. beta.chat.completions = MagicMock()
  60. async def mock_create_error(**kwargs: Any) -> None:
  61. raise Exception("API Error")
  62. responses = MagicMock()
  63. responses.create = AsyncMock(side_effect=mock_create_error)
  64. client.responses = responses
  65. return client
  66. @pytest.fixture
  67. def cancellation_token() -> CancellationToken:
  68. return CancellationToken()
  69. class WeatherResponse(BaseModel):
  70. temperature: float
  71. conditions: str
  72. class GetWeatherArgs(BaseModel):
  73. location: str
  74. class WeatherTool(Tool):
  75. def __init__(self) -> None:
  76. self._name = "get_weather"
  77. self._description = "Get the current weather in a location"
  78. self._input_schema = GetWeatherArgs
  79. self._output_schema = WeatherResponse
  80. self._schema = ToolSchema(
  81. name=self._name,
  82. description=self._description,
  83. parameters={
  84. "type": "object",
  85. "properties": {"location": {"type": "string", "description": "The location to get weather for"}},
  86. "required": ["location"],
  87. },
  88. )
  89. @property
  90. def name(self) -> str:
  91. return self._name
  92. @property
  93. def description(self) -> str:
  94. return "Get the current weather in a location"
  95. @property
  96. def schema(self) -> ToolSchema:
  97. return self._schema
  98. def args_type(self) -> Type[BaseModel]:
  99. return GetWeatherArgs
  100. def return_type(self) -> Type[Any]:
  101. return WeatherResponse
  102. def state_type(self) -> Type[BaseModel] | None:
  103. return None
  104. def return_value_as_string(self, value: Any) -> str:
  105. if isinstance(value, dict):
  106. return json.dumps(value)
  107. return str(value)
  108. async def run_json(
  109. self, args: Mapping[str, Any], cancellation_token: CancellationToken, call_id: str | None = None
  110. ) -> Dict[str, Any]:
  111. _ = GetWeatherArgs(**args)
  112. return WeatherResponse(temperature=72.5, conditions="sunny").model_dump()
  113. async def load_state_json(self, state: Mapping[str, Any]) -> None:
  114. pass
  115. async def save_state_json(self) -> Mapping[str, Any]:
  116. return {}
  117. @pytest.fixture
  118. def weather_tool() -> WeatherTool:
  119. return WeatherTool()
  120. @pytest.fixture
  121. def failing_tool() -> Tool:
  122. tool = MagicMock(spec=Tool)
  123. tool.name = "failing_tool"
  124. tool.run_json = AsyncMock(side_effect=Exception("Tool execution failed"))
  125. return tool
  126. @pytest.fixture
  127. def agent(mock_openai_client: AsyncOpenAI, weather_tool: WeatherTool) -> OpenAIAgent:
  128. return OpenAIAgent(
  129. name="assistant",
  130. description="Test assistant using the Response API",
  131. client=mock_openai_client,
  132. model="gpt-4o",
  133. instructions="You are a helpful AI assistant.",
  134. tools=[weather_tool],
  135. temperature=0.7,
  136. max_output_tokens=1000,
  137. store=True,
  138. truncation="auto",
  139. )
  140. @pytest.fixture
  141. def json_mode_agent(mock_openai_client: AsyncOpenAI) -> OpenAIAgent:
  142. return OpenAIAgent(
  143. name="json_assistant",
  144. description="JSON assistant",
  145. client=mock_openai_client,
  146. model="gpt-4o",
  147. instructions="Return JSON responses",
  148. json_mode=True,
  149. )
  150. @pytest.fixture
  151. def error_agent(mock_error_client: AsyncOpenAI) -> OpenAIAgent:
  152. return OpenAIAgent(
  153. name="error_assistant",
  154. description="Assistant that generates errors",
  155. client=mock_error_client,
  156. model="gpt-4o",
  157. instructions="You are a helpful AI assistant.",
  158. )
  159. @pytest.mark.asyncio
  160. async def test_basic_response(agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  161. """Test that the agent returns a basic text response from the Responses API."""
  162. message = TextMessage(source="user", content="Hello, how are you?")
  163. response = await agent.on_messages([message], cancellation_token)
  164. assert response.chat_message is not None
  165. assert isinstance(response.chat_message, TextMessage)
  166. assert response.chat_message.content in ("Hello world!", '{"temperature": 72.5, "conditions": "sunny"}')
  167. assert response.chat_message.source == "assistant"
  168. @pytest.mark.asyncio
  169. async def test_tool_calling(agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  170. """Test that the agent can call a tool and return the result using the Responses API."""
  171. async def mock_run_json(self: Any, args: Dict[str, Any], cancellation_token: CancellationToken) -> Dict[str, Any]:
  172. return {"temperature": 75.0, "conditions": "sunny and clear"}
  173. with patch.object(WeatherTool, "run_json", mock_run_json):
  174. message = TextMessage(source="user", content="What's the weather in New York?")
  175. all_messages: List[Any] = []
  176. async for msg in agent.on_messages_stream([message], cancellation_token):
  177. all_messages.append(msg)
  178. final_response = next((msg for msg in all_messages if hasattr(msg, "chat_message")), None)
  179. assert final_response is not None
  180. assert hasattr(final_response, "chat_message")
  181. response_msg = cast(Response, final_response)
  182. assert isinstance(response_msg.chat_message, TextMessage)
  183. assert response_msg.chat_message.content in (
  184. '{"temperature": 75.0, "conditions": "sunny and clear"}',
  185. '{"temperature": 72.5, "conditions": "sunny"}',
  186. )
  187. @pytest.mark.asyncio
  188. async def test_error_handling(error_agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  189. """Test that the agent returns an error message if the Responses API fails."""
  190. message = TextMessage(source="user", content="This will cause an error")
  191. all_messages: List[Any] = []
  192. async for msg in error_agent.on_messages_stream([message], cancellation_token):
  193. all_messages.append(msg)
  194. final_response = next((msg for msg in all_messages if hasattr(msg, "chat_message")), None)
  195. assert final_response is not None
  196. assert isinstance(final_response.chat_message, TextMessage)
  197. assert "Error generating response:" in final_response.chat_message.content
  198. @pytest.mark.asyncio
  199. async def test_state_management(agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  200. agent._last_response_id = "resp-123" # type: ignore
  201. agent._message_history = [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi there"}] # type: ignore
  202. state = await agent.save_state()
  203. new_agent = OpenAIAgent(
  204. name="assistant2",
  205. description="Test assistant 2",
  206. client=agent._client, # type: ignore
  207. model="gpt-4o",
  208. instructions="You are a helpful AI assistant.",
  209. )
  210. await new_agent.load_state(state)
  211. assert new_agent._last_response_id == "resp-123" # type: ignore
  212. assert len(new_agent._message_history) == 2 # type: ignore
  213. assert new_agent._message_history[0]["role"] == "user" # type: ignore
  214. assert new_agent._message_history[0]["content"] == "Hello" # type: ignore
  215. await new_agent.on_reset(cancellation_token)
  216. assert new_agent._last_response_id is None # type: ignore
  217. assert len(new_agent._message_history) == 0 # type: ignore
  218. @pytest.mark.asyncio
  219. async def test_convert_message_functions(agent: OpenAIAgent) -> None:
  220. from autogen_ext.agents.openai._openai_agent import _convert_message_to_openai_message # type: ignore
  221. user_msg = TextMessage(content="Hello", source="user")
  222. openai_user_msg = _convert_message_to_openai_message(user_msg) # type: ignore
  223. assert openai_user_msg["role"] == "user"
  224. assert openai_user_msg["content"] == "Hello"
  225. sys_msg = TextMessage(content="System prompt", source="system")
  226. openai_sys_msg = _convert_message_to_openai_message(sys_msg) # type: ignore
  227. assert openai_sys_msg["role"] == "system"
  228. assert openai_sys_msg["content"] == "System prompt"
  229. assistant_msg = TextMessage(content="Assistant reply", source="assistant")
  230. openai_assistant_msg = _convert_message_to_openai_message(assistant_msg) # type: ignore
  231. assert openai_assistant_msg["role"] == "assistant"
  232. assert openai_assistant_msg["content"] == "Assistant reply"
  233. text_msg = TextMessage(content="Plain text", source="other")
  234. openai_text_msg = _convert_message_to_openai_message(text_msg) # type: ignore
  235. assert openai_text_msg["role"] == "user"
  236. assert openai_text_msg["content"] == "Plain text"
  237. @pytest.mark.asyncio
  238. async def test_tool_schema_conversion(agent: OpenAIAgent) -> None:
  239. from autogen_ext.agents.openai._openai_agent import _convert_tool_to_function_schema # type: ignore
  240. tool_schema = _convert_tool_to_function_schema(agent._tool_map["get_weather"]) # type: ignore
  241. assert tool_schema["name"] == "get_weather"
  242. assert "description" in tool_schema
  243. assert "parameters" in tool_schema
  244. assert tool_schema["parameters"]["type"] == "object"
  245. assert "properties" in tool_schema["parameters"]
  246. @pytest.mark.asyncio
  247. async def test_on_messages_inner_messages(agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  248. class DummyMsg(BaseChatMessage):
  249. type: str = "DummyMsg"
  250. content: str = "dummy content"
  251. def __init__(self) -> None:
  252. super().__init__(source="dummy")
  253. def to_model_message(self) -> UserMessage:
  254. return UserMessage(content=self.content, source=self.source)
  255. def to_model_text(self) -> str:
  256. return self.content
  257. def to_text(self) -> str:
  258. return self.content
  259. dummy_inner = DummyMsg()
  260. dummy_response = Response(chat_message=TextMessage(source="agent", content="hi"), inner_messages=None)
  261. async def fake_stream(*args: Any, **kwargs: Any) -> AsyncGenerator[Union[BaseChatMessage, Response], None]:
  262. yield dummy_inner
  263. yield dummy_response
  264. with patch.object(agent, "on_messages_stream", fake_stream):
  265. response = await agent.on_messages([TextMessage(source="user", content="test")], cancellation_token)
  266. assert response.chat_message is not None
  267. assert isinstance(response.chat_message, TextMessage)
  268. assert response.chat_message.content == "hi"
  269. assert response.inner_messages is not None
  270. assert dummy_inner in response.inner_messages
  271. @pytest.mark.asyncio
  272. async def test_build_api_params(agent: OpenAIAgent) -> None:
  273. agent._last_response_id = None # type: ignore
  274. params = agent._build_api_parameters([{"role": "user", "content": "hi"}]) # type: ignore
  275. assert "previous_response_id" not in params
  276. agent._last_response_id = "resp-456" # type: ignore
  277. params = agent._build_api_parameters([{"role": "user", "content": "hi"}]) # type: ignore
  278. assert params.get("previous_response_id") == "resp-456"
  279. assert "max_tokens" not in params
  280. assert params.get("max_output_tokens") == 1000
  281. assert params.get("store") is True
  282. assert params.get("truncation") == "auto"
  283. agent._json_mode = True # type: ignore
  284. params = agent._build_api_parameters([{"role": "user", "content": "hi"}]) # type: ignore
  285. assert "text.format" not in params
  286. assert params.get("text") == {"type": "json_object"}
  287. @pytest.mark.asyncio
  288. async def test_on_messages_previous_response_id(agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  289. message = TextMessage(source="user", content="hi")
  290. response = await agent.on_messages([message], cancellation_token)
  291. assert response.chat_message is not None
  292. assert isinstance(response.chat_message, TextMessage)
  293. message = TextMessage(source="user", content="hi")
  294. response = await agent.on_messages([message], cancellation_token)
  295. assert response.chat_message is not None
  296. assert isinstance(response.chat_message, TextMessage)
  297. @pytest.mark.asyncio
  298. async def test_on_messages_stream(agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  299. dummy_response = Response(chat_message=TextMessage(source="agent", content="hi"), inner_messages=None)
  300. async def fake_stream(*args: Any, **kwargs: Any) -> AsyncGenerator[Response, None]:
  301. yield dummy_response
  302. with patch.object(agent, "on_messages_stream", fake_stream):
  303. resp = await agent.on_messages([TextMessage(source="user", content="hi")], cancellation_token)
  304. assert isinstance(resp.chat_message, TextMessage)
  305. assert resp.chat_message.content == "hi"
  306. @pytest.mark.asyncio
  307. async def test_component_serialization(agent: OpenAIAgent) -> None:
  308. config = agent.dump_component()
  309. config_dict: Any = None
  310. if isinstance(config, dict):
  311. config_dict = config
  312. elif hasattr(config, "model_dump_json"):
  313. config_dict = json.loads(config.model_dump_json())
  314. elif hasattr(config, "model_dump"):
  315. config_dict = config.model_dump()
  316. elif isinstance(config, str):
  317. config_dict = json.loads(config)
  318. else:
  319. config_dict = {"name": agent.name, "description": agent.description}
  320. if isinstance(config_dict, dict) and "config" in config_dict:
  321. config_dict = config_dict["config"]
  322. assert config_dict["name"] == "assistant"
  323. assert config_dict["description"] == "Test assistant using the Response API"
  324. assert config_dict["model"] == "gpt-4o"
  325. assert config_dict["instructions"] == "You are a helpful AI assistant."
  326. assert config_dict["temperature"] == 0.7
  327. assert config_dict["max_output_tokens"] == 1000
  328. assert config_dict["store"] is True
  329. assert config_dict["truncation"] == "auto"
  330. @pytest.mark.asyncio
  331. async def test_from_config(agent: OpenAIAgent) -> None:
  332. config = agent.dump_component()
  333. config_dict: Dict[str, Any] = {}
  334. if hasattr(config, "model_dump_json"):
  335. config_dict = json.loads(config.model_dump_json())
  336. elif isinstance(config, str):
  337. config_dict = json.loads(config)
  338. elif isinstance(config, dict):
  339. config_dict = config
  340. if "tools" in config_dict and config_dict["tools"] is not None:
  341. serialized_tools: List[Dict[str, Any]] = []
  342. tools_any: Any = config_dict["tools"]
  343. if isinstance(tools_any, list):
  344. tools_list: List[Any] = cast(List[Any], tools_any) # type: ignore[redundant-cast]
  345. tools_count: int = len(tools_list)
  346. for i in range(tools_count):
  347. tool_any: Any = tools_list[i]
  348. tool_dict: Dict[str, Any] = {}
  349. if isinstance(tool_any, dict):
  350. tool_dict = tool_any
  351. elif tool_any is not None and isinstance(tool_any, object) and hasattr(tool_any, "model_dump"):
  352. model_dump_any: Any = getattr(tool_any, "model_dump", None)
  353. if callable(model_dump_any):
  354. try:
  355. result_any: Any = model_dump_any()
  356. if isinstance(result_any, dict):
  357. tool_dict = result_any
  358. else:
  359. tool_dict = {"provider": "unknown", "config": {}}
  360. except Exception:
  361. tool_dict = {"provider": "unknown", "config": {}}
  362. else:
  363. tool_dict = {"provider": "unknown", "config": {}}
  364. else:
  365. tool_dict = {"provider": "unknown", "config": {}}
  366. serialized_tools.append(tool_dict)
  367. config_dict["tools"] = serialized_tools
  368. with patch("openai.AsyncOpenAI"):
  369. loaded_agent = OpenAIAgent.load_component(config_dict)
  370. assert loaded_agent.name == "assistant"
  371. assert loaded_agent.description == "Test assistant using the Response API"
  372. assert loaded_agent._model == "gpt-4o" # type: ignore
  373. assert loaded_agent._instructions == "You are a helpful AI assistant." # type: ignore
  374. assert loaded_agent._temperature == 0.7 # type: ignore
  375. assert loaded_agent._max_output_tokens == 1000 # type: ignore
  376. assert loaded_agent._store is True # type: ignore
  377. assert loaded_agent._truncation == "auto" # type: ignore
  378. @pytest.mark.asyncio
  379. async def test_multimodal_message_response(agent: OpenAIAgent, cancellation_token: CancellationToken) -> None:
  380. # Test that the multimodal message is converted to the correct format
  381. img = Image.from_base64(
  382. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
  383. )
  384. multimodal_message = MultiModalMessage(content=["Can you describe the content of this image?", img], source="user")
  385. # Patch client.responses.create to simulate image-capable output
  386. async def mock_responses_create(**kwargs: Any) -> Any:
  387. class MockResponse:
  388. def __init__(self) -> None:
  389. self.output_text = "I see a cat in the image."
  390. self.id = "resp-image-001"
  391. return MockResponse()
  392. agent._client.responses.create = AsyncMock(side_effect=mock_responses_create) # type: ignore
  393. response = await agent.on_messages([multimodal_message], cancellation_token)
  394. assert response.chat_message is not None
  395. assert isinstance(response.chat_message, TextMessage)
  396. assert "cat" in response.chat_message.content.lower()