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_model_client.py 64 kB

feat: Add thought process handling in tool calls and expose ThoughtEvent through stream in AgentChat (#5500) Resolves #5192 Test ```python import asyncio import os from random import randint from typing import List from autogen_core.tools import BaseTool, FunctionTool from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.ui import Console async def get_current_time(city: str) -> str: return f"The current time in {city} is {randint(0, 23)}:{randint(0, 59)}." tools: List[BaseTool] = [ FunctionTool( get_current_time, name="get_current_time", description="Get current time for a city.", ), ] model_client = OpenAIChatCompletionClient( model="anthropic/claude-3.5-haiku-20241022", base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"], model_info={ "family": "claude-3.5-haiku", "function_calling": True, "vision": False, "json_output": False, } ) agent = AssistantAgent( name="Agent", model_client=model_client, tools=tools, system_message= "You are an assistant with some tools that can be used to answer some questions", ) async def main() -> None: await Console(agent.run_stream(task="What is current time of Paris and Toronto?")) asyncio.run(main()) ``` ``` ---------- user ---------- What is current time of Paris and Toronto? ---------- Agent ---------- I'll help you find the current time for Paris and Toronto by using the get_current_time function for each city. ---------- Agent ---------- [FunctionCall(id='toolu_01NwP3fNAwcYKn1x656Dq9xW', arguments='{"city": "Paris"}', name='get_current_time'), FunctionCall(id='toolu_018d4cWSy3TxXhjgmLYFrfRt', arguments='{"city": "Toronto"}', name='get_current_time')] ---------- Agent ---------- [FunctionExecutionResult(content='The current time in Paris is 1:10.', call_id='toolu_01NwP3fNAwcYKn1x656Dq9xW', is_error=False), FunctionExecutionResult(content='The current time in Toronto is 7:28.', call_id='toolu_018d4cWSy3TxXhjgmLYFrfRt', is_error=False)] ---------- Agent ---------- The current time in Paris is 1:10. The current time in Toronto is 7:28. ``` --------- Co-authored-by: Jack Gerrits <jackgerrits@users.noreply.github.com>
1 year ago
feat: Add thought process handling in tool calls and expose ThoughtEvent through stream in AgentChat (#5500) Resolves #5192 Test ```python import asyncio import os from random import randint from typing import List from autogen_core.tools import BaseTool, FunctionTool from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.ui import Console async def get_current_time(city: str) -> str: return f"The current time in {city} is {randint(0, 23)}:{randint(0, 59)}." tools: List[BaseTool] = [ FunctionTool( get_current_time, name="get_current_time", description="Get current time for a city.", ), ] model_client = OpenAIChatCompletionClient( model="anthropic/claude-3.5-haiku-20241022", base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"], model_info={ "family": "claude-3.5-haiku", "function_calling": True, "vision": False, "json_output": False, } ) agent = AssistantAgent( name="Agent", model_client=model_client, tools=tools, system_message= "You are an assistant with some tools that can be used to answer some questions", ) async def main() -> None: await Console(agent.run_stream(task="What is current time of Paris and Toronto?")) asyncio.run(main()) ``` ``` ---------- user ---------- What is current time of Paris and Toronto? ---------- Agent ---------- I'll help you find the current time for Paris and Toronto by using the get_current_time function for each city. ---------- Agent ---------- [FunctionCall(id='toolu_01NwP3fNAwcYKn1x656Dq9xW', arguments='{"city": "Paris"}', name='get_current_time'), FunctionCall(id='toolu_018d4cWSy3TxXhjgmLYFrfRt', arguments='{"city": "Toronto"}', name='get_current_time')] ---------- Agent ---------- [FunctionExecutionResult(content='The current time in Paris is 1:10.', call_id='toolu_01NwP3fNAwcYKn1x656Dq9xW', is_error=False), FunctionExecutionResult(content='The current time in Toronto is 7:28.', call_id='toolu_018d4cWSy3TxXhjgmLYFrfRt', is_error=False)] ---------- Agent ---------- The current time in Paris is 1:10. The current time in Toronto is 7:28. ``` --------- Co-authored-by: Jack Gerrits <jackgerrits@users.noreply.github.com>
1 year ago
feat: Add thought process handling in tool calls and expose ThoughtEvent through stream in AgentChat (#5500) Resolves #5192 Test ```python import asyncio import os from random import randint from typing import List from autogen_core.tools import BaseTool, FunctionTool from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.ui import Console async def get_current_time(city: str) -> str: return f"The current time in {city} is {randint(0, 23)}:{randint(0, 59)}." tools: List[BaseTool] = [ FunctionTool( get_current_time, name="get_current_time", description="Get current time for a city.", ), ] model_client = OpenAIChatCompletionClient( model="anthropic/claude-3.5-haiku-20241022", base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"], model_info={ "family": "claude-3.5-haiku", "function_calling": True, "vision": False, "json_output": False, } ) agent = AssistantAgent( name="Agent", model_client=model_client, tools=tools, system_message= "You are an assistant with some tools that can be used to answer some questions", ) async def main() -> None: await Console(agent.run_stream(task="What is current time of Paris and Toronto?")) asyncio.run(main()) ``` ``` ---------- user ---------- What is current time of Paris and Toronto? ---------- Agent ---------- I'll help you find the current time for Paris and Toronto by using the get_current_time function for each city. ---------- Agent ---------- [FunctionCall(id='toolu_01NwP3fNAwcYKn1x656Dq9xW', arguments='{"city": "Paris"}', name='get_current_time'), FunctionCall(id='toolu_018d4cWSy3TxXhjgmLYFrfRt', arguments='{"city": "Toronto"}', name='get_current_time')] ---------- Agent ---------- [FunctionExecutionResult(content='The current time in Paris is 1:10.', call_id='toolu_01NwP3fNAwcYKn1x656Dq9xW', is_error=False), FunctionExecutionResult(content='The current time in Toronto is 7:28.', call_id='toolu_018d4cWSy3TxXhjgmLYFrfRt', is_error=False)] ---------- Agent ---------- The current time in Paris is 1:10. The current time in Toronto is 7:28. ``` --------- Co-authored-by: Jack Gerrits <jackgerrits@users.noreply.github.com>
1 year ago
feat: Add thought process handling in tool calls and expose ThoughtEvent through stream in AgentChat (#5500) Resolves #5192 Test ```python import asyncio import os from random import randint from typing import List from autogen_core.tools import BaseTool, FunctionTool from autogen_ext.models.openai import OpenAIChatCompletionClient from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.ui import Console async def get_current_time(city: str) -> str: return f"The current time in {city} is {randint(0, 23)}:{randint(0, 59)}." tools: List[BaseTool] = [ FunctionTool( get_current_time, name="get_current_time", description="Get current time for a city.", ), ] model_client = OpenAIChatCompletionClient( model="anthropic/claude-3.5-haiku-20241022", base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"], model_info={ "family": "claude-3.5-haiku", "function_calling": True, "vision": False, "json_output": False, } ) agent = AssistantAgent( name="Agent", model_client=model_client, tools=tools, system_message= "You are an assistant with some tools that can be used to answer some questions", ) async def main() -> None: await Console(agent.run_stream(task="What is current time of Paris and Toronto?")) asyncio.run(main()) ``` ``` ---------- user ---------- What is current time of Paris and Toronto? ---------- Agent ---------- I'll help you find the current time for Paris and Toronto by using the get_current_time function for each city. ---------- Agent ---------- [FunctionCall(id='toolu_01NwP3fNAwcYKn1x656Dq9xW', arguments='{"city": "Paris"}', name='get_current_time'), FunctionCall(id='toolu_018d4cWSy3TxXhjgmLYFrfRt', arguments='{"city": "Toronto"}', name='get_current_time')] ---------- Agent ---------- [FunctionExecutionResult(content='The current time in Paris is 1:10.', call_id='toolu_01NwP3fNAwcYKn1x656Dq9xW', is_error=False), FunctionExecutionResult(content='The current time in Toronto is 7:28.', call_id='toolu_018d4cWSy3TxXhjgmLYFrfRt', is_error=False)] ---------- Agent ---------- The current time in Paris is 1:10. The current time in Toronto is 7:28. ``` --------- Co-authored-by: Jack Gerrits <jackgerrits@users.noreply.github.com>
1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608
  1. import asyncio
  2. import json
  3. import os
  4. from typing import Annotated, Any, AsyncGenerator, Dict, List, Literal, Tuple, TypeVar
  5. from unittest.mock import MagicMock
  6. import httpx
  7. import pytest
  8. from autogen_core import CancellationToken, FunctionCall, Image
  9. from autogen_core.models import (
  10. AssistantMessage,
  11. CreateResult,
  12. FunctionExecutionResult,
  13. FunctionExecutionResultMessage,
  14. LLMMessage,
  15. ModelInfo,
  16. RequestUsage,
  17. SystemMessage,
  18. UserMessage,
  19. )
  20. from autogen_core.models._model_client import ModelFamily
  21. from autogen_core.tools import BaseTool, FunctionTool
  22. from autogen_ext.models.openai import AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient
  23. from autogen_ext.models.openai._model_info import resolve_model
  24. from autogen_ext.models.openai._openai_client import calculate_vision_tokens, convert_tools, to_oai_type
  25. from openai.resources.beta.chat.completions import ( # type: ignore
  26. AsyncChatCompletionStreamManager as BetaAsyncChatCompletionStreamManager, # type: ignore
  27. )
  28. # type: ignore
  29. from openai.resources.beta.chat.completions import (
  30. AsyncCompletions as BetaAsyncCompletions,
  31. )
  32. from openai.resources.chat.completions import AsyncCompletions
  33. from openai.types.chat.chat_completion import ChatCompletion, Choice
  34. from openai.types.chat.chat_completion_chunk import (
  35. ChatCompletionChunk,
  36. ChoiceDelta,
  37. ChoiceDeltaToolCall,
  38. ChoiceDeltaToolCallFunction,
  39. )
  40. from openai.types.chat.chat_completion_chunk import (
  41. Choice as ChunkChoice,
  42. )
  43. from openai.types.chat.chat_completion_message import ChatCompletionMessage
  44. from openai.types.chat.chat_completion_message_tool_call import (
  45. ChatCompletionMessageToolCall,
  46. Function,
  47. )
  48. from openai.types.chat.parsed_chat_completion import ParsedChatCompletion, ParsedChatCompletionMessage, ParsedChoice
  49. from openai.types.chat.parsed_function_tool_call import ParsedFunction, ParsedFunctionToolCall
  50. from openai.types.completion_usage import CompletionUsage
  51. from pydantic import BaseModel, Field
  52. ResponseFormatT = TypeVar("ResponseFormatT", bound=BaseModel)
  53. def _pass_function(input: str) -> str:
  54. return "pass"
  55. async def _fail_function(input: str) -> str:
  56. return "fail"
  57. async def _echo_function(input: str) -> str:
  58. return input
  59. class MyResult(BaseModel):
  60. result: str = Field(description="The other description.")
  61. class MyArgs(BaseModel):
  62. query: str = Field(description="The description.")
  63. class MockChunkDefinition(BaseModel):
  64. # defining elements for diffentiating mocking chunks
  65. chunk_choice: ChunkChoice
  66. usage: CompletionUsage | None
  67. class MockChunkEvent(BaseModel):
  68. type: Literal["chunk"]
  69. chunk: ChatCompletionChunk
  70. async def _mock_create_stream(*args: Any, **kwargs: Any) -> AsyncGenerator[ChatCompletionChunk, None]:
  71. model = resolve_model(kwargs.get("model", "gpt-4o"))
  72. mock_chunks_content = ["Hello", " Another Hello", " Yet Another Hello"]
  73. # The openai api implementations (OpenAI and Litellm) stream chunks of tokens
  74. # with content as string, and then at the end a token with stop set and finally if
  75. # usage requested with `"stream_options": {"include_usage": True}` a chunk with the usage data
  76. mock_chunks = [
  77. # generate the list of mock chunk content
  78. MockChunkDefinition(
  79. chunk_choice=ChunkChoice(
  80. finish_reason=None,
  81. index=0,
  82. delta=ChoiceDelta(
  83. content=mock_chunk_content,
  84. role="assistant",
  85. ),
  86. ),
  87. usage=None,
  88. )
  89. for mock_chunk_content in mock_chunks_content
  90. ] + [
  91. # generate the stop chunk
  92. MockChunkDefinition(
  93. chunk_choice=ChunkChoice(
  94. finish_reason="stop",
  95. index=0,
  96. delta=ChoiceDelta(
  97. content=None,
  98. role="assistant",
  99. ),
  100. ),
  101. usage=None,
  102. )
  103. ]
  104. # generate the usage chunk if configured
  105. if kwargs.get("stream_options", {}).get("include_usage") is True:
  106. mock_chunks = mock_chunks + [
  107. # ---- API differences
  108. # OPENAI API does NOT create a choice
  109. # LITELLM (proxy) DOES create a choice
  110. # Not simulating all the API options, just implementing the LITELLM variant
  111. MockChunkDefinition(
  112. chunk_choice=ChunkChoice(
  113. finish_reason=None,
  114. index=0,
  115. delta=ChoiceDelta(
  116. content=None,
  117. role="assistant",
  118. ),
  119. ),
  120. usage=CompletionUsage(prompt_tokens=3, completion_tokens=3, total_tokens=6),
  121. )
  122. ]
  123. elif kwargs.get("stream_options", {}).get("include_usage") is False:
  124. pass
  125. else:
  126. pass
  127. for mock_chunk in mock_chunks:
  128. await asyncio.sleep(0.1)
  129. yield ChatCompletionChunk(
  130. id="id",
  131. choices=[mock_chunk.chunk_choice],
  132. created=0,
  133. model=model,
  134. object="chat.completion.chunk",
  135. usage=mock_chunk.usage,
  136. )
  137. async def _mock_create(*args: Any, **kwargs: Any) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]:
  138. stream = kwargs.get("stream", False)
  139. model = resolve_model(kwargs.get("model", "gpt-4o"))
  140. if not stream:
  141. await asyncio.sleep(0.1)
  142. return ChatCompletion(
  143. id="id",
  144. choices=[
  145. Choice(finish_reason="stop", index=0, message=ChatCompletionMessage(content="Hello", role="assistant"))
  146. ],
  147. created=0,
  148. model=model,
  149. object="chat.completion",
  150. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  151. )
  152. else:
  153. return _mock_create_stream(*args, **kwargs)
  154. @pytest.mark.asyncio
  155. async def test_openai_chat_completion_client() -> None:
  156. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  157. assert client
  158. @pytest.mark.asyncio
  159. async def test_openai_chat_completion_client_with_gemini_model() -> None:
  160. client = OpenAIChatCompletionClient(model="gemini-1.5-flash", api_key="api_key")
  161. assert client
  162. @pytest.mark.asyncio
  163. async def test_openai_chat_completion_client_raise_on_unknown_model() -> None:
  164. with pytest.raises(ValueError, match="model_info is required"):
  165. _ = OpenAIChatCompletionClient(model="unknown", api_key="api_key")
  166. @pytest.mark.asyncio
  167. async def test_custom_model_with_capabilities() -> None:
  168. with pytest.raises(ValueError, match="model_info is required"):
  169. client = OpenAIChatCompletionClient(model="dummy_model", base_url="https://api.dummy.com/v0", api_key="api_key")
  170. client = OpenAIChatCompletionClient(
  171. model="dummy_model",
  172. base_url="https://api.dummy.com/v0",
  173. api_key="api_key",
  174. model_info={"vision": False, "function_calling": False, "json_output": False, "family": ModelFamily.UNKNOWN},
  175. )
  176. assert client
  177. @pytest.mark.asyncio
  178. async def test_azure_openai_chat_completion_client() -> None:
  179. client = AzureOpenAIChatCompletionClient(
  180. azure_deployment="gpt-4o-1",
  181. model="gpt-4o",
  182. api_key="api_key",
  183. api_version="2020-08-04",
  184. azure_endpoint="https://dummy.com",
  185. model_info={"vision": True, "function_calling": True, "json_output": True, "family": ModelFamily.GPT_4O},
  186. )
  187. assert client
  188. @pytest.mark.asyncio
  189. async def test_openai_chat_completion_client_create(monkeypatch: pytest.MonkeyPatch) -> None:
  190. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  191. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  192. result = await client.create(messages=[UserMessage(content="Hello", source="user")])
  193. assert result.content == "Hello"
  194. @pytest.mark.asyncio
  195. async def test_openai_chat_completion_client_create_stream_with_usage(monkeypatch: pytest.MonkeyPatch) -> None:
  196. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  197. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  198. chunks: List[str | CreateResult] = []
  199. async for chunk in client.create_stream(
  200. messages=[UserMessage(content="Hello", source="user")],
  201. # include_usage not the default of the OPENAI API and must be explicitly set
  202. extra_create_args={"stream_options": {"include_usage": True}},
  203. ):
  204. chunks.append(chunk)
  205. assert chunks[0] == "Hello"
  206. assert chunks[1] == " Another Hello"
  207. assert chunks[2] == " Yet Another Hello"
  208. assert isinstance(chunks[-1], CreateResult)
  209. assert chunks[-1].content == "Hello Another Hello Yet Another Hello"
  210. assert chunks[-1].usage == RequestUsage(prompt_tokens=3, completion_tokens=3)
  211. @pytest.mark.asyncio
  212. async def test_openai_chat_completion_client_create_stream_no_usage_default(monkeypatch: pytest.MonkeyPatch) -> None:
  213. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  214. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  215. chunks: List[str | CreateResult] = []
  216. async for chunk in client.create_stream(
  217. messages=[UserMessage(content="Hello", source="user")],
  218. # include_usage not the default of the OPENAI APIis ,
  219. # it can be explicitly set
  220. # or just not declared which is the default
  221. # extra_create_args={"stream_options": {"include_usage": False}},
  222. ):
  223. chunks.append(chunk)
  224. assert chunks[0] == "Hello"
  225. assert chunks[1] == " Another Hello"
  226. assert chunks[2] == " Yet Another Hello"
  227. assert isinstance(chunks[-1], CreateResult)
  228. assert chunks[-1].content == "Hello Another Hello Yet Another Hello"
  229. assert chunks[-1].usage == RequestUsage(prompt_tokens=0, completion_tokens=0)
  230. @pytest.mark.asyncio
  231. async def test_openai_chat_completion_client_create_stream_no_usage_explicit(monkeypatch: pytest.MonkeyPatch) -> None:
  232. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  233. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  234. chunks: List[str | CreateResult] = []
  235. async for chunk in client.create_stream(
  236. messages=[UserMessage(content="Hello", source="user")],
  237. # include_usage is not the default of the OPENAI API ,
  238. # it can be explicitly set
  239. # or just not declared which is the default
  240. extra_create_args={"stream_options": {"include_usage": False}},
  241. ):
  242. chunks.append(chunk)
  243. assert chunks[0] == "Hello"
  244. assert chunks[1] == " Another Hello"
  245. assert chunks[2] == " Yet Another Hello"
  246. assert isinstance(chunks[-1], CreateResult)
  247. assert chunks[-1].content == "Hello Another Hello Yet Another Hello"
  248. assert chunks[-1].usage == RequestUsage(prompt_tokens=0, completion_tokens=0)
  249. @pytest.mark.asyncio
  250. async def test_openai_chat_completion_client_create_cancel(monkeypatch: pytest.MonkeyPatch) -> None:
  251. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  252. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  253. cancellation_token = CancellationToken()
  254. task = asyncio.create_task(
  255. client.create(messages=[UserMessage(content="Hello", source="user")], cancellation_token=cancellation_token)
  256. )
  257. cancellation_token.cancel()
  258. with pytest.raises(asyncio.CancelledError):
  259. await task
  260. @pytest.mark.asyncio
  261. async def test_openai_chat_completion_client_create_stream_cancel(monkeypatch: pytest.MonkeyPatch) -> None:
  262. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  263. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  264. cancellation_token = CancellationToken()
  265. stream = client.create_stream(
  266. messages=[UserMessage(content="Hello", source="user")], cancellation_token=cancellation_token
  267. )
  268. assert await anext(stream)
  269. cancellation_token.cancel()
  270. with pytest.raises(asyncio.CancelledError):
  271. async for _ in stream:
  272. pass
  273. @pytest.mark.asyncio
  274. async def test_openai_chat_completion_client_count_tokens(monkeypatch: pytest.MonkeyPatch) -> None:
  275. client = OpenAIChatCompletionClient(model="gpt-4o", api_key="api_key")
  276. messages: List[LLMMessage] = [
  277. SystemMessage(content="Hello"),
  278. UserMessage(content="Hello", source="user"),
  279. AssistantMessage(content="Hello", source="assistant"),
  280. UserMessage(
  281. content=[
  282. "str1",
  283. Image.from_base64(
  284. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
  285. ),
  286. ],
  287. source="user",
  288. ),
  289. FunctionExecutionResultMessage(content=[FunctionExecutionResult(content="Hello", call_id="1", is_error=False)]),
  290. ]
  291. def tool1(test: str, test2: str) -> str:
  292. return test + test2
  293. def tool2(test1: int, test2: List[int]) -> str:
  294. return str(test1) + str(test2)
  295. tools = [FunctionTool(tool1, description="example tool 1"), FunctionTool(tool2, description="example tool 2")]
  296. mockcalculate_vision_tokens = MagicMock()
  297. monkeypatch.setattr("autogen_ext.models.openai._openai_client.calculate_vision_tokens", mockcalculate_vision_tokens)
  298. num_tokens = client.count_tokens(messages, tools=tools)
  299. assert num_tokens
  300. # Check that calculate_vision_tokens was called
  301. mockcalculate_vision_tokens.assert_called_once()
  302. remaining_tokens = client.remaining_tokens(messages, tools=tools)
  303. assert remaining_tokens
  304. @pytest.mark.parametrize(
  305. "mock_size, expected_num_tokens",
  306. [
  307. ((1, 1), 255),
  308. ((512, 512), 255),
  309. ((2048, 512), 765),
  310. ((2048, 2048), 765),
  311. ((512, 1024), 425),
  312. ],
  313. )
  314. def test_openai_count_image_tokens(mock_size: Tuple[int, int], expected_num_tokens: int) -> None:
  315. # Step 1: Mock the Image class with only the 'image' attribute
  316. mock_image_attr = MagicMock()
  317. mock_image_attr.size = mock_size
  318. mock_image = MagicMock()
  319. mock_image.image = mock_image_attr
  320. # Directly call calculate_vision_tokens and check the result
  321. calculated_tokens = calculate_vision_tokens(mock_image, detail="auto")
  322. assert calculated_tokens == expected_num_tokens
  323. def test_convert_tools_accepts_both_func_tool_and_schema() -> None:
  324. def my_function(arg: str, other: Annotated[int, "int arg"], nonrequired: int = 5) -> MyResult:
  325. return MyResult(result="test")
  326. tool = FunctionTool(my_function, description="Function tool.")
  327. schema = tool.schema
  328. converted_tool_schema = convert_tools([tool, schema])
  329. assert len(converted_tool_schema) == 2
  330. assert converted_tool_schema[0] == converted_tool_schema[1]
  331. def test_convert_tools_accepts_both_tool_and_schema() -> None:
  332. class MyTool(BaseTool[MyArgs, MyResult]):
  333. def __init__(self) -> None:
  334. super().__init__(
  335. args_type=MyArgs,
  336. return_type=MyResult,
  337. name="TestTool",
  338. description="Description of test tool.",
  339. )
  340. async def run(self, args: MyArgs, cancellation_token: CancellationToken) -> MyResult:
  341. return MyResult(result="value")
  342. tool = MyTool()
  343. schema = tool.schema
  344. converted_tool_schema = convert_tools([tool, schema])
  345. assert len(converted_tool_schema) == 2
  346. assert converted_tool_schema[0] == converted_tool_schema[1]
  347. @pytest.mark.asyncio
  348. async def test_structured_output(monkeypatch: pytest.MonkeyPatch) -> None:
  349. class AgentResponse(BaseModel):
  350. thoughts: str
  351. response: Literal["happy", "sad", "neutral"]
  352. model = "gpt-4o-2024-11-20"
  353. async def _mock_parse(*args: Any, **kwargs: Any) -> ParsedChatCompletion[AgentResponse]:
  354. return ParsedChatCompletion(
  355. id="id1",
  356. choices=[
  357. ParsedChoice(
  358. finish_reason="stop",
  359. index=0,
  360. message=ParsedChatCompletionMessage(
  361. content=json.dumps(
  362. {
  363. "thoughts": "The user explicitly states that they are happy without any indication of sadness or neutrality.",
  364. "response": "happy",
  365. }
  366. ),
  367. role="assistant",
  368. ),
  369. )
  370. ],
  371. created=0,
  372. model=model,
  373. object="chat.completion",
  374. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  375. )
  376. monkeypatch.setattr(BetaAsyncCompletions, "parse", _mock_parse)
  377. model_client = OpenAIChatCompletionClient(
  378. model=model,
  379. api_key="",
  380. response_format=AgentResponse, # type: ignore
  381. )
  382. # Test that the openai client was called with the correct response format.
  383. create_result = await model_client.create(messages=[UserMessage(content="I am happy.", source="user")])
  384. assert isinstance(create_result.content, str)
  385. response = AgentResponse.model_validate(json.loads(create_result.content))
  386. assert (
  387. response.thoughts
  388. == "The user explicitly states that they are happy without any indication of sadness or neutrality."
  389. )
  390. assert response.response == "happy"
  391. @pytest.mark.asyncio
  392. async def test_structured_output_with_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
  393. class AgentResponse(BaseModel):
  394. thoughts: str
  395. response: Literal["happy", "sad", "neutral"]
  396. model = "gpt-4o-2024-11-20"
  397. async def _mock_parse(*args: Any, **kwargs: Any) -> ParsedChatCompletion[AgentResponse]:
  398. return ParsedChatCompletion(
  399. id="id1",
  400. choices=[
  401. ParsedChoice(
  402. finish_reason="tool_calls",
  403. index=0,
  404. message=ParsedChatCompletionMessage(
  405. content=json.dumps(
  406. {
  407. "thoughts": "The user explicitly states that they are happy without any indication of sadness or neutrality.",
  408. "response": "happy",
  409. }
  410. ),
  411. role="assistant",
  412. tool_calls=[
  413. ParsedFunctionToolCall(
  414. id="1",
  415. type="function",
  416. function=ParsedFunction(
  417. name="_pass_function",
  418. arguments=json.dumps({"input": "happy"}),
  419. ),
  420. )
  421. ],
  422. ),
  423. )
  424. ],
  425. created=0,
  426. model=model,
  427. object="chat.completion",
  428. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  429. )
  430. monkeypatch.setattr(BetaAsyncCompletions, "parse", _mock_parse)
  431. model_client = OpenAIChatCompletionClient(
  432. model=model,
  433. api_key="",
  434. response_format=AgentResponse, # type: ignore
  435. )
  436. # Test that the openai client was called with the correct response format.
  437. create_result = await model_client.create(messages=[UserMessage(content="I am happy.", source="user")])
  438. assert isinstance(create_result.content, list)
  439. assert len(create_result.content) == 1
  440. assert create_result.content[0] == FunctionCall(
  441. id="1", name="_pass_function", arguments=json.dumps({"input": "happy"})
  442. )
  443. assert isinstance(create_result.thought, str)
  444. response = AgentResponse.model_validate(json.loads(create_result.thought))
  445. assert (
  446. response.thoughts
  447. == "The user explicitly states that they are happy without any indication of sadness or neutrality."
  448. )
  449. assert response.response == "happy"
  450. @pytest.mark.asyncio
  451. async def test_structured_output_with_streaming(monkeypatch: pytest.MonkeyPatch) -> None:
  452. class AgentResponse(BaseModel):
  453. thoughts: str
  454. response: Literal["happy", "sad", "neutral"]
  455. raw_content = json.dumps(
  456. {
  457. "thoughts": "The user explicitly states that they are happy without any indication of sadness or neutrality.",
  458. "response": "happy",
  459. }
  460. )
  461. chunked_content = [raw_content[i : i + 5] for i in range(0, len(raw_content), 5)]
  462. assert "".join(chunked_content) == raw_content
  463. model = "gpt-4o-2024-11-20"
  464. mock_chunk_events = [
  465. MockChunkEvent(
  466. type="chunk",
  467. chunk=ChatCompletionChunk(
  468. id="id",
  469. choices=[
  470. ChunkChoice(
  471. finish_reason=None,
  472. index=0,
  473. delta=ChoiceDelta(
  474. content=mock_chunk_content,
  475. role="assistant",
  476. ),
  477. )
  478. ],
  479. created=0,
  480. model=model,
  481. object="chat.completion.chunk",
  482. usage=None,
  483. ),
  484. )
  485. for mock_chunk_content in chunked_content
  486. ]
  487. async def _mock_create_stream(*args: Any) -> AsyncGenerator[MockChunkEvent, None]:
  488. async def _stream() -> AsyncGenerator[MockChunkEvent, None]:
  489. for mock_chunk_event in mock_chunk_events:
  490. await asyncio.sleep(0.1)
  491. yield mock_chunk_event
  492. return _stream()
  493. # Mock the context manager __aenter__ method which returns the stream.
  494. monkeypatch.setattr(BetaAsyncChatCompletionStreamManager, "__aenter__", _mock_create_stream)
  495. model_client = OpenAIChatCompletionClient(
  496. model=model,
  497. api_key="",
  498. response_format=AgentResponse, # type: ignore
  499. )
  500. # Test that the openai client was called with the correct response format.
  501. chunks: List[str | CreateResult] = []
  502. async for chunk in model_client.create_stream(messages=[UserMessage(content="I am happy.", source="user")]):
  503. chunks.append(chunk)
  504. assert len(chunks) > 0
  505. assert isinstance(chunks[-1], CreateResult)
  506. assert isinstance(chunks[-1].content, str)
  507. response = AgentResponse.model_validate(json.loads(chunks[-1].content))
  508. assert (
  509. response.thoughts
  510. == "The user explicitly states that they are happy without any indication of sadness or neutrality."
  511. )
  512. assert response.response == "happy"
  513. @pytest.mark.asyncio
  514. async def test_structured_output_with_streaming_tool_calls(monkeypatch: pytest.MonkeyPatch) -> None:
  515. class AgentResponse(BaseModel):
  516. thoughts: str
  517. response: Literal["happy", "sad", "neutral"]
  518. raw_content = json.dumps(
  519. {
  520. "thoughts": "The user explicitly states that they are happy without any indication of sadness or neutrality.",
  521. "response": "happy",
  522. }
  523. )
  524. chunked_content = [raw_content[i : i + 5] for i in range(0, len(raw_content), 5)]
  525. assert "".join(chunked_content) == raw_content
  526. model = "gpt-4o-2024-11-20"
  527. # generate the list of mock chunk content
  528. mock_chunk_events = [
  529. MockChunkEvent(
  530. type="chunk",
  531. chunk=ChatCompletionChunk(
  532. id="id",
  533. choices=[
  534. ChunkChoice(
  535. finish_reason=None,
  536. index=0,
  537. delta=ChoiceDelta(
  538. content=mock_chunk_content,
  539. role="assistant",
  540. ),
  541. )
  542. ],
  543. created=0,
  544. model=model,
  545. object="chat.completion.chunk",
  546. usage=None,
  547. ),
  548. )
  549. for mock_chunk_content in chunked_content
  550. ]
  551. # add the tool call chunk.
  552. mock_chunk_events += [
  553. MockChunkEvent(
  554. type="chunk",
  555. chunk=ChatCompletionChunk(
  556. id="id",
  557. choices=[
  558. ChunkChoice(
  559. finish_reason="tool_calls",
  560. index=0,
  561. delta=ChoiceDelta(
  562. content=None,
  563. role="assistant",
  564. tool_calls=[
  565. ChoiceDeltaToolCall(
  566. id="1",
  567. index=0,
  568. type="function",
  569. function=ChoiceDeltaToolCallFunction(
  570. name="_pass_function",
  571. arguments=json.dumps({"input": "happy"}),
  572. ),
  573. )
  574. ],
  575. ),
  576. )
  577. ],
  578. created=0,
  579. model=model,
  580. object="chat.completion.chunk",
  581. usage=None,
  582. ),
  583. )
  584. ]
  585. async def _mock_create_stream(*args: Any) -> AsyncGenerator[MockChunkEvent, None]:
  586. async def _stream() -> AsyncGenerator[MockChunkEvent, None]:
  587. for mock_chunk_event in mock_chunk_events:
  588. await asyncio.sleep(0.1)
  589. yield mock_chunk_event
  590. return _stream()
  591. # Mock the context manager __aenter__ method which returns the stream.
  592. monkeypatch.setattr(BetaAsyncChatCompletionStreamManager, "__aenter__", _mock_create_stream)
  593. model_client = OpenAIChatCompletionClient(
  594. model=model,
  595. api_key="",
  596. response_format=AgentResponse, # type: ignore
  597. )
  598. # Test that the openai client was called with the correct response format.
  599. chunks: List[str | CreateResult] = []
  600. async for chunk in model_client.create_stream(messages=[UserMessage(content="I am happy.", source="user")]):
  601. chunks.append(chunk)
  602. assert len(chunks) > 0
  603. assert isinstance(chunks[-1], CreateResult)
  604. assert isinstance(chunks[-1].content, list)
  605. assert len(chunks[-1].content) == 1
  606. assert chunks[-1].content[0] == FunctionCall(
  607. id="1", name="_pass_function", arguments=json.dumps({"input": "happy"})
  608. )
  609. assert isinstance(chunks[-1].thought, str)
  610. response = AgentResponse.model_validate(json.loads(chunks[-1].thought))
  611. assert (
  612. response.thoughts
  613. == "The user explicitly states that they are happy without any indication of sadness or neutrality."
  614. )
  615. assert response.response == "happy"
  616. @pytest.mark.asyncio
  617. async def test_r1_think_field(monkeypatch: pytest.MonkeyPatch) -> None:
  618. async def _mock_create_stream(*args: Any, **kwargs: Any) -> AsyncGenerator[ChatCompletionChunk, None]:
  619. chunks = ["<think> Hello</think>", " Another Hello", " Yet Another Hello"]
  620. for i, chunk in enumerate(chunks):
  621. await asyncio.sleep(0.1)
  622. yield ChatCompletionChunk(
  623. id="id",
  624. choices=[
  625. ChunkChoice(
  626. finish_reason="stop" if i == len(chunks) - 1 else None,
  627. index=0,
  628. delta=ChoiceDelta(
  629. content=chunk,
  630. role="assistant",
  631. ),
  632. ),
  633. ],
  634. created=0,
  635. model="r1",
  636. object="chat.completion.chunk",
  637. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  638. )
  639. async def _mock_create(*args: Any, **kwargs: Any) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]:
  640. stream = kwargs.get("stream", False)
  641. if not stream:
  642. await asyncio.sleep(0.1)
  643. return ChatCompletion(
  644. id="id",
  645. choices=[
  646. Choice(
  647. finish_reason="stop",
  648. index=0,
  649. message=ChatCompletionMessage(
  650. content="<think> Hello</think> Another Hello Yet Another Hello", role="assistant"
  651. ),
  652. )
  653. ],
  654. created=0,
  655. model="r1",
  656. object="chat.completion",
  657. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  658. )
  659. else:
  660. return _mock_create_stream(*args, **kwargs)
  661. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  662. model_client = OpenAIChatCompletionClient(
  663. model="r1",
  664. api_key="",
  665. model_info={"family": ModelFamily.R1, "vision": False, "function_calling": False, "json_output": False},
  666. )
  667. # Successful completion with think field.
  668. create_result = await model_client.create(messages=[UserMessage(content="I am happy.", source="user")])
  669. assert create_result.content == "Another Hello Yet Another Hello"
  670. assert create_result.finish_reason == "stop"
  671. assert not create_result.cached
  672. assert create_result.thought == "Hello"
  673. # Stream completion with think field.
  674. chunks: List[str | CreateResult] = []
  675. async for chunk in model_client.create_stream(messages=[UserMessage(content="Hello", source="user")]):
  676. chunks.append(chunk)
  677. assert len(chunks) > 0
  678. assert isinstance(chunks[-1], CreateResult)
  679. assert chunks[-1].content == "Another Hello Yet Another Hello"
  680. assert chunks[-1].thought == "Hello"
  681. assert not chunks[-1].cached
  682. @pytest.mark.asyncio
  683. async def test_r1_think_field_not_present(monkeypatch: pytest.MonkeyPatch) -> None:
  684. async def _mock_create_stream(*args: Any, **kwargs: Any) -> AsyncGenerator[ChatCompletionChunk, None]:
  685. chunks = ["Hello", " Another Hello", " Yet Another Hello"]
  686. for i, chunk in enumerate(chunks):
  687. await asyncio.sleep(0.1)
  688. yield ChatCompletionChunk(
  689. id="id",
  690. choices=[
  691. ChunkChoice(
  692. finish_reason="stop" if i == len(chunks) - 1 else None,
  693. index=0,
  694. delta=ChoiceDelta(
  695. content=chunk,
  696. role="assistant",
  697. ),
  698. ),
  699. ],
  700. created=0,
  701. model="r1",
  702. object="chat.completion.chunk",
  703. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  704. )
  705. async def _mock_create(*args: Any, **kwargs: Any) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]:
  706. stream = kwargs.get("stream", False)
  707. if not stream:
  708. await asyncio.sleep(0.1)
  709. return ChatCompletion(
  710. id="id",
  711. choices=[
  712. Choice(
  713. finish_reason="stop",
  714. index=0,
  715. message=ChatCompletionMessage(
  716. content="Hello Another Hello Yet Another Hello", role="assistant"
  717. ),
  718. )
  719. ],
  720. created=0,
  721. model="r1",
  722. object="chat.completion",
  723. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  724. )
  725. else:
  726. return _mock_create_stream(*args, **kwargs)
  727. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  728. model_client = OpenAIChatCompletionClient(
  729. model="r1",
  730. api_key="",
  731. model_info={"family": ModelFamily.R1, "vision": False, "function_calling": False, "json_output": False},
  732. )
  733. # Warning completion when think field is not present.
  734. with pytest.warns(UserWarning, match="Could not find <think>..</think> field in model response content."):
  735. create_result = await model_client.create(messages=[UserMessage(content="I am happy.", source="user")])
  736. assert create_result.content == "Hello Another Hello Yet Another Hello"
  737. assert create_result.finish_reason == "stop"
  738. assert not create_result.cached
  739. assert create_result.thought is None
  740. # Stream completion with think field.
  741. with pytest.warns(UserWarning, match="Could not find <think>..</think> field in model response content."):
  742. chunks: List[str | CreateResult] = []
  743. async for chunk in model_client.create_stream(messages=[UserMessage(content="Hello", source="user")]):
  744. chunks.append(chunk)
  745. assert len(chunks) > 0
  746. assert isinstance(chunks[-1], CreateResult)
  747. assert chunks[-1].content == "Hello Another Hello Yet Another Hello"
  748. assert chunks[-1].thought is None
  749. assert not chunks[-1].cached
  750. @pytest.mark.asyncio
  751. async def test_tool_calling(monkeypatch: pytest.MonkeyPatch) -> None:
  752. model = "gpt-4o-2024-05-13"
  753. chat_completions = [
  754. # Successful completion, single tool call
  755. ChatCompletion(
  756. id="id1",
  757. choices=[
  758. Choice(
  759. finish_reason="tool_calls",
  760. index=0,
  761. message=ChatCompletionMessage(
  762. content=None,
  763. tool_calls=[
  764. ChatCompletionMessageToolCall(
  765. id="1",
  766. type="function",
  767. function=Function(
  768. name="_pass_function",
  769. arguments=json.dumps({"input": "task"}),
  770. ),
  771. )
  772. ],
  773. role="assistant",
  774. ),
  775. )
  776. ],
  777. created=0,
  778. model=model,
  779. object="chat.completion",
  780. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  781. ),
  782. # Successful completion, parallel tool calls
  783. ChatCompletion(
  784. id="id2",
  785. choices=[
  786. Choice(
  787. finish_reason="tool_calls",
  788. index=0,
  789. message=ChatCompletionMessage(
  790. content=None,
  791. tool_calls=[
  792. ChatCompletionMessageToolCall(
  793. id="1",
  794. type="function",
  795. function=Function(
  796. name="_pass_function",
  797. arguments=json.dumps({"input": "task"}),
  798. ),
  799. ),
  800. ChatCompletionMessageToolCall(
  801. id="2",
  802. type="function",
  803. function=Function(
  804. name="_fail_function",
  805. arguments=json.dumps({"input": "task"}),
  806. ),
  807. ),
  808. ChatCompletionMessageToolCall(
  809. id="3",
  810. type="function",
  811. function=Function(
  812. name="_echo_function",
  813. arguments=json.dumps({"input": "task"}),
  814. ),
  815. ),
  816. ],
  817. role="assistant",
  818. ),
  819. )
  820. ],
  821. created=0,
  822. model=model,
  823. object="chat.completion",
  824. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  825. ),
  826. # Warning completion when finish reason is not tool_calls.
  827. ChatCompletion(
  828. id="id3",
  829. choices=[
  830. Choice(
  831. finish_reason="stop",
  832. index=0,
  833. message=ChatCompletionMessage(
  834. content=None,
  835. tool_calls=[
  836. ChatCompletionMessageToolCall(
  837. id="1",
  838. type="function",
  839. function=Function(
  840. name="_pass_function",
  841. arguments=json.dumps({"input": "task"}),
  842. ),
  843. )
  844. ],
  845. role="assistant",
  846. ),
  847. )
  848. ],
  849. created=0,
  850. model=model,
  851. object="chat.completion",
  852. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  853. ),
  854. # Thought field is populated when content is not None.
  855. ChatCompletion(
  856. id="id4",
  857. choices=[
  858. Choice(
  859. finish_reason="tool_calls",
  860. index=0,
  861. message=ChatCompletionMessage(
  862. content="I should make a tool call.",
  863. tool_calls=[
  864. ChatCompletionMessageToolCall(
  865. id="1",
  866. type="function",
  867. function=Function(
  868. name="_pass_function",
  869. arguments=json.dumps({"input": "task"}),
  870. ),
  871. )
  872. ],
  873. role="assistant",
  874. ),
  875. )
  876. ],
  877. created=0,
  878. model=model,
  879. object="chat.completion",
  880. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  881. ),
  882. # Should not be returning tool calls when the tool_calls are empty
  883. ChatCompletion(
  884. id="id5",
  885. choices=[
  886. Choice(
  887. finish_reason="stop",
  888. index=0,
  889. message=ChatCompletionMessage(
  890. content="I should make a tool call.",
  891. tool_calls=[],
  892. role="assistant",
  893. ),
  894. )
  895. ],
  896. created=0,
  897. model=model,
  898. object="chat.completion",
  899. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  900. ),
  901. # Should raise warning when function arguments is not a string.
  902. ChatCompletion(
  903. id="id6",
  904. choices=[
  905. Choice(
  906. finish_reason="tool_calls",
  907. index=0,
  908. message=ChatCompletionMessage(
  909. content=None,
  910. tool_calls=[
  911. ChatCompletionMessageToolCall(
  912. id="1",
  913. type="function",
  914. function=Function.construct(name="_pass_function", arguments={"input": "task"}), # type: ignore
  915. )
  916. ],
  917. role="assistant",
  918. ),
  919. )
  920. ],
  921. created=0,
  922. model=model,
  923. object="chat.completion",
  924. usage=CompletionUsage(prompt_tokens=10, completion_tokens=5, total_tokens=0),
  925. ),
  926. ]
  927. class _MockChatCompletion:
  928. def __init__(self, completions: List[ChatCompletion]):
  929. self.completions = list(completions)
  930. self.calls: List[Dict[str, Any]] = []
  931. async def mock_create(
  932. self, *args: Any, **kwargs: Any
  933. ) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]:
  934. if kwargs.get("stream", False):
  935. raise NotImplementedError("Streaming not supported in this test.")
  936. self.calls.append(kwargs)
  937. return self.completions.pop(0)
  938. mock = _MockChatCompletion(chat_completions)
  939. monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create)
  940. pass_tool = FunctionTool(_pass_function, description="pass tool.")
  941. fail_tool = FunctionTool(_fail_function, description="fail tool.")
  942. echo_tool = FunctionTool(_echo_function, description="echo tool.")
  943. model_client = OpenAIChatCompletionClient(model=model, api_key="")
  944. # Single tool call
  945. create_result = await model_client.create(messages=[UserMessage(content="Hello", source="user")], tools=[pass_tool])
  946. assert create_result.content == [FunctionCall(id="1", arguments=r'{"input": "task"}', name="_pass_function")]
  947. # Verify that the tool schema was passed to the model client.
  948. kwargs = mock.calls[0]
  949. assert kwargs["tools"] == [{"function": pass_tool.schema, "type": "function"}]
  950. # Verify finish reason
  951. assert create_result.finish_reason == "function_calls"
  952. # Parallel tool calls
  953. create_result = await model_client.create(
  954. messages=[UserMessage(content="Hello", source="user")], tools=[pass_tool, fail_tool, echo_tool]
  955. )
  956. assert create_result.content == [
  957. FunctionCall(id="1", arguments=r'{"input": "task"}', name="_pass_function"),
  958. FunctionCall(id="2", arguments=r'{"input": "task"}', name="_fail_function"),
  959. FunctionCall(id="3", arguments=r'{"input": "task"}', name="_echo_function"),
  960. ]
  961. # Verify that the tool schema was passed to the model client.
  962. kwargs = mock.calls[1]
  963. assert kwargs["tools"] == [
  964. {"function": pass_tool.schema, "type": "function"},
  965. {"function": fail_tool.schema, "type": "function"},
  966. {"function": echo_tool.schema, "type": "function"},
  967. ]
  968. # Verify finish reason
  969. assert create_result.finish_reason == "function_calls"
  970. # Warning completion when finish reason is not tool_calls.
  971. with pytest.warns(UserWarning, match="Finish reason mismatch"):
  972. create_result = await model_client.create(
  973. messages=[UserMessage(content="Hello", source="user")], tools=[pass_tool]
  974. )
  975. assert create_result.content == [FunctionCall(id="1", arguments=r'{"input": "task"}', name="_pass_function")]
  976. assert create_result.finish_reason == "function_calls"
  977. # Thought field is populated when content is not None.
  978. create_result = await model_client.create(messages=[UserMessage(content="Hello", source="user")], tools=[pass_tool])
  979. assert create_result.content == [FunctionCall(id="1", arguments=r'{"input": "task"}', name="_pass_function")]
  980. assert create_result.finish_reason == "function_calls"
  981. assert create_result.thought == "I should make a tool call."
  982. # Should not be returning tool calls when the tool_calls are empty
  983. create_result = await model_client.create(messages=[UserMessage(content="Hello", source="user")], tools=[pass_tool])
  984. assert create_result.content == "I should make a tool call."
  985. assert create_result.finish_reason == "stop"
  986. # Should raise warning when function arguments is not a string.
  987. with pytest.warns(UserWarning, match="Tool call function arguments field is not a string"):
  988. create_result = await model_client.create(
  989. messages=[UserMessage(content="Hello", source="user")], tools=[pass_tool]
  990. )
  991. assert create_result.content == [FunctionCall(id="1", arguments=r'{"input": "task"}', name="_pass_function")]
  992. assert create_result.finish_reason == "function_calls"
  993. @pytest.mark.asyncio
  994. async def test_tool_calling_with_stream(monkeypatch: pytest.MonkeyPatch) -> None:
  995. async def _mock_create_stream(*args: Any, **kwargs: Any) -> AsyncGenerator[ChatCompletionChunk, None]:
  996. model = resolve_model(kwargs.get("model", "gpt-4o"))
  997. mock_chunks_content = ["Hello", " Another Hello", " Yet Another Hello"]
  998. mock_chunks = [
  999. # generate the list of mock chunk content
  1000. MockChunkDefinition(
  1001. chunk_choice=ChunkChoice(
  1002. finish_reason=None,
  1003. index=0,
  1004. delta=ChoiceDelta(
  1005. content=mock_chunk_content,
  1006. role="assistant",
  1007. ),
  1008. ),
  1009. usage=None,
  1010. )
  1011. for mock_chunk_content in mock_chunks_content
  1012. ] + [
  1013. # generate the function call chunk
  1014. MockChunkDefinition(
  1015. chunk_choice=ChunkChoice(
  1016. finish_reason="tool_calls",
  1017. index=0,
  1018. delta=ChoiceDelta(
  1019. content=None,
  1020. role="assistant",
  1021. tool_calls=[
  1022. ChoiceDeltaToolCall(
  1023. index=0,
  1024. id="1",
  1025. type="function",
  1026. function=ChoiceDeltaToolCallFunction(
  1027. name="_pass_function",
  1028. arguments=json.dumps({"input": "task"}),
  1029. ),
  1030. )
  1031. ],
  1032. ),
  1033. ),
  1034. usage=None,
  1035. )
  1036. ]
  1037. for mock_chunk in mock_chunks:
  1038. await asyncio.sleep(0.1)
  1039. yield ChatCompletionChunk(
  1040. id="id",
  1041. choices=[mock_chunk.chunk_choice],
  1042. created=0,
  1043. model=model,
  1044. object="chat.completion.chunk",
  1045. usage=mock_chunk.usage,
  1046. )
  1047. async def _mock_create(*args: Any, **kwargs: Any) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]:
  1048. stream = kwargs.get("stream", False)
  1049. if not stream:
  1050. raise ValueError("Stream is not False")
  1051. else:
  1052. return _mock_create_stream(*args, **kwargs)
  1053. monkeypatch.setattr(AsyncCompletions, "create", _mock_create)
  1054. model_client = OpenAIChatCompletionClient(model="gpt-4o", api_key="")
  1055. pass_tool = FunctionTool(_pass_function, description="pass tool.")
  1056. stream = model_client.create_stream(messages=[UserMessage(content="Hello", source="user")], tools=[pass_tool])
  1057. chunks: List[str | CreateResult] = []
  1058. async for chunk in stream:
  1059. chunks.append(chunk)
  1060. assert chunks[0] == "Hello"
  1061. assert chunks[1] == " Another Hello"
  1062. assert chunks[2] == " Yet Another Hello"
  1063. assert isinstance(chunks[-1], CreateResult)
  1064. assert chunks[-1].content == [FunctionCall(id="1", arguments=r'{"input": "task"}', name="_pass_function")]
  1065. assert chunks[-1].finish_reason == "function_calls"
  1066. assert chunks[-1].thought == "Hello Another Hello Yet Another Hello"
  1067. async def _test_model_client_basic_completion(model_client: OpenAIChatCompletionClient) -> None:
  1068. # Test basic completion
  1069. create_result = await model_client.create(
  1070. messages=[
  1071. SystemMessage(content="You are a helpful assistant."),
  1072. UserMessage(content="Explain to me how AI works.", source="user"),
  1073. ]
  1074. )
  1075. assert isinstance(create_result.content, str)
  1076. assert len(create_result.content) > 0
  1077. async def _test_model_client_with_function_calling(model_client: OpenAIChatCompletionClient) -> None:
  1078. # Test tool calling
  1079. pass_tool = FunctionTool(_pass_function, name="pass_tool", description="pass session.")
  1080. fail_tool = FunctionTool(_fail_function, name="fail_tool", description="fail session.")
  1081. messages: List[LLMMessage] = [UserMessage(content="Call the pass tool with input 'task'", source="user")]
  1082. create_result = await model_client.create(messages=messages, tools=[pass_tool, fail_tool])
  1083. assert isinstance(create_result.content, list)
  1084. assert len(create_result.content) == 1
  1085. assert isinstance(create_result.content[0], FunctionCall)
  1086. assert create_result.content[0].name == "pass_tool"
  1087. assert json.loads(create_result.content[0].arguments) == {"input": "task"}
  1088. assert create_result.finish_reason == "function_calls"
  1089. assert create_result.usage is not None
  1090. # Test reflection on tool call response.
  1091. messages.append(AssistantMessage(content=create_result.content, source="assistant"))
  1092. messages.append(
  1093. FunctionExecutionResultMessage(
  1094. content=[FunctionExecutionResult(content="passed", call_id=create_result.content[0].id, is_error=False)]
  1095. )
  1096. )
  1097. create_result = await model_client.create(messages=messages)
  1098. assert isinstance(create_result.content, str)
  1099. assert len(create_result.content) > 0
  1100. # Test parallel tool calling
  1101. messages = [
  1102. UserMessage(
  1103. content="Call both the pass tool with input 'task' and the fail tool also with input 'task'", source="user"
  1104. )
  1105. ]
  1106. create_result = await model_client.create(messages=messages, tools=[pass_tool, fail_tool])
  1107. assert isinstance(create_result.content, list)
  1108. assert len(create_result.content) == 2
  1109. assert isinstance(create_result.content[0], FunctionCall)
  1110. assert create_result.content[0].name == "pass_tool"
  1111. assert json.loads(create_result.content[0].arguments) == {"input": "task"}
  1112. assert isinstance(create_result.content[1], FunctionCall)
  1113. assert create_result.content[1].name == "fail_tool"
  1114. assert json.loads(create_result.content[1].arguments) == {"input": "task"}
  1115. assert create_result.finish_reason == "function_calls"
  1116. assert create_result.usage is not None
  1117. # Test reflection on parallel tool call response.
  1118. messages.append(AssistantMessage(content=create_result.content, source="assistant"))
  1119. messages.append(
  1120. FunctionExecutionResultMessage(
  1121. content=[
  1122. FunctionExecutionResult(content="passed", call_id=create_result.content[0].id, is_error=False),
  1123. FunctionExecutionResult(content="failed", call_id=create_result.content[1].id, is_error=True),
  1124. ]
  1125. )
  1126. )
  1127. create_result = await model_client.create(messages=messages)
  1128. assert isinstance(create_result.content, str)
  1129. assert len(create_result.content) > 0
  1130. @pytest.mark.asyncio
  1131. async def test_openai() -> None:
  1132. api_key = os.getenv("OPENAI_API_KEY")
  1133. if not api_key:
  1134. pytest.skip("OPENAI_API_KEY not found in environment variables")
  1135. model_client = OpenAIChatCompletionClient(
  1136. model="gpt-4o-mini",
  1137. api_key=api_key,
  1138. )
  1139. await _test_model_client_basic_completion(model_client)
  1140. await _test_model_client_with_function_calling(model_client)
  1141. @pytest.mark.asyncio
  1142. async def test_openai_structured_output() -> None:
  1143. api_key = os.getenv("OPENAI_API_KEY")
  1144. if not api_key:
  1145. pytest.skip("OPENAI_API_KEY not found in environment variables")
  1146. class AgentResponse(BaseModel):
  1147. thoughts: str
  1148. response: Literal["happy", "sad", "neutral"]
  1149. model_client = OpenAIChatCompletionClient(
  1150. model="gpt-4o-mini",
  1151. api_key=api_key,
  1152. response_format=AgentResponse, # type: ignore
  1153. )
  1154. # Test that the openai client was called with the correct response format.
  1155. create_result = await model_client.create(messages=[UserMessage(content="I am happy.", source="user")])
  1156. assert isinstance(create_result.content, str)
  1157. response = AgentResponse.model_validate(json.loads(create_result.content))
  1158. assert response.thoughts
  1159. assert response.response in ["happy", "sad", "neutral"]
  1160. @pytest.mark.asyncio
  1161. async def test_openai_structured_output_with_streaming() -> None:
  1162. api_key = os.getenv("OPENAI_API_KEY")
  1163. if not api_key:
  1164. pytest.skip("OPENAI_API_KEY not found in environment variables")
  1165. class AgentResponse(BaseModel):
  1166. thoughts: str
  1167. response: Literal["happy", "sad", "neutral"]
  1168. model_client = OpenAIChatCompletionClient(
  1169. model="gpt-4o-mini",
  1170. api_key=api_key,
  1171. response_format=AgentResponse, # type: ignore
  1172. )
  1173. # Test that the openai client was called with the correct response format.
  1174. stream = model_client.create_stream(messages=[UserMessage(content="I am happy.", source="user")])
  1175. chunks: List[str | CreateResult] = []
  1176. async for chunk in stream:
  1177. chunks.append(chunk)
  1178. assert len(chunks) > 0
  1179. assert isinstance(chunks[-1], CreateResult)
  1180. assert isinstance(chunks[-1].content, str)
  1181. response = AgentResponse.model_validate(json.loads(chunks[-1].content))
  1182. assert response.thoughts
  1183. assert response.response in ["happy", "sad", "neutral"]
  1184. @pytest.mark.asyncio
  1185. async def test_openai_structured_output_with_tool_calls() -> None:
  1186. api_key = os.getenv("OPENAI_API_KEY")
  1187. if not api_key:
  1188. pytest.skip("OPENAI_API_KEY not found in environment variables")
  1189. class AgentResponse(BaseModel):
  1190. thoughts: str
  1191. response: Literal["happy", "sad", "neutral"]
  1192. def sentiment_analysis(text: str) -> str:
  1193. """Given a text, return the sentiment."""
  1194. return "happy" if "happy" in text else "sad" if "sad" in text else "neutral"
  1195. tool = FunctionTool(sentiment_analysis, description="Sentiment Analysis", strict=True)
  1196. model_client = OpenAIChatCompletionClient(
  1197. model="gpt-4o-mini",
  1198. api_key=api_key,
  1199. response_format=AgentResponse, # type: ignore
  1200. )
  1201. response1 = await model_client.create(
  1202. messages=[
  1203. SystemMessage(content="Analyze input text sentiment using the tool provided."),
  1204. UserMessage(content="I am happy.", source="user"),
  1205. ],
  1206. tools=[tool],
  1207. extra_create_args={"tool_choice": "required"},
  1208. )
  1209. assert isinstance(response1.content, list)
  1210. assert len(response1.content) == 1
  1211. assert isinstance(response1.content[0], FunctionCall)
  1212. assert response1.content[0].name == "sentiment_analysis"
  1213. assert json.loads(response1.content[0].arguments) == {"text": "I am happy."}
  1214. assert response1.finish_reason == "function_calls"
  1215. response2 = await model_client.create(
  1216. messages=[
  1217. SystemMessage(content="Analyze input text sentiment using the tool provided."),
  1218. UserMessage(content="I am happy.", source="user"),
  1219. AssistantMessage(content=response1.content, source="assistant"),
  1220. FunctionExecutionResultMessage(
  1221. content=[FunctionExecutionResult(content="happy", call_id=response1.content[0].id, is_error=False)]
  1222. ),
  1223. ],
  1224. )
  1225. assert isinstance(response2.content, str)
  1226. parsed_response = AgentResponse.model_validate(json.loads(response2.content))
  1227. assert parsed_response.thoughts
  1228. assert parsed_response.response in ["happy", "sad", "neutral"]
  1229. @pytest.mark.asyncio
  1230. async def test_openai_structured_output_with_streaming_tool_calls() -> None:
  1231. api_key = os.getenv("OPENAI_API_KEY")
  1232. if not api_key:
  1233. pytest.skip("OPENAI_API_KEY not found in environment variables")
  1234. class AgentResponse(BaseModel):
  1235. thoughts: str
  1236. response: Literal["happy", "sad", "neutral"]
  1237. def sentiment_analysis(text: str) -> str:
  1238. """Given a text, return the sentiment."""
  1239. return "happy" if "happy" in text else "sad" if "sad" in text else "neutral"
  1240. tool = FunctionTool(sentiment_analysis, description="Sentiment Analysis", strict=True)
  1241. model_client = OpenAIChatCompletionClient(
  1242. model="gpt-4o-mini",
  1243. api_key=api_key,
  1244. response_format=AgentResponse, # type: ignore
  1245. )
  1246. chunks1: List[str | CreateResult] = []
  1247. stream1 = model_client.create_stream(
  1248. messages=[
  1249. SystemMessage(content="Analyze input text sentiment using the tool provided."),
  1250. UserMessage(content="I am happy.", source="user"),
  1251. ],
  1252. tools=[tool],
  1253. extra_create_args={"tool_choice": "required"},
  1254. )
  1255. async for chunk in stream1:
  1256. chunks1.append(chunk)
  1257. assert len(chunks1) > 0
  1258. create_result1 = chunks1[-1]
  1259. assert isinstance(create_result1, CreateResult)
  1260. assert isinstance(create_result1.content, list)
  1261. assert len(create_result1.content) == 1
  1262. assert isinstance(create_result1.content[0], FunctionCall)
  1263. assert create_result1.content[0].name == "sentiment_analysis"
  1264. assert json.loads(create_result1.content[0].arguments) == {"text": "I am happy."}
  1265. assert create_result1.finish_reason == "function_calls"
  1266. stream2 = model_client.create_stream(
  1267. messages=[
  1268. SystemMessage(content="Analyze input text sentiment using the tool provided."),
  1269. UserMessage(content="I am happy.", source="user"),
  1270. AssistantMessage(content=create_result1.content, source="assistant"),
  1271. FunctionExecutionResultMessage(
  1272. content=[FunctionExecutionResult(content="happy", call_id=create_result1.content[0].id, is_error=False)]
  1273. ),
  1274. ],
  1275. )
  1276. chunks2: List[str | CreateResult] = []
  1277. async for chunk in stream2:
  1278. chunks2.append(chunk)
  1279. assert len(chunks2) > 0
  1280. create_result2 = chunks2[-1]
  1281. assert isinstance(create_result2, CreateResult)
  1282. assert isinstance(create_result2.content, str)
  1283. parsed_response = AgentResponse.model_validate(json.loads(create_result2.content))
  1284. assert parsed_response.thoughts
  1285. assert parsed_response.response in ["happy", "sad", "neutral"]
  1286. @pytest.mark.asyncio
  1287. async def test_gemini() -> None:
  1288. api_key = os.getenv("GEMINI_API_KEY")
  1289. if not api_key:
  1290. pytest.skip("GEMINI_API_KEY not found in environment variables")
  1291. model_client = OpenAIChatCompletionClient(
  1292. model="gemini-1.5-flash",
  1293. )
  1294. await _test_model_client_basic_completion(model_client)
  1295. await _test_model_client_with_function_calling(model_client)
  1296. @pytest.mark.asyncio
  1297. async def test_hugging_face() -> None:
  1298. api_key = os.getenv("HF_TOKEN")
  1299. if not api_key:
  1300. pytest.skip("HF_TOKEN not found in environment variables")
  1301. model_client = OpenAIChatCompletionClient(
  1302. model="microsoft/Phi-3.5-mini-instruct",
  1303. api_key=api_key,
  1304. base_url="https://api-inference.huggingface.co/v1/",
  1305. model_info={
  1306. "function_calling": False,
  1307. "json_output": False,
  1308. "vision": False,
  1309. "family": ModelFamily.UNKNOWN,
  1310. },
  1311. )
  1312. await _test_model_client_basic_completion(model_client)
  1313. @pytest.mark.asyncio
  1314. async def test_ollama() -> None:
  1315. model = "deepseek-r1:1.5b"
  1316. model_info: ModelInfo = {
  1317. "function_calling": False,
  1318. "json_output": False,
  1319. "vision": False,
  1320. "family": ModelFamily.R1,
  1321. }
  1322. # Check if the model is running locally.
  1323. try:
  1324. async with httpx.AsyncClient() as client:
  1325. response = await client.get(f"http://localhost:11434/v1/models/{model}")
  1326. response.raise_for_status()
  1327. except httpx.HTTPStatusError as e:
  1328. pytest.skip(f"{model} model is not running locally: {e}")
  1329. except httpx.ConnectError as e:
  1330. pytest.skip(f"Ollama is not running locally: {e}")
  1331. model_client = OpenAIChatCompletionClient(
  1332. model=model,
  1333. api_key="placeholder",
  1334. base_url="http://localhost:11434/v1",
  1335. model_info=model_info,
  1336. )
  1337. # Test basic completion with the Ollama deepseek-r1:1.5b model.
  1338. create_result = await model_client.create(
  1339. messages=[
  1340. UserMessage(
  1341. content="Taking two balls from a bag of 10 green balls and 20 red balls, "
  1342. "what is the probability of getting a green and a red balls?",
  1343. source="user",
  1344. ),
  1345. ]
  1346. )
  1347. assert isinstance(create_result.content, str)
  1348. assert len(create_result.content) > 0
  1349. assert create_result.finish_reason == "stop"
  1350. assert create_result.usage is not None
  1351. if model_info["family"] == ModelFamily.R1:
  1352. assert create_result.thought is not None
  1353. # Test streaming completion with the Ollama deepseek-r1:1.5b model.
  1354. chunks: List[str | CreateResult] = []
  1355. async for chunk in model_client.create_stream(
  1356. messages=[
  1357. UserMessage(
  1358. content="Taking two balls from a bag of 10 green balls and 20 red balls, "
  1359. "what is the probability of getting a green and a red balls?",
  1360. source="user",
  1361. ),
  1362. ]
  1363. ):
  1364. chunks.append(chunk)
  1365. assert len(chunks) > 0
  1366. assert isinstance(chunks[-1], CreateResult)
  1367. assert chunks[-1].finish_reason == "stop"
  1368. assert len(chunks[-1].content) > 0
  1369. assert chunks[-1].usage is not None
  1370. if model_info["family"] == ModelFamily.R1:
  1371. assert chunks[-1].thought is not None
  1372. @pytest.mark.asyncio
  1373. async def test_add_name_prefixes(monkeypatch: pytest.MonkeyPatch) -> None:
  1374. sys_message = SystemMessage(content="You are a helpful AI agent, and you answer questions in a friendly way.")
  1375. assistant_message = AssistantMessage(content="Hello, how can I help you?", source="Assistant")
  1376. user_text_message = UserMessage(content="Hello, I am from Seattle.", source="Adam")
  1377. user_mm_message = UserMessage(
  1378. content=[
  1379. "Here is a postcard from Seattle:",
  1380. Image.from_base64(
  1381. "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"
  1382. ),
  1383. ],
  1384. source="Adam",
  1385. )
  1386. # Default conversion
  1387. oai_sys = to_oai_type(sys_message)[0]
  1388. oai_asst = to_oai_type(assistant_message)[0]
  1389. oai_text = to_oai_type(user_text_message)[0]
  1390. oai_mm = to_oai_type(user_mm_message)[0]
  1391. converted_sys = to_oai_type(sys_message, prepend_name=True)[0]
  1392. converted_asst = to_oai_type(assistant_message, prepend_name=True)[0]
  1393. converted_text = to_oai_type(user_text_message, prepend_name=True)[0]
  1394. converted_mm = to_oai_type(user_mm_message, prepend_name=True)[0]
  1395. # Invariants
  1396. assert "content" in oai_sys
  1397. assert "content" in oai_asst
  1398. assert "content" in oai_text
  1399. assert "content" in oai_mm
  1400. assert "content" in converted_sys
  1401. assert "content" in converted_asst
  1402. assert "content" in converted_text
  1403. assert "content" in converted_mm
  1404. assert oai_sys["role"] == converted_sys["role"]
  1405. assert oai_sys["content"] == converted_sys["content"]
  1406. assert oai_asst["role"] == converted_asst["role"]
  1407. assert oai_asst["content"] == converted_asst["content"]
  1408. assert oai_text["role"] == converted_text["role"]
  1409. assert oai_mm["role"] == converted_mm["role"]
  1410. assert isinstance(oai_mm["content"], list)
  1411. assert isinstance(converted_mm["content"], list)
  1412. assert len(oai_mm["content"]) == len(converted_mm["content"])
  1413. assert "text" in converted_mm["content"][0]
  1414. assert "text" in oai_mm["content"][0]
  1415. # Name prepended
  1416. assert str(converted_text["content"]) == "Adam said:\n" + str(oai_text["content"])
  1417. assert str(converted_mm["content"][0]["text"]) == "Adam said:\n" + str(oai_mm["content"][0]["text"])
  1418. # TODO: add integration tests for Azure OpenAI using AAD token.