Skip to content
Open
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
ddf4b6e
add azure-ai-agentserver-core
lusu-msft Nov 3, 2025
3c8c265
ignore non-exist namespace
lusu-msft Nov 3, 2025
ad1b592
try fix version
lusu-msft Nov 3, 2025
9aef09f
try fix version
lusu-msft Nov 3, 2025
f238c60
try build with updated dependency
lusu-msft Nov 4, 2025
0939aec
fix typo
lusu-msft Nov 4, 2025
31d59b2
try debug build
lusu-msft Nov 4, 2025
b07370f
try debug build
lusu-msft Nov 4, 2025
5059963
try enable build by changing required python version
lusu-msft Nov 4, 2025
1f70e4e
try build with 3.10
lusu-msft Nov 4, 2025
3a120dd
revert ai matrix setting
lusu-msft Nov 4, 2025
94606a6
override azure-ai-agentserver-core required python version to enable …
lusu-msft Nov 4, 2025
253c119
remove unused project files
lusu-msft Nov 4, 2025
5822186
fix init.py
lusu-msft Nov 4, 2025
e897e6f
fix dependency
lusu-msft Nov 4, 2025
070ed65
try fix pylint
lusu-msft Nov 4, 2025
491f5c4
try fix pylint
lusu-msft Nov 4, 2025
5533774
add pytest cases
lusu-msft Nov 4, 2025
7533353
revert eng changes
lusu-msft Nov 4, 2025
be5612d
add azure-ai-agentserver-langgraph package
lusu-msft Nov 4, 2025
92e7443
try fix sphinx
lusu-msft Nov 4, 2025
0303735
Merge branch 'main' into lusu/agentserver-core
lusu-msft Nov 4, 2025
9f516a5
fix format
lusu-msft Nov 4, 2025
e4e0eac
fix dependency check
lusu-msft Nov 4, 2025
d980ecc
fix sphinx build
lusu-msft Nov 4, 2025
c7d1b4c
fix pylint
lusu-msft Nov 5, 2025
15d1b3b
fix proj
lusu-msft Nov 5, 2025
3e9dfb5
update proj
lusu-msft Nov 5, 2025
ec75aed
fix format
lusu-msft Nov 5, 2025
76b0fe3
remove index.md
lusu-msft Nov 5, 2025
cb7e4df
Merge branch 'lusu/agentserver-core' into lusu/agentserver-langgraph
lusu-msft Nov 5, 2025
319f394
fix pylint
lusu-msft Nov 5, 2025
d725470
update pyproject.toml
lusu-msft Nov 5, 2025
e39b638
add manifest
lusu-msft Nov 5, 2025
920d262
fix sphinx
lusu-msft Nov 5, 2025
d302ab2
fix format, synced
lusu-msft Nov 5, 2025
ab8660d
fix mindependency
lusu-msft Nov 5, 2025
5d82677
fix ci.yml
lusu-msft Nov 5, 2025
62fdd71
add azure-ai-agentserver-agentframework
lusu-msft Nov 5, 2025
1ba6441
fix sphinx
lusu-msft Nov 5, 2025
92f2333
fix pylint
lusu-msft Nov 5, 2025
750e958
try fix pywintypes
lusu-msft Nov 5, 2025
9aa86ed
fix format, synced
lusu-msft Nov 5, 2025
fbda85c
add unit test
lusu-msft Nov 5, 2025
58268f6
update test case
lusu-msft Nov 5, 2025
3c5dd4f
Merge branch 'lusu/agentserver-core' into lusu/agentserver-langgraph
lusu-msft Nov 5, 2025
e1260b4
Merge branch 'lusu/agentserver-langgraph' into lusu/agentserver-af
lusu-msft Nov 5, 2025
62d8a22
update requirements
lusu-msft Nov 5, 2025
a12ebc7
remove unused requirement
lusu-msft Nov 5, 2025
d9b97bd
add license
lusu-msft Nov 5, 2025
5e6a254
remove unused requirement
lusu-msft Nov 5, 2025
5117fe7
Merge branch 'lusu/agentserver-core' into lusu/agentserver-langgraph
lusu-msft Nov 5, 2025
be69d49
add license
lusu-msft Nov 5, 2025
32aaa02
Merge branch 'lusu/agentserver-langgraph' into lusu/agentserver-af
lusu-msft Nov 5, 2025
b72112f
add license
lusu-msft Nov 5, 2025
3e8bcf0
try fix build
lusu-msft Nov 6, 2025
5492d7a
try fix build
lusu-msft Nov 6, 2025
a96d3db
add agent id to trace context
lusu-msft Nov 6, 2025
1f32af7
try fix cspell fail
lusu-msft Nov 6, 2025
93369cb
try fix cspell fail
lusu-msft Nov 6, 2025
dee9a5e
Merge branch 'lusu/agentserver-core' into lusu/agentserver-af
lusu-msft Nov 6, 2025
27a11e5
fix build
lusu-msft Nov 6, 2025
128ec2d
fix build
lusu-msft Nov 6, 2025
b78f4f1
fix cspell
lusu-msft Nov 6, 2025
ef8e67b
fix build
lusu-msft Nov 6, 2025
a7c1107
fix cspell
lusu-msft Nov 6, 2025
8bf2290
Merge branch 'lusu/agentserver-core' into lusu/agentserver-af
lusu-msft Nov 6, 2025
a9769c3
fix pyright for langgraph
lusu-msft Nov 6, 2025
9af712f
fix mypy
lusu-msft Nov 6, 2025
2e09ef2
fix mypy
lusu-msft Nov 6, 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
7 changes: 6 additions & 1 deletion eng/tools/azure-sdk-tools/ci_tools/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@
"sdk/textanalytics/azure-ai-textanalytics",
]

TEST_COMPATIBILITY_MAP = {"azure-ai-ml": ">=3.7"}
TEST_COMPATIBILITY_MAP = {
"azure-ai-ml": ">=3.7",
"azure-ai-agentserver-core": ">=3.9", # override to allow build with python 3.9
"azure-ai-agentserver-langgraph": ">=3.9", # override to allow build with python 3.9
"azure-ai-agentserver-agentframework": ">=3.9", # override to allow build with python 3.9
}
TEST_PYTHON_DISTRO_INCOMPATIBILITY_MAP = {
"azure-storage-blob": "pypy",
"azure-storage-queue": "pypy",
Expand Down
7 changes: 7 additions & 0 deletions sdk/ai/azure-ai-agentserver-agentframework/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Release History

## 1.0.0

### Features Added

First version
8 changes: 8 additions & 0 deletions sdk/ai/azure-ai-agentserver-agentframework/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
include *.md
include LICENSE
recursive-include tests *.py
recursive-include samples *.py *.md
recursive-include doc *.rst *.md
include azure/__init__.py
include azure/ai/__init__.py
include azure/ai/agentserver/__init__.py
23 changes: 23 additions & 0 deletions sdk/ai/azure-ai-agentserver-agentframework/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Azure AI Agent Server adapter for agent-framework

## Install

In current folder, run:
```bash
pip install -e .
```

## Usage

```python
# your existing agent
from my_framework_agent import my_awesome_agent

# agent framework utils
from azure.ai.agentserver.agentframework import from_agent_framework

if __name__ == "__main__":
# with this simple line, your agent will be hosted on http://localhost:8088
from_agent_framework(my_awesome_agent).run()

```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
__path__ = __import__("pkgutil").extend_path(__path__, __name__)

from ._version import VERSION


def from_agent_framework(agent):
from .agent_framework import AgentFrameworkCBAgent

return AgentFrameworkCBAgent(agent)


__all__ = ["from_agent_framework"]
__version__ = VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# coding=utf-8
# --------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# Code generated by Microsoft (R) Python Code Generator.
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
# --------------------------------------------------------------------------

VERSION = "1.0.0a1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
# pylint: disable=logging-fstring-interpolation
from __future__ import annotations

import asyncio
import os
from typing import Any, AsyncGenerator, Union

from agent_framework import AgentProtocol
from agent_framework.azure import AzureAIAgentClient
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.models import (
CreateResponse,
Response as OpenAIResponse,
ResponseStreamEvent,
)
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential
from opentelemetry import trace

from .models.agent_framework_input_converters import AgentFrameworkInputConverter
from .models.agent_framework_output_non_streaming_converter import (
AgentFrameworkOutputNonStreamingConverter,
)
from .models.agent_framework_output_streaming_converter import AgentFrameworkOutputStreamingConverter
from .models.constants import Constants

logger = get_logger()


class AgentFrameworkCBAgent(FoundryCBAgent):
"""
Adapter class for integrating Agent Framework agents with the FoundryCB agent interface.

This class wraps an Agent Framework `AgentProtocol` instance and provides a unified interface
for running agents in both streaming and non-streaming modes. It handles input and output
conversion between the Agent Framework and the expected formats for FoundryCB agents.

Parameters:
agent (AgentProtocol): An instance of an Agent Framework agent to be adapted.

Usage:
- Instantiate with an Agent Framework agent.
- Call `agent_run` with a `CreateResponse` request body to execute the agent.
- 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 _resolve_stream_timeout(self, request_body: CreateResponse) -> float:
"""Resolve idle timeout for streaming updates.

Order of precedence:
1) request_body.stream_timeout_s (if provided)
2) env var Constants.AGENTS_ADAPTER_STREAM_TIMEOUT_S
3) Constants.DEFAULT_STREAM_TIMEOUT_S
"""
override = request_body.get("stream_timeout_s", None)
if override is not None:
return float(override)
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)

def init_tracing(self):
exporter = os.environ.get(AdapterConstants.OTEL_EXPORTER_ENDPOINT)
app_insights_conn_str = os.environ.get(AdapterConstants.APPLICATION_INSIGHTS_CONNECTION_STRING)
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"
from agent_framework.observability import setup_observability

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

async def agent_run(
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:
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:
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------

__path__ = __import__("pkgutil").extend_path(__path__, __name__)
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
# pylint: disable=too-many-nested-blocks,too-many-return-statements,too-many-branches
from __future__ import annotations

from typing import Dict, List

from agent_framework import ChatMessage, Role as ChatRole
from agent_framework._types import TextContent
from azure.ai.agentserver.core.logger import get_logger

logger = get_logger()


class AgentFrameworkInputConverter:
"""Normalize inputs for agent.run.

Accepts: str | List | None
Returns: None | str | ChatMessage | list[str] | list[ChatMessage]
"""

def transform_input(
self,
input: str | List[Dict] | None,
) -> str | ChatMessage | list[str] | list[ChatMessage] | None:
logger.debug("Transforming input of type: %s", type(input))

if input is None:
return None

if isinstance(input, str):
return input

try:
if isinstance(input, list):
messages: list[str | ChatMessage] = []

for item in input:
# Case 1: ImplicitUserMessage with content as str or list of ItemContentInputText
if self._is_implicit_user_message(item):
content = item.get("content", None)
if isinstance(content, str):
messages.append(content)
elif isinstance(content, list):
text_parts: list[str] = []
for content_item in content:
text_content = self._extract_input_text(content_item)
if text_content:
text_parts.append(text_content)
if text_parts:
messages.append(" ".join(text_parts))

# Case 2: Explicit message params (user/assistant/system)
elif (
item.get("type") == "message"
and item.get("role") is not None
and item.get("content") is not None
):
role_map = {
"user": ChatRole.USER,
"assistant": ChatRole.ASSISTANT,
"system": ChatRole.SYSTEM,
}
role = role_map.get(item.get("role", "user"), ChatRole.USER)

content_text = ""
item_content = item.get("content", None)
if item_content and isinstance(item_content, list):
text_parts: list[str] = []
for content_item in item_content:
item_text = self._extract_input_text(content_item)
if item_text:
text_parts.append(item_text)
content_text = " ".join(text_parts) if text_parts else ""
elif item_content and isinstance(item_content, str):
content_text = str(item_content)

if content_text:
messages.append(ChatMessage(role=role, text=content_text))

# Determine the most natural return type
if not messages:
return None
if len(messages) == 1:
return messages[0]
if all(isinstance(m, str) for m in messages):
return [m for m in messages if isinstance(m, str)]
if all(isinstance(m, ChatMessage) for m in messages):
return [m for m in messages if isinstance(m, ChatMessage)]

# Mixed content: coerce ChatMessage to str by extracting TextContent parts
result: list[str] = []
for msg in messages:
if isinstance(msg, ChatMessage):
text_parts: list[str] = []
for c in getattr(msg, "contents", []) or []:
if isinstance(c, TextContent):
text_parts.append(c.text)
result.append(" ".join(text_parts) if text_parts else str(msg))
else:
result.append(str(msg))
return result

raise TypeError(f"Unsupported input type: {type(input)}")
except Exception as e:
logger.error("Error processing messages: %s", e, exc_info=True)
raise Exception(f"Error processing messages: {e}") from e # pylint: disable=broad-exception-raised

def _is_implicit_user_message(self, item: Dict) -> bool:
return "content" in item and "role" not in item and "type" not in item

def _extract_input_text(self, content_item: Dict) -> str:
if content_item.get("type") == "input_text" and "text" in content_item:
text_content = content_item.get("text")
if isinstance(text_content, str):
return text_content
return None # type: ignore
Loading
Loading