Merge branch 'main' into feature-foundry-agents

This commit is contained in:
Chris
2025-11-12 16:57:50 -08:00
committed by GitHub
Unverified
49 changed files with 5620 additions and 3912 deletions
@@ -2,6 +2,7 @@
// This sample demonstrates basic usage of the DevUI in an ASP.NET Core application with AI agents.
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
@@ -18,10 +19,11 @@ namespace DevUI_Step01_BasicUsage;
/// <remarks>
/// This sample shows how to:
/// 1. Set up Azure OpenAI as the chat client
/// 2. Register agents and workflows using the hosting packages
/// 3. Map the DevUI endpoint which automatically configures the middleware
/// 4. Map the dynamic OpenAI Responses API for Python DevUI compatibility
/// 5. Access the DevUI in a web browser
/// 2. Create function tools for agents to use
/// 3. Register agents and workflows using the hosting packages with tools
/// 4. Map the DevUI endpoint which automatically configures the middleware
/// 5. Map the dynamic OpenAI Responses API for Python DevUI compatibility
/// 6. Access the DevUI in a web browser
///
/// The DevUI provides an interactive web interface for testing and debugging AI agents.
/// DevUI assets are served from embedded resources within the assembly.
@@ -50,10 +52,30 @@ internal static class Program
builder.Services.AddChatClient(chatClient);
// Register sample agents
builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.");
// Define some example tools
[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
=> $"The weather in {location} is cloudy with a high of 15°C.";
[Description("Calculate the sum of two numbers.")]
static double Add([Description("The first number.")] double a, [Description("The second number.")] double b)
=> a + b;
[Description("Get the current time.")]
static string GetCurrentTime()
=> DateTime.Now.ToString("HH:mm:ss");
// Register sample agents with tools
builder.AddAIAgent("assistant", "You are a helpful assistant. Answer questions concisely and accurately.")
.WithAITools(
AIFunctionFactory.Create(GetWeather, name: "get_weather"),
AIFunctionFactory.Create(GetCurrentTime, name: "get_current_time")
);
builder.AddAIAgent("poet", "You are a creative poet. Respond to all requests with beautiful poetry.");
builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.");
builder.AddAIAgent("coder", "You are an expert programmer. Help users with coding questions and provide code examples.")
.WithAITool(AIFunctionFactory.Create(Add, name: "add"));
// Register sample workflows
var assistantBuilder = builder.AddAIAgent("workflow-assistant", "You are a helpful assistant in a workflow.");
@@ -18,9 +18,13 @@ namespace Microsoft.Agents.AI.DevUI.Entities;
[JsonSerializable(typeof(MetaResponse))]
[JsonSerializable(typeof(EnvVarRequirement))]
[JsonSerializable(typeof(List<EntityInfo>))]
[JsonSerializable(typeof(List<JsonElement>))]
[JsonSerializable(typeof(List<Dictionary<string, JsonElement>>))]
[JsonSerializable(typeof(List<Dictionary<string, string>>))]
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
[JsonSerializable(typeof(Dictionary<string, bool>))]
[JsonSerializable(typeof(Dictionary<string, Dictionary<string, string>>))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
[ExcludeFromCodeCoverage]
internal sealed partial class EntitiesJsonContext : JsonSerializerContext;
@@ -36,16 +36,16 @@ internal sealed record EntityInfo(
string Name,
[property: JsonPropertyName("description")]
string? Description = null,
string? Description,
[property: JsonPropertyName("framework")]
string Framework = "dotnet",
string Framework,
[property: JsonPropertyName("tools")]
List<string>? Tools = null,
List<string> Tools,
[property: JsonPropertyName("metadata")]
Dictionary<string, JsonElement>? Metadata = null
Dictionary<string, JsonElement> Metadata
)
{
[JsonPropertyName("source")]
@@ -54,6 +54,32 @@ internal sealed record EntityInfo(
[JsonPropertyName("original_url")]
public string? OriginalUrl { get; init; }
// Deployment support
[JsonPropertyName("deployment_supported")]
public bool DeploymentSupported { get; init; }
[JsonPropertyName("deployment_reason")]
public string? DeploymentReason { get; init; }
// Agent-specific fields
[JsonPropertyName("instructions")]
public string? Instructions { get; init; }
[JsonPropertyName("model_id")]
public string? ModelId { get; init; }
[JsonPropertyName("chat_client_type")]
public string? ChatClientType { get; init; }
[JsonPropertyName("context_providers")]
public List<string>? ContextProviders { get; init; }
[JsonPropertyName("middleware")]
public List<string>? Middleware { get; init; }
[JsonPropertyName("module_path")]
public string? ModulePath { get; init; }
// Workflow-specific fields
[JsonPropertyName("required_env_vars")]
public List<EnvVarRequirement>? RequiredEnvVars { get; init; }
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Checkpointing;
@@ -17,31 +19,37 @@ internal static class WorkflowSerializationExtensions
/// Converts a workflow to a dictionary representation compatible with DevUI frontend.
/// This matches the Python workflow.to_dict() format expected by the UI.
/// </summary>
public static Dictionary<string, object> ToDevUIDict(this Workflow workflow)
/// <param name="workflow">The workflow to convert.</param>
/// <returns>A dictionary with string keys and JsonElement values containing the workflow data.</returns>
public static Dictionary<string, JsonElement> ToDevUIDict(this Workflow workflow)
{
var result = new Dictionary<string, object>
var result = new Dictionary<string, JsonElement>
{
["id"] = workflow.Name ?? Guid.NewGuid().ToString(),
["start_executor_id"] = workflow.StartExecutorId,
["max_iterations"] = MaxIterationsDefault
["id"] = Serialize(workflow.Name ?? Guid.NewGuid().ToString(), EntitiesJsonContext.Default.String),
["start_executor_id"] = Serialize(workflow.StartExecutorId, EntitiesJsonContext.Default.String),
["max_iterations"] = Serialize(MaxIterationsDefault, EntitiesJsonContext.Default.Int32)
};
// Add optional fields
if (!string.IsNullOrEmpty(workflow.Name))
{
result["name"] = workflow.Name;
result["name"] = Serialize(workflow.Name, EntitiesJsonContext.Default.String);
}
if (!string.IsNullOrEmpty(workflow.Description))
{
result["description"] = workflow.Description;
result["description"] = Serialize(workflow.Description, EntitiesJsonContext.Default.String);
}
// Convert executors to Python-compatible format
result["executors"] = ConvertExecutorsToDict(workflow);
result["executors"] = Serialize(
ConvertExecutorsToDict(workflow),
EntitiesJsonContext.Default.DictionaryStringDictionaryStringString);
// Convert edges to edge_groups format
result["edge_groups"] = ConvertEdgesToEdgeGroups(workflow);
result["edge_groups"] = Serialize(
ConvertEdgesToEdgeGroups(workflow),
EntitiesJsonContext.Default.ListDictionaryStringJsonElement);
return result;
}
@@ -49,9 +57,9 @@ internal static class WorkflowSerializationExtensions
/// <summary>
/// Converts workflow executors to a dictionary format compatible with Python
/// </summary>
private static Dictionary<string, object> ConvertExecutorsToDict(Workflow workflow)
private static Dictionary<string, Dictionary<string, string>> ConvertExecutorsToDict(Workflow workflow)
{
var executors = new Dictionary<string, object>();
var executors = new Dictionary<string, Dictionary<string, string>>();
// Extract executor IDs from edges and start executor
// (Registrations is internal, so we infer executors from the graph structure)
@@ -73,7 +81,7 @@ internal static class WorkflowSerializationExtensions
// Create executor entries (we can't access internal Registrations for type info)
foreach (var executorId in executorIds)
{
executors[executorId] = new Dictionary<string, object>
executors[executorId] = new Dictionary<string, string>
{
["id"] = executorId,
["type"] = "Executor"
@@ -86,9 +94,9 @@ internal static class WorkflowSerializationExtensions
/// <summary>
/// Converts workflow edges to edge_groups format expected by the UI
/// </summary>
private static List<object> ConvertEdgesToEdgeGroups(Workflow workflow)
private static List<Dictionary<string, JsonElement>> ConvertEdgesToEdgeGroups(Workflow workflow)
{
var edgeGroups = new List<object>();
var edgeGroups = new List<Dictionary<string, JsonElement>>();
var edgeGroupId = 0;
// Get edges using the public ReflectEdges method
@@ -101,13 +109,13 @@ internal static class WorkflowSerializationExtensions
if (edgeInfo is DirectEdgeInfo directEdge)
{
// Single edge group for direct edges
var edges = new List<object>();
var edges = new List<Dictionary<string, string>>();
foreach (var source in directEdge.Connection.SourceIds)
{
foreach (var sink in directEdge.Connection.SinkIds)
{
var edge = new Dictionary<string, object>
var edge = new Dictionary<string, string>
{
["source_id"] = source,
["target_id"] = sink
@@ -123,23 +131,25 @@ internal static class WorkflowSerializationExtensions
}
}
edgeGroups.Add(new Dictionary<string, object>
var edgeGroup = new Dictionary<string, JsonElement>
{
["id"] = $"edge_group_{edgeGroupId++}",
["type"] = "SingleEdgeGroup",
["edges"] = edges
});
["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
["type"] = Serialize("SingleEdgeGroup", EntitiesJsonContext.Default.String),
["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
};
edgeGroups.Add(edgeGroup);
}
else if (edgeInfo is FanOutEdgeInfo fanOutEdge)
{
// FanOut edge group
var edges = new List<object>();
var edges = new List<Dictionary<string, string>>();
foreach (var source in fanOutEdge.Connection.SourceIds)
{
foreach (var sink in fanOutEdge.Connection.SinkIds)
{
edges.Add(new Dictionary<string, object>
edges.Add(new Dictionary<string, string>
{
["source_id"] = source,
["target_id"] = sink
@@ -147,16 +157,16 @@ internal static class WorkflowSerializationExtensions
}
}
var fanOutGroup = new Dictionary<string, object>
var fanOutGroup = new Dictionary<string, JsonElement>
{
["id"] = $"edge_group_{edgeGroupId++}",
["type"] = "FanOutEdgeGroup",
["edges"] = edges
["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
["type"] = Serialize("FanOutEdgeGroup", EntitiesJsonContext.Default.String),
["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
};
if (fanOutEdge.HasAssigner)
{
fanOutGroup["selection_func_name"] = "selector";
fanOutGroup["selection_func_name"] = Serialize("selector", EntitiesJsonContext.Default.String);
}
edgeGroups.Add(fanOutGroup);
@@ -164,13 +174,13 @@ internal static class WorkflowSerializationExtensions
else if (edgeInfo is FanInEdgeInfo fanInEdge)
{
// FanIn edge group
var edges = new List<object>();
var edges = new List<Dictionary<string, string>>();
foreach (var source in fanInEdge.Connection.SourceIds)
{
foreach (var sink in fanInEdge.Connection.SinkIds)
{
edges.Add(new Dictionary<string, object>
edges.Add(new Dictionary<string, string>
{
["source_id"] = source,
["target_id"] = sink
@@ -178,16 +188,20 @@ internal static class WorkflowSerializationExtensions
}
}
edgeGroups.Add(new Dictionary<string, object>
var edgeGroup = new Dictionary<string, JsonElement>
{
["id"] = $"edge_group_{edgeGroupId++}",
["type"] = "FanInEdgeGroup",
["edges"] = edges
});
["id"] = Serialize($"edge_group_{edgeGroupId++}", EntitiesJsonContext.Default.String),
["type"] = Serialize("FanInEdgeGroup", EntitiesJsonContext.Default.String),
["edges"] = Serialize(edges, EntitiesJsonContext.Default.ListDictionaryStringString)
};
edgeGroups.Add(edgeGroup);
}
}
}
return edgeGroups;
}
private static JsonElement Serialize<T>(T value, JsonTypeInfo<T> typeInfo) => JsonSerializer.SerializeToElement(value, typeInfo);
}
@@ -6,6 +6,7 @@ using System.Text.Json;
using Microsoft.Agents.AI.DevUI.Entities;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.DevUI;
@@ -56,21 +57,21 @@ internal static class EntitiesApiExtensions
{
try
{
var entities = new List<EntityInfo>();
var entities = new Dictionary<string, EntityInfo>();
// Discover agents
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
{
entities.Add(agentInfo);
entities[agentInfo.Id] = agentInfo;
}
// Discover workflows
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityIdFilter: null, cancellationToken).ConfigureAwait(false))
{
entities.Add(workflowInfo);
entities[workflowInfo.Id] = workflowInfo;
}
return Results.Json(new DiscoveryResponse([.. entities]), EntitiesJsonContext.Default.DiscoveryResponse);
return Results.Json(new DiscoveryResponse([.. entities.Values.OrderBy(e => e.Id)]), EntitiesJsonContext.Default.DiscoveryResponse);
}
catch (Exception ex)
{
@@ -90,14 +91,6 @@ internal static class EntitiesApiExtensions
{
try
{
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
{
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
{
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
}
}
if (type is null || string.Equals(type, "workflow", StringComparison.OrdinalIgnoreCase))
{
await foreach (var workflowInfo in DiscoverWorkflowsAsync(workflowCatalog, entityId, cancellationToken).ConfigureAwait(false))
@@ -106,6 +99,14 @@ internal static class EntitiesApiExtensions
}
}
if (type is null || string.Equals(type, "agent", StringComparison.OrdinalIgnoreCase))
{
await foreach (var agentInfo in DiscoverAgentsAsync(agentCatalog, entityId, cancellationToken).ConfigureAwait(false))
{
return Results.Json(agentInfo, EntitiesJsonContext.Default.EntityInfo);
}
}
return Results.NotFound(new { error = new { message = $"Entity '{entityId}' not found.", type = "invalid_request_error" } });
}
catch (Exception ex)
@@ -180,17 +181,82 @@ internal static class EntitiesApiExtensions
private static EntityInfo CreateAgentEntityInfo(AIAgent agent)
{
var entityId = agent.Name ?? agent.Id;
// Extract tools and other metadata using GetService
List<string> tools = [];
var metadata = new Dictionary<string, JsonElement>();
// Try to get ChatOptions from the agent which may contain tools
if (agent.GetService<ChatOptions>() is { Tools: { Count: > 0 } agentTools })
{
tools = agentTools
.Where(tool => !string.IsNullOrWhiteSpace(tool.Name))
.Select(tool => tool.Name!)
.Distinct()
.ToList();
}
// Extract agent-specific fields (top-level properties for compatibility with Python)
string? instructions = null;
string? modelId = null;
string? chatClientType = null;
// Get instructions from ChatClientAgent
if (agent is ChatClientAgent chatAgent && !string.IsNullOrWhiteSpace(chatAgent.Instructions))
{
instructions = chatAgent.Instructions;
}
// Get IChatClient to extract metadata
IChatClient? chatClient = agent.GetService<IChatClient>();
if (chatClient != null)
{
// Get chat client type
chatClientType = chatClient.GetType().Name;
// Get model ID from ChatClientMetadata
if (chatClient.GetService<ChatClientMetadata>() is { } chatClientMetadata)
{
modelId = chatClientMetadata.DefaultModelId;
// Add additional metadata for compatibility
if (!string.IsNullOrWhiteSpace(chatClientMetadata.ProviderName))
{
metadata["chat_client_provider"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderName, EntitiesJsonContext.Default.String);
}
if (chatClientMetadata.ProviderUri is not null)
{
metadata["provider_uri"] = JsonSerializer.SerializeToElement(chatClientMetadata.ProviderUri.ToString(), EntitiesJsonContext.Default.String);
}
}
}
// Add provider name from AIAgentMetadata if available
if (agent.GetService<AIAgentMetadata>() is { } agentMetadata && !string.IsNullOrWhiteSpace(agentMetadata.ProviderName))
{
metadata["provider_name"] = JsonSerializer.SerializeToElement(agentMetadata.ProviderName, EntitiesJsonContext.Default.String);
}
// Add agent type information to metadata (in addition to chat_client_type)
var agentTypeName = agent.GetType().Name;
metadata["agent_type"] = JsonSerializer.SerializeToElement(agentTypeName, EntitiesJsonContext.Default.String);
return new EntityInfo(
Id: entityId,
Type: "agent",
Name: entityId,
Name: agent.DisplayName,
Description: agent.Description,
Framework: "agent-framework",
Tools: null,
Metadata: []
Framework: "agent_framework",
Tools: tools,
Metadata: metadata
)
{
Source = "in_memory"
Source = "in_memory",
Instructions = instructions,
ModelId = modelId,
ChatClientType = chatClientType,
Executors = [], // Agents have empty executors list (workflows use this field)
};
}
@@ -212,7 +278,7 @@ internal static class EntitiesApiExtensions
}
// Create a default input schema (string type)
var defaultInputSchema = new Dictionary<string, object>
var defaultInputSchema = new Dictionary<string, string>
{
["type"] = "string"
};
@@ -223,14 +289,17 @@ internal static class EntitiesApiExtensions
Type: "workflow",
Name: workflowId,
Description: workflow.Description,
Framework: "agent-framework",
Tools: [.. executorIds],
Framework: "agent_framework",
Tools: [],
Metadata: []
)
{
Source = "in_memory",
WorkflowDump = JsonSerializer.SerializeToElement(workflow.ToDevUIDict()),
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema),
Executors = [.. executorIds], // Workflows use Executors instead of Tools
WorkflowDump = JsonSerializer.SerializeToElement(
workflow.ToDevUIDict(),
EntitiesJsonContext.Default.DictionaryStringJsonElement),
InputSchema = JsonSerializer.SerializeToElement(defaultInputSchema, EntitiesJsonContext.Default.DictionaryStringString),
InputTypeName = "string",
StartExecutorId = workflow.StartExecutorId
};
@@ -281,6 +281,8 @@ public sealed partial class ChatClientAgent : AIAgent
base.GetService(serviceType, serviceKey) ??
(serviceType == typeof(AIAgentMetadata) ? this._agentMetadata
: serviceType == typeof(IChatClient) ? this.ChatClient
: serviceType == typeof(ChatOptions) ? this._agentOptions?.ChatOptions
: serviceType == typeof(ChatClientAgentOptions) ? this._agentOptions
: this.ChatClient.GetService(serviceType, serviceKey));
/// <inheritdoc/>
+1 -1
View File
@@ -16,7 +16,7 @@
"name": "AG-UI Examples Server",
"type": "debugpy",
"request": "launch",
"module": "examples",
"module": "agent_framework_ag_ui_examples",
"cwd": "${workspaceFolder}/packages/ag-ui",
"console": "integratedTerminal",
"justMyCode": false
+17 -1
View File
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0b251112] - 2025-11-12
### Added
- **agent-framework-azure-ai**: Azure AI client based on new `azure-ai-projects` package ([#1910](https://github.com/microsoft/agent-framework/pull/1910))
- **agent-framework-anthropic**: Add convenience method on data content ([#2083](https://github.com/microsoft/agent-framework/pull/2083))
### Changed
- **agent-framework-core**: Update OpenAI samples to use agents ([#2012](https://github.com/microsoft/agent-framework/pull/2012))
### Fixed
- **agent-framework-anthropic**: Fixed image handling in Anthropic client ([#2083](https://github.com/microsoft/agent-framework/pull/2083))
## [1.0.0b251111] - 2025-11-11
### Added
@@ -204,7 +219,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251111...HEAD
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251112...HEAD
[1.0.0b251112]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251111...python-1.0.0b251112
[1.0.0b251111]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251108...python-1.0.0b251111
[1.0.0b251108]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251106.post1...python-1.0.0b251108
[1.0.0b251106.post1]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b251106...python-1.0.0b251106.post1
+1 -1
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -85,6 +85,7 @@ class AgentFrameworkEventBridge:
self.input_messages = input_messages or []
self.pending_tool_calls: list[dict[str, Any]] = [] # Track tool calls for assistant message
self.tool_results: list[dict[str, Any]] = [] # Track tool results
self.tool_calls_ended: set[str] = set() # Track which tool calls have had ToolCallEndEvent emitted
async def from_agent_run_update(self, update: AgentRunResponseUpdate) -> list[BaseEvent]:
"""
@@ -118,12 +119,14 @@ class AgentFrameworkEventBridge:
message_id=self.current_message_id,
role="assistant",
)
logger.debug(f"Emitting TextMessageStartEvent with message_id={self.current_message_id}")
events.append(start_event)
event = TextMessageContentEvent(
message_id=self.current_message_id,
delta=content.text,
)
logger.debug(f"Emitting TextMessageContentEvent with delta: {content.text}")
events.append(event)
elif isinstance(content, FunctionCallContent):
@@ -378,6 +381,7 @@ class AgentFrameworkEventBridge:
)
logger.info(f"Emitting ToolCallEndEvent for completed tool call '{content.call_id}'")
events.append(end_event)
self.tool_calls_ended.add(content.call_id) # Track that we emitted end event
# Log total StateDeltaEvent count for this tool call
if self.state_delta_count > 0:
@@ -617,6 +621,7 @@ class AgentFrameworkEventBridge:
f"Emitting ToolCallEndEvent for approval-required tool '{content.function_call.call_id}'"
)
events.append(end_event)
self.tool_calls_ended.add(content.function_call.call_id) # Track that we emitted end event
# Emit custom event for approval request
# Note: In AG-UI protocol, the frontend handles interrupts automatically
@@ -38,22 +38,69 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
"""
result: list[ChatMessage] = []
for msg in messages:
# Check for backend tool rendering results FIRST (may not have role field)
if "actionExecutionId" in msg or "actionName" in msg:
# Backend tool rendering - convert to FunctionResultContent
from agent_framework import FunctionResultContent
# Handle standard tool result messages early (role="tool") to preserve provider invariants
# This path maps AGUI tool messages to FunctionResultContent with the correct tool_call_id
role_str = msg.get("role", "user")
if role_str == "tool":
# Prefer explicit tool_call_id fields; fall back to backend fields only if necessary
tool_call_id = msg.get("tool_call_id") or msg.get("toolCallId")
tool_call_id = msg.get("actionExecutionId", "")
# If no explicit tool_call_id, treat as backend tool rendering payloads where
# AGUI may send actionExecutionId/actionName. This must still map to the
# assistant's tool call id to satisfy provider requirements.
if not tool_call_id:
tool_call_id = msg.get("actionExecutionId") or ""
# Extract raw content text
result_content = msg.get("content")
if result_content is None:
result_content = msg.get("result", "")
# Distinguish approval payloads from actual tool results
is_approval = False
if isinstance(result_content, str) and result_content:
import json as _json
try:
parsed = _json.loads(result_content)
is_approval = isinstance(parsed, dict) and "accepted" in parsed
except Exception:
is_approval = False
if is_approval:
# Approval responses should be treated as user messages to trigger human-in-the-loop flow
chat_msg = ChatMessage(
role=Role.USER,
contents=[TextContent(text=str(result_content))],
additional_properties={"is_tool_result": True, "tool_call_id": str(tool_call_id or "")},
)
if "id" in msg:
chat_msg.message_id = msg["id"]
result.append(chat_msg)
continue
chat_msg = ChatMessage(
role=Role.TOOL,
contents=[FunctionResultContent(call_id=str(tool_call_id), result=result_content)],
)
if "id" in msg:
chat_msg.message_id = msg["id"]
result.append(chat_msg)
continue
# Backend tool rendering payloads without an explicit role
# Prefer standard tool mapping above; this block only covers legacy/minimal payloads
if "actionExecutionId" in msg or "actionName" in msg:
# Prefer toolCallId if present; otherwise fall back to actionExecutionId
tool_call_id = msg.get("toolCallId") or msg.get("tool_call_id") or msg.get("actionExecutionId", "")
result_content = msg.get("result", msg.get("content", ""))
chat_msg = ChatMessage(
role=Role.TOOL, # Tool results must be tool role
contents=[FunctionResultContent(call_id=tool_call_id, result=result_content)],
role=Role.TOOL,
contents=[FunctionResultContent(call_id=str(tool_call_id), result=result_content)],
)
if "id" in msg:
chat_msg.message_id = msg["id"]
result.append(chat_msg)
continue
@@ -93,55 +140,7 @@ def agui_messages_to_agent_framework(messages: list[dict[str, Any]]) -> list[Cha
result.append(chat_msg)
continue
role_str = msg.get("role", "user")
# Handle tool result messages (with role="tool")
if role_str == "tool":
# Check if this is a standard tool result (has tool_call_id or toolCallId)
tool_call_id = msg.get("tool_call_id") or msg.get("toolCallId")
result_content = msg.get("content", "")
# Distinguish between backend tool results and approval responses
# Approval responses have {"accepted": ...} structure
is_approval = False
if result_content:
import json
try:
parsed_content = json.loads(result_content)
is_approval = "accepted" in parsed_content
except (json.JSONDecodeError, TypeError):
is_approval = False
# Backend tool results have non-empty content WITHOUT "accepted" field
if tool_call_id and result_content and not is_approval:
# Tool execution result - convert to FunctionResultContent with correct role
from agent_framework import FunctionResultContent
chat_msg = ChatMessage(
role=Role.TOOL,
contents=[FunctionResultContent(call_id=tool_call_id, result=result_content)],
)
if "id" in msg:
chat_msg.message_id = msg["id"]
result.append(chat_msg)
continue
else:
# Human-in-the-loop approval response - mark for special handling
content = msg.get("content", "")
chat_msg = ChatMessage(
role=Role.USER, # Approval responses are user messages
contents=[TextContent(text=content)],
additional_properties={"is_tool_result": True, "tool_call_id": msg.get("toolCallId", "")},
)
if "id" in msg:
chat_msg.message_id = msg["id"]
result.append(chat_msg)
continue
# No special handling required for assistant/plain messages here
role = _AGUI_TO_FRAMEWORK_ROLE.get(role_str, Role.USER)
@@ -16,7 +16,15 @@ from ag_ui.core import (
TextMessageEndEvent,
TextMessageStartEvent,
)
from agent_framework import AgentProtocol, AgentThread, ChatAgent, TextContent
from agent_framework import (
AgentProtocol,
AgentThread,
ChatAgent,
ChatMessage,
FunctionCallContent,
FunctionResultContent,
TextContent,
)
from ._utils import convert_agui_tools_to_agent_framework, generate_event_id
@@ -276,6 +284,98 @@ class DefaultOrchestrator(Orchestrator):
response_format = context.agent.chat_options.response_format
skip_text_content = response_format is not None
# Sanitizer: ensure tool results only follow assistant tool calls
# Also inject synthetic tool results for confirm_changes
def sanitize_tool_history(messages: list[ChatMessage]) -> list[ChatMessage]:
sanitized: list[ChatMessage] = []
pending_tool_call_ids: set[str] | None = None
pending_confirm_changes_id: str | None = None
for msg in messages:
role_value = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
if role_value == "assistant":
tool_ids = {
str(content.call_id)
for content in msg.contents or []
if isinstance(content, FunctionCallContent) and content.call_id
}
# Check for confirm_changes tool call
confirm_changes_call = None
for content in msg.contents or []:
if isinstance(content, FunctionCallContent) and content.name == "confirm_changes":
confirm_changes_call = content
break
sanitized.append(msg)
pending_tool_call_ids = tool_ids if tool_ids else None
pending_confirm_changes_id = (
str(confirm_changes_call.call_id)
if confirm_changes_call and confirm_changes_call.call_id
else None
)
continue
if role_value == "user" and pending_confirm_changes_id:
# Check if this is a confirm_changes response (JSON with "accepted" field)
user_text = ""
for content in msg.contents or []:
if isinstance(content, TextContent):
user_text = content.text
break
try:
parsed = json.loads(user_text)
if "accepted" in parsed:
# This is a confirm_changes response - inject synthetic tool result
logger.info(
f"Injecting synthetic tool result for confirm_changes call_id={pending_confirm_changes_id}"
)
synthetic_result = ChatMessage(
role="tool",
contents=[
FunctionResultContent(
call_id=pending_confirm_changes_id,
result="Confirmed" if parsed.get("accepted") else "Rejected",
)
],
)
sanitized.append(synthetic_result)
if pending_tool_call_ids:
pending_tool_call_ids.discard(pending_confirm_changes_id)
pending_confirm_changes_id = None
# Don't add the user message to sanitized - it's been converted to tool result
continue
except (json.JSONDecodeError, KeyError) as e:
# Failed to parse user message as confirm_changes response; continue normal processing
logger.debug(f"Could not parse user message as confirm_changes response: {e}")
# Not a confirm_changes response, continue normal processing
sanitized.append(msg)
pending_tool_call_ids = None
pending_confirm_changes_id = None
continue
if role_value == "tool":
if not pending_tool_call_ids:
continue
keep = False
for content in msg.contents or []:
if isinstance(content, FunctionResultContent):
call_id = str(content.call_id)
if call_id in pending_tool_call_ids:
keep = True
break
if keep:
sanitized.append(msg)
continue
sanitized.append(msg)
pending_tool_call_ids = None
pending_confirm_changes_id = None
return sanitized
# Create event bridge
event_bridge = AgentFrameworkEventBridge(
run_id=context.run_id,
@@ -328,22 +428,151 @@ class DefaultOrchestrator(Orchestrator):
if current_state:
thread.metadata["current_state"] = current_state # type: ignore[attr-defined]
# Add incoming AG-UI messages to the thread history
if context.messages:
await thread.on_new_messages(context.messages)
# Use the full incoming message batch to preserve tool-call adjacency
if not context.messages:
raw_messages = context.messages or []
if not raw_messages:
logger.warning("No messages provided in AG-UI input")
yield event_bridge.create_run_finished_event()
return
logger.info(f"Received {len(raw_messages)} raw messages from client")
for i, msg in enumerate(raw_messages):
role = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
msg_id = getattr(msg, "message_id", None)
logger.info(f" Raw message {i}: role={role}, id={msg_id}")
if hasattr(msg, "contents") and msg.contents:
for j, content in enumerate(msg.contents):
content_type = type(content).__name__
if isinstance(content, TextContent):
logger.debug(f" Content {j}: {content_type} - {content.text}")
elif isinstance(content, FunctionCallContent):
logger.debug(f" Content {j}: {content_type} - {content.name}({content.arguments})")
elif isinstance(content, FunctionResultContent):
logger.debug(
f" Content {j}: {content_type} - call_id={content.call_id}, result={content.result}"
)
else:
logger.debug(f" Content {j}: {content_type} - {content}")
# After getting sanitized_messages, deduplicate them
def deduplicate_messages(messages: list[ChatMessage]) -> list[ChatMessage]:
"""Remove duplicate messages while preserving order.
For tool results with the same call_id, prefer the one with actual data.
"""
seen_keys: dict[Any, int] = {} # key -> index in unique_messages (key can be various tuple types)
unique_messages: list[ChatMessage] = []
for idx, msg in enumerate(messages):
role_value = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
# For tool messages, use call_id as unique key
if role_value == "tool" and msg.contents and isinstance(msg.contents[0], FunctionResultContent):
call_id = str(msg.contents[0].call_id)
key: Any = (role_value, call_id)
# Check if we already have this tool result
if key in seen_keys:
existing_idx = seen_keys[key]
existing_msg = unique_messages[existing_idx]
# Compare results - prefer non-empty over empty
existing_result = None
if existing_msg.contents and isinstance(existing_msg.contents[0], FunctionResultContent):
existing_result = existing_msg.contents[0].result
new_result = msg.contents[0].result
# Replace if existing is empty/None and new has data
if (not existing_result or existing_result == "") and new_result:
logger.info(
f"Replacing empty tool result at index {existing_idx} with data from index {idx}"
)
unique_messages[existing_idx] = msg
else:
logger.info(f"Skipping duplicate tool result at index {idx}: call_id={call_id}")
continue
seen_keys[key] = len(unique_messages)
unique_messages.append(msg)
elif (
role_value == "assistant"
and msg.contents
and any(isinstance(c, FunctionCallContent) for c in msg.contents)
):
# For assistant messages with tool_calls, use the tool call IDs
tool_call_ids = tuple(
sorted(str(c.call_id) for c in msg.contents if isinstance(c, FunctionCallContent) and c.call_id)
)
key = (role_value, tool_call_ids)
if key in seen_keys:
logger.info(f"Skipping duplicate assistant tool call at index {idx}")
continue
seen_keys[key] = len(unique_messages)
unique_messages.append(msg)
else:
# For other messages (system, user, assistant without tools), hash the content
content_str = str([str(c) for c in msg.contents]) if msg.contents else ""
key = (role_value, hash(content_str))
if key in seen_keys:
logger.info(f"Skipping duplicate message at index {idx}: role={role_value}")
continue
seen_keys[key] = len(unique_messages)
unique_messages.append(msg)
return unique_messages
# Then use it:
sanitized_messages = sanitize_tool_history(raw_messages)
provider_messages = deduplicate_messages(sanitized_messages)
if not provider_messages:
logger.info("No provider-eligible messages after filtering; finishing run without invoking agent.")
yield event_bridge.create_run_finished_event()
return
logger.info(f"Processing {len(provider_messages)} provider messages after sanitization/deduplication")
for i, msg in enumerate(provider_messages):
role = msg.role.value if hasattr(msg.role, "value") else str(msg.role)
logger.info(f" Message {i}: role={role}")
if hasattr(msg, "contents") and msg.contents:
for j, content in enumerate(msg.contents):
content_type = type(content).__name__
if isinstance(content, TextContent):
logger.info(f" Content {j}: {content_type} - {content.text}")
elif isinstance(content, FunctionCallContent):
logger.info(f" Content {j}: {content_type} - {content.name}({content.arguments})")
elif isinstance(content, FunctionResultContent):
logger.info(
f" Content {j}: {content_type} - call_id={content.call_id}, result={content.result}"
)
else:
logger.info(f" Content {j}: {content_type} - {content}")
# NOTE: For AG-UI, the client sends the full conversation history on each request.
# We should NOT add to thread.on_new_messages() as that would cause duplication.
# Instead, we pass messages directly to the agent via messages_to_run.
# Inject current state as system message context if we have state
messages_to_run: list[Any] = []
if current_state and context.config.state_schema:
state_json = json.dumps(current_state, indent=2)
from agent_framework import ChatMessage
conversation_has_tool_calls = False
logger.debug(f"Checking {len(provider_messages)} provider messages for tool calls")
for i, msg in enumerate(provider_messages):
logger.debug(
f" Message {i}: role={msg.role.value}, contents={len(msg.contents) if hasattr(msg, 'contents') and msg.contents else 0}"
)
for msg in provider_messages:
if msg.role.value == "assistant" and hasattr(msg, "contents") and msg.contents:
if any(isinstance(content, FunctionCallContent) for content in msg.contents):
conversation_has_tool_calls = True
break
if current_state and context.config.state_schema and not conversation_has_tool_calls:
state_json = json.dumps(current_state, indent=2)
state_context_msg = ChatMessage(
role="system",
contents=[
@@ -359,9 +588,9 @@ Never replace existing data - always append or merge."""
)
messages_to_run.append(state_context_msg)
# Preserve order from client to satisfy provider constraints (assistant tool_calls must
# immediately precede tool result messages). Using the full batch avoids reordering.
messages_to_run.extend(context.messages)
# Add all provider messages to messages_to_run
# AG-UI sends full conversation history on each request, so we pass it directly to the agent
messages_to_run.extend(provider_messages)
# Handle client tools for hybrid execution
# Client sends tool metadata, server merges with its own tools.
@@ -370,11 +599,23 @@ Never replace existing data - always append or merge."""
from agent_framework import BaseChatClient
client_tools = convert_agui_tools_to_agent_framework(context.input_data.get("tools"))
logger.info(f"[TOOLS] Client sent {len(client_tools) if client_tools else 0} tools")
if client_tools:
for tool in client_tools:
tool_name = getattr(tool, "name", "unknown")
declaration_only = getattr(tool, "declaration_only", None)
logger.info(f"[TOOLS] - Client tool: {tool_name}, declaration_only={declaration_only}")
# Extract server tools - use type narrowing when possible
server_tools: list[Any] = []
if isinstance(context.agent, ChatAgent):
server_tools = context.agent.chat_options.tools or []
tools_from_agent = context.agent.chat_options.tools
server_tools = list(tools_from_agent) if tools_from_agent else []
logger.info(f"[TOOLS] Agent has {len(server_tools)} configured tools")
for tool in server_tools:
tool_name = getattr(tool, "name", "unknown")
approval_mode = getattr(tool, "approval_mode", None)
logger.info(f"[TOOLS] - {tool_name}: approval_mode={approval_mode}")
else:
# AgentProtocol allows duck-typed implementations - fallback to attribute access
# This supports test mocks and custom agent implementations
@@ -412,15 +653,37 @@ Never replace existing data - always append or merge."""
except AttributeError:
pass
combined_tools: list[Any] = []
if server_tools:
combined_tools.extend(server_tools)
# For tools parameter: only pass if we have client tools to add
# If we pass tools=, it overrides the agent's configured tools and loses metadata like approval_mode
# So only pass tools when we need to add client tools on top of server tools
# IMPORTANT: Don't include client tools that duplicate server tools (same name)
tools_param = None
if client_tools:
combined_tools.extend(client_tools)
# Get server tool names
server_tool_names = {getattr(tool, "name", None) for tool in server_tools}
# Filter out client tools that duplicate server tools
unique_client_tools = [
tool for tool in client_tools if getattr(tool, "name", None) not in server_tool_names
]
if unique_client_tools:
combined_tools: list[Any] = []
if server_tools:
combined_tools.extend(server_tools)
combined_tools.extend(unique_client_tools)
tools_param = combined_tools
logger.info(
f"[TOOLS] Passing tools= parameter with {len(combined_tools)} tools ({len(server_tools)} server + {len(unique_client_tools)} unique client)"
)
else:
logger.info("[TOOLS] All client tools duplicate server tools - not passing tools= parameter")
else:
logger.info("[TOOLS] No client tools - not passing tools= parameter (using agent's configured tools)")
# Collect all updates to get the final structured output
all_updates: list[Any] = []
async for update in context.agent.run_stream(messages_to_run, thread=thread, tools=combined_tools or None):
async for update in context.agent.run_stream(messages_to_run, thread=thread, tools=tools_param):
all_updates.append(update)
events = await event_bridge.from_agent_run_update(update)
for event in events:
@@ -432,6 +695,27 @@ Never replace existing data - always append or merge."""
yield event_bridge.create_run_finished_event()
return
# Check if there are pending tool calls (declaration-only tools that weren't executed)
# These need ToolCallEndEvent to signal the client to execute them
# Only emit for tool calls that haven't already had ToolCallEndEvent emitted
# (approval-required tools already had their end event emitted)
if event_bridge.pending_tool_calls:
pending_without_end = [
tc for tc in event_bridge.pending_tool_calls if tc.get("id") not in event_bridge.tool_calls_ended
]
if pending_without_end:
logger.info(
f"Found {len(pending_without_end)} pending tool calls without end event - emitting ToolCallEndEvent"
)
for tool_call in pending_without_end:
tool_call_id = tool_call.get("id")
if tool_call_id:
from ag_ui.core import ToolCallEndEvent
end_event = ToolCallEndEvent(tool_call_id=tool_call_id)
logger.info(f"Emitting ToolCallEndEvent for declaration-only tool call '{tool_call_id}'")
yield end_event
# After streaming completes, check if agent has response_format and extract structured output
if all_updates and response_format:
from agent_framework import AgentRunResponse
@@ -10,11 +10,37 @@ pip install agent-framework-ag-ui
## Quick Start
### Using Example Agents with Any Chat Client
All example agents are factory functions that accept any `ChatClientProtocol`-compatible chat client:
```python
from fastapi import FastAPI
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.openai import OpenAIChatClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework_ag_ui_examples.agents import simple_agent, weather_agent
app = FastAPI()
# Option 1: Use Azure OpenAI
azure_client = AzureOpenAIChatClient(model_id="gpt-4")
add_agent_framework_fastapi_endpoint(app, simple_agent(azure_client), "/chat")
# Option 2: Use OpenAI
openai_client = OpenAIChatClient(model_id="gpt-4o")
add_agent_framework_fastapi_endpoint(app, weather_agent(openai_client), "/weather")
# Run with: uvicorn main:app --reload
```
### Creating Your Own Agent
```python
from fastapi import FastAPI
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
# Create your agent
agent = ChatAgent(
@@ -44,38 +70,97 @@ This integration supports all 7 AG-UI features:
## Examples
Complete examples for all features are in the `examples/` directory:
All example agents are implemented as **factory functions** that accept any chat client implementing `ChatClientProtocol`. This provides maximum flexibility to use Azure OpenAI, OpenAI, Anthropic, or any custom chat client implementation.
- `examples/agents/simple_agent.py` - Basic agentic chat
- `examples/agents/weather_agent.py` - Backend tool rendering
- `examples/agents/task_planner_agent.py` - Human in the loop with approvals
- `examples/agents/research_assistant_agent.py` - Agentic generative UI
- `examples/agents/ui_generator_agent.py` - Tool-based generative UI
- `examples/agents/recipe_agent.py` - Shared state management
- `examples/agents/document_writer_agent.py` - Predictive state updates
- `examples/server/main.py` - FastAPI server with all endpoints
### Available Example Agents
Run the example server:
Complete examples for all AG-UI features are available:
```bash
cd examples/server
uvicorn main:app --reload
- `simple_agent(chat_client)` - Basic agentic chat (Feature 1)
- `weather_agent(chat_client)` - Backend tool rendering (Feature 2)
- `human_in_the_loop_agent(chat_client)` - Human-in-the-loop with step customization (Feature 3)
- `task_steps_agent_wrapped(chat_client)` - Agentic generative UI with step execution (Feature 4)
- `ui_generator_agent(chat_client)` - Tool-based generative UI (Feature 5)
- `recipe_agent(chat_client)` - Shared state management (Feature 6)
- `document_writer_agent(chat_client)` - Predictive state updates (Feature 7)
- `research_assistant_agent(chat_client)` - Research with progress events
- `task_planner_agent(chat_client)` - Task planning with approvals
### Using Example Agents
```python
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.openai import OpenAIChatClient
from agent_framework_ag_ui_examples.agents import (
simple_agent,
weather_agent,
recipe_agent,
)
# Create a chat client (use any ChatClientProtocol implementation)
azure_client = AzureOpenAIChatClient(model_id="gpt-4")
openai_client = OpenAIChatClient(model_id="gpt-4o")
# Create agent instances by calling the factory functions
agent1 = simple_agent(azure_client)
agent2 = weather_agent(openai_client)
agent3 = recipe_agent(azure_client)
```
To enable debug logging:
### Running the Example Server
The example server demonstrates all 7 AG-UI features:
```bash
ENABLE_DEBUG_LOGGING=1 uvicorn main:app --reload
# Install the package
pip install agent-framework-ag-ui
# Run the example server
python -m agent_framework_ag_ui_examples
# Or with debug logging
ENABLE_DEBUG_LOGGING=1 python -m agent_framework_ag_ui_examples
```
The server exposes endpoints at:
- `/agentic_chat`
- `/backend_tool_rendering`
- `/human_in_the_loop`
- `/agentic_generative_ui`
- `/tool_based_generative_ui`
- `/shared_state`
- `/predictive_state_updates`
- `/agentic_chat` - Simple chat with `simple_agent`
- `/backend_tool_rendering` - Weather tools with `weather_agent`
- `/human_in_the_loop` - Step approval with `human_in_the_loop_agent`
- `/agentic_generative_ui` - Task steps with `task_steps_agent_wrapped`
- `/tool_based_generative_ui` - Custom UI components with `ui_generator_agent`
- `/shared_state` - Recipe management with `recipe_agent`
- `/predictive_state_updates` - Document writing with `document_writer_agent`
### Complete FastAPI Example
```python
from fastapi import FastAPI
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from agent_framework_ag_ui_examples.agents import (
simple_agent,
weather_agent,
human_in_the_loop_agent,
task_steps_agent_wrapped,
ui_generator_agent,
recipe_agent,
document_writer_agent,
)
app = FastAPI(title="AG-UI Examples")
# Create a chat client (shared across all agents, or create individual ones)
chat_client = AzureOpenAIChatClient(model_id="gpt-4")
# Add all example endpoints
add_agent_framework_fastapi_endpoint(app, simple_agent(chat_client), "/agentic_chat")
add_agent_framework_fastapi_endpoint(app, weather_agent(chat_client), "/backend_tool_rendering")
add_agent_framework_fastapi_endpoint(app, human_in_the_loop_agent(chat_client), "/human_in_the_loop")
add_agent_framework_fastapi_endpoint(app, task_steps_agent_wrapped(chat_client), "/agentic_generative_ui") # type: ignore[arg-type]
add_agent_framework_fastapi_endpoint(app, ui_generator_agent(chat_client), "/tool_based_generative_ui")
add_agent_framework_fastapi_endpoint(app, recipe_agent(chat_client), "/shared_state")
add_agent_framework_fastapi_endpoint(app, document_writer_agent(chat_client), "/predictive_state_updates")
```
## Architecture
@@ -97,6 +182,48 @@ The package uses a clean, orchestrator-based architecture:
## Advanced Usage
### Creating Custom Agent Factories
You can create your own agent factories following the same pattern as the examples:
```python
from agent_framework import ChatAgent, ai_function
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent
@ai_function
def my_tool(param: str) -> str:
"""My custom tool."""
return f"Result: {param}"
def my_custom_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"""Create a custom agent with the specified chat client.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured AgentFrameworkAgent instance
"""
agent = ChatAgent(
name="my_custom_agent",
instructions="Custom instructions here",
chat_client=chat_client,
tools=[my_tool],
)
return AgentFrameworkAgent(
agent=agent,
name="MyCustomAgent",
description="My custom agent description",
)
# Use it
from agent_framework.azure import AzureOpenAIChatClient
chat_client = AzureOpenAIChatClient()
agent = my_custom_agent(chat_client)
```
### Shared State
State is injected as system messages and updated via predictive state updates:
@@ -6,7 +6,7 @@ from .document_writer_agent import document_writer_agent
from .human_in_the_loop_agent import human_in_the_loop_agent
from .recipe_agent import recipe_agent
from .research_assistant_agent import research_assistant_agent
from .simple_agent import agent as simple_agent
from .simple_agent import simple_agent
from .task_planner_agent import task_planner_agent
from .task_steps_agent import task_steps_agent_wrapped
from .ui_generator_agent import ui_generator_agent
@@ -3,7 +3,7 @@
"""Example agent demonstrating predictive state updates with document writing."""
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent, DocumentWriterConfirmationStrategy
@@ -28,31 +28,43 @@ def write_document_local(document: str) -> str:
return "Document written."
agent = ChatAgent(
name="document_writer",
instructions=(
"You are a helpful assistant for writing documents. "
"To write the document, you MUST use the write_document_local tool. "
"You MUST write the full document, even when changing only a few words. "
"When you wrote the document, DO NOT repeat it as a message. "
"Just briefly summarize the changes you made. 2 sentences max. "
"\n\n"
"The current state of the document will be provided to you. "
"When editing, make minimal changes - do not change every word unless requested."
),
chat_client=AzureOpenAIChatClient(),
tools=[write_document_local],
_DOCUMENT_WRITER_INSTRUCTIONS = (
"You are a helpful assistant for writing documents. "
"To write the document, you MUST use the write_document_local tool. "
"You MUST write the full document, even when changing only a few words. "
"When you wrote the document, DO NOT repeat it as a message. "
"Just briefly summarize the changes you made. 2 sentences max. "
"\n\n"
"The current state of the document will be provided to you. "
"When editing, make minimal changes - do not change every word unless requested."
)
document_writer_agent = AgentFrameworkAgent(
agent=agent,
name="DocumentWriter",
description="Writes and edits documents with predictive state updates",
state_schema={
"document": {"type": "string", "description": "The current document content"},
},
predict_state_config={
"document": {"tool": "write_document_local", "tool_argument": "document"},
},
confirmation_strategy=DocumentWriterConfirmationStrategy(),
)
def document_writer_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"""Create a document writer agent with predictive state updates.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured AgentFrameworkAgent instance with document writing capabilities
"""
agent = ChatAgent(
name="document_writer",
instructions=_DOCUMENT_WRITER_INSTRUCTIONS,
chat_client=chat_client,
tools=[write_document_local],
)
return AgentFrameworkAgent(
agent=agent,
name="DocumentWriter",
description="Writes and edits documents with predictive state updates",
state_schema={
"document": {"type": "string", "description": "The current document content"},
},
predict_state_config={
"document": {"tool": "write_document_local", "tool_argument": "document"},
},
confirmation_strategy=DocumentWriterConfirmationStrategy(),
)
@@ -5,7 +5,7 @@
from enum import Enum
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
from pydantic import BaseModel, Field
@@ -43,10 +43,18 @@ def generate_task_steps(steps: list[TaskStep]) -> str:
return f"Generated {len(steps)} execution steps for the task."
# Create the human-in-the-loop agent using tool-based approach for predictive state
human_in_the_loop_agent = ChatAgent(
name="human_in_the_loop_agent",
instructions="""You are a helpful assistant that can perform any task by breaking it down into steps.
def human_in_the_loop_agent(chat_client: ChatClientProtocol) -> ChatAgent:
"""Create a human-in-the-loop agent using tool-based approach for predictive state.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured ChatAgent instance with human-in-the-loop capabilities
"""
return ChatAgent(
name="human_in_the_loop_agent",
instructions="""You are a helpful assistant that can perform any task by breaking it down into steps.
When asked to perform a task, you MUST call the `generate_task_steps` function with the proper
number of steps per the request.
@@ -71,6 +79,6 @@ human_in_the_loop_agent = ChatAgent(
After calling the function, provide a brief acknowledgment like:
"I've created a plan with 10 steps. You can customize which steps to enable before I proceed."
""",
chat_client=AzureOpenAIChatClient(),
tools=[generate_task_steps],
)
chat_client=chat_client,
tools=[generate_task_steps],
)
@@ -5,7 +5,7 @@
from enum import Enum
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
from pydantic import BaseModel, Field
from agent_framework_ag_ui import AgentFrameworkAgent, RecipeConfirmationStrategy
@@ -67,10 +67,7 @@ def update_recipe(recipe: Recipe) -> str:
return "Recipe updated."
# Create the recipe agent using tool-based approach for streaming
agent = ChatAgent(
name="recipe_agent",
instructions="""You are a helpful recipe assistant that creates and modifies recipes.
_RECIPE_INSTRUCTIONS = """You are a helpful recipe assistant that creates and modifies recipes.
CRITICAL RULES:
1. You will receive the current recipe state in the system context
@@ -103,20 +100,34 @@ agent = ChatAgent(
- Add aromatics: garlic, shallots
- Add finishing touches: lemon zest, fresh parsley
- Make instructions more detailed and professional
""",
chat_client=AzureOpenAIChatClient(),
tools=[update_recipe],
)
"""
recipe_agent = AgentFrameworkAgent(
agent=agent,
name="RecipeAgent",
description="Creates and modifies recipes with streaming state updates",
state_schema={
"recipe": {"type": "object", "description": "The current recipe"},
},
predict_state_config={
"recipe": {"tool": "update_recipe", "tool_argument": "recipe"},
},
confirmation_strategy=RecipeConfirmationStrategy(),
)
def recipe_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"""Create a recipe agent with streaming state updates.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured AgentFrameworkAgent instance with recipe management
"""
agent = ChatAgent(
name="recipe_agent",
instructions=_RECIPE_INSTRUCTIONS,
chat_client=chat_client,
tools=[update_recipe],
)
return AgentFrameworkAgent(
agent=agent,
name="RecipeAgent",
description="Creates and modifies recipes with streaming state updates",
state_schema={
"recipe": {"type": "object", "description": "The current recipe"},
},
predict_state_config={
"recipe": {"tool": "update_recipe", "tool_argument": "recipe"},
},
confirmation_strategy=RecipeConfirmationStrategy(),
)
@@ -5,7 +5,7 @@
import asyncio
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent
@@ -82,19 +82,31 @@ async def analyze_data(dataset: str) -> str:
return f"Analysis of '{dataset}':\n" + "\n".join(insights)
agent = ChatAgent(
name="research_assistant",
instructions=(
"You are a research and analysis assistant. "
"You can research topics, create presentations, and analyze data. "
"Use the available tools to help users with their research needs."
),
chat_client=AzureOpenAIChatClient(),
tools=[research_topic, create_presentation, analyze_data],
_RESEARCH_ASSISTANT_INSTRUCTIONS = (
"You are a research and analysis assistant. "
"You can research topics, create presentations, and analyze data. "
"Use the available tools to help users with their research needs."
)
research_assistant_agent = AgentFrameworkAgent(
agent=agent,
name="ResearchAssistant",
description="Research assistant that emits progress events during task execution",
)
def research_assistant_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"""Create a research assistant agent with progress events.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured AgentFrameworkAgent instance with research capabilities
"""
agent = ChatAgent(
name="research_assistant",
instructions=_RESEARCH_ASSISTANT_INSTRUCTIONS,
chat_client=chat_client,
tools=[research_topic, create_presentation, analyze_data],
)
return AgentFrameworkAgent(
agent=agent,
name="ResearchAssistant",
description="Research assistant that emits progress events during task execution",
)
@@ -3,11 +3,20 @@
"""Simple agentic chat example (Feature 1: Agentic Chat)."""
from agent_framework import ChatAgent
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
# Create a simple chat agent
agent = ChatAgent(
name="simple_chat_agent",
instructions="You are a helpful assistant. Be concise and friendly.",
chat_client=AzureOpenAIChatClient(),
)
def simple_agent(chat_client: ChatClientProtocol) -> ChatAgent:
"""Create a simple chat agent.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured ChatAgent instance
"""
return ChatAgent(
name="simple_chat_agent",
instructions="You are a helpful assistant. Be concise and friendly.",
chat_client=chat_client,
)
@@ -3,7 +3,7 @@
"""Example agent demonstrating human-in-the-loop with function approvals."""
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent, TaskPlannerConfirmationStrategy
@@ -54,20 +54,32 @@ def book_meeting_room(room_name: str, date: str, start_time: str, end_time: str)
return f"Meeting room '{room_name}' booked for {date} from {start_time} to {end_time}"
agent = ChatAgent(
name="task_planner",
instructions=(
"You are a helpful assistant that plans and executes tasks. "
"You have access to calendar, email, and meeting room booking functions. "
"All of these actions require user approval before execution."
),
chat_client=AzureOpenAIChatClient(),
tools=[create_calendar_event, send_email, book_meeting_room],
_TASK_PLANNER_INSTRUCTIONS = (
"You are a helpful assistant that plans and executes tasks. "
"You have access to calendar, email, and meeting room booking functions. "
"All of these actions require user approval before execution."
)
task_planner_agent = AgentFrameworkAgent(
agent=agent,
name="TaskPlanner",
description="Plans and executes tasks with user approval",
confirmation_strategy=TaskPlannerConfirmationStrategy(),
)
def task_planner_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"""Create a task planner agent with user approval for actions.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured AgentFrameworkAgent instance with task planning capabilities
"""
agent = ChatAgent(
name="task_planner",
instructions=_TASK_PLANNER_INSTRUCTIONS,
chat_client=chat_client,
tools=[create_calendar_event, send_email, book_meeting_room],
)
return AgentFrameworkAgent(
agent=agent,
name="TaskPlanner",
description="Plans and executes tasks with user approval",
confirmation_strategy=TaskPlannerConfirmationStrategy(),
)
@@ -19,7 +19,7 @@ from ag_ui.core import (
ToolCallStartEvent,
)
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
from pydantic import BaseModel, Field
from agent_framework_ag_ui import AgentFrameworkAgent
@@ -54,10 +54,18 @@ def generate_task_steps(steps: list[TaskStep]) -> str:
return "Steps generated."
# Create the task steps agent using tool-based approach for streaming
agent = ChatAgent(
name="task_steps_agent",
instructions="""You are a helpful assistant that breaks down tasks into actionable steps.
def _create_task_steps_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"""Create the task steps agent using tool-based approach for streaming.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured AgentFrameworkAgent instance
"""
agent = ChatAgent(
name="task_steps_agent",
instructions="""You are a helpful assistant that breaks down tasks into actionable steps.
When asked to perform a task, you MUST:
1. Use the generate_task_steps tool to create the steps
@@ -75,25 +83,25 @@ agent = ChatAgent(
- "Installing platform"
- "Adding finishing touches"
""",
chat_client=AzureOpenAIChatClient(),
tools=[generate_task_steps],
)
chat_client=chat_client,
tools=[generate_task_steps],
)
task_steps_agent = AgentFrameworkAgent(
agent=agent,
name="TaskStepsAgent",
description="Generates task steps with streaming state updates",
state_schema={
"steps": {"type": "array", "description": "The list of task steps"},
},
predict_state_config={
"steps": {
"tool": "generate_task_steps",
"tool_argument": "steps",
}
},
require_confirmation=False, # Agentic generative UI updates automatically without confirmation
)
return AgentFrameworkAgent(
agent=agent,
name="TaskStepsAgent",
description="Generates task steps with streaming state updates",
state_schema={
"steps": {"type": "array", "description": "The list of task steps"},
},
predict_state_config={
"steps": {
"tool": "generate_task_steps",
"tool_argument": "steps",
}
},
require_confirmation=False, # Agentic generative UI updates automatically without confirmation
)
# Wrap the agent's run method to add step execution simulation
@@ -131,7 +139,7 @@ class TaskStepsAgentWithExecution:
logger.info("TaskStepsAgentWithExecution.run_agent() called - wrapper is active")
# First, run the base agent to generate the plan - buffer text messages
final_state: dict[str, Any] | None = None
final_state: dict[str, Any] = {}
run_finished_event: Any = None
tool_call_id: str | None = None
buffered_text_events: list[Any] = [] # Buffer text from first LLM call
@@ -142,9 +150,20 @@ class TaskStepsAgentWithExecution:
match event:
case StateSnapshotEvent(snapshot=snapshot):
final_state = snapshot
final_state = snapshot.copy() if snapshot else {}
logger.info(f"Captured STATE_SNAPSHOT event with state: {final_state}")
yield event
case StateDeltaEvent(delta=delta):
# Apply state delta to final_state
if delta:
for patch in delta:
if patch.get("op") == "replace" and patch.get("path") == "/steps":
final_state["steps"] = patch.get("value", [])
logger.info(
f"Applied STATE_DELTA: updated steps to {len(final_state.get('steps', []))} items"
)
logger.info(f"Yielding event immediately: {event_type_str}")
yield event
case RunFinishedEvent():
run_finished_event = event
logger.info("Captured RUN_FINISHED event - will send after step execution and summary")
@@ -314,5 +333,14 @@ class TaskStepsAgentWithExecution:
yield run_finished_event
# Export the wrapped agent
task_steps_agent_wrapped = TaskStepsAgentWithExecution(task_steps_agent)
def task_steps_agent_wrapped(chat_client: ChatClientProtocol) -> TaskStepsAgentWithExecution:
"""Create a task steps agent with execution simulation.
Args:
chat_client: The chat client to use for the agent
Returns:
A wrapped agent instance with step execution simulation
"""
base_agent = _create_task_steps_agent(chat_client)
return TaskStepsAgentWithExecution(base_agent)
@@ -4,23 +4,39 @@
from typing import Any
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework import AIFunction, ChatAgent
from agent_framework._clients import ChatClientProtocol
from agent_framework_ag_ui import AgentFrameworkAgent
@ai_function
def generate_haiku(english: list[str], japanese: list[str], image_name: str | None, gradient: str) -> str:
"""Generate a haiku with image and gradient background (FRONTEND_RENDER).
# Declaration-only tools (func=None) - actual rendering happens on the client side
generate_haiku = AIFunction[Any, str](
name="generate_haiku",
description="""Generate a haiku with image and gradient background (FRONTEND_RENDER).
This tool generates UI for displaying a haiku with an image and gradient background.
The frontend should render this as a custom haiku component.
Args:
english: English haiku lines (exactly 3 lines)
japanese: Japanese haiku lines (exactly 3 lines)
image_name: Image filename for visual accompaniment. Must be one of:
The frontend should render this as a custom haiku component.""",
func=None, # Makes declaration_only=True so client renders the UI
input_model={
"type": "object",
"properties": {
"english": {
"type": "array",
"items": {"type": "string"},
"description": "English haiku lines (exactly 3 lines)",
"minItems": 3,
"maxItems": 3,
},
"japanese": {
"type": "array",
"items": {"type": "string"},
"description": "Japanese haiku lines (exactly 3 lines)",
"minItems": 3,
"maxItems": 3,
},
"image_name": {
"type": "string",
"description": """Image filename for visual accompaniment. Must be one of:
- "Osaka_Castle_Turret_Stone_Wall_Pine_Trees_Daytime.jpg"
- "Tokyo_Skyline_Night_Tokyo_Tower_Mount_Fuji_View.jpg"
- "Itsukushima_Shrine_Miyajima_Floating_Torii_Gate_Sunset_Long_Exposure.jpg"
@@ -31,71 +47,100 @@ def generate_haiku(english: list[str], japanese: list[str], image_name: str | No
- "Senso-ji_Temple_Asakusa_Cherry_Blossoms_Kimono_Umbrella.jpg"
- "Cherry_Blossoms_Sakura_Night_View_City_Lights_Japan.jpg"
- "Mount_Fuji_Lake_Reflection_Cherry_Blossoms_Sakura_Spring.jpg"
gradient: CSS gradient string for background (e.g., "linear-gradient(135deg, #667eea 0%, #764ba2 100%)")
""",
},
"gradient": {
"type": "string",
"description": 'CSS gradient string for background (e.g., "linear-gradient(135deg, #667eea 0%, #764ba2 100%)")',
},
},
"required": ["english", "japanese", "image_name", "gradient"],
},
)
Returns:
Haiku metadata for frontend rendering
"""
return f"Haiku generated with image: {image_name}"
@ai_function
def create_chart(chart_type: str, data_points: list[dict[str, Any]], title: str) -> str:
"""Create an interactive chart (FRONTEND_RENDER).
create_chart = AIFunction[Any, str](
name="create_chart",
description="""Create an interactive chart (FRONTEND_RENDER).
This tool creates chart specifications for frontend rendering.
The frontend should render this as an interactive chart component.
The frontend should render this as an interactive chart component.""",
func=None, # Makes declaration_only=True so client renders the UI
input_model={
"type": "object",
"properties": {
"chart_type": {
"type": "string",
"description": "Type of chart (bar, line, pie, scatter)",
},
"data_points": {
"type": "array",
"items": {"type": "object"},
"description": "Data points for the chart",
},
"title": {
"type": "string",
"description": "Chart title",
},
},
"required": ["chart_type", "data_points", "title"],
},
)
Args:
chart_type: Type of chart (bar, line, pie, scatter)
data_points: Data points for the chart
title: Chart title
Returns:
Chart specification for frontend rendering
"""
return f"Chart '{title}' created with {len(data_points)} data points"
@ai_function
def display_timeline(events: list[dict[str, Any]], start_date: str, end_date: str) -> str:
"""Display an interactive timeline (FRONTEND_RENDER).
display_timeline = AIFunction[Any, str](
name="display_timeline",
description="""Display an interactive timeline (FRONTEND_RENDER).
This tool creates timeline specifications for frontend rendering.
The frontend should render this as an interactive timeline component.
The frontend should render this as an interactive timeline component.""",
func=None, # Makes declaration_only=True so client renders the UI
input_model={
"type": "object",
"properties": {
"events": {
"type": "array",
"items": {"type": "object"},
"description": "Events to display on the timeline",
},
"start_date": {
"type": "string",
"description": "Timeline start date",
},
"end_date": {
"type": "string",
"description": "Timeline end date",
},
},
"required": ["events", "start_date", "end_date"],
},
)
Args:
events: Events to display on the timeline
start_date: Timeline start date
end_date: Timeline end date
Returns:
Timeline specification for frontend rendering
"""
return f"Timeline created with {len(events)} events from {start_date} to {end_date}"
@ai_function
def show_comparison_table(items: list[dict[str, Any]], columns: list[str]) -> str:
"""Show a comparison table (FRONTEND_RENDER).
show_comparison_table = AIFunction[Any, str](
name="show_comparison_table",
description="""Show a comparison table (FRONTEND_RENDER).
This tool creates table specifications for frontend rendering.
The frontend should render this as an interactive comparison table.
Args:
items: Items to compare
columns: Column names
Returns:
Table specification for frontend rendering
"""
return f"Comparison table created with {len(items)} items and {len(columns)} columns"
The frontend should render this as an interactive comparison table.""",
func=None, # Makes declaration_only=True so client renders the UI
input_model={
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "object"},
"description": "Items to compare",
},
"columns": {
"type": "array",
"items": {"type": "string"},
"description": "Column names",
},
},
"required": ["items", "columns"],
},
)
# Create the UI generator agent using tool-based approach with forced tool usage
agent = ChatAgent(
name="ui_generator",
instructions="""You MUST use the provided tools to generate content. Never respond with plain text descriptions.
_UI_GENERATOR_INSTRUCTIONS = """You MUST use the provided tools to generate content. Never respond with plain text descriptions.
For haiku requests:
- Call generate_haiku tool with all 4 required parameters
@@ -105,15 +150,29 @@ agent = ChatAgent(
- gradient: CSS gradient string
For other requests, use the appropriate tool (create_chart, display_timeline, show_comparison_table).
""",
chat_client=AzureOpenAIChatClient(),
tools=[generate_haiku, create_chart, display_timeline, show_comparison_table],
# Force tool usage - the LLM MUST call a tool, cannot respond with plain text
chat_options={"tool_choice": "required"},
)
"""
ui_generator_agent = AgentFrameworkAgent(
agent=agent,
name="UIGenerator",
description="Generates custom UI components through tool calls",
)
def ui_generator_agent(chat_client: ChatClientProtocol) -> AgentFrameworkAgent:
"""Create a UI generator agent with frontend rendering tools.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured AgentFrameworkAgent instance with UI generation tools
"""
agent = ChatAgent(
name="ui_generator",
instructions=_UI_GENERATOR_INSTRUCTIONS,
chat_client=chat_client,
tools=[generate_haiku, create_chart, display_timeline, show_comparison_table],
# Force tool usage - the LLM MUST call a tool, cannot respond with plain text
chat_options={"tool_choice": "required"},
)
return AgentFrameworkAgent(
agent=agent,
name="UIGenerator",
description="Generates custom UI components through tool calls",
)
@@ -5,7 +5,7 @@
from typing import Any
from agent_framework import ChatAgent, ai_function
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework._clients import ChatClientProtocol
@ai_function
@@ -58,14 +58,22 @@ def get_forecast(location: str, days: int = 3) -> str:
return f"{days}-day forecast for {location}:\n" + "\n".join(forecast)
# Create the weather agent
weather_agent = ChatAgent(
name="weather_agent",
instructions=(
"You are a helpful weather assistant. "
"Use the get_weather and get_forecast functions to help users with weather information. "
"Always provide friendly and informative responses."
),
chat_client=AzureOpenAIChatClient(),
tools=[get_weather, get_forecast],
)
def weather_agent(chat_client: ChatClientProtocol) -> ChatAgent:
"""Create a weather agent with get_weather and get_forecast tools.
Args:
chat_client: The chat client to use for the agent
Returns:
A configured ChatAgent instance with weather tools
"""
return ChatAgent(
name="weather_agent",
instructions=(
"You are a helpful weather assistant. "
"Use the get_weather and get_forecast functions to help users with weather information. "
"Always provide friendly and informative responses."
),
chat_client=chat_client,
tools=[get_weather, get_forecast],
)
@@ -1,3 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""API endpoints for AG-UI examples."""
@@ -2,6 +2,7 @@
"""Backend tool rendering endpoint."""
from agent_framework.azure import AzureOpenAIChatClient
from fastapi import FastAPI
from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
@@ -15,8 +16,11 @@ def register_backend_tool_rendering(app: FastAPI) -> None:
Args:
app: The FastAPI application.
"""
# Create a chat client and call the factory function
chat_client = AzureOpenAIChatClient()
add_agent_framework_fastapi_endpoint(
app,
weather_agent,
weather_agent(chat_client),
"/backend_tool_rendering",
)
@@ -6,6 +6,7 @@ import logging
import os
import uvicorn
from agent_framework.azure import AzureOpenAIChatClient
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -14,8 +15,8 @@ from agent_framework_ag_ui import add_agent_framework_fastapi_endpoint
from ..agents.document_writer_agent import document_writer_agent
from ..agents.human_in_the_loop_agent import human_in_the_loop_agent
from ..agents.recipe_agent import recipe_agent
from ..agents.simple_agent import agent as simple_agent
from ..agents.task_steps_agent import task_steps_agent_wrapped as task_steps_agent # Custom wrapper
from ..agents.simple_agent import simple_agent
from ..agents.task_steps_agent import task_steps_agent_wrapped
from ..agents.ui_generator_agent import ui_generator_agent
from ..agents.weather_agent import weather_agent
@@ -58,38 +59,42 @@ app.add_middleware(
allow_headers=["*"],
)
# Create a shared chat client for all agents
# You can use different chat clients for different agents if needed
chat_client = AzureOpenAIChatClient()
# Agentic Chat - basic chat agent
add_agent_framework_fastapi_endpoint(
app=app,
agent=simple_agent,
agent=simple_agent(chat_client),
path="/agentic_chat",
)
# Backend Tool Rendering - agent with tools
add_agent_framework_fastapi_endpoint(
app=app,
agent=weather_agent,
agent=weather_agent(chat_client),
path="/backend_tool_rendering",
)
# Shared State - recipe agent with structured output
add_agent_framework_fastapi_endpoint(
app=app,
agent=recipe_agent,
agent=recipe_agent(chat_client),
path="/shared_state",
)
# Predictive State Updates - document writer with predictive state
add_agent_framework_fastapi_endpoint(
app=app,
agent=document_writer_agent,
agent=document_writer_agent(chat_client),
path="/predictive_state_updates",
)
# Human in the Loop - human-in-the-loop agent with step customization
add_agent_framework_fastapi_endpoint(
app=app,
agent=human_in_the_loop_agent,
agent=human_in_the_loop_agent(chat_client),
path="/human_in_the_loop",
state_schema={"steps": {"type": "array"}},
predict_state_config={"steps": {"tool": "generate_task_steps", "tool_argument": "steps"}},
@@ -98,23 +103,26 @@ add_agent_framework_fastapi_endpoint(
# Agentic Generative UI - task steps agent with streaming state updates
add_agent_framework_fastapi_endpoint(
app=app,
agent=task_steps_agent, # type: ignore[arg-type]
agent=task_steps_agent_wrapped(chat_client), # type: ignore[arg-type]
path="/agentic_generative_ui",
)
# Tool-based Generative UI - UI generator with frontend-rendered tools
add_agent_framework_fastapi_endpoint(
app=app,
agent=ui_generator_agent,
agent=ui_generator_agent(chat_client),
path="/tool_based_generative_ui",
)
def main():
"""Run the server."""
port = int(os.getenv("PORT", "8888"))
port = int(os.getenv("PORT", "8887"))
host = os.getenv("HOST", "127.0.0.1")
print(f"\nAG-UI Examples Server starting on http://{host}:{port}")
print("Set ENABLE_DEBUG_LOGGING=1 for detailed request logging\n")
# Use log_config=None to prevent uvicorn from reconfiguring logging
# This preserves our file + console logging setup
uvicorn.run(
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b251111"
version = "1.0.0b251112"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
@@ -505,17 +505,20 @@ async def test_error_handling_with_exception():
async def test_json_decode_error_in_tool_result():
"""Test handling of JSONDecodeError when parsing tool result."""
"""Test handling of orphaned tool result - should be sanitized out."""
from agent_framework_ag_ui import AgentFrameworkAgent
class MockChatClient:
async def get_streaming_response(self, messages, chat_options, **kwargs):
yield ChatResponseUpdate(contents=[TextContent(text="Fallback response")])
# Should not be called since orphaned tool result is dropped
if False:
yield
raise AssertionError("ChatClient should not be called with orphaned tool result")
agent = ChatAgent(name="test_agent", instructions="Test", chat_client=MockChatClient())
wrapper = AgentFrameworkAgent(agent=agent)
# Send invalid JSON as tool result
# Send invalid JSON as tool result without preceding tool call
input_data = {
"messages": [
{
@@ -530,10 +533,12 @@ async def test_json_decode_error_in_tool_result():
async for event in wrapper.run_agent(input_data):
events.append(event)
# Should fall through to normal agent processing
# Orphaned tool result should be sanitized out
# Only run lifecycle events should be emitted, no text/tool events
text_events = [e for e in events if e.type == "TEXT_MESSAGE_CONTENT"]
assert len(text_events) > 0
assert text_events[0].delta == "Fallback response"
tool_events = [e for e in events if e.type.startswith("TOOL_CALL")]
assert len(text_events) == 0
assert len(tool_events) == 0
async def test_suppressed_summary_with_document_state():
@@ -0,0 +1,811 @@
# Copyright (c) Microsoft. All rights reserved.
"""Comprehensive tests for orchestrator coverage."""
from collections.abc import AsyncGenerator
from types import SimpleNamespace
from typing import Any
from agent_framework import (
AgentRunResponseUpdate,
ChatMessage,
TextContent,
ai_function,
)
from pydantic import BaseModel
from agent_framework_ag_ui._agent import AgentConfig
from agent_framework_ag_ui._orchestrators import (
DefaultOrchestrator,
ExecutionContext,
HumanInTheLoopOrchestrator,
)
@ai_function(approval_mode="always_require")
def approval_tool(param: str) -> str:
"""Tool requiring approval."""
return f"executed: {param}"
class MockAgent:
"""Mock agent for testing."""
def __init__(self, updates: list[AgentRunResponseUpdate] | None = None) -> None:
self.updates = updates or [AgentRunResponseUpdate(contents=[TextContent(text="response")], role="assistant")]
self.chat_options = SimpleNamespace(tools=[approval_tool], response_format=None)
self.chat_client = SimpleNamespace(function_invocation_configuration=None)
self.messages_received: list[Any] = []
self.tools_received: list[Any] | None = None
async def run_stream(
self,
messages: list[Any],
*,
thread: Any = None,
tools: list[Any] | None = None,
) -> AsyncGenerator[AgentRunResponseUpdate, None]:
self.messages_received = messages
self.tools_received = tools
for update in self.updates:
yield update
async def test_human_in_the_loop_json_decode_error() -> None:
"""Test HumanInTheLoopOrchestrator handles invalid JSON in tool result."""
orchestrator = HumanInTheLoopOrchestrator()
input_data = {
"messages": [
{
"role": "tool",
"content": [{"type": "text", "text": "not valid json {"}],
}
],
}
messages = [
ChatMessage(
role="tool",
contents=[TextContent(text="not valid json {")],
additional_properties={"is_tool_result": True},
)
]
context = ExecutionContext(
input_data=input_data,
agent=MockAgent(),
config=AgentConfig(),
)
context._messages = messages
assert orchestrator.can_handle(context)
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should emit RunErrorEvent for invalid JSON
error_events = [e for e in events if e.type == "RUN_ERROR"]
assert len(error_events) == 1
assert "Invalid tool result format" in error_events[0].message
async def test_sanitize_tool_history_confirm_changes() -> None:
"""Test sanitize_tool_history logic for confirm_changes synthetic result."""
from agent_framework import ChatMessage, FunctionCallContent, TextContent
# Create messages that will trigger confirm_changes synthetic result injection
messages = [
ChatMessage(
role="assistant",
contents=[
FunctionCallContent(
name="confirm_changes",
call_id="call_confirm_123",
arguments='{"changes": "test"}',
)
],
),
ChatMessage(
role="user",
contents=[TextContent(text='{"accepted": true}')],
),
]
# The sanitize_tool_history function is internal to DefaultOrchestrator.run
# We'll test it indirectly by checking the orchestrator processes it correctly
orchestrator = DefaultOrchestrator()
# Use pre-constructed ChatMessage objects to bypass message adapter
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
# Override the messages property to use our pre-constructed messages
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Agent should receive synthetic tool result
assert len(agent.messages_received) > 0
tool_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "tool"
]
assert len(tool_messages) == 1
assert str(tool_messages[0].contents[0].call_id) == "call_confirm_123"
assert tool_messages[0].contents[0].result == "Confirmed"
async def test_sanitize_tool_history_orphaned_tool_result() -> None:
"""Test sanitize_tool_history removes orphaned tool results."""
from agent_framework import ChatMessage, FunctionResultContent, TextContent
# Tool result without preceding assistant tool call
messages = [
ChatMessage(
role="tool",
contents=[FunctionResultContent(call_id="orphan_123", result="orphaned data")],
),
ChatMessage(
role="user",
contents=[TextContent(text="Hello")],
),
]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Orphaned tool result should be filtered out
tool_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "tool"
]
assert len(tool_messages) == 0
async def test_orphaned_tool_result_sanitization() -> None:
"""Test that orphaned tool results are filtered out."""
orchestrator = DefaultOrchestrator()
input_data = {
"messages": [
{
"role": "tool",
"content": [{"type": "tool_result", "tool_call_id": "orphan_123", "content": "result"}],
},
{
"role": "user",
"content": [{"type": "text", "text": "Hello"}],
},
],
}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
events = []
async for event in orchestrator.run(context):
events.append(event)
# Orphaned tool result should be filtered, only user message remains
tool_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "tool"
]
assert len(tool_messages) == 0
async def test_deduplicate_messages_empty_tool_results() -> None:
"""Test deduplicate_messages prefers non-empty tool results."""
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent
messages = [
ChatMessage(
role="assistant",
contents=[FunctionCallContent(name="test_tool", call_id="call_789", arguments="{}")],
),
ChatMessage(
role="tool",
contents=[FunctionResultContent(call_id="call_789", result="")],
),
ChatMessage(
role="tool",
contents=[FunctionResultContent(call_id="call_789", result="real data")],
),
]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should have only one tool result with actual data
tool_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "tool"
]
assert len(tool_messages) == 1
assert tool_messages[0].contents[0].result == "real data"
async def test_deduplicate_messages_duplicate_assistant_tool_calls() -> None:
"""Test deduplicate_messages removes duplicate assistant tool call messages."""
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent
messages = [
ChatMessage(
role="assistant",
contents=[FunctionCallContent(name="test_tool", call_id="call_abc", arguments="{}")],
),
ChatMessage(
role="assistant",
contents=[FunctionCallContent(name="test_tool", call_id="call_abc", arguments="{}")],
),
ChatMessage(
role="tool",
contents=[FunctionResultContent(call_id="call_abc", result="result")],
),
]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should have only one assistant message
assistant_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "assistant"
]
assert len(assistant_messages) == 1
async def test_deduplicate_messages_duplicate_system_messages() -> None:
"""Test that deduplication logic is invoked for system messages."""
from agent_framework import ChatMessage, TextContent
messages = [
ChatMessage(
role="system",
contents=[TextContent(text="You are a helpful assistant.")],
),
ChatMessage(
role="system",
contents=[TextContent(text="You are a helpful assistant.")],
),
ChatMessage(
role="user",
contents=[TextContent(text="Hello")],
),
]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Deduplication uses hash() which may not deduplicate identical content
# This test verifies deduplication logic runs without errors
system_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "system"
]
# At least one system message should be present
assert len(system_messages) >= 1
async def test_state_context_injection() -> None:
"""Test state context message injection for first request."""
orchestrator = DefaultOrchestrator()
input_data = {
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Hello"}],
}
],
"state": {"items": ["apple", "banana"]},
}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(state_schema={"items": {"type": "array"}}),
)
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should inject system message with current state
system_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "system"
]
assert len(system_messages) == 1
assert "apple" in system_messages[0].contents[0].text
assert "banana" in system_messages[0].contents[0].text
async def test_no_state_context_injection_with_tool_calls() -> None:
"""Test state context is NOT injected if conversation has tool calls."""
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent, TextContent
messages = [
ChatMessage(
role="assistant",
contents=[FunctionCallContent(name="get_weather", call_id="call_xyz", arguments="{}")],
),
ChatMessage(
role="tool",
contents=[FunctionResultContent(call_id="call_xyz", result="sunny")],
),
ChatMessage(
role="user",
contents=[TextContent(text="Thanks")],
),
]
orchestrator = DefaultOrchestrator()
input_data = {"messages": [], "state": {"weather": "sunny"}}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(state_schema={"weather": {"type": "string"}}),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should NOT inject state context system message since conversation has tool calls
system_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "system"
]
assert len(system_messages) == 0
async def test_structured_output_processing() -> None:
"""Test structured output extraction and state update."""
class RecipeState(BaseModel):
ingredients: list[str]
message: str
orchestrator = DefaultOrchestrator()
input_data = {
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Add tomato"}],
}
],
}
# Agent with structured output
agent = MockAgent(
updates=[
AgentRunResponseUpdate(
contents=[TextContent(text='{"ingredients": ["tomato"], "message": "Added tomato"}')],
role="assistant",
)
]
)
agent.chat_options.response_format = RecipeState
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(state_schema={"ingredients": {"type": "array"}}),
)
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should emit StateSnapshotEvent with ingredients
state_events = [e for e in events if e.type == "STATE_SNAPSHOT"]
assert len(state_events) >= 1
# Should emit TextMessage with message field
text_content_events = [e for e in events if e.type == "TEXT_MESSAGE_CONTENT"]
assert len(text_content_events) >= 1
assert any("Added tomato" in e.delta for e in text_content_events)
async def test_duplicate_client_tools_filtered() -> None:
"""Test that client tools duplicating server tools are filtered out."""
@ai_function
def get_weather(location: str) -> str:
"""Get weather for location."""
return f"Weather in {location}"
orchestrator = DefaultOrchestrator()
input_data = {
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Hello"}],
}
],
"tools": [
{
"name": "get_weather",
"description": "Client weather tool.",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
}
],
}
agent = MockAgent()
agent.chat_options.tools = [get_weather]
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
events = []
async for event in orchestrator.run(context):
events.append(event)
# tools parameter should not be passed since client tool duplicates server tool
assert agent.tools_received is None
async def test_unique_client_tools_merged() -> None:
"""Test that unique client tools are merged with server tools."""
@ai_function
def server_tool() -> str:
"""Server tool."""
return "server"
orchestrator = DefaultOrchestrator()
input_data = {
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "Hello"}],
}
],
"tools": [
{
"name": "client_tool",
"description": "Unique client tool.",
"parameters": {
"type": "object",
"properties": {"param": {"type": "string"}},
"required": ["param"],
},
}
],
}
agent = MockAgent()
agent.chat_options.tools = [server_tool]
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
events = []
async for event in orchestrator.run(context):
events.append(event)
# tools parameter should be passed with both server and client tools
assert agent.tools_received is not None
tool_names = [getattr(tool, "name", None) for tool in agent.tools_received]
assert "server_tool" in tool_names
assert "client_tool" in tool_names
async def test_empty_messages_handling() -> None:
"""Test orchestrator handles empty message list gracefully."""
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should emit run lifecycle events but not call agent
assert len(agent.messages_received) == 0
run_started = [e for e in events if e.type == "RUN_STARTED"]
run_finished = [e for e in events if e.type == "RUN_FINISHED"]
assert len(run_started) == 1
assert len(run_finished) == 1
async def test_all_messages_filtered_handling() -> None:
"""Test orchestrator handles case where all messages are filtered out."""
orchestrator = DefaultOrchestrator()
input_data = {
"messages": [
{
"role": "tool",
"content": [{"type": "tool_result", "tool_call_id": "orphan", "content": "data"}],
}
]
}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should finish without calling agent
assert len(agent.messages_received) == 0
run_finished = [e for e in events if e.type == "RUN_FINISHED"]
assert len(run_finished) == 1
async def test_confirm_changes_with_invalid_json_fallback() -> None:
"""Test confirm_changes with invalid JSON falls back to normal processing."""
from agent_framework import ChatMessage, FunctionCallContent, TextContent
messages = [
ChatMessage(
role="assistant",
contents=[
FunctionCallContent(
name="confirm_changes",
call_id="call_confirm_invalid",
arguments='{"changes": "test"}',
)
],
),
ChatMessage(
role="user",
contents=[TextContent(text="invalid json {")],
),
]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Invalid JSON should fall back - user message should be included
user_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "user"
]
assert len(user_messages) == 1
async def test_tool_result_kept_when_call_id_matches() -> None:
"""Test tool result is kept when call_id matches pending tool calls."""
from agent_framework import ChatMessage, FunctionCallContent, FunctionResultContent
messages = [
ChatMessage(
role="assistant",
contents=[FunctionCallContent(name="get_data", call_id="call_match", arguments="{}")],
),
ChatMessage(
role="tool",
contents=[FunctionResultContent(call_id="call_match", result="data")],
),
]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Tool result should be kept
tool_messages = [
msg
for msg in agent.messages_received
if (msg.role.value if hasattr(msg.role, "value") else str(msg.role)) == "tool"
]
assert len(tool_messages) == 1
assert tool_messages[0].contents[0].result == "data"
async def test_agent_protocol_fallback_paths() -> None:
"""Test fallback paths for non-ChatAgent implementations."""
class CustomAgent:
"""Custom agent without ChatAgent type."""
def __init__(self) -> None:
self.chat_options = SimpleNamespace(tools=[], response_format=None)
self.chat_client = SimpleNamespace(function_invocation_configuration=SimpleNamespace())
self.messages_received: list[Any] = []
async def run_stream(
self,
messages: list[Any],
*,
thread: Any = None,
tools: list[Any] | None = None,
) -> AsyncGenerator[AgentRunResponseUpdate, None]:
self.messages_received = messages
yield AgentRunResponseUpdate(contents=[TextContent(text="response")], role="assistant")
from agent_framework import ChatMessage, TextContent
messages = [ChatMessage(role="user", contents=[TextContent(text="Hello")])]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = CustomAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent, # type: ignore
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should work with custom agent implementation
assert len(agent.messages_received) > 0
async def test_initial_state_snapshot_with_array_schema() -> None:
"""Test state initialization with array type schema."""
from agent_framework import ChatMessage, TextContent
messages = [ChatMessage(role="user", contents=[TextContent(text="Hello")])]
orchestrator = DefaultOrchestrator()
input_data = {"messages": [], "state": {}}
agent = MockAgent()
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(state_schema={"items": {"type": "array"}}),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Should emit state snapshot with empty array for items
state_events = [e for e in events if e.type == "STATE_SNAPSHOT"]
assert len(state_events) >= 1
async def test_response_format_skip_text_content() -> None:
"""Test that response_format causes skip_text_content to be set."""
class OutputModel(BaseModel):
result: str
from agent_framework import ChatMessage, TextContent
messages = [ChatMessage(role="user", contents=[TextContent(text="Hello")])]
orchestrator = DefaultOrchestrator()
input_data = {"messages": []}
agent = MockAgent()
agent.chat_options.response_format = OutputModel
context = ExecutionContext(
input_data=input_data,
agent=agent,
config=AgentConfig(),
)
context._messages = messages
events = []
async for event in orchestrator.run(context):
events.append(event)
# Test passes if no errors occur - verifies response_format code path
assert len(events) > 0
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
File diff suppressed because one or more lines are too long
@@ -94,14 +94,14 @@ export function AgentDetailsModal({
{/* Grid Layout for Metadata */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
{/* Model & Client */}
{(agent.model || agent.chat_client_type) && (
{(agent.model_id || agent.chat_client_type) && (
<DetailCard
title="Model & Client"
icon={<Bot className="h-4 w-4 text-muted-foreground" />}
>
<div className="space-y-1">
{agent.model && (
<div className="font-mono text-foreground">{agent.model}</div>
{agent.model_id && (
<div className="font-mono text-foreground">{agent.model_id}</div>
)}
{agent.chat_client_type && (
<div className="text-xs">({agent.chat_client_type})</div>
@@ -136,7 +136,9 @@ export function AgentDetailsModal({
>
<div
className={
agent.has_env ? "text-orange-600 dark:text-orange-400" : "text-green-600 dark:text-green-400"
agent.has_env
? "text-orange-600 dark:text-orange-400"
: "text-green-600 dark:text-green-400"
}
>
{agent.has_env
@@ -162,11 +164,11 @@ export function AgentDetailsModal({
{/* Tools and Middleware Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Tools */}
<DetailCard
title={`Tools (${agent.tools.length})`}
icon={<Package className="h-4 w-4 text-muted-foreground" />}
>
{agent.tools.length > 0 ? (
{agent.tools && agent.tools.length > 0 && (
<DetailCard
title={`Tools (${agent.tools.length})`}
icon={<Package className="h-4 w-4 text-muted-foreground" />}
>
<ul className="space-y-1">
{agent.tools.map((tool, index) => (
<li key={index} className="font-mono text-xs text-foreground">
@@ -174,10 +176,8 @@ export function AgentDetailsModal({
</li>
))}
</ul>
) : (
<div className="text-muted-foreground">No tools configured</div>
)}
</DetailCard>
</DetailCard>
)}
{/* Middleware */}
{agent.middleware && agent.middleware.length > 0 && (
@@ -64,8 +64,8 @@ export function WorkflowDetailsModal({
workflow.source === "directory"
? "Local"
: workflow.source === "in_memory"
? "In-Memory"
: "Gallery";
? "In-Memory"
: "Gallery";
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -151,7 +151,8 @@ export function WorkflowDetailsModal({
{workflow.executors.map((executor, index) => (
<div
key={index}
className="font-mono text-xs text-foreground bg-muted px-2 py-1 rounded"
className="font-mono text-xs text-foreground bg-muted px-2 py-1 rounded truncate"
title={executor}
>
{executor}
</div>
@@ -33,12 +33,13 @@ interface BackendEntityInfo {
tools?: (string | Record<string, unknown>)[];
metadata: Record<string, unknown>;
source?: string;
required_env_vars?: import("@/types").EnvVarRequirement[];
// Deployment support
deployment_supported?: boolean;
deployment_reason?: string;
// Agent-specific fields (present when type === "agent")
instructions?: string;
model?: string;
model_id?: string;
chat_client_type?: string;
context_providers?: string[];
middleware?: string[];
@@ -205,41 +206,51 @@ class ApiClient {
tools: (entity.tools || []).map((tool) =>
typeof tool === "string" ? tool : JSON.stringify(tool)
),
has_env: false, // Default value
has_env: !!(entity.required_env_vars && entity.required_env_vars.length > 0),
module_path:
typeof entity.metadata?.module_path === "string"
? entity.metadata.module_path
: undefined,
required_env_vars: entity.required_env_vars,
metadata: entity.metadata, // Preserve metadata including lazy_loaded flag
// Deployment support
deployment_supported: entity.deployment_supported,
deployment_reason: entity.deployment_reason,
// Agent-specific fields
instructions: entity.instructions,
model: entity.model,
model_id: entity.model_id,
chat_client_type: entity.chat_client_type,
context_providers: entity.context_providers,
middleware: entity.middleware,
};
} else {
// Workflow
const firstTool = entity.tools?.[0];
const startExecutorId = typeof firstTool === "string" ? firstTool : "";
// Workflow - prefer executors field, fall back to tools for backward compatibility
const executorList = entity.executors || entity.tools || [];
// Determine start_executor_id: use entity value, or first executor if it's a string
let startExecutorId = entity.start_executor_id || "";
if (!startExecutorId && executorList.length > 0) {
const firstExecutor = executorList[0];
if (typeof firstExecutor === "string") {
startExecutorId = firstExecutor;
}
}
return {
id: entity.id,
name: entity.name,
description: entity.description,
type: "workflow" as const,
source: (entity.source as AgentSource) || "directory",
executors: (entity.tools || []).map((tool) =>
typeof tool === "string" ? tool : JSON.stringify(tool)
executors: executorList.map((executor) =>
typeof executor === "string" ? executor : JSON.stringify(executor)
),
has_env: false,
has_env: !!(entity.required_env_vars && entity.required_env_vars.length > 0),
module_path:
typeof entity.metadata?.module_path === "string"
? entity.metadata.module_path
: undefined,
required_env_vars: entity.required_env_vars,
metadata: entity.metadata, // Preserve metadata including lazy_loaded flag
// Deployment support
deployment_supported: entity.deployment_supported,
@@ -250,6 +261,7 @@ class ApiClient {
}, // Default schema
input_type_name: entity.input_type_name || "Input",
start_executor_id: startExecutorId,
tools: [],
};
}
});
@@ -37,7 +37,7 @@ export interface AgentInfo {
deployment_reason?: string;
// Agent-specific fields
instructions?: string;
model?: string;
model_id?: string;
chat_client_type?: string;
context_providers?: string[];
middleware?: string[];
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b251111"
version = "1.0.0b251112"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -17,6 +17,8 @@ This folder contains examples demonstrating different ways to create and use age
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate hosted Model Context Protocol (MCP) tools with Azure AI Agent. |
| [`azure_ai_with_response_format.py`](azure_ai_with_response_format.py) | Shows how to use structured outputs (response format) with Azure AI agents using Pydantic models to enforce specific response schemas. |
| [`azure_ai_with_thread.py`](azure_ai_with_thread.py) | Demonstrates thread management with Azure AI agents, including automatic thread creation for stateless conversations and explicit thread management for maintaining conversation context across multiple interactions. |
| [`azure_ai_with_image_generation.py`](azure_ai_with_image_generation.py) | Shows how to use the `ImageGenTool` with Azure AI agents to generate images based on text prompts. |
| [`azure_ai_with_web_search.py`](azure_ai_with_web_search.py) | Shows how to use the `HostedWebSearchTool` with Azure AI agents to perform web searches and retrieve up-to-date information from the internet. |
## Environment Variables
@@ -0,0 +1,77 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from pathlib import Path
import aiofiles
from agent_framework import DataContent
from agent_framework.azure import AzureAIClient
from azure.ai.projects.models import ImageGenTool
from azure.identity.aio import AzureCliCredential
"""
Azure AI Agent With Image Generation
This sample demonstrates basic usage of AzureAIClient to create an agent
that can generate images based on user requirements.
Pre-requisites:
- Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME
environment variables before running this sample.
"""
async def main() -> None:
# Since no Agent ID is provided, the agent will be automatically created.
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
async with (
AzureCliCredential() as credential,
AzureAIClient(async_credential=credential).create_agent(
name="ImageGenAgent",
instructions="Generate images based on user requirements.",
tools=[ImageGenTool(quality="low", size="1024x1024")],
) as agent,
):
query = "Generate an image of Microsoft logo."
print(f"User: {query}")
result = await agent.run(
query,
# These additional options are required for image generation
additional_chat_options={
"extra_headers": {"x-ms-oai-image-generation-deployment": "gpt-image-1"},
},
)
print(f"Agent: {result}\n")
# Save the image to a file
print("Downloading generated image...")
image_data = [
content
for content in result.messages[0].contents
if isinstance(content, DataContent) and content.media_type == "image/png"
]
if image_data and image_data[0]:
# Save to the same directory as this script
filename = "microsoft.png"
current_dir = Path(__file__).parent.resolve()
file_path = current_dir / filename
async with aiofiles.open(file_path, "wb") as f:
await f.write(image_data[0].get_data_bytes())
print(f"Image downloaded and saved to: {file_path}")
else:
print("No image data found in the agent response.")
"""
Sample output:
User: Generate an image of Microsoft logo.
Agent: Here is the Microsoft logo image featuring its iconic four quadrants.
Downloading generated image...
Image downloaded and saved to: .../microsoft.png
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from agent_framework import HostedWebSearchTool
from agent_framework.azure import AzureAIClient
from azure.identity.aio import AzureCliCredential
"""
Azure AI Agent With Web Search
This sample demonstrates basic usage of AzureAIClient to create an agent
that can perform web searches using the HostedWebSearchTool.
Pre-requisites:
- Make sure to set up the AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME
environment variables before running this sample.
"""
async def main() -> None:
# Since no Agent ID is provided, the agent will be automatically created.
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
# authentication option.
async with (
AzureCliCredential() as credential,
AzureAIClient(async_credential=credential).create_agent(
name="WebsearchAgent",
instructions="You are a helpful assistant that can search the web",
tools=[HostedWebSearchTool()],
) as agent,
):
query = "What's the weather today in Seattle?"
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result}\n")
"""
Sample output:
User: What's the weather today in Seattle?
Agent: Here is the updated weather forecast for Seattle: The current temperature is approximately 57°F,
mostly cloudy conditions, with light winds and a chance of rain later tonight. Check out more details
at the [National Weather Service](https://forecast.weather.gov/zipcity.php?inputstring=Seattle%2CWA).
"""
if __name__ == "__main__":
asyncio.run(main())
+3456 -3440
View File
File diff suppressed because it is too large Load Diff