Skip to content

Commit 27f7af2

Browse files
Python: moved prepare tools into class (#215)
* moved prepare tools into class * moved test * changed tool handling * fix test * second fix
1 parent 84e5ee9 commit 27f7af2

4 files changed

Lines changed: 80 additions & 79 deletions

File tree

python/packages/foundry/agent_framework_foundry/_chat_client.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from agent_framework import (
99
AFBaseSettings,
1010
AIContents,
11-
AITool,
11+
AIFunction,
1212
ChatClientBase,
1313
ChatMessage,
1414
ChatOptions,
@@ -25,7 +25,7 @@
2525
UsageDetails,
2626
use_tool_calling,
2727
)
28-
from agent_framework._clients import tool_to_json_schema_spec
28+
from agent_framework._clients import ai_function_to_json_schema_spec
2929
from agent_framework.exceptions import ServiceInitializationError
3030
from azure.ai.agents.models import (
3131
AgentsNamedToolChoice,
@@ -455,13 +455,14 @@ def _create_run_options(
455455
run_options["parallel_tool_calls"] = chat_options.allow_multiple_tool_calls
456456

457457
if chat_options.tools is not None:
458+
# TODO (eavanvalkenburg): replace with _prepare_tools_and_tool_choice overload
458459
tool_definitions: list[MutableMapping[str, Any]] = []
459460

460461
for tool in chat_options.tools:
461-
if isinstance(tool, AITool):
462-
tool_definitions.append(tool_to_json_schema_spec(tool))
462+
if isinstance(tool, AIFunction):
463+
tool_definitions.append(ai_function_to_json_schema_spec(tool))
463464
else:
464-
tool_definitions.append(tool)
465+
tool_definitions.append(tool) # type: ignore
465466

466467
if len(tool_definitions) > 0:
467468
run_options["tools"] = tool_definitions

python/packages/main/agent_framework/_clients.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -75,35 +75,18 @@ async def _auto_invoke_function(
7575
)
7676

7777

78-
def tool_to_json_schema_spec(tool: AITool) -> dict[str, Any]:
79-
"""Convert a AITool to the JSON Schema function specification format."""
78+
def ai_function_to_json_schema_spec(function: AIFunction[BaseModel, Any]) -> dict[str, Any]:
79+
"""Convert a AIFunction to the JSON Schema function specification format."""
8080
return {
8181
"type": "function",
8282
"function": {
83-
"name": tool.name,
84-
"description": tool.description,
85-
"parameters": tool.parameters(),
83+
"name": function.name,
84+
"description": function.description,
85+
"parameters": function.parameters(),
8686
},
8787
}
8888

8989

90-
def _prepare_tools_and_tool_choice(chat_options: ChatOptions) -> None:
91-
"""Prepare the tools and tool choice for the chat options."""
92-
chat_tool_mode: ChatToolMode | None = chat_options.tool_choice # type: ignore
93-
if chat_tool_mode is None or chat_tool_mode == ChatToolMode.NONE:
94-
chat_options.tools = None
95-
chat_options.tool_choice = ChatToolMode.NONE.mode
96-
return
97-
chat_options.tools = [
98-
(tool_to_json_schema_spec(t) if isinstance(t, AITool) else t)
99-
for t in chat_options._ai_tools or [] # type: ignore[reportPrivateUsage]
100-
]
101-
if not chat_options.tools:
102-
chat_options.tool_choice = ChatToolMode.NONE.mode
103-
else:
104-
chat_options.tool_choice = chat_tool_mode.mode
105-
106-
10790
def _tool_call_non_streaming(func: TInnerGetResponse) -> TInnerGetResponse:
10891
"""Decorate the internal _inner_get_response method to enable tool calls."""
10992

@@ -163,7 +146,7 @@ async def wrapper(
163146

164147
# Failsafe: give up on tools, ask model for plain answer
165148
chat_options.tool_choice = "none"
166-
_prepare_tools_and_tool_choice(chat_options=chat_options)
149+
self._prepare_tools_and_tool_choice(chat_options=chat_options) # type: ignore[reportPrivateUsage]
167150
response = await func(self, messages=messages, chat_options=chat_options)
168151
if fcc_messages:
169152
for msg in reversed(fcc_messages):
@@ -231,7 +214,7 @@ async def wrapper(
231214

232215
# Failsafe: give up on tools, ask model for plain answer
233216
chat_options.tool_choice = "none"
234-
_prepare_tools_and_tool_choice(chat_options=chat_options)
217+
self._prepare_tools_and_tool_choice(chat_options=chat_options) # type: ignore[reportPrivateUsage]
235218
async for update in func(self, messages=messages, chat_options=chat_options, **kwargs):
236219
yield update
237220

@@ -542,7 +525,7 @@ async def get_response(
542525
additional_properties=additional_properties or {},
543526
)
544527
prepped_messages = self._prepare_messages(messages)
545-
_prepare_tools_and_tool_choice(chat_options=chat_options)
528+
self._prepare_tools_and_tool_choice(chat_options=chat_options)
546529
return await self._inner_get_response(messages=prepped_messages, chat_options=chat_options, **kwargs)
547530

548531
async def get_streaming_response(
@@ -623,12 +606,32 @@ async def get_streaming_response(
623606
**kwargs,
624607
)
625608
prepped_messages = self._prepare_messages(messages)
626-
_prepare_tools_and_tool_choice(chat_options=chat_options)
609+
self._prepare_tools_and_tool_choice(chat_options=chat_options)
627610
async for update in self._inner_get_streaming_response(
628611
messages=prepped_messages, chat_options=chat_options, **kwargs
629612
):
630613
yield update
631614

615+
def _prepare_tools_and_tool_choice(self, chat_options: ChatOptions) -> None:
616+
"""Prepare the tools and tool choice for the chat options.
617+
618+
This function should be overridden by subclasses to customize tool handling.
619+
Because it currently parses only AIFunctions.
620+
"""
621+
chat_tool_mode: ChatToolMode | None = chat_options.tool_choice # type: ignore
622+
if chat_tool_mode is None or chat_tool_mode == ChatToolMode.NONE:
623+
chat_options.tools = None
624+
chat_options.tool_choice = ChatToolMode.NONE.mode
625+
return
626+
chat_options.tools = [
627+
(ai_function_to_json_schema_spec(t) if isinstance(t, AIFunction) else t) # type: ignore[reportUnknownArgumentType]
628+
for t in chat_options._ai_tools or [] # type: ignore[reportPrivateUsage]
629+
]
630+
if not chat_options.tools:
631+
chat_options.tool_choice = ChatToolMode.NONE.mode
632+
else:
633+
chat_options.tool_choice = chat_tool_mode.mode
634+
632635

633636
# region: Embedding Client
634637

python/packages/main/tests/test_clients.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,48 @@ def ai_func(arg1: str) -> str:
305305
updates.append(update)
306306
assert len(updates) == 1
307307
assert exec_counter == 0
308+
309+
310+
def test_chat_options_parsing_tools(chat_client_base, ai_function_tool) -> None:
311+
"""Test that chat options can parse tools correctly."""
312+
313+
def echo() -> str:
314+
"""Echo the input."""
315+
return "Echo"
316+
317+
dict_function = {
318+
"type": "function",
319+
"function": {
320+
"name": "get_weather",
321+
"description": "Retrieves current weather for the given location.",
322+
"parameters": {
323+
"type": "object",
324+
"properties": {
325+
"location": {"type": "string", "description": "City and country e.g. Bogotá, Colombia"},
326+
"units": {
327+
"type": "string",
328+
"enum": ["celsius", "fahrenheit"],
329+
"description": "Units the temperature will be returned in.",
330+
},
331+
},
332+
"required": ["location", "units"],
333+
"additionalProperties": False,
334+
},
335+
"strict": True,
336+
},
337+
}
338+
339+
options = ChatOptions(tools=[ai_function_tool, echo, dict_function], tool_choice="auto")
340+
assert len(options.tools) == 3
341+
assert options.tools[0] == ai_function_tool
342+
assert options.tools[1] != echo
343+
assert options.tools[2] == dict_function
344+
# after prepare, the tools should be represented as dicts
345+
# while ai_tools is still the same.
346+
chat_client_base._prepare_tools_and_tool_choice(chat_options=options)
347+
assert options._ai_tools[0] == ai_function_tool
348+
assert options._ai_tools[2] == dict_function
349+
assert len(options.tools) == 3
350+
assert options.tools[0]["function"]["name"] == "simple_function"
351+
assert options.tools[1]["function"]["name"] == "echo"
352+
assert options.tools[2]["function"]["name"] == "get_weather"

python/packages/main/tests/test_types.py

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -539,54 +539,6 @@ def test_chat_options_and(ai_function_tool, ai_tool) -> None:
539539
assert options3.tools == [ai_function_tool, ai_tool]
540540

541541

542-
def test_chat_options_parsing_tools(ai_function_tool, ai_tool) -> None:
543-
from agent_framework._clients import _prepare_tools_and_tool_choice
544-
545-
def echo() -> str:
546-
"""Echo the input."""
547-
return "Echo"
548-
549-
dict_function = {
550-
"type": "function",
551-
"function": {
552-
"name": "get_weather",
553-
"description": "Retrieves current weather for the given location.",
554-
"parameters": {
555-
"type": "object",
556-
"properties": {
557-
"location": {"type": "string", "description": "City and country e.g. Bogotá, Colombia"},
558-
"units": {
559-
"type": "string",
560-
"enum": ["celsius", "fahrenheit"],
561-
"description": "Units the temperature will be returned in.",
562-
},
563-
},
564-
"required": ["location", "units"],
565-
"additionalProperties": False,
566-
},
567-
"strict": True,
568-
},
569-
}
570-
571-
options = ChatOptions(tools=[ai_function_tool, ai_tool, echo, dict_function], tool_choice="auto")
572-
assert len(options.tools) == 4
573-
assert options.tools[0] == ai_function_tool
574-
assert options.tools[1] == ai_tool
575-
assert options.tools[2] != echo
576-
assert options.tools[3] == dict_function
577-
# after prepare, the tools should be represented as dicts
578-
# while ai_tools is still the same.
579-
_prepare_tools_and_tool_choice(options)
580-
assert options._ai_tools[0] == ai_function_tool
581-
assert options._ai_tools[1] == ai_tool
582-
assert options._ai_tools[3] == dict_function
583-
assert len(options.tools) == 4
584-
assert options.tools[0]["function"]["name"] == "simple_function"
585-
assert options.tools[1]["function"]["name"] == "generic_tool"
586-
assert options.tools[2]["function"]["name"] == "echo"
587-
assert options.tools[3]["function"]["name"] == "get_weather"
588-
589-
590542
# region Agent Response Fixtures
591543

592544

0 commit comments

Comments
 (0)