Merge branch 'main' into fix/mcp-allowed-tools-empty-list-handling

This commit is contained in:
Giles Odigwe
2026-06-03 09:48:19 -07:00
committed by GitHub
Unverified
11 changed files with 981 additions and 25 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ agent_framework/
- **`AgentMiddleware`** - Intercepts agent `run()` calls
- **`ChatMiddleware`** - Intercepts chat client `get_response()` calls
- **`FunctionMiddleware`** - Intercepts function/tool invocations
- **`AgentContext`** / **`ChatContext`** / **`FunctionInvocationContext`** - Context objects passed through middleware
- **`AgentContext`** / **`ChatContext`** / **`FunctionInvocationContext`** - Context objects passed through middleware. A tool can declare a `FunctionInvocationContext` parameter to receive it; `context.tools` is the live, mutable tools list for the run, and `context.add_tools(...)` / `context.remove_tools(...)` enable progressive tool exposure (changes apply on the next function-calling iteration).
### Sessions (`_sessions.py`)
@@ -58,6 +58,7 @@ class ExperimentalFeature(str, Enum):
FOUNDRY_PREVIEW_TOOLS = "FOUNDRY_PREVIEW_TOOLS"
FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
HARNESS = "HARNESS"
PROGRESSIVE_TOOLS = "PROGRESSIVE_TOOLS"
SKILLS = "SKILLS"
TO_PROMPT_AGENT = "TO_PROMPT_AGENT"
@@ -11,6 +11,7 @@ from enum import Enum
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, cast, overload
from ._clients import SupportsChatGetResponse
from ._feature_stage import ExperimentalFeature, experimental
from ._types import (
AgentResponse,
AgentResponseUpdate,
@@ -214,6 +215,12 @@ class FunctionInvocationContext:
result: Function execution result. Can be observed after calling ``call_next()``
to see the actual execution result or can be set to override the execution result.
kwargs: Additional runtime keyword arguments forwarded to the function invocation.
tools: The live, mutable list of tools available to the model for the current
agent run, or ``None`` when the function is invoked outside of a
function-calling loop (for example via ``FunctionTool.invoke`` directly).
Tools can add or remove tools during execution using :meth:`add_tools`
and :meth:`remove_tools` (progressive tool exposure). Mutations take
effect on the **next** model iteration, not the in-flight batch.
Examples:
.. code-block:: python
@@ -232,6 +239,18 @@ class FunctionInvocationContext:
# Continue execution
await call_next()
Progressive tool exposure from inside a tool:
.. code-block:: python
from agent_framework import FunctionInvocationContext, tool
@tool(approval_mode="never_require")
def load_math_tools(ctx: FunctionInvocationContext) -> str:
ctx.add_tools([factorial, fibonacci])
return "Math tools are now available."
"""
def __init__(
@@ -242,6 +261,7 @@ class FunctionInvocationContext:
metadata: Mapping[str, Any] | None = None,
result: Any = None,
kwargs: Mapping[str, Any] | None = None,
tools: list[ToolTypes] | None = None,
) -> None:
"""Initialize the FunctionInvocationContext.
@@ -252,6 +272,9 @@ class FunctionInvocationContext:
metadata: Metadata dictionary for sharing data between function middleware.
result: Function execution result.
kwargs: Additional runtime keyword arguments forwarded to the function invocation.
tools: The live, mutable list of tools for the current agent run. When provided,
this is the same list object the model sees on the next iteration, so
appending or removing tools changes the model's available tools.
"""
self.function = function
self.arguments = arguments
@@ -259,6 +282,96 @@ class FunctionInvocationContext:
self.metadata: dict[str, Any] = dict(metadata) if metadata is not None else {}
self.result = result
self.kwargs: dict[str, Any] = dict(kwargs) if kwargs is not None else {}
self.tools = tools
@experimental(feature_id=ExperimentalFeature.PROGRESSIVE_TOOLS)
def add_tools(
self,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]],
) -> None:
"""Add one or more tools to the current agent run (progressive tool exposure).
Callable inputs are converted to :class:`FunctionTool`, and tool collections are
flattened, using the same normalization as the rest of the framework. Added tools
become available to the model on the **next** iteration of the function-calling
loop; they do not affect tool calls already requested in the in-flight batch.
Adding a tool whose name already exists is a no-op when it is the same object, and
raises ``ValueError`` when it is a different object with a duplicate name.
Args:
tools: A single tool/callable or a sequence of tools/callables to add.
Raises:
RuntimeError: If the context has no live tools list (for example when the
function is invoked outside of a function-calling loop).
ValueError: If a different tool with a duplicate name is added.
"""
from ._tools import _append_unique_tools, normalize_tools # type: ignore[reportPrivateUsage]
if self.tools is None:
raise RuntimeError(
"Cannot add tools: this FunctionInvocationContext is not bound to a live "
"agent run. add_tools is only available for functions invoked within an "
"agent's function-calling loop."
)
# Validate the whole batch against a throwaway copy first, so a duplicate-name
# clash partway through the batch raises before the live tool list is mutated
# (all-or-nothing semantics).
merged = _append_unique_tools(list(self.tools), normalize_tools(tools))
self.tools[:] = merged
@experimental(feature_id=ExperimentalFeature.PROGRESSIVE_TOOLS)
def remove_tools(
self,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | str | Sequence[str],
) -> None:
"""Remove one or more tools from the current agent run (progressive tool exposure).
Tools may be specified by name, by tool object, or by the original callable. Names
that are not currently present are ignored. Removals take effect on the **next**
iteration of the function-calling loop; tool calls already requested in the
in-flight batch still execute.
Args:
tools: A tool name, tool/callable, or a sequence of any of these to remove.
Raises:
RuntimeError: If the context has no live tools list (for example when the
function is invoked outside of a function-calling loop).
"""
from ._tools import _get_tool_name, normalize_tools # type: ignore[reportPrivateUsage]
if self.tools is None:
raise RuntimeError(
"Cannot remove tools: this FunctionInvocationContext is not bound to a live "
"agent run. remove_tools is only available for functions invoked within an "
"agent's function-calling loop."
)
names_to_remove: set[str] = set()
raw_items: list[Any]
if isinstance(tools, str):
raw_items = [tools]
elif isinstance(tools, Sequence) and not isinstance(tools, (bytes, bytearray)):
raw_items = list(cast("Sequence[Any]", tools))
else:
raw_items = [tools]
for item in raw_items:
if isinstance(item, str):
names_to_remove.add(item)
continue
for normalized in normalize_tools(item):
if name := _get_tool_name(normalized): # type: ignore[reportPrivateUsage]
names_to_remove.add(name)
if not names_to_remove:
return
self.tools[:] = [
tool
for tool in self.tools
if _get_tool_name(tool) not in names_to_remove # type: ignore[reportPrivateUsage]
]
class ChatContext:
@@ -1418,6 +1418,7 @@ async def _auto_invoke_function(
sequence_index: int | None = None,
request_index: int | None = None,
middleware_pipeline: FunctionMiddlewarePipeline | None = None,
live_tools: list[ToolTypes] | None = None,
) -> Content:
"""Invoke a function call requested by the agent, applying middleware that is defined.
@@ -1432,6 +1433,8 @@ async def _auto_invoke_function(
sequence_index: The index of the function call in the sequence.
request_index: The index of the request iteration.
middleware_pipeline: Optional middleware pipeline to apply during execution.
live_tools: The live, mutable tools list for the current agent run, exposed on
the FunctionInvocationContext so tools can add/remove tools at runtime.
Returns:
The function result content.
@@ -1523,6 +1526,7 @@ async def _auto_invoke_function(
arguments=args,
session=invocation_session,
kwargs=runtime_kwargs.copy(),
tools=live_tools,
)
function_result = await tool.invoke(
arguments=args,
@@ -1537,6 +1541,10 @@ async def _auto_invoke_function(
except UserInputRequiredException:
raise
except Exception as exc:
logger.warning(
f"Function '{tool.name}' raised an exception; returning an error result to the "
f"model. Set include_detailed_errors=True for the full detail. Exception: {exc!r}"
)
message = "Error: Function failed."
if config.get("include_detailed_errors", False):
message = f"{message} Exception: {exc}"
@@ -1552,6 +1560,7 @@ async def _auto_invoke_function(
arguments=args,
session=invocation_session,
kwargs=runtime_kwargs.copy(),
tools=live_tools,
)
call_id = function_call_content.call_id
@@ -1608,6 +1617,10 @@ async def _auto_invoke_function(
except UserInputRequiredException:
raise
except Exception as exc:
logger.warning(
f"Function '{tool.name}' raised an exception; returning an error result to the "
f"model. Set include_detailed_errors=True for the full detail. Exception: {exc!r}"
)
message = "Error: Function failed."
if config.get("include_detailed_errors", False):
message = f"{message} Exception: {exc}"
@@ -1659,6 +1672,9 @@ async def _try_execute_function_calls(
from ._types import Content
tool_map = _get_tool_map(tools)
# The live tools list (when tools is the run-local list) is exposed on the
# FunctionInvocationContext so tools can add/remove tools during the run.
live_tools: list[ToolTypes] | None = cast("list[ToolTypes]", tools) if isinstance(tools, list) else None
approval_tools = [tool_name for tool_name, tool in tool_map.items() if tool.approval_mode == "always_require"]
logger.debug(
"_try_execute_function_calls: tool_map keys=%s, approval_tools=%s",
@@ -1733,6 +1749,7 @@ async def _try_execute_function_calls(
request_index=attempt_idx,
middleware_pipeline=middleware_pipeline,
config=config,
live_tools=live_tools,
)
return (result, False)
except MiddlewareTermination as exc:
@@ -2371,6 +2388,13 @@ class FunctionInvocationLayer(Generic[OptionsCoT]):
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=filtered_kwargs,
)
# Establish a single, run-local mutable tools list so that tools can add or remove
# tools during the run (progressive tool exposure). A fresh list is created via
# normalize_tools so the caller's original tools container is never mutated, while
# the same list object is shared with the model (options["tools"]) and the tool map
# rebuilt on every loop iteration.
if mutable_options.get("tools"):
mutable_options["tools"] = normalize_tools(mutable_options["tools"])
if not stream:
async def _get_response() -> ChatResponse[Any]:
@@ -3975,3 +3975,425 @@ async def test_user_input_request_empty_contents_returns_fallback(chat_client_ba
]
assert len(function_results) >= 1
assert any("user input" in (fr.result or "").lower() for fr in function_results)
# region Progressive tool exposure (FunctionInvocationContext.add_tools / remove_tools)
def _pte_function_call_response(call_id: str, name: str, arguments: str = "{}") -> ChatResponse:
return ChatResponse(
messages=Message(
role="assistant",
contents=[Content.from_function_call(call_id=call_id, name=name, arguments=arguments)],
)
)
def _pte_text_response(text: str = "done") -> ChatResponse:
return ChatResponse(messages=Message(role="assistant", contents=[text]))
@tool(name="factorial", approval_mode="never_require")
def _pte_factorial(n: int) -> int:
"""Compute the factorial of n."""
result = 1
for value in range(2, n + 1):
result *= value
return result
async def test_context_exposes_live_tools(chat_client_base: SupportsChatGetResponse):
from agent_framework import FunctionTool
seen_names: list[str] = []
@tool(name="inspect_tools", approval_mode="never_require")
def inspect_tools(ctx: FunctionInvocationContext) -> str:
assert ctx.tools is not None
seen_names.extend(t.name for t in ctx.tools if isinstance(t, FunctionTool))
return "inspected"
chat_client_base.run_responses = [
_pte_function_call_response("1", "inspect_tools"),
_pte_text_response(),
]
await chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
options={"tool_choice": "auto", "tools": [inspect_tools]},
)
assert "inspect_tools" in seen_names
async def test_add_tools_available_next_iteration(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
@tool(name="factorial", approval_mode="never_require")
def factorial(n: int) -> int:
nonlocal exec_counter
exec_counter += 1
return 120
@tool(name="load_math", approval_mode="never_require")
def load_math(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(factorial)
return "math tools loaded"
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "load_math"),
_pte_function_call_response("2", "factorial", '{"n": 5}'),
_pte_text_response(),
]
response = await chat_client_base.get_response(
[Message(role="user", contents=["compute 5!"])],
options={"tool_choice": "auto", "tools": [load_math]},
)
assert exec_counter == 1
assert response.messages[-1].text == "done"
async def test_add_tools_model_sees_added_tools_in_options(chat_client_base: SupportsChatGetResponse):
from agent_framework import FunctionTool
recorded: list[list[str]] = []
client_cls = type(chat_client_base)
original = client_cls._get_non_streaming_response
async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse:
tools = options.get("tools") or []
recorded.append([t.name for t in tools if isinstance(t, FunctionTool)])
return await original(self, messages=messages, options=options, **kwargs)
@tool(name="load_math", approval_mode="never_require")
def load_math(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(_pte_factorial)
return "loaded"
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "load_math"),
_pte_function_call_response("2", "factorial", '{"n": 5}'),
_pte_text_response(),
]
monkey = pytest.MonkeyPatch()
monkey.setattr(client_cls, "_get_non_streaming_response", recording)
try:
await chat_client_base.get_response(
[Message(role="user", contents=["compute 5!"])],
options={"tool_choice": "auto", "tools": [load_math]},
)
finally:
monkey.undo()
assert recorded[0] == ["load_math"]
assert "factorial" in recorded[1]
async def test_remove_tools_next_iteration(chat_client_base: SupportsChatGetResponse):
from agent_framework import FunctionTool
recorded: list[list[str]] = []
client_cls = type(chat_client_base)
original = client_cls._get_non_streaming_response
async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse:
tools = options.get("tools") or []
recorded.append([t.name for t in tools if isinstance(t, FunctionTool)])
return await original(self, messages=messages, options=options, **kwargs)
@tool(name="get_weather", approval_mode="never_require")
def get_weather(location: str) -> str:
return "sunny"
@tool(name="drop_weather", approval_mode="never_require")
def drop_weather(ctx: FunctionInvocationContext) -> str:
ctx.remove_tools("get_weather")
return "removed"
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "drop_weather"),
_pte_text_response(),
]
monkey = pytest.MonkeyPatch()
monkey.setattr(client_cls, "_get_non_streaming_response", recording)
try:
await chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
options={"tool_choice": "auto", "tools": [get_weather, drop_weather]},
)
finally:
monkey.undo()
assert set(recorded[0]) == {"get_weather", "drop_weather"}
assert "get_weather" not in recorded[1]
async def test_add_tools_does_not_mutate_caller_tools_list(chat_client_base: SupportsChatGetResponse):
@tool(name="load_math", approval_mode="never_require")
def load_math(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(_pte_factorial)
return "loaded"
original_tools: list[Any] = [load_math]
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "load_math"),
_pte_text_response(),
]
await chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
options={"tool_choice": "auto", "tools": original_tools},
)
assert original_tools == [load_math]
async def test_add_tools_persists_across_iterations(chat_client_base: SupportsChatGetResponse):
from agent_framework import FunctionTool
recorded: list[list[str]] = []
client_cls = type(chat_client_base)
original = client_cls._get_non_streaming_response
async def recording(self: Any, *, messages: Any, options: dict[str, Any], **kwargs: Any) -> ChatResponse:
tools = options.get("tools") or []
recorded.append([t.name for t in tools if isinstance(t, FunctionTool)])
return await original(self, messages=messages, options=options, **kwargs)
@tool(name="load_math", approval_mode="never_require")
def load_math(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(_pte_factorial)
return "loaded"
chat_client_base.function_invocation_configuration["max_iterations"] = 4 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "load_math"),
_pte_function_call_response("2", "factorial", '{"n": 5}'),
_pte_function_call_response("3", "factorial", '{"n": 3}'),
_pte_text_response(),
]
monkey = pytest.MonkeyPatch()
monkey.setattr(client_cls, "_get_non_streaming_response", recording)
try:
await chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
options={"tool_choice": "auto", "tools": [load_math]},
)
finally:
monkey.undo()
assert "factorial" in recorded[1]
assert "factorial" in recorded[2]
async def test_add_tools_through_function_middleware(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
class PassthroughMiddleware(FunctionMiddleware):
async def process(self, context: FunctionInvocationContext, call_next: Any) -> None:
await call_next()
@tool(name="factorial", approval_mode="never_require")
def factorial(n: int) -> int:
nonlocal exec_counter
exec_counter += 1
return 120
@tool(name="load_math", approval_mode="never_require")
def load_math(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(factorial)
return "loaded"
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "load_math"),
_pte_function_call_response("2", "factorial", '{"n": 5}'),
_pte_text_response(),
]
await chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
options={"tool_choice": "auto", "tools": [load_math]},
middleware=[PassthroughMiddleware()],
)
assert exec_counter == 1
async def test_add_tools_with_approval_required_tool(chat_client_base: SupportsChatGetResponse):
@tool(name="secure_tool", approval_mode="always_require")
def secure_tool(value: str) -> str:
return f"secure: {value}"
@tool(name="load_secure", approval_mode="never_require")
def load_secure(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(secure_tool)
return "loaded"
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "load_secure"),
_pte_function_call_response("2", "secure_tool", '{"value": "x"}'),
_pte_text_response(),
]
response = await chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
options={"tool_choice": "auto", "tools": [load_secure]},
)
assert any(item.type == "function_approval_request" for msg in response.messages for item in msg.contents)
async def test_add_tools_accepts_plain_callable(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
def plain_factorial(n: int) -> int:
"""Compute factorial."""
nonlocal exec_counter
exec_counter += 1
return 120
@tool(name="load_math", approval_mode="never_require")
def load_math(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(plain_factorial)
return "loaded"
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.run_responses = [
_pte_function_call_response("1", "load_math"),
_pte_function_call_response("2", "plain_factorial", '{"n": 5}'),
_pte_text_response(),
]
await chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
options={"tool_choice": "auto", "tools": [load_math]},
)
assert exec_counter == 1
async def test_add_tools_streaming(chat_client_base: SupportsChatGetResponse):
exec_counter = 0
@tool(name="factorial", approval_mode="never_require")
def factorial(n: int) -> int:
nonlocal exec_counter
exec_counter += 1
return 120
@tool(name="load_math", approval_mode="never_require")
def load_math(ctx: FunctionInvocationContext) -> str:
ctx.add_tools(factorial)
return "loaded"
chat_client_base.function_invocation_configuration["max_iterations"] = 3 # type: ignore[attr-defined]
chat_client_base.streaming_responses = [
[
ChatResponseUpdate(
contents=[Content.from_function_call(call_id="1", name="load_math", arguments="{}")],
role="assistant",
)
],
[
ChatResponseUpdate(
contents=[Content.from_function_call(call_id="2", name="factorial", arguments='{"n": 5}')],
role="assistant",
)
],
[ChatResponseUpdate(contents=[Content.from_text("done")], role="assistant", finish_reason="stop")],
]
async for _ in chat_client_base.get_response(
[Message(role="user", contents=["hi"])],
stream=True,
options={"tool_choice": "auto", "tools": [load_math]},
):
pass
assert exec_counter == 1
def test_add_tools_duplicate_same_object_is_noop():
@tool(name="dup", approval_mode="never_require")
def dup(x: int) -> int:
return x
ctx = FunctionInvocationContext(function=dup, arguments={}, tools=[dup])
ctx.add_tools(dup)
assert ctx.tools is not None
assert len(ctx.tools) == 1
def test_add_tools_duplicate_name_different_object_raises():
@tool(name="dup", approval_mode="never_require")
def dup_a(x: int) -> int:
return x
@tool(name="dup", approval_mode="never_require")
def dup_b(x: int) -> int:
return x
ctx = FunctionInvocationContext(function=dup_a, arguments={}, tools=[dup_a])
with pytest.raises(ValueError):
ctx.add_tools(dup_b)
def test_add_tools_batch_with_duplicate_is_atomic():
"""A duplicate-name clash partway through a batch must leave the live list unchanged."""
@tool(name="existing", approval_mode="never_require")
def existing(x: int) -> int:
return x
@tool(name="fresh", approval_mode="never_require")
def fresh(x: int) -> int:
return x
@tool(name="existing", approval_mode="never_require")
def clashing(x: int) -> int:
return x
ctx = FunctionInvocationContext(function=existing, arguments={}, tools=[existing])
with pytest.raises(ValueError):
ctx.add_tools([fresh, clashing])
assert ctx.tools is not None
# The valid "fresh" tool must not have been committed before the clash raised.
assert ctx.tools == [existing]
def test_remove_tools_by_name_and_object():
@tool(name="a", approval_mode="never_require")
def a(x: int) -> int:
return x
@tool(name="b", approval_mode="never_require")
def b(x: int) -> int:
return x
ctx = FunctionInvocationContext(function=a, arguments={}, tools=[a, b])
ctx.remove_tools("a")
assert ctx.tools is not None
assert [t.name for t in ctx.tools] == ["b"]
ctx.remove_tools(b)
assert ctx.tools == []
def test_remove_tools_unknown_name_is_noop():
@tool(name="a", approval_mode="never_require")
def a(x: int) -> int:
return x
ctx = FunctionInvocationContext(function=a, arguments={}, tools=[a])
ctx.remove_tools("nonexistent")
assert ctx.tools is not None
assert [t.name for t in ctx.tools] == ["a"]
def test_progressive_tools_helpers_raise_without_live_tools():
@tool(name="a", approval_mode="never_require")
def a(x: int) -> int:
return x
ctx = FunctionInvocationContext(function=a, arguments={})
assert ctx.tools is None
with pytest.raises(RuntimeError):
ctx.add_tools(a)
with pytest.raises(RuntimeError):
ctx.remove_tools("a")
# endregion
+75
View File
@@ -0,0 +1,75 @@
# Tools
Samples that show how to define, configure, and control function tools for an
agent — from basic declarations to approvals, invocation limits, session
injection, and dynamic (progressive) tool exposure.
## Function tools
| File | Demonstrates |
|------|--------------|
| [`function_tool_with_explicit_schema.py`](function_tool_with_explicit_schema.py) | Defining a tool with an explicit JSON schema. |
| [`function_tool_declaration_only.py`](function_tool_declaration_only.py) | A declaration-only tool (schema without a local implementation). |
| [`function_tool_with_kwargs.py`](function_tool_with_kwargs.py) | Passing extra keyword arguments into a tool. |
| [`function_tool_from_dict_with_dependency_injection.py`](function_tool_from_dict_with_dependency_injection.py) | Dependency injection into a tool defined from a dict. |
| [`function_tool_with_session_injection.py`](function_tool_with_session_injection.py) | Injecting the session into a tool. |
| [`tool_in_class.py`](tool_in_class.py) | Using a method on a class as a tool. |
| [`agent_as_tool_with_session_propagation.py`](agent_as_tool_with_session_propagation.py) | Exposing an agent as a tool with session propagation. |
## Approvals & invocation control
| File | Demonstrates |
|------|--------------|
| [`function_tool_with_approval.py`](function_tool_with_approval.py) | Requiring human approval before a tool runs. |
| [`function_tool_with_approval_and_sessions.py`](function_tool_with_approval_and_sessions.py) | Tool approvals combined with sessions. |
| [`function_invocation_configuration.py`](function_invocation_configuration.py) | Configuring function-invocation settings (e.g. max iterations). |
| [`control_total_tool_executions.py`](control_total_tool_executions.py) | All the ways to cap how many times tools run. |
| [`function_tool_with_max_invocations.py`](function_tool_with_max_invocations.py) | Limiting the number of invocations per tool. |
| [`function_tool_with_max_exceptions.py`](function_tool_with_max_exceptions.py) | Limiting the number of exceptions a tool may raise. |
| [`function_tool_recover_from_failures.py`](function_tool_recover_from_failures.py) | Returning errors so the agent can recover from tool failures. |
## Progressive tool exposure (dynamic loading)
| File | Demonstrates |
|------|--------------|
| [`dynamic_tool_exposure.py`](dynamic_tool_exposure.py) | A "loader" tool that adds more tools at runtime via `FunctionInvocationContext`. |
Frontloading a model with hundreds of tools hurts tool-selection accuracy,
bloats context, and raises cost. Instead, start with a small set of loader
tools and let the model pull in more on demand. Inside a tool, the injected
`ctx: FunctionInvocationContext` exposes a live `ctx.tools` list plus
`ctx.add_tools(...)` / `ctx.remove_tools(...)` helpers. Tools added or removed
take effect on the **next iteration** of the function-calling loop.
> [!NOTE]
> Progressive tool exposure applies to the standard function-calling loop. It
> does **not** apply to CodeAct providers (`agent-framework-monty`,
> `agent-framework-hyperlight`). In CodeAct the model only sees a single
> `execute_code` tool, and host tools are exposed *inside the sandbox* as typed
> Python functions rather than as model tool-schemas. Host tools there are
> invoked without a `FunctionInvocationContext`, so `ctx.add_tools()` is not
> available; the helpers fail fast with a clear `RuntimeError` instead of
> silently doing nothing. To change a CodeAct agent's tool set, use the
> provider's own `add_tools` / `remove_tool` / `clear_tools` methods (applied
> between runs). The recommended provider-driven path for Monty and Hyperlight
> is shown in [`../context_providers/code_act/`](../context_providers/code_act/)
> ([`code_act.py`](../context_providers/code_act/code_act.py) for Hyperlight,
> [`monty_code_act.py`](../context_providers/code_act/monty_code_act.py) for
> Monty).
## Local shell & code interpreters
| Path | Demonstrates |
|------|--------------|
| [`local_shell_with_allowlist.py`](local_shell_with_allowlist.py) | `LocalShellTool` restricted by a strict command allow-list. |
| [`local_shell_with_environment_provider.py`](local_shell_with_environment_provider.py) | `LocalShellTool` wired with a `ShellEnvironmentProvider`. |
| [`local_code_interpreter/`](local_code_interpreter/) | Hyperlight-backed sandboxed code interpreter (standalone tool — *extra* pattern). |
| [`monty_code_interpreter/`](monty_code_interpreter/) | Monty-backed sandboxed code interpreter (standalone tool — *extra* pattern). |
> [!TIP]
> The `local_code_interpreter/` and `monty_code_interpreter/` samples show the
> standalone-tool wiring and are provided as *extra* reference. For most
> Monty/Hyperlight use cases the **recommended** path is the provider-driven
> CodeAct setup in
> [`../context_providers/code_act/`](../context_providers/code_act/), which adds
> dynamic tool / capability management.
@@ -0,0 +1,79 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from typing import Annotated
from agent_framework import Agent, FunctionInvocationContext, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
"""
Dynamic Tool Exposure (Progressive Tool Loading) Example
This example demonstrates "progressive tool exposure": a tool that adds more tools to
the agent at runtime, in the same run, via ``FunctionInvocationContext``.
Frontloading a model with hundreds of tools hurts tool-selection accuracy, bloats
context, and raises cost. Instead, you can start with a small set of "loader" tools and
let the model pull in additional tools on demand. Tools added with ``ctx.add_tools(...)``
(or removed with ``ctx.remove_tools(...)``) become available to the model on the next
iteration of the function-calling loop.
"""
# These math tools are not registered on the agent up front. They are added on demand by
# the ``load_math_tools`` tool below, and only then become callable by the model.
@tool(approval_mode="never_require")
def factorial(n: Annotated[int, Field(description="A non-negative integer.")]) -> str:
"""Compute the factorial of n."""
if n < 0:
return "Error: n must be a non-negative integer."
result = 1
for value in range(2, n + 1):
result *= value
return f"{n}! = {result}"
@tool(approval_mode="never_require")
def fibonacci(n: Annotated[int, Field(description="The 0-based index in the Fibonacci sequence.")]) -> str:
"""Compute the n-th Fibonacci number."""
if n < 0:
return "Error: n must be a non-negative integer."
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return f"fib({n}) = {a}"
# The only tool the agent starts with. When called, it exposes the math tools above so the
# model can use them on the next turn. Note the ``ctx`` parameter is injected by the
# framework and is not visible to the model.
@tool(approval_mode="never_require")
def load_math_tools(ctx: FunctionInvocationContext) -> str:
"""Load additional math tools (factorial, fibonacci) so they can be used."""
ctx.add_tools([factorial, fibonacci])
return "Loaded math tools: factorial, fibonacci. You can now call them."
async def main() -> None:
agent = Agent(
client=OpenAIChatClient(),
name="MathAgent",
instructions=(
"You are a math assistant. If you need math capabilities that are not yet "
"available, call load_math_tools first, then use the newly available tools."
),
tools=[load_math_tools],
)
# The agent starts with only ``load_math_tools``. To answer the question it must first
# load the math tools, then call ``factorial`` on the next iteration.
print(f"Agent: {await agent.run('What is 5 factorial?')}")
if __name__ == "__main__":
asyncio.run(main())
+102 -9
View File
@@ -562,7 +562,7 @@ requires-dist = [
{ name = "agent-framework-core", editable = "packages/core" },
{ name = "azure-ai-agentserver-core", specifier = ">=2.0.0b3,<3" },
{ name = "azure-ai-agentserver-invocations", specifier = ">=1.0.0b3,<2" },
{ name = "azure-ai-agentserver-responses", specifier = ">=1.0.0b5,<2" },
{ name = "azure-ai-agentserver-responses", specifier = ">=1.0.0b7,<2" },
]
[[package]]
@@ -1171,19 +1171,18 @@ wheels = [
[[package]]
name = "azure-ai-agentserver-core"
version = "2.0.0b3"
version = "2.0.0b5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "hypercorn", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "microsoft-opentelemetry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-exporter-otlp-proto-grpc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "starlette", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/29/1a9606d5252b02d77070a1b633dd0c26fe65a0f4a0fb0cfdaa751e2ed458/azure_ai_agentserver_core-2.0.0b3.tar.gz", hash = "sha256:e295b19a65d53c513929f52f0862bbb815cc9e9fc29d2a2825452f3136260123", size = 42573, upload-time = "2026-04-23T04:13:16.717Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/06/7c88b6506d26ee625a967cef762e6a155ed7ab8812f3f1e45ec1a950b8ae/azure_ai_agentserver_core-2.0.0b5.tar.gz", hash = "sha256:f03dc737351e5d847e9fc18c5b78b261436de368f1317a0c29957cc2179c37d1", size = 46273, upload-time = "2026-05-25T12:48:01.739Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/9b/1fc87c05b55821f33c46c5e8a3b97a573aa2fc4bff387e75cca1a87800b4/azure_ai_agentserver_core-2.0.0b3-py3-none-any.whl", hash = "sha256:5ef921eb9fd9c0f15682fe930320fae50dccfa915d7518f9a16d99014bbcb3cb", size = 29127, upload-time = "2026-04-23T04:13:17.976Z" },
{ url = "https://files.pythonhosted.org/packages/68/80/a43a269601512793b220c36dc0864b44d806b969dbfe14f1ecc3b5f5202b/azure_ai_agentserver_core-2.0.0b5-py3-none-any.whl", hash = "sha256:0d00c298892e2ff466b32235d5d9c55b57054f0e8fcedb0726eacd7684e1aa89", size = 31521, upload-time = "2026-05-25T12:48:03.072Z" },
]
[[package]]
@@ -1200,7 +1199,7 @@ wheels = [
[[package]]
name = "azure-ai-agentserver-responses"
version = "1.0.0b5"
version = "1.0.0b7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -1208,9 +1207,9 @@ dependencies = [
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e6/27/3ecb7fe704ff8764199bfbe4cc1e584a520a9affe042470d9d50b6e1e73a/azure_ai_agentserver_responses-1.0.0b5.tar.gz", hash = "sha256:0b627b810359c792ea7b6fa6782abaf6df32d9bc9e5a569ad722afcffd0ce8d9", size = 410908, upload-time = "2026-04-23T04:31:15.414Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/53/febb6f3453f5dc1e0b6dc47d4e5198b64605d1f83c847255946f74bc300e/azure_ai_agentserver_responses-1.0.0b7.tar.gz", hash = "sha256:2f67cdfc0219cb0ab86800dadb1cfdb40ab4aa0413dae7ffa5ea4ea84eec3eb0", size = 419032, upload-time = "2026-05-25T12:48:38.81Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/91/1e5c0d7ce95ca8b022e69e4ca6b23e413fc2d57f0191429c4633e02213d2/azure_ai_agentserver_responses-1.0.0b5-py3-none-any.whl", hash = "sha256:4c2a6ab56e71eeb330aa52b7cb2cc71b8ec6b5bbe0e7dc84310f2c7fbda393a3", size = 268362, upload-time = "2026-04-23T04:31:17.014Z" },
{ url = "https://files.pythonhosted.org/packages/b3/94/48825357e009f7db3b6b5d0a9344a7ab3304e32f06f50328b2393e3b06cb/azure_ai_agentserver_responses-1.0.0b7-py3-none-any.whl", hash = "sha256:efb5271f24a297bacde9769359308e54e870f66ad4d3b4826ae97a77e40e94d4", size = 268063, upload-time = "2026-05-25T12:48:40.817Z" },
]
[[package]]
@@ -3887,6 +3886,41 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/1b/543ddaa2daf8593911a02a07a6a78366d4a6a0053ec86a557c19fa97b60e/microsoft_agents_hosting_core-0.3.1-py3-none-any.whl", hash = "sha256:a4b41556b15321b74f539c5a0a89f70955459b7ec57e9e4b24e61bba27f1cbbc", size = 94573, upload-time = "2025-09-09T23:19:53.855Z" },
]
[[package]]
name = "microsoft-opentelemetry"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "azure-core-tracing-opentelemetry", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "azure-monitor-opentelemetry-exporter", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-exporter-otlp-proto-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-fastapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-flask", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-logging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-openai-agents-v2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-openai-v2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-psycopg2", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-urllib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation-urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-resource-detector-azure", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-util-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pyjwt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/cf/74885d07d38e225b84b63a8a2720de846e518fe4c7e89457f4c150a9c7d5/microsoft_opentelemetry-1.3.2.tar.gz", hash = "sha256:d36f31731740170624b53f370358a9700f503bb4f9bd25c7f81c0c88c66f511c", size = 178031, upload-time = "2026-05-29T22:05:53.442Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/8d/6960be61c8fe236fef730b0cae1d97a1898f62355b2d6679ef46abe1e4be/microsoft_opentelemetry-1.3.2-py3-none-any.whl", hash = "sha256:65292474ce7efee115f671457188e92edc4a8d432fad163e49e504155be66ae5", size = 198419, upload-time = "2026-05-29T22:05:54.849Z" },
]
[[package]]
name = "mistralai"
version = "2.4.2"
@@ -4633,6 +4667,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/41/619f3530324a58491f2d20f216a10dd7393629b29db4610dda642a27f4ed/opentelemetry_instrumentation_flask-0.61b0-py3-none-any.whl", hash = "sha256:e8ce474d7ce543bfbbb3e93f8a6f8263348af9d7b45502f387420cf3afa71253", size = 15996, upload-time = "2026-03-04T14:19:31.304Z" },
]
[[package]]
name = "opentelemetry-instrumentation-httpx"
version = "0.61b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/2a/e2becd55e33c29d1d9ef76e2579040ed1951cb33bacba259f6aff2fdd2a6/opentelemetry_instrumentation_httpx-0.61b0.tar.gz", hash = "sha256:6569ec097946c5551c2a4252f74c98666addd1bf047c1dde6b4ef426719ff8dd", size = 24104, upload-time = "2026-03-04T14:20:34.752Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/88/dde310dce56e2d85cf1a09507f5888544955309edc4b8d22971d6d3d1417/opentelemetry_instrumentation_httpx-0.61b0-py3-none-any.whl", hash = "sha256:dee05c93a6593a5dc3ae5d9d5c01df8b4e2c5d02e49275e5558534ee46343d5e", size = 17198, upload-time = "2026-03-04T14:19:33.585Z" },
]
[[package]]
name = "opentelemetry-instrumentation-logging"
version = "0.61b0"
@@ -4646,6 +4696,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/0e/2137db5239cc5e564495549a4d11488a7af9b48fc76520a0eea20e69ddae/opentelemetry_instrumentation_logging-0.61b0-py3-none-any.whl", hash = "sha256:6d87e5ded6a0128d775d41511f8380910a1b610671081d16efb05ac3711c0074", size = 17076, upload-time = "2026-03-04T14:19:36.765Z" },
]
[[package]]
name = "opentelemetry-instrumentation-openai-agents-v2"
version = "0.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-util-genai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/15/b6a303454d2800d772cdebc490c1d598d06d0e541619db80195eb9ea85c6/opentelemetry_instrumentation_openai_agents_v2-0.1.0.tar.gz", hash = "sha256:1033f4b261ce07f65d197ac0e9c499302c805eae987a6cc4e7f99bb279363477", size = 22423, upload-time = "2025-10-15T19:04:59.912Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/0a/b6f47734e1d7f936cbc52ef8e673d3e08d9c3c8a13d9549c03f978758076/opentelemetry_instrumentation_openai_agents_v2-0.1.0-py3-none-any.whl", hash = "sha256:e4e3dfba32bd6eeee0624eca9be54341ab7cc4f7a3bb895354f2f9d6f7afe2f3", size = 25002, upload-time = "2025-10-15T19:04:58.562Z" },
]
[[package]]
name = "opentelemetry-instrumentation-openai-v2"
version = "2.3b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/4e/21f8cd16ccb471dd217ed85eb817796a10c4f2718ae2c91e752a57180cf0/opentelemetry_instrumentation_openai_v2-2.3b0.tar.gz", hash = "sha256:5de9d70cc9536eea1fe48ea016e0c5f25735fa9a13709076a64b20657fadb6ba", size = 170838, upload-time = "2025-12-24T13:20:58.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/02/7ff0a9282520592772a356dd39d1559f3726610ccc3854a2f598b756c66f/opentelemetry_instrumentation_openai_v2-2.3b0-py3-none-any.whl", hash = "sha256:c6aca87be0da0289ea1d8167fea4b0f227ea5ef0e90496e2822121e47340d36a", size = 18053, upload-time = "2025-12-24T13:20:57.233Z" },
]
[[package]]
name = "opentelemetry-instrumentation-psycopg2"
version = "0.61b0"
@@ -4772,6 +4851,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" },
]
[[package]]
name = "opentelemetry-util-genai"
version = "0.3b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/d8/4dd2fb622d26ec45b10ef63eb87fd512f5d7467c7bd35ce390629bd6dff8/opentelemetry_util_genai-0.3b0.tar.gz", hash = "sha256:83e127789a9ad615b8ca65f05fc36955a67ce257b06142bfd46159a3b7ed73d3", size = 31800, upload-time = "2026-02-20T16:16:14.807Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/e5/fada54909e445d7b4007f8b96221d571999efeab9446f3127cc1cebe5e07/opentelemetry_util_genai-0.3b0-py3-none-any.whl", hash = "sha256:ebc2b01bcb891ddc7218452470d189d3321cd742653299ff8e7de45debcfb986", size = 28426, upload-time = "2026-02-20T16:16:12.027Z" },
]
[[package]]
name = "opentelemetry-util-http"
version = "0.61b0"