Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e000eeb
foundry hosted mcp ops checkpoint
JC-386 Dec 31, 2025
1d0db86
refine hosted mcp tools; complete refactoring of invoking connected t…
JC-386 Jan 3, 2026
eafe8cc
Refine FoundryToolClient
JC-386 Jan 3, 2026
7ac2d25
move FoundryToolClientConfiguration
JC-386 Jan 3, 2026
e102134
high-level tool api
JC-386 Jan 6, 2026
530773f
high-level api
JC-386 Jan 7, 2026
f7189e8
user provider & starlette hook
JC-386 Jan 7, 2026
cbca5af
impl default cached catalog
JC-386 Jan 8, 2026
66c7510
fix comments
JC-386 Jan 8, 2026
c494b8e
concurrency-safe caching for cached catalog
JC-386 Jan 8, 2026
dbd6ff1
concurrency-safe caching for cached catalog
JC-386 Jan 8, 2026
4a37db5
default runtime
JC-386 Jan 9, 2026
100d1e4
remove MetadataMapper
JC-386 Jan 9, 2026
fc09d16
remove MetadataMapper
JC-386 Jan 9, 2026
8de025a
init global AgentServerContext
JC-386 Jan 10, 2026
e21061c
refine core.tools impl
JC-386 Jan 15, 2026
95676c4
langgraph tool support
JC-386 Jan 16, 2026
e1da813
optimize import
JC-386 Jan 16, 2026
4167786
create top-level context for langgraph run
JC-386 Jan 16, 2026
f158295
refine foundry tools support for agent framework (#44652)
melionel Jan 17, 2026
ea0879e
fix happy path of react agent + hosted mcp tool
JC-386 Jan 17, 2026
98a7174
optimize cache: cache value after cached task is done. Make FoundryTo…
JC-386 Jan 18, 2026
7402092
optimize cache: reduce loop
JC-386 Jan 18, 2026
c91d802
simplify cache code
JC-386 Jan 18, 2026
b5768ff
fix key issue of cache
JC-386 Jan 18, 2026
25418e7
Merge branch 'lusu/agentserver-1110' into jc/refactor-tools-api
JC-386 Jan 19, 2026
ee2e66e
resolve conflict
JC-386 Jan 19, 2026
a1bf632
Merge branch 'lusu/agentserver-1110' into jc/refactor-tools-api
JC-386 Jan 19, 2026
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 @@ -3,22 +3,30 @@
# ---------------------------------------------------------
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

from typing import TYPE_CHECKING, Optional, Any
from typing import TYPE_CHECKING, Any, Optional

from .agent_framework import AgentFrameworkCBAgent
from .tool_client import ToolClient
from ._version import VERSION
from azure.ai.agentserver.agentframework._version import VERSION
from azure.ai.agentserver.agentframework._agent_framework import AgentFrameworkCBAgent
from azure.ai.agentserver.agentframework._foundry_tools import FoundryToolsChatMiddleware
from azure.ai.agentserver.core.application import PackageMetadata, set_current_app

if TYPE_CHECKING: # pragma: no cover
from azure.core.credentials_async import AsyncTokenCredential


def from_agent_framework(agent,
credentials: Optional["AsyncTokenCredential"] = None,
**kwargs: Any) -> "AgentFrameworkCBAgent":
def from_agent_framework(
agent,
credentials: Optional["AsyncTokenCredential"] = None,
**kwargs: Any,
) -> "AgentFrameworkCBAgent":

return AgentFrameworkCBAgent(agent, credentials=credentials, **kwargs)


__all__ = ["from_agent_framework", "ToolClient"]
__all__ = [
"from_agent_framework",
"FoundryToolsChatMiddleware",
]
__version__ = VERSION

set_current_app(PackageMetadata.from_dist("azure-ai-agentserver-agentframework"))

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
from __future__ import annotations

import inspect
from typing import Any, Awaitable, Callable, Dict, List, Optional, Sequence

from agent_framework import AIFunction, ChatContext, ChatOptions, ChatMiddleware
from pydantic import Field, create_model

from azure.ai.agentserver.core import AgentServerContext
from azure.ai.agentserver.core.logger import get_logger
from azure.ai.agentserver.core.tools import FoundryToolLike, ResolvedFoundryTool

logger = get_logger()


def _attach_signature_from_pydantic_model(func, input_model) -> None:
params = []
annotations: Dict[str, Any] = {}

for name, field in input_model.model_fields.items():
ann = field.annotation or Any
annotations[name] = ann

default = inspect._empty if field.is_required() else field.default
params.append(
inspect.Parameter(
name=name,
kind=inspect.Parameter.KEYWORD_ONLY,
default=default,
annotation=ann,
)
)

func.__signature__ = inspect.Signature(parameters=params, return_annotation=Any)
func.__annotations__ = {**annotations, "return": Any}

class FoundryToolClient:

def __init__(
self,
tools: Sequence[FoundryToolLike],
) -> None:
self._allowed_tools: List[FoundryToolLike] = list(tools)

async def list_tools(self) -> List[AIFunction]:
server_context = AgentServerContext.get()
foundry_tool_catalog = server_context.tools.catalog
resolved_tools = await foundry_tool_catalog.list(self._allowed_tools)
return [self._to_aifunction(tool) for tool in resolved_tools]

def _to_aifunction(self, foundry_tool: "ResolvedFoundryTool") -> AIFunction:
"""Convert an FoundryTool to an Agent Framework AI Function

:param foundry_tool: The FoundryTool to convert.
:type foundry_tool: ~azure.ai.agentserver.core.client.tools.aio.FoundryTool
:return: An AI Function Tool.
:rtype: AIFunction
"""
# Get the input schema from the tool descriptor
input_schema = foundry_tool.input_schema or {}

# Create a Pydantic model from the input schema
properties = input_schema.properties or {}
required_fields = set(input_schema.required or [])

# Build field definitions for the Pydantic model
field_definitions: Dict[str, Any] = {}
for field_name, field_info in properties.items():
field_type = self._json_schema_type_to_python(field_info.type or "string")
field_description = field_info.description or ""
is_required = field_name in required_fields

if is_required:
field_definitions[field_name] = (field_type, Field(description=field_description))
else:
field_definitions[field_name] = (Optional[field_type],
Field(default=None, description=field_description))

# Create the Pydantic model dynamically
input_model = create_model(
f"{foundry_tool.name}_input",
**field_definitions
)

# Create a wrapper function that calls the Azure tool
async def tool_func(**kwargs: Any) -> Any:
"""Dynamically generated function to invoke the Azure AI tool.

:return: The result from the tool invocation.
:rtype: Any
"""
server_context = AgentServerContext.get()
logger.debug("Invoking tool: %s with input: %s", foundry_tool.name, kwargs)
return await server_context.tools.invoke(foundry_tool, kwargs)
_attach_signature_from_pydantic_model(tool_func, input_model)

# Create and return the AIFunction
return AIFunction(
name=foundry_tool.name,
description=foundry_tool.description or "No description available",
func=tool_func,
input_model=input_model
)

def _json_schema_type_to_python(self, json_type: str) -> type:
"""Convert JSON schema type to Python type.

:param json_type: The JSON schema type string.
:type json_type: str
:return: The corresponding Python type.
:rtype: type
"""
type_map = {
"string": str,
"number": float,
"integer": int,
"boolean": bool,
"array": list,
"object": dict,
}
return type_map.get(json_type, str)


class FoundryToolsChatMiddleware(ChatMiddleware):
"""Chat middleware to inject Foundry tools into ChatOptions on each call."""

def __init__(
self,
tools: Sequence[FoundryToolLike]) -> None:
self._foundry_tool_client = FoundryToolClient(tools=tools)

async def process(
self,
context: ChatContext,
next: Callable[[ChatContext], Awaitable[None]],
) -> None:
tools = await self._foundry_tool_client.list_tools()
base_chat_options = context.chat_options
if not base_chat_options:
logger.debug("No existing ChatOptions found, creating new one with Foundry tools.")
base_chat_options = ChatOptions(tools=tools)
context.chat_options = base_chat_options
else:
logger.debug("Adding Foundry tools to existing ChatOptions.")
base_tools = base_chat_options.tools or []
context.chat_options.tools = base_tools + tools
await next(context)

This file was deleted.

Loading