mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature/python-foundry-hosted-agent-vnext
This commit is contained in:
@@ -49,6 +49,7 @@ class ExperimentalFeature(str, Enum):
|
||||
EVALS = "EVALS"
|
||||
FILE_HISTORY = "FILE_HISTORY"
|
||||
SKILLS = "SKILLS"
|
||||
TOOLBOXES = "TOOLBOXES"
|
||||
|
||||
|
||||
class ReleaseCandidateFeature(str, Enum):
|
||||
|
||||
@@ -12,6 +12,7 @@ from collections.abc import (
|
||||
AsyncIterable,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Iterable,
|
||||
Mapping,
|
||||
Sequence,
|
||||
)
|
||||
@@ -89,6 +90,7 @@ logger = logging.getLogger("agent_framework")
|
||||
DEFAULT_MAX_ITERATIONS: Final[int] = 40
|
||||
DEFAULT_MAX_CONSECUTIVE_ERRORS_PER_REQUEST: Final[int] = 3
|
||||
SHELL_TOOL_KIND_VALUE: Final[str] = "shell"
|
||||
ApprovalMode: TypeAlias = Literal["always_require", "never_require"]
|
||||
ChatClientT = TypeVar("ChatClientT", bound="SupportsChatGetResponse[Any]")
|
||||
ResponseModelBoundT = TypeVar("ResponseModelBoundT", bound=BaseModel)
|
||||
|
||||
@@ -270,7 +272,7 @@ class FunctionTool(SerializationMixin):
|
||||
*,
|
||||
name: str,
|
||||
description: str = "",
|
||||
approval_mode: Literal["always_require", "never_require"] | None = None,
|
||||
approval_mode: ApprovalMode | None = None,
|
||||
kind: str | None = None,
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
@@ -858,6 +860,15 @@ def normalize_tools(
|
||||
Returns:
|
||||
A normalized list where callable inputs are converted to ``FunctionTool``
|
||||
using :func:`tool`, and existing tool objects are passed through unchanged.
|
||||
|
||||
Tool-collection wrappers are flattened in two forms:
|
||||
|
||||
- non-tool, non-callable iterables
|
||||
- mapping-like objects that expose a ``.tools`` collection (for example
|
||||
``ToolboxVersionObject`` from azure-ai-projects)
|
||||
|
||||
This lets callers write ``tools=[toolbox, my_func]`` and have the
|
||||
toolbox's contents spread in alongside individual tools.
|
||||
"""
|
||||
if not tools:
|
||||
return []
|
||||
@@ -882,6 +893,24 @@ def normalize_tools(
|
||||
if callable(tool_item): # type: ignore[reportUnknownArgumentType]
|
||||
normalized.append(tool(tool_item))
|
||||
continue
|
||||
# Mapping-like tool collections (for example ToolboxVersionObject) are
|
||||
# not flattened by the generic Iterable branch below because they are
|
||||
# also Mapping instances. If they expose a ``tools`` collection, spread
|
||||
# that collection into the normalized list.
|
||||
collection_tools = getattr(tool_item, "tools", None) # type: ignore[reportUnknownArgumentType]
|
||||
if isinstance(collection_tools, Iterable) and not isinstance(
|
||||
collection_tools, (str, bytes, bytearray, Mapping)
|
||||
):
|
||||
normalized.extend(normalize_tools(list(collection_tools))) # type: ignore[reportUnknownArgumentType]
|
||||
continue
|
||||
# Tool-collection wrapper (e.g. FoundryToolbox): a non-tool, non-callable
|
||||
# iterable. Flatten its contents so ``tools=[toolbox, my_func]`` works.
|
||||
# Strings, mappings, and Pydantic BaseModel are excluded — BaseModel
|
||||
# instances iterate over (field, value) tuples, not tools, so they
|
||||
# should pass through as leaf tool specs (handled below).
|
||||
if isinstance(tool_item, Iterable) and not isinstance(tool_item, (str, bytes, bytearray, Mapping, BaseModel)):
|
||||
normalized.extend(normalize_tools(list(tool_item))) # type: ignore[reportUnknownArgumentType]
|
||||
continue
|
||||
normalized.append(tool_item) # type: ignore[reportUnknownArgumentType]
|
||||
return normalized
|
||||
|
||||
@@ -1033,7 +1062,7 @@ def tool(
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
schema: type[BaseModel] | Mapping[str, Any] | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | None = None,
|
||||
approval_mode: ApprovalMode | None = None,
|
||||
kind: str | None = None,
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
@@ -1049,7 +1078,7 @@ def tool(
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
schema: type[BaseModel] | Mapping[str, Any] | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | None = None,
|
||||
approval_mode: ApprovalMode | None = None,
|
||||
kind: str | None = None,
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
@@ -1064,7 +1093,7 @@ def tool(
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
schema: type[BaseModel] | Mapping[str, Any] | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | None = None,
|
||||
approval_mode: ApprovalMode | None = None,
|
||||
kind: str | None = None,
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
|
||||
@@ -351,6 +351,8 @@ ContentType = Literal[
|
||||
"image_generation_tool_result",
|
||||
"mcp_server_tool_call",
|
||||
"mcp_server_tool_result",
|
||||
"search_tool_call",
|
||||
"search_tool_result",
|
||||
"shell_tool_call",
|
||||
"shell_tool_result",
|
||||
"shell_command_output",
|
||||
@@ -864,6 +866,56 @@ class Content:
|
||||
raw_representation=raw_representation,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_search_tool_call(
|
||||
cls: type[ContentT],
|
||||
call_id: str,
|
||||
*,
|
||||
tool_name: str,
|
||||
arguments: str | Mapping[str, Any] | None = None,
|
||||
status: str | None = None,
|
||||
annotations: Sequence[Annotation] | None = None,
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
raw_representation: Any = None,
|
||||
) -> ContentT:
|
||||
"""Create search tool call content."""
|
||||
return cls(
|
||||
"search_tool_call",
|
||||
call_id=call_id,
|
||||
tool_name=tool_name,
|
||||
arguments=arguments,
|
||||
status=status,
|
||||
annotations=annotations,
|
||||
additional_properties=additional_properties,
|
||||
raw_representation=raw_representation,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_search_tool_result(
|
||||
cls: type[ContentT],
|
||||
call_id: str,
|
||||
*,
|
||||
tool_name: str,
|
||||
result: Any = None,
|
||||
items: Sequence[Content] | None = None,
|
||||
status: str | None = None,
|
||||
annotations: Sequence[Annotation] | None = None,
|
||||
additional_properties: MutableMapping[str, Any] | None = None,
|
||||
raw_representation: Any = None,
|
||||
) -> ContentT:
|
||||
"""Create search tool result content."""
|
||||
return cls(
|
||||
"search_tool_result",
|
||||
call_id=call_id,
|
||||
tool_name=tool_name,
|
||||
result=result,
|
||||
items=list(items) if items is not None else None,
|
||||
status=status,
|
||||
annotations=annotations,
|
||||
additional_properties=additional_properties,
|
||||
raw_representation=raw_representation,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_usage(
|
||||
cls: type[ContentT],
|
||||
@@ -1478,7 +1530,7 @@ class Content:
|
||||
return span.lower() == top_level_media_type.lower()
|
||||
|
||||
def parse_arguments(self) -> dict[str, Any | None] | None:
|
||||
"""Parse arguments from function_call or mcp_server_tool_call content.
|
||||
"""Parse arguments from function_call, mcp_server_tool_call, or search_tool_call content.
|
||||
|
||||
If arguments cannot be parsed as JSON or the result is not a dict,
|
||||
they are returned as a dictionary with a single key "raw".
|
||||
|
||||
@@ -20,6 +20,7 @@ _IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"FoundryEmbeddingOptions": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"FoundryEmbeddingSettings": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"FoundryEvals": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"FoundryHostedToolType": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"FoundryMemoryProvider": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"FoundryLocalChatOptions": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
|
||||
"FoundryLocalClient": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
|
||||
@@ -31,6 +32,9 @@ _IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"RawFoundryEmbeddingClient": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"evaluate_foundry_target": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"evaluate_traces": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"get_toolbox_tool_name": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"get_toolbox_tool_type": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
"select_toolbox_tools": ("agent_framework_foundry", "agent-framework-foundry"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from agent_framework_foundry import (
|
||||
FoundryEmbeddingOptions,
|
||||
FoundryEmbeddingSettings,
|
||||
FoundryEvals,
|
||||
FoundryHostedToolType,
|
||||
FoundryMemoryProvider,
|
||||
RawFoundryAgent,
|
||||
RawFoundryAgentChatClient,
|
||||
@@ -19,6 +20,9 @@ from agent_framework_foundry import (
|
||||
RawFoundryEmbeddingClient,
|
||||
evaluate_foundry_target,
|
||||
evaluate_traces,
|
||||
get_toolbox_tool_name,
|
||||
get_toolbox_tool_type,
|
||||
select_toolbox_tools,
|
||||
)
|
||||
from agent_framework_foundry_local import (
|
||||
FoundryLocalChatOptions,
|
||||
@@ -35,6 +39,7 @@ __all__ = [
|
||||
"FoundryEmbeddingOptions",
|
||||
"FoundryEmbeddingSettings",
|
||||
"FoundryEvals",
|
||||
"FoundryHostedToolType",
|
||||
"FoundryLocalChatOptions",
|
||||
"FoundryLocalClient",
|
||||
"FoundryLocalSettings",
|
||||
@@ -46,4 +51,7 @@ __all__ = [
|
||||
"RawFoundryEmbeddingClient",
|
||||
"evaluate_foundry_target",
|
||||
"evaluate_traces",
|
||||
"get_toolbox_tool_name",
|
||||
"get_toolbox_tool_type",
|
||||
"select_toolbox_tools",
|
||||
]
|
||||
|
||||
@@ -1144,3 +1144,160 @@ def test_parse_annotation_with_annotated_and_literal():
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region normalize_tools flattening of tool-collection wrappers
|
||||
|
||||
|
||||
def _make_flatten_function_tool(name: str) -> FunctionTool:
|
||||
"""Build a FunctionTool for flattening tests."""
|
||||
|
||||
@tool(name=name, description=f"{name} tool")
|
||||
def _impl(x: int) -> int:
|
||||
return x
|
||||
|
||||
return _impl # type: ignore[return-value]
|
||||
|
||||
|
||||
def test_normalize_tools_flattens_tool_collection_wrapper() -> None:
|
||||
"""A non-tool, non-callable iterable inside the tools list is flattened."""
|
||||
from agent_framework._tools import normalize_tools
|
||||
|
||||
inner_a = _make_flatten_function_tool("inner_a")
|
||||
inner_b = _make_flatten_function_tool("inner_b")
|
||||
|
||||
class ToolBundle:
|
||||
"""Minimal stand-in for a tool-collection wrapper like FoundryToolbox."""
|
||||
|
||||
def __init__(self, tools: list[FunctionTool]) -> None:
|
||||
self._tools = tools
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._tools)
|
||||
|
||||
bundle = ToolBundle([inner_a, inner_b])
|
||||
|
||||
normalized = normalize_tools([bundle])
|
||||
|
||||
assert len(normalized) == 2
|
||||
assert normalized[0] is inner_a
|
||||
assert normalized[1] is inner_b
|
||||
|
||||
|
||||
def test_normalize_tools_combines_bundle_with_individual_tools() -> None:
|
||||
"""The canonical ``tools=[bundle, my_func]`` call site spreads bundle + individual."""
|
||||
from agent_framework._tools import normalize_tools
|
||||
|
||||
bundled = _make_flatten_function_tool("bundled")
|
||||
standalone = _make_flatten_function_tool("standalone")
|
||||
|
||||
class ToolBundle:
|
||||
def __init__(self, tools: list[FunctionTool]) -> None:
|
||||
self._tools = tools
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._tools)
|
||||
|
||||
normalized = normalize_tools([ToolBundle([bundled]), standalone])
|
||||
|
||||
assert len(normalized) == 2
|
||||
assert normalized[0] is bundled
|
||||
assert normalized[1] is standalone
|
||||
|
||||
|
||||
def test_normalize_tools_flattens_nested_bundles() -> None:
|
||||
"""Bundles inside bundles are flattened recursively via the recursive call."""
|
||||
from agent_framework._tools import normalize_tools
|
||||
|
||||
inner = _make_flatten_function_tool("deep")
|
||||
|
||||
class ToolBundle:
|
||||
def __init__(self, tools: list[Any]) -> None:
|
||||
self._tools = tools
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._tools)
|
||||
|
||||
nested = ToolBundle([ToolBundle([inner])])
|
||||
|
||||
normalized = normalize_tools([nested])
|
||||
|
||||
assert len(normalized) == 1
|
||||
assert normalized[0] is inner
|
||||
|
||||
|
||||
def test_normalize_tools_bundle_only_form() -> None:
|
||||
"""Passing a bundle directly (no outer list) also flattens its contents.
|
||||
|
||||
``tools=bundle`` — the outer wrap-in-list happens in the non-Sequence
|
||||
branch, then the flattening logic kicks in on the inner pass.
|
||||
"""
|
||||
from agent_framework._tools import normalize_tools
|
||||
|
||||
a = _make_flatten_function_tool("a")
|
||||
b = _make_flatten_function_tool("b")
|
||||
|
||||
class ToolBundle:
|
||||
def __init__(self, tools: list[FunctionTool]) -> None:
|
||||
self._tools = tools
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._tools)
|
||||
|
||||
normalized = normalize_tools(ToolBundle([a, b])) # type: ignore[arg-type]
|
||||
|
||||
assert len(normalized) == 2
|
||||
assert normalized[0] is a
|
||||
assert normalized[1] is b
|
||||
|
||||
|
||||
def test_normalize_tools_does_not_flatten_known_tool_types() -> None:
|
||||
"""FunctionTool / dict / callable are detected before the flatten branch."""
|
||||
from agent_framework._tools import normalize_tools
|
||||
|
||||
func_tool = _make_flatten_function_tool("ft")
|
||||
dict_tool: dict[str, Any] = {"type": "code_interpreter", "container": {"type": "auto"}}
|
||||
|
||||
def plain_callable(x: int) -> int:
|
||||
return x
|
||||
|
||||
normalized = normalize_tools([func_tool, dict_tool, plain_callable])
|
||||
|
||||
assert len(normalized) == 3
|
||||
assert normalized[0] is func_tool
|
||||
assert normalized[1] is dict_tool
|
||||
# plain_callable was wrapped in a FunctionTool via the @tool helper
|
||||
assert isinstance(normalized[2], FunctionTool)
|
||||
|
||||
|
||||
def test_normalize_tools_flattens_mapping_like_toolbox_with_tools_attr() -> None:
|
||||
"""Mapping-like toolbox objects with ``.tools`` should still flatten."""
|
||||
from collections.abc import Mapping as MappingABC
|
||||
|
||||
from agent_framework._tools import normalize_tools
|
||||
|
||||
bundled = _make_flatten_function_tool("bundled")
|
||||
standalone = _make_flatten_function_tool("standalone")
|
||||
|
||||
class ToolBundleMapping(MappingABC[str, Any]):
|
||||
def __init__(self, tools: list[FunctionTool]) -> None:
|
||||
self.tools = tools
|
||||
self._data = {"name": "research_tools", "version": "v1", "tools": tools}
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self._data[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._data)
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._data)
|
||||
|
||||
normalized = normalize_tools([ToolBundleMapping([bundled]), standalone])
|
||||
|
||||
assert len(normalized) == 2
|
||||
assert normalized[0] is bundled
|
||||
assert normalized[1] is standalone
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
Reference in New Issue
Block a user