diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/agent_framework.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/agent_framework.py index 7177b522d2a9..5671dcd2b8a1 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/agent_framework.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/agent_framework.py @@ -4,31 +4,25 @@ # pylint: disable=logging-fstring-interpolation from __future__ import annotations -import asyncio # pylint: disable=do-not-import-asyncio import os from typing import Any, AsyncGenerator, Union -from agent_framework import AgentProtocol -from agent_framework.azure import AzureAIAgentClient # pylint: disable=no-name-in-module +from agent_framework import SupportsAgentRun from opentelemetry import trace 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 .models.agent_framework_input_converters import AgentFrameworkInputConverter +from .models.agent_framework_input_converters import transform_input 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() @@ -37,12 +31,12 @@ 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 + This class wraps an Agent Framework `SupportsAgentRun` 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. + agent (SupportsAgentRun): An instance of an Agent Framework agent to be adapted. Usage: - Instantiate with an Agent Framework agent. @@ -50,49 +44,32 @@ class AgentFrameworkCBAgent(FoundryCBAgent): - Supports both streaming and non-streaming responses based on the `stream` flag. """ - def __init__(self, agent: AgentProtocol): + def __init__(self, agent: SupportsAgentRun): super().__init__() self.agent = agent + self.tracer = None 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 - - :param request_body: The CreateResponse request body. - :type request_body: CreateResponse - - :return: The resolved stream timeout in seconds. - :rtype: float - """ - 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, + + if app_insights_conn_str: + from agent_framework.observability import create_resource, enable_instrumentation + + from azure.monitor.opentelemetry import configure_azure_monitor + + configure_azure_monitor( + connection_string=app_insights_conn_str, + resource=create_resource(), ) + enable_instrumentation(enable_sensitive_data=True) + elif exporter: + from agent_framework.observability import configure_otel_providers + + os.environ.setdefault("OTEL_EXPORTER_OTLP_ENDPOINT", exporter) + configure_otel_providers(enable_sensitive_data=True) + self.tracer = trace.get_tracer(__name__) async def agent_run( @@ -102,10 +79,7 @@ async def agent_run( 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) + message = transform_input(context.request.get("input")) logger.debug(f"Transformed input message type: {type(message)}") # Use split converters @@ -115,24 +89,12 @@ async def agent_run( 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 + # agent.run(stream=True) returns a ResponseStream (async iterable) + response_stream = self.agent.run(message, stream=True) + async for update in response_stream: update_count += 1 transformed = streaming_converter.transform_output_for_streaming(update) for event in transformed: diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_input_converters.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_input_converters.py index 993be43e85c8..3d53657b5f73 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_input_converters.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_input_converters.py @@ -1,120 +1,139 @@ # --------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- -# pylint: disable=too-many-nested-blocks,too-many-return-statements,too-many-branches # mypy: disable-error-code="no-redef" from __future__ import annotations from typing import Dict, List -from agent_framework import ChatMessage, Role as ChatRole -from agent_framework._types import TextContent +from agent_framework import Content, Message from azure.ai.agentserver.core.logger import get_logger logger = get_logger() -class AgentFrameworkInputConverter: +def transform_input( # pylint: disable=too-many-return-statements + input_item: str | List[Dict] | None, +) -> str | Message | list[str | Message] | None: """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)) + Returns: None | str | Message | list[str] | list[Message] - if input is None: + :param input_item: The raw input to normalize. + :type input_item: str or List[Dict] or None + """ + logger.debug("Transforming input of type: %s", type(input_item)) + + if input_item is None: + return None + + if isinstance(input_item, str): + return input_item + + try: + if isinstance(input_item, list): + messages: list[str | Message] = [] + + for item in input_item: + match item: + # Case 1: ImplicitUserMessage — no "role" or "type" key + case {"content": content} if "role" not in item and "type" not in item: + messages.extend(_parse_implicit_user_content(content)) + + # Case 2: Explicit message with role + case {"type": "message", "role": role, "content": content}: + _parse_explicit_message(role, content, messages) + + # 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, Message) for m in messages): + return [m for m in messages if isinstance(m, Message)] + + # Mixed content: coerce Message to str by extracting text content parts + return _coerce_to_strings(messages) + + raise TypeError(f"Unsupported input type: {type(input_item)}") + except Exception as e: + logger.debug("Error processing messages: %s", e, exc_info=True) + raise Exception(f"Error processing messages: {e}") from e # pylint: disable=broad-exception-raised + + +def _parse_implicit_user_content(content: str | list | None) -> list[str]: + """Extract text from an implicit user message (no role/type keys). + + :param content: The content to parse. + :type content: str or list or None + :return: A list of extracted text strings. + :rtype: list[str] + """ + match content: + case str(): + return [content] + case list(): + text_parts = [_extract_input_text(item) for item in content] + joined = " ".join(t for t in text_parts if t) + return [joined] if joined else [] + case _: + return [] + + +def _parse_explicit_message(role: str, content: str | list | None, sink: list[str | Message]) -> None: + """Parse an explicit message dict and append to sink. + + :param role: The role of the message sender. + :type role: str + :param content: The message content. + :type content: str or list or None + :param sink: The list to append parsed messages to. + :type sink: list[str | Message] + """ + match role: + case "user" | "assistant" | "system" | "tool": + pass + case _: + raise ValueError(f"Unsupported message role: {role!r}") + + content_text = "" + match content: + case str(): + content_text = content + case list(): + text_parts = [_extract_input_text(item) for item in content] + content_text = " ".join(t for t in text_parts if t) + + if content_text: + sink.append(Message(role=role, contents=[Content.from_text(content_text)])) + + +def _coerce_to_strings(messages: list[str | Message]) -> list[str | Message]: + """Coerce a mixed list of str/Message into all strings. + + :param messages: The mixed list of strings and Messages. + :type messages: list[str | Message] + :return: A list with Messages coerced to strings. + :rtype: list[str | Message] + """ + result: list[str | Message] = [] + for msg in messages: + match msg: + case Message(): + text_parts = [c.text for c in (getattr(msg, "contents", None) or []) if c.type == "text"] + result.append(" ".join(text_parts) if text_parts else str(msg)) + case str(): + result.append(msg) + return result + + +def _extract_input_text(content_item: Dict) -> str | None: + match content_item: + case {"type": "input_text", "text": str() as text}: + return text + case _: 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 diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_non_streaming_converter.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_non_streaming_converter.py index 805a5eeb9dec..a8849dd684b4 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_non_streaming_converter.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_non_streaming_converter.py @@ -5,10 +5,9 @@ import datetime import json -from typing import Any, List +from typing import Any, Dict, List -from agent_framework import AgentRunResponse, FunctionResultContent -from agent_framework._types import FunctionCallContent, TextContent +from agent_framework import AgentResponse, Content from azure.ai.agentserver.core import AgentRunContext from azure.ai.agentserver.core.logger import get_logger @@ -18,14 +17,14 @@ ResponsesAssistantMessageItemResource, ) -from .agent_id_generator import AgentIdGenerator -from .constants import Constants +from . import constants +from .agent_id_generator import generate_agent_id logger = get_logger() class AgentFrameworkOutputNonStreamingConverter: # pylint: disable=name-too-long - """Non-streaming converter: AgentRunResponse -> OpenAIResponse.""" + """Non-streaming converter: AgentResponse -> OpenAIResponse.""" def __init__(self, context: AgentRunContext): self._context = context @@ -47,17 +46,17 @@ def _new_assistant_message_item(self, message_text: str) -> ResponsesAssistantMe id=self._context.id_generator.generate_message_id(), status="completed", content=[item_content] ) - def transform_output_for_response(self, response: AgentRunResponse) -> OpenAIResponse: + def transform_output_for_response(self, response: AgentResponse) -> OpenAIResponse: """Build an OpenAIResponse capturing all supported content types. Previously this method only emitted text message items. We now also capture: - - FunctionCallContent -> function_call output item - - FunctionResultContent -> function_call_output item + - function_call content -> function_call output item + - function_result content -> function_call_output item to stay aligned with the streaming converter so no output is lost. - :param response: The AgentRunResponse from the agent framework. - :type response: AgentRunResponse + :param response: The AgentResponse from the agent framework. + :type response: AgentResponse :return: The constructed OpenAIResponse. :rtype: OpenAIResponse @@ -90,27 +89,24 @@ def transform_output_for_response(self, response: AgentRunResponse) -> OpenAIRes def _append_content_item(self, content: Any, sink: List[dict]) -> None: """Dispatch a content object to the appropriate append helper. - Adding this indirection keeps the main transform method compact and makes it - simpler to extend with new content types later. - - :param content: The content object to append. + :param content: The content object to dispatch. :type content: Any - :param sink: The list to append the converted content dict to. + :param sink: The list to append items to. :type sink: List[dict] - - :return: None - :rtype: None """ - if isinstance(content, TextContent): - self._append_text_content(content, sink) - elif isinstance(content, FunctionCallContent): - self._append_function_call_content(content, sink) - elif isinstance(content, FunctionResultContent): - self._append_function_result_content(content, sink) - else: - logger.debug("unsupported content type skipped: %s", type(content).__name__) - - def _append_text_content(self, content: TextContent, sink: List[dict]) -> None: + match content.type: + case "text" | "text_reasoning": + self._append_text_content(content, sink) + case "function_call": + self._append_function_call_content(content, sink) + case "function_result": + self._append_function_result_content(content, sink) + case "usage": + logger.debug("Skipping usage content (input/output token counts)") + case _: + logger.warning("Unhandled content type in non-streaming: %s", content.type) + + def _append_text_content(self, content: Content, sink: List[dict]) -> None: text_value = getattr(content, "text", None) if not text_value: return @@ -133,7 +129,7 @@ def _append_text_content(self, content: TextContent, sink: List[dict]) -> None: ) logger.debug(" added message item id=%s text_len=%d", item_id, len(text_value)) - def _append_function_call_content(self, content: FunctionCallContent, sink: List[dict]) -> None: + def _append_function_call_content(self, content: Content, sink: List[dict]) -> None: name = getattr(content, "name", "") or "" arguments = getattr(content, "arguments", "") if not isinstance(arguments, str): @@ -161,15 +157,15 @@ def _append_function_call_content(self, content: FunctionCallContent, sink: List len(arguments or ""), ) - def _append_function_result_content(self, content: FunctionResultContent, sink: List[dict]) -> None: + def _append_function_result_content(self, content: Content, sink: List[dict]) -> None: # Coerce the function result into a simple display string. - result = [] raw = getattr(content, "result", None) - if isinstance(raw, str): - result = [raw] - elif isinstance(raw, list): - for item in raw: - result.append(self._coerce_result_text(item)) # type: ignore + result: list[str | dict[str, Any]] = [] + match raw: + case str(): + result = [raw] + case list(): + result = [self._coerce_result_text(item) for item in raw] call_id = getattr(content, "call_id", None) or "" func_out_id = self._context.id_generator.generate_function_output_id() sink.append( @@ -190,38 +186,35 @@ def _append_function_result_content(self, content: FunctionResultContent, sink: # ------------- simple normalization helper ------------------------- def _coerce_result_text(self, value: Any) -> str | dict: - """ - Return a string if value is already str or a TextContent-like object; else str(value). + """Return a string or dict representation of a result value. - :param value: The value to coerce. + :param value: The result value to coerce. :type value: Any - - :return: The coerced string or dict. - :rtype: str | dict + :return: A string or dict representation. + :rtype: str or dict """ - if value is None: - return "" - if isinstance(value, str): - return value - # Direct TextContent instance - if isinstance(value, TextContent): - content_payload = {"type": "text", "text": getattr(value, "text", "")} - return content_payload - - return "" + match value: + case None: + return "" + case str(): + return value + case _ if hasattr(value, 'type') and value.type == "text": + return {"type": "text", "text": getattr(value, "text", "")} + case _: + return "" def _construct_response_data(self, output_items: List[dict]) -> dict: - agent_id = AgentIdGenerator.generate(self._context) + agent_id = generate_agent_id(self._context) - response_data = { + response_data: Dict[str, Any] = { "object": "response", "metadata": {}, "agent": agent_id, "conversation": self._context.get_conversation_object(), "type": "message", "role": "assistant", - "temperature": Constants.DEFAULT_TEMPERATURE, - "top_p": Constants.DEFAULT_TOP_P, + "temperature": constants.DEFAULT_TEMPERATURE, + "top_p": constants.DEFAULT_TOP_P, "user": "", "id": self._context.response_id, "created_at": self._response_created_at, diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_streaming_converter.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_streaming_converter.py index d9bc3199efb5..266f4d570ea0 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_streaming_converter.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_framework_output_streaming_converter.py @@ -10,12 +10,7 @@ import uuid from typing import Any, List, Optional, cast -from agent_framework import AgentRunResponseUpdate, FunctionApprovalRequestContent, FunctionResultContent -from agent_framework._types import ( - ErrorContent, - FunctionCallContent, - TextContent, -) +from agent_framework import AgentResponseUpdate, Content from azure.ai.agentserver.core import AgentRunContext from azure.ai.agentserver.core.logger import get_logger @@ -42,7 +37,7 @@ ResponseTextDoneEvent, ) -from .agent_id_generator import AgentIdGenerator +from .agent_id_generator import generate_agent_id logger = get_logger() @@ -108,12 +103,15 @@ def prework(self, ctx: Any) -> List[ResponseStreamEvent]: self.text_part_started = True return events - def convert_content(self, ctx: Any, content: TextContent) -> List[ResponseStreamEvent]: + def convert_content(self, ctx: Any, content: Content) -> List[ResponseStreamEvent]: events: List[ResponseStreamEvent] = [] - if isinstance(content, TextContent): - delta = content.text or "" - else: - delta = getattr(content, "text", None) or getattr(content, "reasoning", "") or "" + match content.type: + case "text": + delta = content.text or "" + case "text_reasoning": + delta = getattr(content, "reasoning", "") or "" + case _: + delta = getattr(content, "text", None) or getattr(content, "reasoning", "") or "" # buffer accumulated text self.text_buffer += delta @@ -233,7 +231,7 @@ def prework(self, ctx: Any) -> List[ResponseStreamEvent]: ) return events - def convert_content(self, ctx: Any, content: FunctionCallContent) -> List[ResponseStreamEvent]: + def convert_content(self, ctx: Any, content: Content) -> List[ResponseStreamEvent]: events: List[ResponseStreamEvent] = [] # record identifiers (once available) self.name = getattr(content, "name", None) or self.name or "" @@ -368,38 +366,35 @@ def prework(self, ctx: Any) -> List[ResponseStreamEvent]: def convert_content(self, ctx: Any, content: Any) -> List[ResponseStreamEvent]: # no delta events for now events: List[ResponseStreamEvent] = [] # treat entire output as final - result = [] raw = getattr(content, "result", None) - if isinstance(raw, str): - result = [raw or self.output] - elif isinstance(raw, list): - for item in raw: - result.append(self._coerce_result_text(item)) + result: list[str | dict[str, Any]] = [] + match raw: + case str(): + result = [raw] if raw else [str(self.output)] + case list(): + result = [self._coerce_result_text(item) for item in raw] self.output = json.dumps(result) if len(result) > 0 else "" events.extend(self.afterwork(ctx)) return events def _coerce_result_text(self, value: Any) -> str | dict: - """ - Return a string if value is already str or a TextContent-like object; else str(value). + """Return a string or dict representation of a result value. - :param value: The value to coerce. + :param value: The result value to coerce. :type value: Any - - :return: The coerced string or dict. - :rtype: str | dict + :return: A string or dict representation. + :rtype: str or dict """ - if value is None: - return "" - if isinstance(value, str): - return value - # Direct TextContent instance - if isinstance(value, TextContent): - content_payload = {"type": "text", "text": getattr(value, "text", "")} - return content_payload - - return "" + match value: + case None: + return "" + case str(): + return value + case _ if hasattr(value, 'type') and value.type == "text": + return {"type": "text", "text": getattr(value, "text", "")} + case _: + return "" def afterwork(self, ctx: Any) -> List[ResponseStreamEvent]: events: List[ResponseStreamEvent] = [] @@ -470,20 +465,21 @@ def _switch_state(self, kind: str) -> List[ResponseStreamEvent]: self._active_kind = None if self._active_state is None: - if kind == "text": - self._active_state = _TextContentStreamingState(self._context) - elif kind == "function_call": - self._active_state = _FunctionCallStreamingState(self._context) - elif kind == "function_call_output": - self._active_state = _FunctionCallOutputStreamingState(self._context) - else: - self._active_state = None + match kind: + case "text": + self._active_state = _TextContentStreamingState(self._context) + case "function_call": + self._active_state = _FunctionCallStreamingState(self._context) + case "function_call_output": + self._active_state = _FunctionCallOutputStreamingState(self._context) + case _: + self._active_state = None self._active_kind = kind if self._active_state: events.extend(self._active_state.prework(self)) return events - def transform_output_for_streaming(self, update: AgentRunResponseUpdate) -> List[ResponseStreamEvent]: + def transform_output_for_streaming(self, update: AgentResponseUpdate) -> List[ResponseStreamEvent]: logger.debug( "Transforming streaming update with %d contents", len(update.contents) if getattr(update, "contents", None) else 0, @@ -493,39 +489,44 @@ def transform_output_for_streaming(self, update: AgentRunResponseUpdate) -> List if getattr(update, "contents", None): for i, content in enumerate(update.contents): - logger.debug("Processing content %d: %s", i, type(content)) - if isinstance(content, TextContent): - events.extend(self._switch_state("text")) - if isinstance(self._active_state, _TextContentStreamingState): - events.extend(self._active_state.convert_content(self, content)) - elif isinstance(content, FunctionCallContent): - events.extend(self._switch_state("function_call")) - if isinstance(self._active_state, _FunctionCallStreamingState): - events.extend(self._active_state.convert_content(self, content)) - elif isinstance(content, FunctionResultContent): - events.extend(self._switch_state("function_call_output")) - if isinstance(self._active_state, _FunctionCallOutputStreamingState): - call_id = getattr(content, "call_id", None) - if call_id: - self._active_state.call_id = call_id - events.extend(self._active_state.convert_content(self, content)) - elif isinstance(content, FunctionApprovalRequestContent): - events.extend(self._switch_state("function_call")) - if isinstance(self._active_state, _FunctionCallStreamingState): - self._active_state.requires_approval = True - self._active_state.approval_request_id = getattr(content, "id", None) - events.extend(self._active_state.convert_content(self, content.function_call)) - elif isinstance(content, ErrorContent): - # errors are stateless; flush current state and emit error - events.extend(self._switch_state("error")) - events.append( - ResponseErrorEvent( - sequence_number=self.next_sequence(), - code=getattr(content, "error_code", None) or "server_error", - message=getattr(content, "message", None) or "An error occurred", - param="", + logger.debug("Processing content %d: %s", i, content.type) + match content.type: + case "text" | "text_reasoning": + events.extend(self._switch_state("text")) + if isinstance(self._active_state, _TextContentStreamingState): + events.extend(self._active_state.convert_content(self, content)) + case "function_call": + events.extend(self._switch_state("function_call")) + if isinstance(self._active_state, _FunctionCallStreamingState): + events.extend(self._active_state.convert_content(self, content)) + case "function_result": + events.extend(self._switch_state("function_call_output")) + if isinstance(self._active_state, _FunctionCallOutputStreamingState): + call_id = getattr(content, "call_id", None) + if call_id: + self._active_state.call_id = call_id + events.extend(self._active_state.convert_content(self, content)) + case "function_approval_request": + events.extend(self._switch_state("function_call")) + if isinstance(self._active_state, _FunctionCallStreamingState): + self._active_state.requires_approval = True + self._active_state.approval_request_id = getattr(content, "id", None) + events.extend(self._active_state.convert_content(self, content.function_call)) + case "error": + events.extend(self._switch_state("error")) + events.append( + ResponseErrorEvent( + sequence_number=self.next_sequence(), + code=getattr(content, "error_code", None) or "server_error", + message=getattr(content, "message", None) or "An error occurred", + param="", + ) ) - ) + case "usage": + # Usage metadata — not emitted as a stream event + logger.debug("Skipping usage content (input/output token counts)") + case _: + logger.warning("Unhandled content type in streaming: %s", content.type) return events def finalize_last_content(self) -> List[ResponseStreamEvent]: @@ -538,7 +539,7 @@ def finalize_last_content(self) -> List[ResponseStreamEvent]: def build_response(self, status: str) -> OpenAIResponse: self._ensure_response_started() - agent_id = AgentIdGenerator.generate(self._context) + agent_id = generate_agent_id(self._context) response_data = { "object": "response", "agent_id": agent_id, diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_id_generator.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_id_generator.py index da4045898a5e..abd2dd2c02ef 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_id_generator.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/agent_id_generator.py @@ -1,13 +1,7 @@ # --------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- -"""Helper utilities for constructing AgentId model instances. - -Centralizes logic for safely building a `models.AgentId` from a request agent -object. We intentionally do not allow overriding the generated model's fixed -`type` literal ("agent_id"). If the provided object lacks a name, `None` is -returned so callers can decide how to handle absence. -""" +"""Helper for constructing AgentId model instances from request context.""" from __future__ import annotations @@ -17,28 +11,20 @@ from azure.ai.agentserver.core.models import projects -class AgentIdGenerator: - @staticmethod - def generate(context: AgentRunContext) -> Optional[projects.AgentId]: - """ - Builds an AgentId model from the request agent object in the provided context. - - :param context: The AgentRunContext containing the request. - :type context: AgentRunContext - - :return: The constructed AgentId model, or None if the request lacks an agent name. - :rtype: Optional[projects.AgentId] - """ - agent = context.request.get("agent") - if not agent: - return None +def generate_agent_id(context: AgentRunContext) -> Optional[projects.AgentId]: + """Build an AgentId model from the request agent object in the provided context. - agent_id = projects.AgentId( - { - "type": agent.type, - "name": agent.name, - "version": agent.version, - } - ) + :param context: The agent run context containing the request. + :type context: AgentRunContext + """ + agent = context.request.get("agent") + if not agent: + return None - return agent_id + return projects.AgentId( + { + "type": agent.type, + "name": agent.name, + "version": agent.version, + } + ) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/constants.py b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/constants.py index 859e115e425e..990eb7d83388 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/constants.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/azure/ai/agentserver/agentframework/models/constants.py @@ -1,13 +1,13 @@ # --------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- -class Constants: - # streaming configuration - # Environment variable name to control idle timeout for streaming updates (seconds) - AGENTS_ADAPTER_STREAM_TIMEOUT_S = "AGENTS_ADAPTER_STREAM_TIMEOUT_S" - # Default idle timeout (seconds) when env var or request override not provided - DEFAULT_STREAM_TIMEOUT_S = 300.0 - # model defaults - DEFAULT_TEMPERATURE = 1.0 - DEFAULT_TOP_P = 1.0 +# streaming configuration +# Environment variable name to control idle timeout for streaming updates (seconds) +AGENTS_ADAPTER_STREAM_TIMEOUT_S = "AGENTS_ADAPTER_STREAM_TIMEOUT_S" +# Default idle timeout (seconds) when env var or request override not provided +DEFAULT_STREAM_TIMEOUT_S = 300.0 + +# model defaults +DEFAULT_TEMPERATURE = 1.0 +DEFAULT_TOP_P = 1.0 diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml b/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml index 814d1d6d1a1e..a9eaeab2aea5 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/pyproject.toml @@ -21,8 +21,8 @@ keywords = ["azure", "azure sdk"] dependencies = [ "azure-ai-agentserver-core", - "agent-framework-azure-ai==1.0.0b251007", - "agent-framework-core==1.0.0b251007", + "agent-framework-azure-ai==1.0.0b260212", + "agent-framework-core==1.0.0b260212", "opentelemetry-exporter-otlp-proto-grpc>=1.36.0", ] diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/basic_simple/minimal_example.py b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/basic_simple/minimal_example.py index 15afa52f42b8..ca37d1db20fc 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/basic_simple/minimal_example.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/basic_simple/minimal_example.py @@ -21,7 +21,7 @@ def get_weather( def main() -> None: - agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).create_agent( + agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_apikey/mcp_apikey.py b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_apikey/mcp_apikey.py index 985d7fd01e0c..00aca8d68522 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_apikey/mcp_apikey.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_apikey/mcp_apikey.py @@ -23,15 +23,15 @@ async def main() -> None: "GITHUB_TOKEN environment variable not set. Provide a GitHub token with MCP access." ) - agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).create_agent( + agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( instructions="You are a helpful assistant that answers GitHub questions. Use only the exposed MCP tools.", - tools=MCPStreamableHTTPTool( + tools=[MCPStreamableHTTPTool( # type: ignore[list-item] name=MCP_TOOL_NAME, url=MCP_TOOL_URL, headers={ "Authorization": f"Bearer {github_token}", }, - ), + )], ) async with agent: diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_simple/mcp_simple.py b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_simple/mcp_simple.py index 6b59771fe0da..7fcc914816b5 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_simple/mcp_simple.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/mcp_simple/mcp_simple.py @@ -16,9 +16,9 @@ async def main() -> None: - agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).create_agent( + agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( instructions="You are a helpful assistant that answers Microsoft documentation questions.", - tools=MCPStreamableHTTPTool(name=MCP_TOOL_NAME, url=MCP_TOOL_URL), + tools=[MCPStreamableHTTPTool(name=MCP_TOOL_NAME, url=MCP_TOOL_URL)], # type: ignore[list-item] ) async with agent: diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/simple_async/minimal_async_example.py b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/simple_async/minimal_async_example.py index 4c69c8afa84d..74c1decfb997 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/simple_async/minimal_async_example.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/simple_async/minimal_async_example.py @@ -22,7 +22,7 @@ def get_weather( async def main() -> None: - agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).create_agent( + agent = AzureOpenAIChatClient(credential=DefaultAzureCredential()).as_agent( instructions="You are a helpful weather agent.", tools=get_weather, ) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/workflow_agent_simple/workflow_agent_simple.py b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/workflow_agent_simple/workflow_agent_simple.py index ce3cca956273..5373e4fe9904 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/samples/workflow_agent_simple/workflow_agent_simple.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/samples/workflow_agent_simple/workflow_agent_simple.py @@ -5,15 +5,14 @@ from uuid import uuid4 from agent_framework import ( - AgentRunResponseUpdate, - AgentRunUpdateEvent, - BaseChatClient, - ChatMessage, - Contents, + AgentResponseUpdate, + Content, Executor, - Role as ChatRole, + Message, + SupportsChatGetResponse, WorkflowBuilder, WorkflowContext, + WorkflowEvent, handler, ) from agent_framework_azure_ai import AzureAIAgentClient @@ -42,7 +41,7 @@ Key concepts demonstrated: - WorkflowAgent: Wraps a workflow to make it behave as an agent - Bidirectional workflow with cycles (Worker ↔ Reviewer) -- AgentRunUpdateEvent: How workflows communicate with external consumers +- WorkflowEvent: How workflows communicate with external consumers - Structured output parsing for review feedback - State management with pending requests tracking """ @@ -51,8 +50,8 @@ @dataclass class ReviewRequest: request_id: str - user_messages: list[ChatMessage] - agent_messages: list[ChatMessage] + user_messages: list[Message] + agent_messages: list[Message] @dataclass @@ -68,7 +67,7 @@ class ReviewResponse: class Reviewer(Executor): """An executor that reviews messages and provides feedback.""" - def __init__(self, chat_client: BaseChatClient) -> None: + def __init__(self, chat_client: SupportsChatGetResponse) -> None: super().__init__(id="reviewer") self._chat_client = chat_client @@ -89,18 +88,20 @@ class _Response(BaseModel): # Define the system prompt. messages = [ - ChatMessage( - role=ChatRole.SYSTEM, - text="You are a reviewer for an AI agent, please provide feedback on the " - "following exchange between a user and the AI agent, " - "and indicate if the agent's responses are approved or not.\n" - "Use the following criteria for your evaluation:\n" - "- Relevance: Does the response address the user's query?\n" - "- Accuracy: Is the information provided correct?\n" - "- Clarity: Is the response easy to understand?\n" - "- Completeness: Does the response cover all aspects of the query?\n" - "Be critical in your evaluation and provide constructive feedback.\n" - "Do not approve until all criteria are met.", + Message( + role="system", + contents=[Content.from_text( + "You are a reviewer for an AI agent, please provide feedback on the " + "following exchange between a user and the AI agent, " + "and indicate if the agent's responses are approved or not.\n" + "Use the following criteria for your evaluation:\n" + "- Relevance: Does the response address the user's query?\n" + "- Accuracy: Is the information provided correct?\n" + "- Clarity: Is the response easy to understand?\n" + "- Completeness: Does the response cover all aspects of the query?\n" + "Be critical in your evaluation and provide constructive feedback.\n" + "Do not approve until all criteria are met." + )], ) ] @@ -112,16 +113,16 @@ class _Response(BaseModel): # Add add one more instruction for the assistant to follow. messages.append( - ChatMessage( - role=ChatRole.USER, - text="Please provide a review of the agent's responses to the user.", + Message( + role="user", + contents=[Content.from_text("Please provide a review of the agent's responses to the user.")], ) ) print("🔍 Reviewer: Sending review request to LLM...") # Get the response from the chat client. response = await self._chat_client.get_response( - messages=messages, response_format=_Response + messages=messages, options={"response_format": _Response} ) # Parse the response. @@ -143,21 +144,21 @@ class _Response(BaseModel): class Worker(Executor): """An executor that performs tasks for the user.""" - def __init__(self, chat_client: BaseChatClient) -> None: + def __init__(self, chat_client: SupportsChatGetResponse) -> None: super().__init__(id="worker") self._chat_client = chat_client - self._pending_requests: dict[str, tuple[ReviewRequest, list[ChatMessage]]] = {} + self._pending_requests: dict[str, tuple[ReviewRequest, list[Message]]] = {} @handler async def handle_user_messages( - self, user_messages: list[ChatMessage], ctx: WorkflowContext[ReviewRequest] + self, user_messages: list[Message], ctx: WorkflowContext[ReviewRequest] ) -> None: print("🔧 Worker: Received user messages, generating response...") # Handle user messages and prepare a review request for the reviewer. # Define the system prompt. messages = [ - ChatMessage(role=ChatRole.SYSTEM, text="You are a helpful assistant.") + Message(role="system", contents=[Content.from_text("You are a helpful assistant.")]) ] # Add user messages. @@ -196,7 +197,7 @@ async def handle_review_response( ) # Handle the review response. Depending on the approval status, - # either emit the approved response as AgentRunUpdateEvent, or + # either emit the approved response as a WorkflowEvent, or # retry given the feedback. if review.request_id not in self._pending_requests: raise ValueError( @@ -207,19 +208,19 @@ async def handle_review_response( if review.approved: print("✅ Worker: Response approved! Emitting to external consumer...") - # If approved, emit the agent run response update to the workflow's + # If approved, emit the agent response update to the workflow's # external consumer. - contents: list[Contents] = [] + contents: list[Content] = [] for message in request.agent_messages: contents.extend(message.contents) - # Emitting an AgentRunUpdateEvent in a workflow wrapped by a WorkflowAgent - # will send the AgentRunResponseUpdate to the WorkflowAgent's + # Emitting a WorkflowEvent in a workflow wrapped by a WorkflowAgent + # will send the AgentResponseUpdate to the WorkflowAgent's # event stream. await ctx.add_event( - AgentRunUpdateEvent( - self.id, - data=AgentRunResponseUpdate( - contents=contents, role=ChatRole.ASSISTANT + WorkflowEvent( + "output", + data=AgentResponseUpdate( + contents=contents, role="assistant" ), ) ) @@ -229,13 +230,15 @@ async def handle_review_response( print("🔧 Worker: Incorporating feedback and regenerating response...") # Construct new messages with feedback. - messages.append(ChatMessage(role=ChatRole.SYSTEM, text=review.feedback)) + messages.append(Message(role="system", contents=[Content.from_text(review.feedback)])) # Add additional instruction to address the feedback. messages.append( - ChatMessage( - role=ChatRole.SYSTEM, - text="Please incorporate the feedback above, and provide a response to user's next message.", + Message( + role="system", + contents=[Content.from_text( + "Please incorporate the feedback above, and provide a response to user's next message." + )], ) ) messages.extend(request.user_messages) @@ -264,18 +267,17 @@ async def handle_review_response( self._pending_requests[new_request.request_id] = (new_request, messages) -def build_agent(chat_client: BaseChatClient): +def build_agent(chat_client: SupportsChatGetResponse): reviewer = Reviewer(chat_client=chat_client) worker = Worker(chat_client=chat_client) return ( - WorkflowBuilder() + WorkflowBuilder(start_executor=worker) .add_edge( worker, reviewer ) # <--- This edge allows the worker to send requests to the reviewer .add_edge( reviewer, worker ) # <--- This edge allows the reviewer to send feedback back to the worker - .set_start_executor(worker) .build() .as_agent() # Convert the workflow to an agent. ) diff --git a/sdk/agentserver/azure-ai-agentserver-agentframework/tests/unit_tests/test_agent_framework_input_converter.py b/sdk/agentserver/azure-ai-agentserver-agentframework/tests/unit_tests/test_agent_framework_input_converter.py index 3dab36131f8d..8e4c8ea862c5 100644 --- a/sdk/agentserver/azure-ai-agentserver-agentframework/tests/unit_tests/test_agent_framework_input_converter.py +++ b/sdk/agentserver/azure-ai-agentserver-agentframework/tests/unit_tests/test_agent_framework_input_converter.py @@ -1,41 +1,35 @@ import importlib import pytest - -from agent_framework import ChatMessage, Role as ChatRole +from agent_framework import Message converter_module = importlib.import_module( "azure.ai.agentserver.agentframework.models.agent_framework_input_converters" ) -AgentFrameworkInputConverter = converter_module.AgentFrameworkInputConverter - - -@pytest.fixture() -def converter() -> AgentFrameworkInputConverter: - return AgentFrameworkInputConverter() +transform_input = converter_module.transform_input @pytest.mark.unit -def test_transform_none_returns_none(converter: AgentFrameworkInputConverter) -> None: - assert converter.transform_input(None) is None +def test_transform_none_returns_none() -> None: + assert transform_input(None) is None @pytest.mark.unit -def test_transform_string_returns_same(converter: AgentFrameworkInputConverter) -> None: - assert converter.transform_input("hello") == "hello" +def test_transform_string_returns_same() -> None: + assert transform_input("hello") == "hello" @pytest.mark.unit -def test_transform_implicit_user_message_with_string(converter: AgentFrameworkInputConverter) -> None: +def test_transform_implicit_user_message_with_string() -> None: payload = [{"content": "How are you?"}] - result = converter.transform_input(payload) + result = transform_input(payload) assert result == "How are you?" @pytest.mark.unit -def test_transform_implicit_user_message_with_input_text_list(converter: AgentFrameworkInputConverter) -> None: +def test_transform_implicit_user_message_with_input_text_list() -> None: payload = [ { "content": [ @@ -45,13 +39,13 @@ def test_transform_implicit_user_message_with_input_text_list(converter: AgentFr } ] - result = converter.transform_input(payload) + result = transform_input(payload) assert result == "Hello world" @pytest.mark.unit -def test_transform_explicit_message_returns_chat_message(converter: AgentFrameworkInputConverter) -> None: +def test_transform_explicit_message_returns_chat_message() -> None: payload = [ { "type": "message", @@ -62,15 +56,15 @@ def test_transform_explicit_message_returns_chat_message(converter: AgentFramewo } ] - result = converter.transform_input(payload) + result = transform_input(payload) - assert isinstance(result, ChatMessage) - assert result.role == ChatRole.ASSISTANT + assert isinstance(result, Message) + assert result.role == "assistant" assert result.text == "Hi there" @pytest.mark.unit -def test_transform_multiple_explicit_messages_returns_list(converter: AgentFrameworkInputConverter) -> None: +def test_transform_multiple_explicit_messages_returns_list() -> None: payload = [ { "type": "message", @@ -86,19 +80,19 @@ def test_transform_multiple_explicit_messages_returns_list(converter: AgentFrame }, ] - result = converter.transform_input(payload) + result = transform_input(payload) assert isinstance(result, list) assert len(result) == 2 - assert all(isinstance(item, ChatMessage) for item in result) - assert result[0].role == ChatRole.USER + assert all(isinstance(item, Message) for item in result) + assert result[0].role == "user" assert result[0].text == "Hello" - assert result[1].role == ChatRole.ASSISTANT + assert result[1].role == "assistant" assert result[1].text == "Greetings" @pytest.mark.unit -def test_transform_mixed_messages_coerces_to_strings(converter: AgentFrameworkInputConverter) -> None: +def test_transform_mixed_messages_coerces_to_strings() -> None: payload = [ {"content": "First"}, { @@ -110,21 +104,21 @@ def test_transform_mixed_messages_coerces_to_strings(converter: AgentFrameworkIn }, ] - result = converter.transform_input(payload) + result = transform_input(payload) assert result == ["First", "Second"] @pytest.mark.unit -def test_transform_invalid_input_type_raises(converter: AgentFrameworkInputConverter) -> None: +def test_transform_invalid_input_type_raises() -> None: with pytest.raises(Exception) as exc_info: - converter.transform_input({"content": "invalid"}) + transform_input({"content": "invalid"}) assert "Unsupported input type" in str(exc_info.value) @pytest.mark.unit -def test_transform_skips_non_text_entries(converter: AgentFrameworkInputConverter) -> None: +def test_transform_skips_non_text_entries() -> None: payload = [ { "content": [ @@ -134,6 +128,6 @@ def test_transform_skips_non_text_entries(converter: AgentFrameworkInputConverte } ] - result = converter.transform_input(payload) + result = transform_input(payload) assert result is None