Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
48f6871
sync from container_agent repo
lusu-msft Nov 11, 2025
aef1c47
sync error format
lusu-msft Nov 11, 2025
43a9c44
updated version and changelog
lusu-msft Nov 11, 2025
f33ec9f
refined changelog
lusu-msft Nov 11, 2025
18d38c4
fix build
lusu-msft Nov 11, 2025
963b06d
update id generator
lusu-msft Nov 11, 2025
b970326
fix agentframework trace init
lusu-msft Nov 11, 2025
6fb2f03
update version and changelog
lusu-msft Nov 11, 2025
5a01655
fix pylint
lusu-msft Nov 11, 2025
f30779b
pin azure-ai-agents and azure-ai-projects version
lusu-msft Nov 13, 2025
2ea401f
Merge branch 'main' into lusu/agentserver-1110
lusu-msft Nov 13, 2025
2891cab
Feature Agent Server support tools (#43961)
ganeshyb Nov 13, 2025
b7d7bea
update changelog and version
lusu-msft Nov 13, 2025
52a9256
fix cspell
lusu-msft Nov 13, 2025
ba4e1fc
fix pylint and mypy for -core
lusu-msft Nov 13, 2025
661ecb3
fix agents sdk version
lusu-msft Nov 13, 2025
8ba9f1b
pylint fixes (#44010)
ganeshyb Nov 13, 2025
5bbf605
Merge branch 'main' into lusu/agentserver-1110
lusu-msft Nov 13, 2025
07af6de
Merge branch 'lusu/agentserver-1110' of https://github.com/Azure/azur…
lusu-msft Nov 13, 2025
76f935b
Lint and mypy fixes
ganeshyb Nov 14, 2025
ba7ba50
fix mypy and pylint
lusu-msft Nov 14, 2025
cb15c94
fix mypy
lusu-msft Nov 14, 2025
1259358
[ai-agentserver] Fix AF streaming issue (#44068)
JC-386 Nov 17, 2025
a26603e
Refactor Azure AI Tool Client Configuration and Enhance OAuth Consen…
ganeshyb Nov 17, 2025
9cdf214
Fix function output parse
JC-386 Nov 17, 2025
82e5ce9
Merge branch 'jc/agentserver/af-streaming-fix' into lusu/agentserver-…
JC-386 Nov 17, 2025
9b46eb9
Refactor ToolClient to handle optional schema properties and required…
ganeshyb Nov 17, 2025
f50155a
fix mypy error on AF
JC-386 Nov 17, 2025
b0dcb07
do not index AgentRunContext
lusu-msft Nov 17, 2025
b72be77
Filter tools and update project dependenceis
ganeshyb Nov 17, 2025
c9c5307
Merge branch 'lusu/agentserver-1110' of https://github.com/Azure/azur…
ganeshyb Nov 17, 2025
bee67f7
fixing pylint
lusu-msft Nov 17, 2025
8898d8d
Merge branch 'lusu/agentserver-1110' of https://github.com/Azure/azur…
lusu-msft Nov 17, 2025
cba5fdc
fix build
lusu-msft Nov 17, 2025
b5b2086
fix mypy
lusu-msft Nov 17, 2025
7d42f0d
remove DONE when getting error
JC-386 Nov 17, 2025
dfd1bd1
fix pylint
lusu-msft Nov 17, 2025
c18d494
do not add user oid to tracing
lusu-msft Nov 17, 2025
30b78e9
[AgentServer][Agentframework] update agent framework version (#44102)
lusu-msft Nov 19, 2025
1137076
fix dependency
lusu-msft Nov 20, 2025
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
@@ -1,5 +1,46 @@
# Release History

## 1.0.0b5 (2025-11-16)

### Feature Added

- Support Tools Oauth

### Bugs Fixed

- Fixed streaming generation issues.

## 1.0.0b4 (2025-11-13)

### Feature Added

- Adapters support tools

### Bugs Fixed

- Pin azure-ai-projects and azure-ai-agents version to avoid version confliction


## 1.0.0b3 (2025-11-11)

### Bugs Fixed

- Fixed Id generator format.

- Fixed trace initialization for agent-framework.


## 1.0.0b2 (2025-11-10)

### Bugs Fixed

- Fixed Id generator format.

- Improved stream mode error message.

- Updated application insights related configuration environment variables.


## 1.0.0b1 (2025-11-07)

### Features Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
# ---------------------------------------------------------
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

from typing import TYPE_CHECKING, Optional, Any

from .agent_framework import AgentFrameworkCBAgent
from .tool_client import ToolClient
from ._version import VERSION

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


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

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


__all__ = ["from_agent_framework"]
__all__ = ["from_agent_framework", "ToolClient"]
__version__ = VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
# --------------------------------------------------------------------------

VERSION = "1.0.0b1"
VERSION = "1.0.0b5"
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
# pylint: disable=logging-fstring-interpolation
# pylint: disable=logging-fstring-interpolation,no-name-in-module,no-member
from __future__ import annotations

import asyncio # pylint: disable=do-not-import-asyncio
import os
from typing import Any, AsyncGenerator, Union
from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Optional, Protocol, Union, List
import inspect

from agent_framework import AgentProtocol
from agent_framework.azure import AzureAIAgentClient # pylint: disable=no-name-in-module
from agent_framework import AgentProtocol, AIFunction
from agent_framework.azure import AzureAIClient # pylint: disable=no-name-in-module
from opentelemetry import trace

from azure.ai.agentserver.core.client.tools import OAuthConsentRequiredError
from azure.ai.agentserver.core import AgentRunContext, FoundryCBAgent
from azure.ai.agentserver.core.constants import Constants as AdapterConstants
from azure.ai.agentserver.core.logger import get_logger
from azure.ai.agentserver.core.logger import APPINSIGHT_CONNSTR_ENV_NAME, get_logger
from azure.ai.agentserver.core.models import (
CreateResponse,
Response as OpenAIResponse,
Expand All @@ -29,10 +30,32 @@
)
from .models.agent_framework_output_streaming_converter import AgentFrameworkOutputStreamingConverter
from .models.constants import Constants
from .tool_client import ToolClient

if TYPE_CHECKING:
from azure.core.credentials_async import AsyncTokenCredential

logger = get_logger()


class AgentFactory(Protocol):
"""Protocol for agent factory functions.

An agent factory is a callable that takes a ToolClient and returns
an AgentProtocol, either synchronously or asynchronously.
"""

def __call__(self, tools: List[AIFunction]) -> Union[AgentProtocol, Awaitable[AgentProtocol]]:
"""Create an AgentProtocol using the provided ToolClient.

:param tools: The list of AIFunction tools available to the agent.
:type tools: List[AIFunction]
:return: An Agent Framework agent, or an awaitable that resolves to one.
:rtype: Union[AgentProtocol, Awaitable[AgentProtocol]]
"""
...


class AgentFrameworkCBAgent(FoundryCBAgent):
"""
Adapter class for integrating Agent Framework agents with the FoundryCB agent interface.
Expand All @@ -50,10 +73,35 @@ class AgentFrameworkCBAgent(FoundryCBAgent):
- Supports both streaming and non-streaming responses based on the `stream` flag.
"""

def __init__(self, agent: AgentProtocol):
super().__init__()
self.agent = agent
logger.info(f"Initialized AgentFrameworkCBAgent with agent: {type(agent).__name__}")
def __init__(self, agent: Union[AgentProtocol, AgentFactory],
credentials: "Optional[AsyncTokenCredential]" = None,
**kwargs: Any):
"""Initialize the AgentFrameworkCBAgent with an AgentProtocol or a factory function.

:param agent: The Agent Framework agent to adapt, or a callable that takes ToolClient
and returns AgentProtocol (sync or async).
:type agent: Union[AgentProtocol, AgentFactory]
:param credentials: Azure credentials for authentication.
:type credentials: Optional[AsyncTokenCredential]
"""
super().__init__(credentials=credentials, **kwargs) # pylint: disable=unexpected-keyword-arg
self._agent_or_factory: Union[AgentProtocol, AgentFactory] = agent
self._resolved_agent: "Optional[AgentProtocol]" = None
# If agent is already instantiated, use it directly
if isinstance(agent, AgentProtocol):
self._resolved_agent = agent
logger.info(f"Initialized AgentFrameworkCBAgent with agent: {type(agent).__name__}")
else:
logger.info("Initialized AgentFrameworkCBAgent with agent factory")

@property
def agent(self) -> "Optional[AgentProtocol]":
"""Get the resolved agent. This property provides backward compatibility.

:return: The resolved AgentProtocol if available, None otherwise.
:rtype: Optional[AgentProtocol]
"""
return self._resolved_agent

def _resolve_stream_timeout(self, request_body: CreateResponse) -> float:
"""Resolve idle timeout for streaming updates.
Expand All @@ -75,79 +123,158 @@ def _resolve_stream_timeout(self, request_body: CreateResponse) -> float:
env_val = os.getenv(Constants.AGENTS_ADAPTER_STREAM_TIMEOUT_S)
return float(env_val) if env_val is not None else float(Constants.DEFAULT_STREAM_TIMEOUT_S)

async def _resolve_agent(self, context: AgentRunContext):
"""Resolve the agent if it's a factory function (for single-use/first-time resolution).
Creates a ToolClient and calls the factory function with it.
This is used for the initial resolution.

:param context: The agent run context containing tools and user information.
:type context: AgentRunContext
"""
if callable(self._agent_or_factory):
logger.debug("Resolving agent from factory function")

# Create ToolClient with credentials
tool_client = self.get_tool_client(tools=context.get_tools(), user_info=context.get_user_info()) # pylint: disable=no-member
tool_client_wrapper = ToolClient(tool_client)
tools = await tool_client_wrapper.list_tools()

result = self._agent_or_factory(tools)
if inspect.iscoroutine(result):
self._resolved_agent = await result
else:
self._resolved_agent = result

logger.debug("Agent resolved successfully")
else:
# Should not reach here, but just in case
self._resolved_agent = self._agent_or_factory

async def _resolve_agent_for_request(self, context: AgentRunContext):

logger.debug("Resolving fresh agent from factory function for request")

# Create ToolClient with credentials
tool_client = self.get_tool_client(tools=context.get_tools(), user_info=context.get_user_info()) # pylint: disable=no-member
tool_client_wrapper = ToolClient(tool_client)
tools = await tool_client_wrapper.list_tools()

result = self._agent_or_factory(tools)
if inspect.iscoroutine(result):
agent = await result
else:
agent = result

logger.debug("Fresh agent resolved successfully for request")
return agent, tool_client_wrapper

def init_tracing(self):
exporter = os.environ.get(AdapterConstants.OTEL_EXPORTER_ENDPOINT)
app_insights_conn_str = os.environ.get(AdapterConstants.APPLICATION_INSIGHTS_CONNECTION_STRING)
app_insights_conn_str = os.environ.get(APPINSIGHT_CONNSTR_ENV_NAME)
project_endpoint = os.environ.get(AdapterConstants.AZURE_AI_PROJECT_ENDPOINT)

if project_endpoint:
project_client = AIProjectClient(endpoint=project_endpoint, credential=DefaultAzureCredential())
agent_client = AzureAIAgentClient(project_client=project_client)
agent_client.setup_azure_ai_observability()
elif exporter or app_insights_conn_str:
os.environ["WORKFLOW_ENABLE_OTEL"] = "true"
if exporter or app_insights_conn_str:
from agent_framework.observability import setup_observability

setup_observability(
enable_sensitive_data=True,
otlp_endpoint=exporter,
applicationinsights_connection_string=app_insights_conn_str,
)
elif project_endpoint:
self.setup_tracing_with_azure_ai_client(project_endpoint)
self.tracer = trace.get_tracer(__name__)

async def agent_run(
def setup_tracing_with_azure_ai_client(self, project_endpoint: str):
async def setup_async():
async with AzureAIClient(
project_endpoint=project_endpoint, async_credential=self.credentials
) as agent_client:
await agent_client.setup_azure_ai_observability()

import asyncio

loop = asyncio.get_event_loop()
if loop.is_running():
# If loop is already running, schedule as a task
asyncio.create_task(setup_async())
else:
# Run in new event loop
loop.run_until_complete(setup_async())

async def agent_run( # pylint: disable=too-many-statements
self, context: AgentRunContext
) -> Union[
OpenAIResponse,
AsyncGenerator[ResponseStreamEvent, Any],
]:
logger.info(f"Starting agent_run with stream={context.stream}")
request_input = context.request.get("input")

input_converter = AgentFrameworkInputConverter()
message = input_converter.transform_input(request_input)
logger.debug(f"Transformed input message type: {type(message)}")

# Use split converters
if context.stream:
logger.info("Running agent in streaming mode")
streaming_converter = AgentFrameworkOutputStreamingConverter(context)

async def stream_updates():
update_count = 0
timeout_s = self._resolve_stream_timeout(context.request)
logger.info("Starting streaming with idle-timeout=%.2fs", timeout_s)
for ev in streaming_converter.initial_events():
yield ev

# Iterate with per-update timeout; terminate if idle too long
aiter = self.agent.run_stream(message).__aiter__()
while True:
# Resolve agent - always resolve if it's a factory function to get fresh agent each time
# For factories, get a new agent instance per request to avoid concurrency issues
tool_client = None
try:
if callable(self._agent_or_factory):
agent, tool_client = await self._resolve_agent_for_request(context)
elif self._resolved_agent is None:
await self._resolve_agent(context)
agent = self._resolved_agent
else:
agent = self._resolved_agent

logger.info(f"Starting agent_run with stream={context.stream}")
request_input = context.request.get("input")

input_converter = AgentFrameworkInputConverter()
message = input_converter.transform_input(request_input)
logger.debug(f"Transformed input message type: {type(message)}")

# Use split converters
if context.stream:
logger.info("Running agent in streaming mode")
streaming_converter = AgentFrameworkOutputStreamingConverter(context)

async def stream_updates():
try:
update = await asyncio.wait_for(aiter.__anext__(), timeout=timeout_s)
except StopAsyncIteration:
logger.debug("Agent streaming iterator finished (StopAsyncIteration)")
break
except asyncio.TimeoutError:
logger.warning("Streaming idle timeout reached (%.1fs); terminating stream.", timeout_s)
for ev in streaming_converter.completion_events():
yield ev
return
update_count += 1
transformed = streaming_converter.transform_output_for_streaming(update)
for event in transformed:
update_count = 0
updates = agent.run_stream(message)
async for event in streaming_converter.convert(updates):
update_count += 1
yield event

logger.info("Streaming completed with %d updates", update_count)
finally:
# Close tool_client if it was created for this request
if tool_client is not None:
try:
await tool_client.close()
logger.debug("Closed tool_client after streaming completed")
except Exception as ex: # pylint: disable=broad-exception-caught
logger.warning(f"Error closing tool_client in stream: {ex}")

return stream_updates()

# Non-streaming path
logger.info("Running agent in non-streaming mode")
non_streaming_converter = AgentFrameworkOutputNonStreamingConverter(context)
result = await agent.run(message)
logger.debug(f"Agent run completed, result type: {type(result)}")
transformed_result = non_streaming_converter.transform_output_for_response(result)
logger.info("Agent run and transformation completed successfully")
return transformed_result
except OAuthConsentRequiredError as e:
logger.info("OAuth consent required during agent run")
if context.stream:
# Yield OAuth consent response events
# Capture e in the closure by passing it as a default argument
async def oauth_consent_stream(error=e):
async for event in self.respond_with_oauth_consent_astream(context, error):
yield event
for ev in streaming_converter.completion_events():
yield ev
logger.info("Streaming completed with %d updates", update_count)

return stream_updates()

# Non-streaming path
logger.info("Running agent in non-streaming mode")
non_streaming_converter = AgentFrameworkOutputNonStreamingConverter(context)
result = await self.agent.run(message)
logger.debug(f"Agent run completed, result type: {type(result)}")
transformed_result = non_streaming_converter.transform_output_for_response(result)
logger.info("Agent run and transformation completed successfully")
return transformed_result
return oauth_consent_stream()
return await self.respond_with_oauth_consent(context, e)
finally:
# Close tool_client if it was created for this request (non-streaming only, streaming handles in generator)
if not context.stream and tool_client is not None:
try:
await tool_client.close()
logger.debug("Closed tool_client after request processing")
except Exception as ex: # pylint: disable=broad-exception-caught
logger.warning(f"Error closing tool_client: {ex}")
Loading
Loading