// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
using Azure.AI.AgentServer.Responses.Models;
using Microsoft.Extensions.AI;
using MeaiTextContent = Microsoft.Extensions.AI.TextContent;
using SdkTextContent = Azure.AI.AgentServer.Responses.Models.TextContent;
namespace Microsoft.Agents.AI.Foundry.Hosting;
///
/// Converts Responses Server SDK input types to agent-framework types.
///
internal static class InputConverter
{
///
/// Converts the SDK request input items into a list of .
///
/// The create response request from the SDK.
/// Optional session state bag carrying the tool-approval id mapping.
/// A list of chat messages representing the request input.
public static List ConvertInputToMessages(CreateResponse request, AgentSessionStateBag? stateBag = null)
{
var messages = new List();
foreach (var item in request.GetInputExpanded())
{
var message = ConvertInputItemToMessage(item, stateBag);
if (message is not null)
{
messages.Add(message);
}
}
return messages;
}
///
/// Converts resolved SDK input items into instances.
///
/// The resolved input items from the SDK context.
/// Optional session state bag carrying the tool-approval id mapping.
/// A list of chat messages.
public static List ConvertItemsToMessages(IReadOnlyList- items, AgentSessionStateBag? stateBag = null)
{
var messages = new List();
foreach (var item in items)
{
var message = ConvertInputItemToMessage(item, stateBag);
if (message is not null)
{
messages.Add(message);
}
}
return messages;
}
///
/// Converts resolved SDK history/input items into instances.
///
/// The resolved output items from the SDK context.
/// Optional session state bag carrying the tool-approval id mapping.
/// A list of chat messages.
public static List ConvertOutputItemsToMessages(IReadOnlyList items, AgentSessionStateBag? stateBag = null)
{
var messages = new List();
foreach (var item in items)
{
var message = ConvertOutputItemToMessage(item, stateBag);
if (message is not null)
{
messages.Add(message);
}
}
return messages;
}
///
/// Creates from the SDK request properties.
///
/// The create response request.
/// A configured instance.
public static ChatOptions ConvertToChatOptions(CreateResponse request)
{
return new ChatOptions
{
Temperature = (float?)request.Temperature,
TopP = (float?)request.TopP,
MaxOutputTokens = (int?)request.MaxOutputTokens,
// Note: We intentionally do NOT set ModelId from request.Model here.
// The hosted agent already has its own model configured, and passing
// the client-provided model would override it (causing failures when
// clients send placeholder values like "hosted-agent").
};
}
///
/// Extracts any Foundry Toolbox markers (foundry-toolbox://) from the request's
/// MCP tool entries so the handler can resolve them server-side.
///
/// The create response request.
/// A list of (name, optional version) pairs, one per detected marker. Never .
public static List<(string Name, string? Version)> ReadMcpToolboxMarkers(CreateResponse request)
{
var markers = new List<(string Name, string? Version)>();
if (request.Tools is null)
{
return markers;
}
foreach (var tool in request.Tools)
{
if (tool is not MCPTool mcp || mcp.ServerUrl is null)
{
continue;
}
if (HostedMcpToolboxAITool.TryParseToolboxAddress(mcp.ServerUrl.ToString(), out var name, out var version))
{
markers.Add((name!, version));
}
}
return markers;
}
private static ChatMessage? ConvertInputItemToMessage(Item item, AgentSessionStateBag? stateBag)
{
return item switch
{
ItemMessage msg => ConvertItemMessage(msg),
FunctionCallOutputItemParam funcOutput => ConvertFunctionCallOutput(funcOutput),
ItemFunctionToolCall funcCall => ConvertItemFunctionToolCall(funcCall),
ItemMcpApprovalRequest approvalRequest => ConvertMcpApprovalRequest(approvalRequest.Id, approvalRequest.Name, approvalRequest.Arguments),
MCPApprovalResponse approvalResponse => ConvertMcpApprovalResponse(approvalResponse.ApprovalRequestId, approvalResponse.Approve, stateBag),
ItemReferenceParam => null,
_ => null
};
}
private static ChatMessage ConvertItemMessage(ItemMessage msg)
{
var role = ConvertMessageRole(msg.Role);
var contents = new List();
foreach (var content in msg.GetContentExpanded())
{
switch (content)
{
case MessageContentInputTextContent textContent:
contents.Add(new MeaiTextContent(textContent.Text));
break;
case SdkTextContent textContent:
contents.Add(new MeaiTextContent(textContent.Text));
break;
case SummaryTextContent summary:
contents.Add(new MeaiTextContent(summary.Text));
break;
case MessageContentReasoningTextContent reasoning:
contents.Add(new TextReasoningContent(reasoning.Text));
break;
case MessageContentInputImageContent imageContent:
AppendImageContent(contents, imageContent.ImageUrl, imageContent.FileId);
break;
case MessageContentInputFileContent fileContent:
AppendFileContent(contents, fileContent.FileUrl, fileContent.FileData, fileContent.FileId, fileContent.Filename);
break;
case ComputerScreenshotContent screenshot:
AppendImageContent(contents, screenshot.ImageUrl, screenshot.FileId);
break;
}
}
if (contents.Count == 0)
{
contents.Add(new MeaiTextContent(string.Empty));
}
return new ChatMessage(role, contents);
}
private static ChatMessage ConvertFunctionCallOutput(FunctionCallOutputItemParam funcOutput)
{
var output = DecodeFunctionResultPayload(funcOutput.Output);
return new ChatMessage(
ChatRole.Tool,
[new FunctionResultContent(funcOutput.CallId, output)]);
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing function call arguments from SDK input.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing function call arguments from SDK input.")]
private static ChatMessage ConvertItemFunctionToolCall(ItemFunctionToolCall funcCall)
{
IDictionary? arguments = null;
if (funcCall.Arguments is not null)
{
try
{
arguments = JsonSerializer.Deserialize>(funcCall.Arguments);
}
catch (JsonException)
{
arguments = new Dictionary { ["_raw"] = funcCall.Arguments };
}
}
return new ChatMessage(
ChatRole.Assistant,
[new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]);
}
///
/// Converts an inbound mcp_approval_request wire item (from history replay
/// or fresh-input) to a wrapping a
/// .
///
private static ChatMessage ConvertMcpApprovalRequest(string id, string name, string? arguments)
{
var functionCall = new FunctionCallContent(id, name, ParseFunctionArgumentsObject(arguments));
return new ChatMessage(
ChatRole.Assistant,
[new ToolApprovalRequestContent(id, functionCall)]);
}
///
/// Converts an inbound mcp_approval_response wire item to a
/// . Looks up the original
/// via so the
/// reconstructed response carries the original tool name, call id, and arguments.
///
///
/// Thrown when no mapping is recorded for .
/// Without the mapping the original call cannot be reconstructed, so we fail the request.
///
private static ChatMessage ConvertMcpApprovalResponse(string approvalRequestId, bool approve, AgentSessionStateBag? stateBag)
{
var entry = ToolApprovalIdMap.ResolveEntry(stateBag, approvalRequestId)
?? throw new InvalidOperationException(
$"No approval mapping recorded for wire id '{approvalRequestId}'.");
var functionCall = new FunctionCallContent(
entry.CallId,
entry.Name,
ParseFunctionArgumentsObject(entry.Arguments));
return new ChatMessage(
ChatRole.User,
[new ToolApprovalResponseContent(entry.AfRequestId, approve, functionCall)]);
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing tool-call arguments from SDK input.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing tool-call arguments from SDK input.")]
private static Dictionary? ParseFunctionArgumentsObject(string? arguments)
{
if (string.IsNullOrWhiteSpace(arguments))
{
return null;
}
try
{
return JsonSerializer.Deserialize>(arguments);
}
catch (JsonException)
{
return new Dictionary { ["_raw"] = arguments };
}
}
private static ChatMessage? ConvertOutputItemToMessage(OutputItem item, AgentSessionStateBag? stateBag)
{
return item switch
{
OutputItemMessage msg => ConvertOutputItemMessageToChat(msg),
OutputItemFunctionToolCall funcCall => ConvertOutputItemFunctionCall(funcCall),
OutputItemFunctionToolCallOutput funcOutput => ConvertFunctionToolCallOutput(funcOutput),
OutputItemMcpApprovalRequest approvalRequest => ConvertMcpApprovalRequest(approvalRequest.Id, approvalRequest.Name, approvalRequest.Arguments),
OutputItemMcpApprovalResponseResource approvalResponse => ConvertMcpApprovalResponse(approvalResponse.ApprovalRequestId, approvalResponse.Approve, stateBag),
OutputItemReasoningItem => null,
_ => null
};
}
private static ChatMessage ConvertOutputItemMessageToChat(OutputItemMessage msg)
{
var role = ConvertMessageRole(msg.Role);
var contents = new List();
foreach (var content in msg.Content)
{
switch (content)
{
case MessageContentInputTextContent textContent:
contents.Add(new MeaiTextContent(textContent.Text));
break;
case MessageContentOutputTextContent textContent:
contents.Add(new MeaiTextContent(textContent.Text));
break;
case SdkTextContent textContent:
contents.Add(new MeaiTextContent(textContent.Text));
break;
case SummaryTextContent summary:
contents.Add(new MeaiTextContent(summary.Text));
break;
case MessageContentReasoningTextContent reasoning:
contents.Add(new TextReasoningContent(reasoning.Text));
break;
case MessageContentRefusalContent refusal:
contents.Add(new MeaiTextContent($"[Refusal: {refusal.Refusal}]"));
break;
case MessageContentInputImageContent imageContent:
AppendImageContent(contents, imageContent.ImageUrl, imageContent.FileId);
break;
case MessageContentInputFileContent fileContent:
AppendFileContent(contents, fileContent.FileUrl, fileContent.FileData, fileContent.FileId, fileContent.Filename);
break;
case ComputerScreenshotContent screenshot:
AppendImageContent(contents, screenshot.ImageUrl, screenshot.FileId);
break;
}
}
if (contents.Count == 0)
{
contents.Add(new MeaiTextContent(string.Empty));
}
return new ChatMessage(role, contents);
}
private static void AppendImageContent(List contents, Uri? imageUrl, string? fileId)
{
if (imageUrl is not null)
{
var url = imageUrl.ToString();
if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
contents.Add(new DataContent(url, "image/*"));
}
else
{
contents.Add(new UriContent(imageUrl, "image/*"));
}
}
else if (!string.IsNullOrEmpty(fileId))
{
contents.Add(new HostedFileContent(fileId));
}
}
private static void AppendFileContent(List contents, Uri? fileUrl, string? fileData, string? fileId, string? filename)
{
if (fileUrl is not null)
{
var content = new UriContent(fileUrl, "application/octet-stream");
if (!string.IsNullOrEmpty(filename))
{
content.AdditionalProperties = new AdditionalPropertiesDictionary { ["filename"] = filename };
}
contents.Add(content);
return;
}
if (!string.IsNullOrEmpty(fileData))
{
// If the data URI carries text/* content, decode it inline as TextContent so
// {System.LastMessageText} (and other text-only consumers) sees the file's
// body rather than an opaque blob.
if (TryDecodeTextDataUri(fileData, filename, out var decodedText))
{
contents.Add(new MeaiTextContent(decodedText));
}
else
{
var dataContent = new DataContent(fileData, "application/octet-stream");
if (!string.IsNullOrEmpty(filename))
{
dataContent.AdditionalProperties = new AdditionalPropertiesDictionary { ["filename"] = filename };
}
contents.Add(dataContent);
}
return;
}
if (!string.IsNullOrEmpty(fileId))
{
var hosted = new HostedFileContent(fileId);
if (!string.IsNullOrEmpty(filename))
{
hosted.AdditionalProperties = new AdditionalPropertiesDictionary { ["filename"] = filename };
}
contents.Add(hosted);
return;
}
if (!string.IsNullOrEmpty(filename))
{
contents.Add(new MeaiTextContent($"[File: {filename}]"));
}
}
private static bool TryDecodeTextDataUri(string dataUri, string? filename, out string text)
{
// Cap the encoded payload so an oversized client-supplied data URI cannot
// trigger an unbounded allocation in Convert.FromBase64String. 16 MiB
// encoded → ~12 MiB decoded, well above any realistic text/* file we'd
// want to inline as content while still bounding the worst case.
const int MaxEncodedLength = 16 * 1024 * 1024;
text = string.Empty;
if (!dataUri.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
return false;
}
const string Marker = ";base64,";
int markerIndex = dataUri.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
if (markerIndex < 0)
{
return false;
}
string mediaType = dataUri.Substring("data:".Length, markerIndex - "data:".Length);
if (!mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
{
return false;
}
string encoded = dataUri.Substring(markerIndex + Marker.Length);
if (encoded.Length > MaxEncodedLength)
{
return false;
}
try
{
byte[] bytes = Convert.FromBase64String(encoded);
string decoded = Encoding.UTF8.GetString(bytes);
text = string.IsNullOrEmpty(filename) ? decoded : $"[File: {filename}]\n{decoded}";
return true;
}
catch (FormatException)
{
return false;
}
catch (DecoderFallbackException)
{
return false;
}
}
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Deserializing function call arguments from SDK output history.")]
[UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Deserializing function call arguments from SDK output history.")]
private static ChatMessage ConvertOutputItemFunctionCall(OutputItemFunctionToolCall funcCall)
{
IDictionary? arguments = null;
if (funcCall.Arguments is not null)
{
try
{
arguments = JsonSerializer.Deserialize>(funcCall.Arguments);
}
catch (JsonException)
{
arguments = new Dictionary { ["_raw"] = funcCall.Arguments };
}
}
return new ChatMessage(
ChatRole.Assistant,
[new FunctionCallContent(funcCall.CallId, funcCall.Name, arguments)]);
}
private static ChatMessage ConvertFunctionToolCallOutput(OutputItemFunctionToolCallOutput funcOutput)
{
var output = DecodeFunctionResultPayload(funcOutput.Output);
return new ChatMessage(
ChatRole.Tool,
[new FunctionResultContent(funcOutput.CallId, output)]);
}
///
/// Decodes the wire payload of a function_call_output.output field back into the
/// underlying tool-result text suitable for replay as .
///
///
/// Mirrors OutputConverter.EncodeFunctionResultAsJsonStringPayload. Per the OpenAI
/// Responses spec, output is a JSON string; we extract its underlying value. Legacy
/// producers that emitted raw JSON values (arrays/objects) are tolerated by passing the raw
/// bytes through unchanged.
///
private static string DecodeFunctionResultPayload(BinaryData? rawOutput)
{
if (rawOutput is null)
{
return string.Empty;
}
var raw = rawOutput.ToString();
if (string.IsNullOrEmpty(raw))
{
return string.Empty;
}
try
{
using var doc = JsonDocument.Parse(raw);
if (doc.RootElement.ValueKind == JsonValueKind.String)
{
return doc.RootElement.GetString() ?? string.Empty;
}
// Legacy/non-conforming producers may have emitted a raw JSON value
// (array/object/number/bool/null). Pass the raw text through as the
// payload so the replayed FunctionResultContent.Result preserves the
// original tool output shape.
return raw;
}
catch (JsonException)
{
// Not valid JSON — treat the bytes as a literal string payload.
return raw;
}
}
private static ChatRole ConvertMessageRole(MessageRole role)
{
return role switch
{
MessageRole.User => ChatRole.User,
MessageRole.Assistant => ChatRole.Assistant,
MessageRole.System => ChatRole.System,
MessageRole.Developer => new ChatRole("developer"),
_ => ChatRole.User
};
}
}