Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,8 @@ def usage(self) -> ResponseUsage | None:
return ResponseUsage(
input_tokens=input_tokens,
input_tokens_details=ResponseUsageInputTokensDetails(
cached_tokens=int(self._usage_details.get("cache_read_input_token_count") or 0)
cached_tokens=int(self._usage_details.get("cache_read_input_token_count") or 0),
cache_write_tokens=int(self._usage_details.get("cache_creation_input_token_count") or 0),
),
output_tokens=output_tokens,
output_tokens_details=ResponseUsageOutputTokensDetails(
Expand Down Expand Up @@ -1418,9 +1419,12 @@ async def _item_to_message(item: Item, *, approval_storage: FunctionApprovalStor

if item["type"] == "function_call_output":
output = item["output"] if isinstance(item["output"], str) else str(item["output"])
call_id = item.get("call_id")
if call_id is None:
raise ValueError("Function call output item is missing a call_id.")
return Message(
role="tool",
contents=[Content.from_function_result(item["call_id"], result=output)],
contents=[Content.from_function_result(call_id, result=output)],
Comment thread
TaoChenOSU marked this conversation as resolved.
)

if item["type"] == "reasoning":
Expand Down Expand Up @@ -1681,9 +1685,12 @@ async def _output_item_to_message(

if item["type"] == "function_call_output":
output = item["output"] if isinstance(item["output"], str) else str(item["output"])
call_id = item.get("call_id")
if call_id is None:
raise ValueError("Function call output item is missing a call_id.")
return Message(
role="tool",
contents=[Content.from_function_result(item["call_id"], result=output)],
contents=[Content.from_function_result(call_id, result=output)],
)

if item["type"] == "reasoning":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import inspect
import logging
import os
from collections.abc import Mapping, Sequence
from contextlib import _AsyncGeneratorContextManager # pyright: ignore[reportPrivateUsage]
from typing import TYPE_CHECKING, Any
from urllib.parse import urlsplit
Expand Down Expand Up @@ -180,7 +181,9 @@ def __init__(
token_scope: str = DEFAULT_TOOLBOX_SCOPE,
load_prompts: bool = False,
load_tools: bool = True,
additional_tool_argument_names: Sequence[str] | Mapping[str, Sequence[str]] | None = None,
timeout: float = _DEFAULT_TIMEOUT,
**kwargs: Any,
) -> None:
"""Initialize a Foundry toolbox tool.

Expand All @@ -200,7 +203,11 @@ def __init__(
load_prompts: Whether to load prompts from the toolbox. Defaults to ``False``
because toolboxes expose tools.
load_tools: Whether to load tools from the toolbox. Defaults to ``True``.
additional_tool_argument_names: Extra argument names to forward in addition to
parameters declared by toolbox functions. A sequence applies globally; a
mapping configures names per remote function, with ``"*"`` as the global key.
timeout: Request timeout in seconds for the underlying HTTP client.
kwargs: Additional options forwarded to :class:`~agent_framework.MCPStreamableHTTPTool`.
"""
endpoint = url or _resolve_toolbox_endpoint()
tool_name = name or os.environ.get("TOOLBOX_NAME") or _toolbox_name_from_endpoint(endpoint)
Expand All @@ -219,6 +226,8 @@ def __init__(
http_client=http_client,
load_prompts=load_prompts,
load_tools=load_tools,
additional_tool_argument_names=additional_tool_argument_names,
**kwargs,
)

@override
Expand Down
6 changes: 3 additions & 3 deletions python/packages/foundry_hosting/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.15.0,<2",
"azure-ai-agentserver-core>=2.1.0b1,<3",
"azure-ai-agentserver-responses>=2.1.0b1,<3",
"azure-ai-agentserver-invocations>=1.1.0b1,<2",
"azure-ai-agentserver-core>=2.1.0,<3",
"azure-ai-agentserver-responses>=2.2.0b1,<3",
"azure-ai-agentserver-invocations>=1.1.0,<2",
"httpx>=0.28,<1",
"mcp>=1.24.0,<2",
]
Expand Down
20 changes: 18 additions & 2 deletions python/packages/foundry_hosting/tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,7 @@ async def test_usage_is_aggregated_in_completed_response(self, caplog: pytest.Lo
"output_token_count": 2,
"total_token_count": 12,
"cache_read_input_token_count": 3,
"cache_creation_input_token_count": 4,
"reasoning_output_token_count": 1,
})
],
Expand All @@ -1373,6 +1374,7 @@ async def test_usage_is_aggregated_in_completed_response(self, caplog: pytest.Lo
"output_token_count": 4,
"total_token_count": 9,
"cache_read_input_token_count": 2,
"cache_creation_input_token_count": 1,
"reasoning_output_token_count": 2,
})
],
Expand All @@ -1393,7 +1395,7 @@ async def test_usage_is_aggregated_in_completed_response(self, caplog: pytest.Lo
completed = events[-1]["data"]["response"]
assert completed["usage"] == {
"input_tokens": 15,
"input_tokens_details": {"cached_tokens": 5},
"input_tokens_details": {"cached_tokens": 5, "cache_write_tokens": 5},
"output_tokens": 6,
"output_tokens_details": {"reasoning_tokens": 3},
"total_tokens": 21,
Expand Down Expand Up @@ -1840,6 +1842,13 @@ async def test_function_call_output(self) -> None:
assert msg.contents[0].call_id == "call_1"
assert msg.contents[0].result == "sunny"

async def test_function_call_output_without_call_id_raises(self) -> None:
from azure.ai.agentserver.responses.models import FunctionCallOutputItemParam

item = FunctionCallOutputItemParam({"type": "function_call_output", "output": "sunny"})
with pytest.raises(ValueError, match="missing a call_id"):
await _output_item_to_message(item) # type: ignore[arg-type] # ty: ignore[invalid-argument-type]

async def test_reasoning(self) -> None:
from azure.ai.agentserver.responses.models import OutputItemReasoningItem, SummaryTextContent

Expand Down Expand Up @@ -2344,6 +2353,13 @@ async def test_function_call_output_non_string(self) -> None:
assert msg.role == "tool"
assert msg.contents[0].result == "42"

async def test_function_call_output_without_call_id_raises(self) -> None:
from azure.ai.agentserver.responses.models import FunctionCallOutputItemParam

item = FunctionCallOutputItemParam({"type": "function_call_output", "output": "sunny"})
with pytest.raises(ValueError, match="missing a call_id"):
await _item_to_message(item)

async def test_reasoning_with_summary(self) -> None:
from azure.ai.agentserver.responses.models import ItemReasoningItem, SummaryTextContent

Expand Down Expand Up @@ -4390,7 +4406,7 @@ def run_failure(*args: Any, **kwargs: Any) -> ResponseStream[AgentResponseUpdate
failed_response = events[-1]["data"]["response"]
assert failed_response["usage"] == {
"input_tokens": 8,
"input_tokens_details": {"cached_tokens": 2},
"input_tokens_details": {"cached_tokens": 2, "cache_write_tokens": 0},
"output_tokens": 3,
"output_tokens_details": {"reasoning_tokens": 1},
"total_tokens": 11,
Expand Down
13 changes: 13 additions & 0 deletions python/packages/foundry_hosting/tests/test_toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ def test_init_derives_name_and_defaults() -> None:
assert toolbox.load_prompts_flag is False


def test_init_forwards_additional_tool_arguments_and_parent_kwargs() -> None:
toolbox = FoundryToolbox(
_FakeCredential(), # type: ignore
url="https://h/toolboxes/sales/mcp?api-version=v1",
description="Sales tools",
additional_tool_argument_names={"*": ["tenant_id"], "search": ["thread"]},
)

assert toolbox.description == "Sales tools"
assert toolbox._global_extra_arg_names == {"tenant_id"}
assert toolbox._tool_extra_arg_names == {"search": {"thread"}}


def test_toolbox_owns_feature_index_53() -> None:
assert FeatureIndex.FOUNDRY_TOOLBOX == 53

Expand Down
Loading
Loading