mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature-azure-functions
This commit is contained in:
@@ -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/>
|
||||
|
||||
@@ -68,7 +68,7 @@ public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture
|
||||
string name = "HelpfulAssistant",
|
||||
string instructions = "You are a helpful assistant.",
|
||||
IList<AITool>? aiTools = null) =>
|
||||
new ChatClientAgent(
|
||||
new(
|
||||
this._openAIResponseClient.AsIChatClient(),
|
||||
options: new()
|
||||
{
|
||||
|
||||
Vendored
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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 AG‑UI 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
|
||||
# AG‑UI 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
|
||||
|
||||
+39
-27
@@ -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(),
|
||||
)
|
||||
|
||||
+16
-8
@@ -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(),
|
||||
)
|
||||
|
||||
+27
-15
@@ -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)
|
||||
|
||||
+136
-77
@@ -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."""
|
||||
+5
-1
@@ -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,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
|
||||
@@ -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"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import importlib.metadata
|
||||
|
||||
from ._chat_client import AzureAIAgentClient
|
||||
from ._client import AzureAIClient
|
||||
from ._shared import AzureAISettings
|
||||
|
||||
try:
|
||||
@@ -12,6 +13,7 @@ except importlib.metadata.PackageNotFoundError:
|
||||
|
||||
__all__ = [
|
||||
"AzureAIAgentClient",
|
||||
"AzureAIClient",
|
||||
"AzureAISettings",
|
||||
"__version__",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from collections.abc import MutableSequence
|
||||
from typing import Any, ClassVar, TypeVar
|
||||
|
||||
from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
HostedMCPTool,
|
||||
TextContent,
|
||||
get_logger,
|
||||
use_chat_middleware,
|
||||
use_function_invocation,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from agent_framework.observability import use_observability
|
||||
from agent_framework.openai._responses_client import OpenAIBaseResponsesClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import (
|
||||
MCPTool,
|
||||
PromptAgentDefinition,
|
||||
PromptAgentDefinitionText,
|
||||
ResponseTextFormatConfigurationJsonSchema,
|
||||
)
|
||||
from azure.core.credentials_async import AsyncTokenCredential
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
from openai.types.responses.parsed_response import (
|
||||
ParsedResponse,
|
||||
)
|
||||
from openai.types.responses.response import Response as OpenAIResponse
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from ._shared import AzureAISettings
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import Self # pragma: no cover
|
||||
|
||||
|
||||
logger = get_logger("agent_framework.azure")
|
||||
|
||||
|
||||
TAzureAIClient = TypeVar("TAzureAIClient", bound="AzureAIClient")
|
||||
|
||||
|
||||
@use_function_invocation
|
||||
@use_observability
|
||||
@use_chat_middleware
|
||||
class AzureAIClient(OpenAIBaseResponsesClient):
|
||||
"""Azure AI Agent client."""
|
||||
|
||||
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai" # type: ignore[reportIncompatibleVariableOverride, misc]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
project_client: AIProjectClient | None = None,
|
||||
agent_name: str | None = None,
|
||||
agent_version: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
project_endpoint: str | None = None,
|
||||
model_deployment_name: str | None = None,
|
||||
async_credential: AsyncTokenCredential | None = None,
|
||||
use_latest_version: bool | None = None,
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize an Azure AI Agent client.
|
||||
|
||||
Keyword Args:
|
||||
project_client: An existing AIProjectClient to use. If not provided, one will be created.
|
||||
agent_name: The name to use when creating new agents.
|
||||
agent_version: The version of the agent to use.
|
||||
conversation_id: Default conversation ID to use for conversations. Can be overridden by
|
||||
conversation_id property when making a request.
|
||||
project_endpoint: The Azure AI Project endpoint URL.
|
||||
Can also be set via environment variable AZURE_AI_PROJECT_ENDPOINT.
|
||||
Ignored when a project_client is passed.
|
||||
model_deployment_name: The model deployment name to use for agent creation.
|
||||
Can also be set via environment variable AZURE_AI_MODEL_DEPLOYMENT_NAME.
|
||||
async_credential: Azure async credential to use for authentication.
|
||||
use_latest_version: Boolean flag that indicates whether to use latest agent version
|
||||
if it exists in the service.
|
||||
env_file_path: Path to environment file for loading settings.
|
||||
env_file_encoding: Encoding of the environment file.
|
||||
kwargs: Additional keyword arguments passed to the parent class.
|
||||
|
||||
Examples:
|
||||
.. code-block:: python
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import DefaultAzureCredential
|
||||
|
||||
# Using environment variables
|
||||
# Set AZURE_AI_PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com
|
||||
# Set AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4
|
||||
credential = DefaultAzureCredential()
|
||||
client = AzureAIClient(async_credential=credential)
|
||||
|
||||
# Or passing parameters directly
|
||||
client = AzureAIClient(
|
||||
project_endpoint="https://your-project.cognitiveservices.azure.com",
|
||||
model_deployment_name="gpt-4",
|
||||
async_credential=credential,
|
||||
)
|
||||
|
||||
# Or loading from a .env file
|
||||
client = AzureAIClient(async_credential=credential, env_file_path="path/to/.env")
|
||||
"""
|
||||
try:
|
||||
azure_ai_settings = AzureAISettings(
|
||||
project_endpoint=project_endpoint,
|
||||
model_deployment_name=model_deployment_name,
|
||||
env_file_path=env_file_path,
|
||||
env_file_encoding=env_file_encoding,
|
||||
)
|
||||
except ValidationError as ex:
|
||||
raise ServiceInitializationError("Failed to create Azure AI settings.", ex) from ex
|
||||
|
||||
# If no project_client is provided, create one
|
||||
should_close_client = False
|
||||
if project_client is None:
|
||||
if not azure_ai_settings.project_endpoint:
|
||||
raise ServiceInitializationError(
|
||||
"Azure AI project endpoint is required. Set via 'project_endpoint' parameter "
|
||||
"or 'AZURE_AI_PROJECT_ENDPOINT' environment variable."
|
||||
)
|
||||
|
||||
# Use provided credential
|
||||
if not async_credential:
|
||||
raise ServiceInitializationError("Azure credential is required when project_client is not provided.")
|
||||
project_client = AIProjectClient(
|
||||
endpoint=azure_ai_settings.project_endpoint,
|
||||
credential=async_credential,
|
||||
user_agent=AGENT_FRAMEWORK_USER_AGENT,
|
||||
)
|
||||
should_close_client = True
|
||||
|
||||
# Initialize parent
|
||||
super().__init__(
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Initialize instance variables
|
||||
self.agent_name = agent_name
|
||||
self.agent_version = agent_version
|
||||
self.use_latest_version = use_latest_version
|
||||
self.project_client = project_client
|
||||
self.credential = async_credential
|
||||
self.model_id = azure_ai_settings.model_deployment_name
|
||||
self.conversation_id = conversation_id
|
||||
self._should_close_client = should_close_client # Track whether we should close client connection
|
||||
|
||||
async def setup_azure_ai_observability(self, enable_sensitive_data: bool | None = None) -> None:
|
||||
"""Use this method to setup tracing in your Azure AI Project.
|
||||
|
||||
This will take the connection string from the project project_client.
|
||||
It will override any connection string that is set in the environment variables.
|
||||
It will disable any OTLP endpoint that might have been set.
|
||||
"""
|
||||
try:
|
||||
conn_string = await self.project_client.telemetry.get_application_insights_connection_string()
|
||||
except ResourceNotFoundError:
|
||||
logger.warning(
|
||||
"No Application Insights connection string found for the Azure AI Project, "
|
||||
"please call setup_observability() manually."
|
||||
)
|
||||
return
|
||||
from agent_framework.observability import setup_observability
|
||||
|
||||
setup_observability(
|
||||
applicationinsights_connection_string=conn_string, enable_sensitive_data=enable_sensitive_data
|
||||
)
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None:
|
||||
"""Async context manager exit."""
|
||||
await self.close()
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the project_client."""
|
||||
await self._close_client_if_needed()
|
||||
|
||||
async def _get_agent_reference_or_create(
|
||||
self, run_options: dict[str, Any], messages_instructions: str | None
|
||||
) -> dict[str, str]:
|
||||
"""Determine which agent to use and create if needed.
|
||||
|
||||
Returns:
|
||||
str: The agent_name to use
|
||||
"""
|
||||
agent_name = self.agent_name or "UnnamedAgent"
|
||||
|
||||
# If no agent_version is provided, either use latest version or create a new agent:
|
||||
if self.agent_version is None:
|
||||
# Try to use latest version if requested and agent exists
|
||||
if self.use_latest_version:
|
||||
try:
|
||||
existing_agent = await self.project_client.agents.get(agent_name)
|
||||
self.agent_name = existing_agent.name
|
||||
self.agent_version = existing_agent.versions.latest.version
|
||||
return {"name": self.agent_name, "version": self.agent_version, "type": "agent_reference"}
|
||||
except ResourceNotFoundError:
|
||||
# Agent doesn't exist, fall through to creation logic
|
||||
pass
|
||||
|
||||
if "model" not in run_options or not run_options["model"]:
|
||||
raise ServiceInitializationError(
|
||||
"Model deployment name is required for agent creation, "
|
||||
"can also be passed to the get_response methods."
|
||||
)
|
||||
|
||||
args: dict[str, Any] = {"model": run_options["model"]}
|
||||
|
||||
if "tools" in run_options:
|
||||
args["tools"] = run_options["tools"]
|
||||
|
||||
if "response_format" in run_options:
|
||||
response_format = run_options["response_format"]
|
||||
args["text"] = PromptAgentDefinitionText(
|
||||
format=ResponseTextFormatConfigurationJsonSchema(
|
||||
name=response_format.__name__,
|
||||
schema=response_format.model_json_schema(),
|
||||
)
|
||||
)
|
||||
|
||||
# Combine instructions from messages and options
|
||||
combined_instructions = [
|
||||
instructions
|
||||
for instructions in [messages_instructions, run_options.get("instructions")]
|
||||
if instructions
|
||||
]
|
||||
if combined_instructions:
|
||||
args["instructions"] = "".join(combined_instructions)
|
||||
|
||||
created_agent = await self.project_client.agents.create_version(
|
||||
agent_name=agent_name, definition=PromptAgentDefinition(**args)
|
||||
)
|
||||
|
||||
self.agent_name = created_agent.name
|
||||
self.agent_version = created_agent.version
|
||||
|
||||
return {"name": agent_name, "version": self.agent_version, "type": "agent_reference"}
|
||||
|
||||
async def _close_client_if_needed(self) -> None:
|
||||
"""Close project_client session if we created it."""
|
||||
if self._should_close_client:
|
||||
await self.project_client.close()
|
||||
|
||||
def _prepare_input(self, messages: MutableSequence[ChatMessage]) -> tuple[list[ChatMessage], str | None]:
|
||||
"""Prepare input from messages and convert system/developer messages to instructions."""
|
||||
result: list[ChatMessage] = []
|
||||
instructions_list: list[str] = []
|
||||
instructions: str | None = None
|
||||
|
||||
# System/developer messages are turned into instructions, since there is no such message roles in Azure AI.
|
||||
for message in messages:
|
||||
if message.role.value in ["system", "developer"]:
|
||||
for text_content in [content for content in message.contents if isinstance(content, TextContent)]:
|
||||
instructions_list.append(text_content.text)
|
||||
else:
|
||||
result.append(message)
|
||||
|
||||
if len(instructions_list) > 0:
|
||||
instructions = "".join(instructions_list)
|
||||
|
||||
return result, instructions
|
||||
|
||||
async def prepare_options(
|
||||
self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions
|
||||
) -> dict[str, Any]:
|
||||
chat_options.store = bool(chat_options.store or chat_options.store is None)
|
||||
prepared_messages, instructions = self._prepare_input(messages)
|
||||
run_options = await super().prepare_options(prepared_messages, chat_options)
|
||||
agent_reference = await self._get_agent_reference_or_create(run_options, instructions)
|
||||
|
||||
run_options["extra_body"] = {"agent": agent_reference}
|
||||
|
||||
conversation_id = chat_options.conversation_id or self.conversation_id
|
||||
|
||||
# Handle different conversation ID formats
|
||||
if conversation_id:
|
||||
if conversation_id.startswith("resp_"):
|
||||
# For response IDs, set previous_response_id and remove conversation property
|
||||
run_options.pop("conversation", None)
|
||||
run_options["previous_response_id"] = conversation_id
|
||||
elif conversation_id.startswith("conv_"):
|
||||
# For conversation IDs, set conversation and remove previous_response_id property
|
||||
run_options.pop("previous_response_id", None)
|
||||
run_options["conversation"] = conversation_id
|
||||
|
||||
# Remove properties that are not supported on request level
|
||||
# but were configured on agent level
|
||||
exclude = ["model", "tools", "response_format"]
|
||||
|
||||
for property in exclude:
|
||||
run_options.pop(property, None)
|
||||
|
||||
return run_options
|
||||
|
||||
async def initialize_client(self) -> None:
|
||||
"""Initialize OpenAI client asynchronously."""
|
||||
self.client = await self.project_client.get_openai_client() # type: ignore
|
||||
|
||||
def _update_agent_name(self, agent_name: str | None) -> None:
|
||||
"""Update the agent name in the chat client.
|
||||
|
||||
Args:
|
||||
agent_name: The new name for the agent.
|
||||
"""
|
||||
# This is a no-op in the base class, but can be overridden by subclasses
|
||||
# to update the agent name in the client.
|
||||
if agent_name and not self.agent_name:
|
||||
self.agent_name = agent_name
|
||||
|
||||
def get_mcp_tool(self, tool: HostedMCPTool) -> Any:
|
||||
"""Get MCP tool from HostedMCPTool."""
|
||||
mcp = MCPTool(server_label=tool.name.replace(" ", "_"), server_url=str(tool.url))
|
||||
|
||||
if tool.allowed_tools:
|
||||
mcp["allowed_tools"] = list(tool.allowed_tools)
|
||||
|
||||
if tool.approval_mode:
|
||||
match tool.approval_mode:
|
||||
case str():
|
||||
mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never"
|
||||
case _:
|
||||
if always_require_approvals := tool.approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}}
|
||||
if never_require_approvals := tool.approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}
|
||||
|
||||
return mcp
|
||||
|
||||
def get_conversation_id(
|
||||
self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None
|
||||
) -> str | None:
|
||||
"""Get the conversation ID from the response if store is True."""
|
||||
if store:
|
||||
# If conversation ID exists, it means that we operate with conversation
|
||||
# so we use conversation ID as input and output.
|
||||
if response.conversation and response.conversation.id:
|
||||
return response.conversation.id
|
||||
# If conversation ID doesn't exist, we operate with responses
|
||||
# so we use response ID as input and output.
|
||||
return response.id
|
||||
return None
|
||||
@@ -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"
|
||||
@@ -24,7 +24,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"azure-ai-projects >= 1.0.0b11",
|
||||
"azure-ai-projects >= 2.0.0b1",
|
||||
"azure-ai-agents == 1.2.0b5",
|
||||
"aiohttp",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,743 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from agent_framework import (
|
||||
ChatClientProtocol,
|
||||
ChatMessage,
|
||||
ChatOptions,
|
||||
Role,
|
||||
TextContent,
|
||||
)
|
||||
from agent_framework.exceptions import ServiceInitializationError
|
||||
from azure.ai.projects.models import (
|
||||
ResponseTextFormatConfigurationJsonSchema,
|
||||
)
|
||||
from openai.types.responses.parsed_response import ParsedResponse
|
||||
from openai.types.responses.response import Response as OpenAIResponse
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError
|
||||
|
||||
from agent_framework_azure_ai import AzureAIClient, AzureAISettings
|
||||
|
||||
|
||||
def create_test_azure_ai_client(
|
||||
mock_project_client: MagicMock,
|
||||
agent_name: str | None = None,
|
||||
agent_version: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
azure_ai_settings: AzureAISettings | None = None,
|
||||
should_close_client: bool = False,
|
||||
use_latest_version: bool | None = None,
|
||||
) -> AzureAIClient:
|
||||
"""Helper function to create AzureAIClient instances for testing, bypassing normal validation."""
|
||||
if azure_ai_settings is None:
|
||||
azure_ai_settings = AzureAISettings(env_file_path="test.env")
|
||||
|
||||
# Create client instance directly
|
||||
client = object.__new__(AzureAIClient)
|
||||
|
||||
# Set attributes directly
|
||||
client.project_client = mock_project_client
|
||||
client.credential = None
|
||||
client.agent_name = agent_name
|
||||
client.agent_version = agent_version
|
||||
client.use_latest_version = use_latest_version
|
||||
client.model_id = azure_ai_settings.model_deployment_name
|
||||
client.conversation_id = conversation_id
|
||||
client._should_close_client = should_close_client # type: ignore
|
||||
client.additional_properties = {}
|
||||
client.middleware = None
|
||||
|
||||
# Mock the OpenAI client attribute
|
||||
mock_openai_client = MagicMock()
|
||||
mock_openai_client.conversations = MagicMock()
|
||||
mock_openai_client.conversations.create = AsyncMock()
|
||||
client.client = mock_openai_client
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def test_azure_ai_settings_init(azure_ai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test AzureAISettings initialization."""
|
||||
settings = AzureAISettings()
|
||||
|
||||
assert settings.project_endpoint == azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"]
|
||||
assert settings.model_deployment_name == azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"]
|
||||
|
||||
|
||||
def test_azure_ai_settings_init_with_explicit_values() -> None:
|
||||
"""Test AzureAISettings initialization with explicit values."""
|
||||
settings = AzureAISettings(
|
||||
project_endpoint="https://custom-endpoint.com/",
|
||||
model_deployment_name="custom-model",
|
||||
)
|
||||
|
||||
assert settings.project_endpoint == "https://custom-endpoint.com/"
|
||||
assert settings.model_deployment_name == "custom-model"
|
||||
|
||||
|
||||
def test_azure_ai_client_init_with_project_client(mock_project_client: MagicMock) -> None:
|
||||
"""Test AzureAIClient initialization with existing project_client."""
|
||||
with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
|
||||
mock_settings.return_value.project_endpoint = None
|
||||
mock_settings.return_value.model_deployment_name = "test-model"
|
||||
|
||||
client = AzureAIClient(
|
||||
project_client=mock_project_client,
|
||||
agent_name="test-agent",
|
||||
agent_version="1.0",
|
||||
)
|
||||
|
||||
assert client.project_client is mock_project_client
|
||||
assert client.agent_name == "test-agent"
|
||||
assert client.agent_version == "1.0"
|
||||
assert not client._should_close_client # type: ignore
|
||||
assert isinstance(client, ChatClientProtocol)
|
||||
|
||||
|
||||
def test_azure_ai_client_init_auto_create_client(
|
||||
azure_ai_unit_test_env: dict[str, str],
|
||||
mock_azure_credential: MagicMock,
|
||||
) -> None:
|
||||
"""Test AzureAIClient initialization with auto-created project_client."""
|
||||
with patch("agent_framework_azure_ai._client.AIProjectClient") as mock_ai_project_client:
|
||||
mock_project_client = MagicMock()
|
||||
mock_ai_project_client.return_value = mock_project_client
|
||||
|
||||
client = AzureAIClient(
|
||||
project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
async_credential=mock_azure_credential,
|
||||
agent_name="test-agent",
|
||||
)
|
||||
|
||||
assert client.project_client is mock_project_client
|
||||
assert client.agent_name == "test-agent"
|
||||
assert client._should_close_client # type: ignore
|
||||
|
||||
# Verify AIProjectClient was called with correct parameters
|
||||
mock_ai_project_client.assert_called_once()
|
||||
|
||||
|
||||
def test_azure_ai_client_init_missing_project_endpoint() -> None:
|
||||
"""Test AzureAIClient initialization when project_endpoint is missing and no project_client provided."""
|
||||
with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
|
||||
mock_settings.return_value.project_endpoint = None
|
||||
mock_settings.return_value.model_deployment_name = "test-model"
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Azure AI project endpoint is required"):
|
||||
AzureAIClient(async_credential=MagicMock())
|
||||
|
||||
|
||||
def test_azure_ai_client_init_missing_credential(azure_ai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test AzureAIClient.__init__ when async_credential is missing and no project_client provided."""
|
||||
with pytest.raises(
|
||||
ServiceInitializationError, match="Azure credential is required when project_client is not provided"
|
||||
):
|
||||
AzureAIClient(
|
||||
project_endpoint=azure_ai_unit_test_env["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
)
|
||||
|
||||
|
||||
def test_azure_ai_client_init_validation_error(mock_azure_credential: MagicMock) -> None:
|
||||
"""Test that ValidationError in AzureAISettings is properly handled."""
|
||||
with patch("agent_framework_azure_ai._client.AzureAISettings") as mock_settings:
|
||||
mock_settings.side_effect = ValidationError.from_exception_data("test", [])
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Failed to create Azure AI settings"):
|
||||
AzureAIClient(async_credential=mock_azure_credential)
|
||||
|
||||
|
||||
async def test_azure_ai_client_get_agent_reference_or_create_existing_version(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _get_agent_reference_or_create when agent_version is already provided."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="existing-agent", agent_version="1.0")
|
||||
|
||||
agent_ref = await client._get_agent_reference_or_create({}, None) # type: ignore
|
||||
|
||||
assert agent_ref == {"name": "existing-agent", "version": "1.0", "type": "agent_reference"}
|
||||
|
||||
|
||||
async def test_azure_ai_client_get_agent_reference_or_create_new_agent(
|
||||
mock_project_client: MagicMock,
|
||||
azure_ai_unit_test_env: dict[str, str],
|
||||
) -> None:
|
||||
"""Test _get_agent_reference_or_create when creating a new agent."""
|
||||
azure_ai_settings = AzureAISettings(model_deployment_name=azure_ai_unit_test_env["AZURE_AI_MODEL_DEPLOYMENT_NAME"])
|
||||
client = create_test_azure_ai_client(
|
||||
mock_project_client, agent_name="new-agent", azure_ai_settings=azure_ai_settings
|
||||
)
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.name = "new-agent"
|
||||
mock_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
|
||||
|
||||
run_options = {"model": azure_ai_settings.model_deployment_name}
|
||||
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
|
||||
|
||||
assert agent_ref == {"name": "new-agent", "version": "1.0", "type": "agent_reference"}
|
||||
assert client.agent_name == "new-agent"
|
||||
assert client.agent_version == "1.0"
|
||||
|
||||
|
||||
async def test_azure_ai_client_get_agent_reference_missing_model(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _get_agent_reference_or_create when model is missing for agent creation."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
|
||||
|
||||
with pytest.raises(ServiceInitializationError, match="Model deployment name is required for agent creation"):
|
||||
await client._get_agent_reference_or_create({}, None) # type: ignore
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_input_with_system_messages(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_input converts system/developer messages to instructions."""
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
messages = [
|
||||
ChatMessage(role=Role.SYSTEM, contents=[TextContent(text="You are a helpful assistant.")]),
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="System response")]),
|
||||
]
|
||||
|
||||
result_messages, instructions = client._prepare_input(messages) # type: ignore
|
||||
|
||||
assert len(result_messages) == 2
|
||||
assert result_messages[0].role == Role.USER
|
||||
assert result_messages[1].role == Role.ASSISTANT
|
||||
assert instructions == "You are a helpful assistant."
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_input_no_system_messages(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _prepare_input with no system/developer messages."""
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
messages = [
|
||||
ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")]),
|
||||
ChatMessage(role=Role.ASSISTANT, contents=[TextContent(text="Hi there!")]),
|
||||
]
|
||||
|
||||
result_messages, instructions = client._prepare_input(messages) # type: ignore
|
||||
|
||||
assert len(result_messages) == 2
|
||||
assert instructions is None
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_options_basic(mock_project_client: MagicMock) -> None:
|
||||
"""Test prepare_options basic functionality."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
chat_options = ChatOptions()
|
||||
|
||||
with (
|
||||
patch.object(client.__class__.__bases__[0], "prepare_options", return_value={"model": "test-model"}),
|
||||
patch.object(
|
||||
client,
|
||||
"_get_agent_reference_or_create",
|
||||
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
|
||||
),
|
||||
):
|
||||
run_options = await client.prepare_options(messages, chat_options)
|
||||
|
||||
assert "extra_body" in run_options
|
||||
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
|
||||
|
||||
|
||||
async def test_azure_ai_client_initialize_client(mock_project_client: MagicMock) -> None:
|
||||
"""Test initialize_client method."""
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
mock_openai_client = MagicMock()
|
||||
mock_project_client.get_openai_client = AsyncMock(return_value=mock_openai_client)
|
||||
|
||||
await client.initialize_client()
|
||||
|
||||
assert client.client is mock_openai_client
|
||||
mock_project_client.get_openai_client.assert_called_once()
|
||||
|
||||
|
||||
def test_azure_ai_client_update_agent_name(mock_project_client: MagicMock) -> None:
|
||||
"""Test _update_agent_name method."""
|
||||
client = create_test_azure_ai_client(mock_project_client)
|
||||
|
||||
# Test updating agent name when current is None
|
||||
with patch.object(client, "_update_agent_name") as mock_update:
|
||||
mock_update.return_value = None
|
||||
client._update_agent_name("new-agent") # type: ignore
|
||||
mock_update.assert_called_once_with("new-agent")
|
||||
|
||||
# Test behavior when agent name is updated
|
||||
assert client.agent_name is None # Should remain None since we didn't actually update
|
||||
client.agent_name = "test-agent" # Manually set for the test
|
||||
|
||||
# Test with None input
|
||||
with patch.object(client, "_update_agent_name") as mock_update:
|
||||
mock_update.return_value = None
|
||||
client._update_agent_name(None) # type: ignore
|
||||
mock_update.assert_called_once_with(None)
|
||||
|
||||
|
||||
async def test_azure_ai_client_async_context_manager(mock_project_client: MagicMock) -> None:
|
||||
"""Test async context manager functionality."""
|
||||
client = create_test_azure_ai_client(mock_project_client, should_close_client=True)
|
||||
|
||||
mock_project_client.close = AsyncMock()
|
||||
|
||||
async with client as ctx_client:
|
||||
assert ctx_client is client
|
||||
|
||||
# Should call close after exiting context
|
||||
mock_project_client.close.assert_called_once()
|
||||
|
||||
|
||||
async def test_azure_ai_client_close_method(mock_project_client: MagicMock) -> None:
|
||||
"""Test close method."""
|
||||
client = create_test_azure_ai_client(mock_project_client, should_close_client=True)
|
||||
|
||||
mock_project_client.close = AsyncMock()
|
||||
|
||||
await client.close()
|
||||
|
||||
mock_project_client.close.assert_called_once()
|
||||
|
||||
|
||||
async def test_azure_ai_client_close_client_when_should_close_false(mock_project_client: MagicMock) -> None:
|
||||
"""Test _close_client_if_needed when should_close_client is False."""
|
||||
client = create_test_azure_ai_client(mock_project_client, should_close_client=False)
|
||||
|
||||
mock_project_client.close = AsyncMock()
|
||||
|
||||
await client._close_client_if_needed() # type: ignore
|
||||
|
||||
# Should not call close when should_close_client is False
|
||||
mock_project_client.close.assert_not_called()
|
||||
|
||||
|
||||
async def test_azure_ai_client_agent_creation_with_instructions(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test agent creation with combined instructions."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.name = "test-agent"
|
||||
mock_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
|
||||
|
||||
run_options = {"model": "test-model", "instructions": "Option instructions. "}
|
||||
messages_instructions = "Message instructions. "
|
||||
|
||||
await client._get_agent_reference_or_create(run_options, messages_instructions) # type: ignore
|
||||
|
||||
# Verify agent was created with combined instructions
|
||||
call_args = mock_project_client.agents.create_version.call_args
|
||||
assert call_args[1]["definition"].instructions == "Message instructions. Option instructions. "
|
||||
|
||||
|
||||
async def test_azure_ai_client_agent_creation_with_tools(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test agent creation with tools."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.name = "test-agent"
|
||||
mock_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
|
||||
|
||||
test_tools = [{"type": "function", "function": {"name": "test_tool"}}]
|
||||
run_options = {"model": "test-model", "tools": test_tools}
|
||||
|
||||
await client._get_agent_reference_or_create(run_options, None) # type: ignore
|
||||
|
||||
# Verify agent was created with tools
|
||||
call_args = mock_project_client.agents.create_version.call_args
|
||||
assert call_args[1]["definition"].tools == test_tools
|
||||
|
||||
|
||||
async def test_azure_ai_client_use_latest_version_existing_agent(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _get_agent_reference_or_create when use_latest_version=True and agent exists."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="existing-agent", use_latest_version=True)
|
||||
|
||||
# Mock existing agent response
|
||||
mock_existing_agent = MagicMock()
|
||||
mock_existing_agent.name = "existing-agent"
|
||||
mock_existing_agent.versions.latest.version = "2.5"
|
||||
mock_project_client.agents.get = AsyncMock(return_value=mock_existing_agent)
|
||||
|
||||
run_options = {"model": "test-model"}
|
||||
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
|
||||
|
||||
# Verify existing agent was retrieved and used
|
||||
mock_project_client.agents.get.assert_called_once_with("existing-agent")
|
||||
mock_project_client.agents.create_version.assert_not_called()
|
||||
|
||||
assert agent_ref == {"name": "existing-agent", "version": "2.5", "type": "agent_reference"}
|
||||
assert client.agent_name == "existing-agent"
|
||||
assert client.agent_version == "2.5"
|
||||
|
||||
|
||||
async def test_azure_ai_client_use_latest_version_agent_not_found(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _get_agent_reference_or_create when use_latest_version=True but agent doesn't exist."""
|
||||
from azure.core.exceptions import ResourceNotFoundError
|
||||
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="non-existing-agent", use_latest_version=True)
|
||||
|
||||
# Mock ResourceNotFoundError when trying to retrieve agent
|
||||
mock_project_client.agents.get = AsyncMock(side_effect=ResourceNotFoundError("Agent not found"))
|
||||
|
||||
# Mock agent creation response for fallback
|
||||
mock_created_agent = MagicMock()
|
||||
mock_created_agent.name = "non-existing-agent"
|
||||
mock_created_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_created_agent)
|
||||
|
||||
run_options = {"model": "test-model"}
|
||||
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
|
||||
|
||||
# Verify retrieval was attempted and creation was used as fallback
|
||||
mock_project_client.agents.get.assert_called_once_with("non-existing-agent")
|
||||
mock_project_client.agents.create_version.assert_called_once()
|
||||
|
||||
assert agent_ref == {"name": "non-existing-agent", "version": "1.0", "type": "agent_reference"}
|
||||
assert client.agent_name == "non-existing-agent"
|
||||
assert client.agent_version == "1.0"
|
||||
|
||||
|
||||
async def test_azure_ai_client_use_latest_version_false(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test _get_agent_reference_or_create when use_latest_version=False (default behavior)."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", use_latest_version=False)
|
||||
|
||||
# Mock agent creation response
|
||||
mock_created_agent = MagicMock()
|
||||
mock_created_agent.name = "test-agent"
|
||||
mock_created_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_created_agent)
|
||||
|
||||
run_options = {"model": "test-model"}
|
||||
agent_ref = await client._get_agent_reference_or_create(run_options, None) # type: ignore
|
||||
|
||||
# Verify retrieval was not attempted and creation was used directly
|
||||
mock_project_client.agents.get.assert_not_called()
|
||||
mock_project_client.agents.create_version.assert_called_once()
|
||||
|
||||
assert agent_ref == {"name": "test-agent", "version": "1.0", "type": "agent_reference"}
|
||||
|
||||
|
||||
async def test_azure_ai_client_use_latest_version_with_existing_agent_version(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that use_latest_version is ignored when agent_version is already provided."""
|
||||
client = create_test_azure_ai_client(
|
||||
mock_project_client, agent_name="test-agent", agent_version="3.0", use_latest_version=True
|
||||
)
|
||||
|
||||
agent_ref = await client._get_agent_reference_or_create({}, None) # type: ignore
|
||||
|
||||
# Verify neither retrieval nor creation was attempted since version is already set
|
||||
mock_project_client.agents.get.assert_not_called()
|
||||
mock_project_client.agents.create_version.assert_not_called()
|
||||
|
||||
assert agent_ref == {"name": "test-agent", "version": "3.0", "type": "agent_reference"}
|
||||
|
||||
|
||||
class ResponseFormatModel(BaseModel):
|
||||
"""Test Pydantic model for response format testing."""
|
||||
|
||||
name: str
|
||||
value: int
|
||||
description: str
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
async def test_azure_ai_client_agent_creation_with_response_format(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test agent creation with response_format configuration."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent")
|
||||
|
||||
# Mock agent creation response
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.name = "test-agent"
|
||||
mock_agent.version = "1.0"
|
||||
mock_project_client.agents.create_version = AsyncMock(return_value=mock_agent)
|
||||
|
||||
run_options = {"model": "test-model", "response_format": ResponseFormatModel}
|
||||
|
||||
await client._get_agent_reference_or_create(run_options, None) # type: ignore
|
||||
|
||||
# Verify agent was created with response format configuration
|
||||
call_args = mock_project_client.agents.create_version.call_args
|
||||
created_definition = call_args[1]["definition"]
|
||||
|
||||
# Check that text format configuration was set
|
||||
assert hasattr(created_definition, "text")
|
||||
assert created_definition.text is not None
|
||||
|
||||
# Check that the format is a ResponseTextFormatConfigurationJsonSchema
|
||||
assert hasattr(created_definition.text, "format")
|
||||
format_config = created_definition.text.format
|
||||
assert isinstance(format_config, ResponseTextFormatConfigurationJsonSchema)
|
||||
|
||||
# Check the schema name matches the model class name
|
||||
assert format_config.name == "ResponseFormatModel"
|
||||
|
||||
# Check that schema was generated correctly
|
||||
assert format_config.schema is not None
|
||||
schema = format_config.schema
|
||||
assert "properties" in schema
|
||||
assert "name" in schema["properties"]
|
||||
assert "value" in schema["properties"]
|
||||
assert "description" in schema["properties"]
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_options_excludes_response_format(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that prepare_options excludes response_format from final run options."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
chat_options = ChatOptions()
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
client.__class__.__bases__[0],
|
||||
"prepare_options",
|
||||
return_value={"model": "test-model", "response_format": ResponseFormatModel},
|
||||
),
|
||||
patch.object(
|
||||
client,
|
||||
"_get_agent_reference_or_create",
|
||||
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
|
||||
),
|
||||
):
|
||||
run_options = await client.prepare_options(messages, chat_options)
|
||||
|
||||
# response_format should be excluded from final run options
|
||||
assert "response_format" not in run_options
|
||||
# But extra_body should contain agent reference
|
||||
assert "extra_body" in run_options
|
||||
assert run_options["extra_body"]["agent"]["name"] == "test-agent"
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_options_with_resp_conversation_id(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test prepare_options with conversation ID starting with 'resp_'."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
chat_options = ChatOptions(conversation_id="resp_12345")
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
client.__class__.__bases__[0],
|
||||
"prepare_options",
|
||||
return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
|
||||
),
|
||||
patch.object(
|
||||
client,
|
||||
"_get_agent_reference_or_create",
|
||||
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
|
||||
),
|
||||
):
|
||||
run_options = await client.prepare_options(messages, chat_options)
|
||||
|
||||
# Should set previous_response_id and remove conversation property
|
||||
assert run_options["previous_response_id"] == "resp_12345"
|
||||
assert "conversation" not in run_options
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_options_with_conv_conversation_id(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test prepare_options with conversation ID starting with 'conv_'."""
|
||||
client = create_test_azure_ai_client(mock_project_client, agent_name="test-agent", agent_version="1.0")
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
chat_options = ChatOptions(conversation_id="conv_67890")
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
client.__class__.__bases__[0],
|
||||
"prepare_options",
|
||||
return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
|
||||
),
|
||||
patch.object(
|
||||
client,
|
||||
"_get_agent_reference_or_create",
|
||||
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
|
||||
),
|
||||
):
|
||||
run_options = await client.prepare_options(messages, chat_options)
|
||||
|
||||
# Should set conversation and remove previous_response_id property
|
||||
assert run_options["conversation"] == "conv_67890"
|
||||
assert "previous_response_id" not in run_options
|
||||
|
||||
|
||||
async def test_azure_ai_client_prepare_options_with_client_conversation_id(
|
||||
mock_project_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test prepare_options using client's default conversation ID when chat options don't have one."""
|
||||
client = create_test_azure_ai_client(
|
||||
mock_project_client, agent_name="test-agent", agent_version="1.0", conversation_id="resp_client_default"
|
||||
)
|
||||
|
||||
messages = [ChatMessage(role=Role.USER, contents=[TextContent(text="Hello")])]
|
||||
chat_options = ChatOptions() # No conversation_id specified
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
client.__class__.__bases__[0],
|
||||
"prepare_options",
|
||||
return_value={"model": "test-model", "previous_response_id": "old_value", "conversation": "old_conv"},
|
||||
),
|
||||
patch.object(
|
||||
client,
|
||||
"_get_agent_reference_or_create",
|
||||
return_value={"name": "test-agent", "version": "1.0", "type": "agent_reference"},
|
||||
),
|
||||
):
|
||||
run_options = await client.prepare_options(messages, chat_options)
|
||||
|
||||
# Should use client's default conversation_id and set previous_response_id
|
||||
assert run_options["previous_response_id"] == "resp_client_default"
|
||||
assert "conversation" not in run_options
|
||||
|
||||
|
||||
def test_get_conversation_id_with_store_true_and_conversation_id() -> None:
|
||||
"""Test get_conversation_id returns conversation ID when store is True and conversation exists."""
|
||||
client = create_test_azure_ai_client(MagicMock())
|
||||
|
||||
# Mock OpenAI response with conversation
|
||||
mock_response = MagicMock(spec=OpenAIResponse)
|
||||
mock_response.id = "resp_12345"
|
||||
mock_conversation = MagicMock()
|
||||
mock_conversation.id = "conv_67890"
|
||||
mock_response.conversation = mock_conversation
|
||||
|
||||
result = client.get_conversation_id(mock_response, store=True)
|
||||
|
||||
assert result == "conv_67890"
|
||||
|
||||
|
||||
def test_get_conversation_id_with_store_true_and_no_conversation() -> None:
|
||||
"""Test get_conversation_id returns response ID when store is True and no conversation exists."""
|
||||
client = create_test_azure_ai_client(MagicMock())
|
||||
|
||||
# Mock OpenAI response without conversation
|
||||
mock_response = MagicMock(spec=OpenAIResponse)
|
||||
mock_response.id = "resp_12345"
|
||||
mock_response.conversation = None
|
||||
|
||||
result = client.get_conversation_id(mock_response, store=True)
|
||||
|
||||
assert result == "resp_12345"
|
||||
|
||||
|
||||
def test_get_conversation_id_with_store_true_and_empty_conversation_id() -> None:
|
||||
"""Test get_conversation_id returns response ID when store is True and conversation ID is empty."""
|
||||
client = create_test_azure_ai_client(MagicMock())
|
||||
|
||||
# Mock OpenAI response with conversation but empty ID
|
||||
mock_response = MagicMock(spec=OpenAIResponse)
|
||||
mock_response.id = "resp_12345"
|
||||
mock_conversation = MagicMock()
|
||||
mock_conversation.id = ""
|
||||
mock_response.conversation = mock_conversation
|
||||
|
||||
result = client.get_conversation_id(mock_response, store=True)
|
||||
|
||||
assert result == "resp_12345"
|
||||
|
||||
|
||||
def test_get_conversation_id_with_store_false() -> None:
|
||||
"""Test get_conversation_id returns None when store is False."""
|
||||
client = create_test_azure_ai_client(MagicMock())
|
||||
|
||||
# Mock OpenAI response with conversation
|
||||
mock_response = MagicMock(spec=OpenAIResponse)
|
||||
mock_response.id = "resp_12345"
|
||||
mock_conversation = MagicMock()
|
||||
mock_conversation.id = "conv_67890"
|
||||
mock_response.conversation = mock_conversation
|
||||
|
||||
result = client.get_conversation_id(mock_response, store=False)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_get_conversation_id_with_parsed_response_and_store_true() -> None:
|
||||
"""Test get_conversation_id works with ParsedResponse when store is True."""
|
||||
client = create_test_azure_ai_client(MagicMock())
|
||||
|
||||
# Mock ParsedResponse with conversation
|
||||
mock_response = MagicMock(spec=ParsedResponse[BaseModel])
|
||||
mock_response.id = "resp_parsed_12345"
|
||||
mock_conversation = MagicMock()
|
||||
mock_conversation.id = "conv_parsed_67890"
|
||||
mock_response.conversation = mock_conversation
|
||||
|
||||
result = client.get_conversation_id(mock_response, store=True)
|
||||
|
||||
assert result == "conv_parsed_67890"
|
||||
|
||||
|
||||
def test_get_conversation_id_with_parsed_response_no_conversation() -> None:
|
||||
"""Test get_conversation_id returns response ID with ParsedResponse when no conversation exists."""
|
||||
client = create_test_azure_ai_client(MagicMock())
|
||||
|
||||
# Mock ParsedResponse without conversation
|
||||
mock_response = MagicMock(spec=ParsedResponse[BaseModel])
|
||||
mock_response.id = "resp_parsed_12345"
|
||||
mock_response.conversation = None
|
||||
|
||||
result = client.get_conversation_id(mock_response, store=True)
|
||||
|
||||
assert result == "resp_parsed_12345"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_project_client() -> MagicMock:
|
||||
"""Fixture that provides a mock AIProjectClient."""
|
||||
mock_client = MagicMock()
|
||||
|
||||
# Mock agents property
|
||||
mock_client.agents = MagicMock()
|
||||
mock_client.agents.create_version = AsyncMock()
|
||||
|
||||
# Mock conversations property
|
||||
mock_client.conversations = MagicMock()
|
||||
mock_client.conversations.create = AsyncMock()
|
||||
|
||||
# Mock telemetry property
|
||||
mock_client.telemetry = MagicMock()
|
||||
mock_client.telemetry.get_application_insights_connection_string = AsyncMock()
|
||||
|
||||
# Mock get_openai_client method
|
||||
mock_client.get_openai_client = AsyncMock()
|
||||
|
||||
# Mock close method
|
||||
mock_client.close = AsyncMock()
|
||||
|
||||
return mock_client
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -564,10 +564,6 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
|
||||
# Validate that store is True when conversation_id is set
|
||||
if chat_options.conversation_id is not None and chat_options.store is not True:
|
||||
logger.warning(
|
||||
"When conversation_id is set, store must be True for service-managed threads. "
|
||||
"Automatically setting store=True."
|
||||
)
|
||||
chat_options.store = True
|
||||
|
||||
if chat_options.instructions:
|
||||
@@ -663,10 +659,6 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
|
||||
# Validate that store is True when conversation_id is set
|
||||
if chat_options.conversation_id is not None and chat_options.store is not True:
|
||||
logger.warning(
|
||||
"When conversation_id is set, store must be True for service-managed threads. "
|
||||
"Automatically setting store=True."
|
||||
)
|
||||
chat_options.store = True
|
||||
|
||||
if chat_options.instructions:
|
||||
|
||||
@@ -1636,7 +1636,7 @@ def _handle_function_calls_response(
|
||||
# this runs in every but the first run
|
||||
# we need to keep track of all function call messages
|
||||
fcc_messages.extend(response.messages)
|
||||
if getattr(kwargs.get("chat_options"), "store", False):
|
||||
if response.conversation_id is not None:
|
||||
prepped_messages.clear()
|
||||
prepped_messages.append(result_message)
|
||||
else:
|
||||
@@ -1839,7 +1839,7 @@ def _handle_function_calls_streaming_response(
|
||||
# this runs in every but the first run
|
||||
# we need to keep track of all function call messages
|
||||
fcc_messages.extend(response.messages)
|
||||
if getattr(kwargs.get("chat_options"), "store", False):
|
||||
if response.conversation_id is not None:
|
||||
prepped_messages.clear()
|
||||
prepped_messages.append(result_message)
|
||||
else:
|
||||
|
||||
@@ -9,6 +9,7 @@ _IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"AgentFunctionApp": ("agent_framework_azurefunctions", "azurefunctions"),
|
||||
"AgentResponseCallbackProtocol": ("agent_framework_azurefunctions", "azurefunctions"),
|
||||
"AzureAIAgentClient": ("agent_framework_azure_ai", "azure-ai"),
|
||||
"AzureAIClient": ("agent_framework_azure_ai", "azure-ai"),
|
||||
"AzureOpenAIAssistantsClient": ("agent_framework.azure._assistants_client", "core"),
|
||||
"AzureOpenAIChatClient": ("agent_framework.azure._chat_client", "core"),
|
||||
"AzureAISettings": ("agent_framework_azure_ai", "azure-ai"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from agent_framework_azure_ai import AzureAIAgentClient, AzureAISettings
|
||||
from agent_framework_azure_ai import AzureAIAgentClient, AzureAIClient, AzureAISettings
|
||||
from agent_framework_azurefunctions import (
|
||||
AgentCallbackContext,
|
||||
AgentFunctionApp,
|
||||
@@ -19,6 +19,7 @@ __all__ = [
|
||||
"AgentFunctionApp",
|
||||
"AgentResponseCallbackProtocol",
|
||||
"AzureAIAgentClient",
|
||||
"AzureAIClient",
|
||||
"AzureAISettings",
|
||||
"AzureOpenAIAssistantsClient",
|
||||
"AzureOpenAIChatClient",
|
||||
|
||||
@@ -161,7 +161,8 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
async def close(self) -> None:
|
||||
"""Clean up any assistants we created."""
|
||||
if self._should_delete_assistant and self.assistant_id is not None:
|
||||
await self.client.beta.assistants.delete(self.assistant_id)
|
||||
client = await self.ensure_client()
|
||||
await client.beta.assistants.delete(self.assistant_id)
|
||||
object.__setattr__(self, "assistant_id", None)
|
||||
object.__setattr__(self, "_should_delete_assistant", False)
|
||||
|
||||
@@ -215,7 +216,11 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
"""
|
||||
# If no assistant is provided, create a temporary assistant
|
||||
if self.assistant_id is None:
|
||||
created_assistant = await self.client.beta.assistants.create(name=self.assistant_name, model=self.model_id)
|
||||
if not self.model_id:
|
||||
raise ServiceInitializationError("Parameter 'model_id' is required for assistant creation.")
|
||||
|
||||
client = await self.ensure_client()
|
||||
created_assistant = await client.beta.assistants.create(name=self.assistant_name, model=self.model_id)
|
||||
self.assistant_id = created_assistant.id
|
||||
self._should_delete_assistant = True
|
||||
|
||||
@@ -233,6 +238,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
Returns:
|
||||
tuple: (stream, final_thread_id)
|
||||
"""
|
||||
client = await self.ensure_client()
|
||||
# Get any active run for this thread
|
||||
thread_run = await self._get_active_thread_run(thread_id)
|
||||
|
||||
@@ -240,7 +246,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
if thread_run is not None and tool_run_id is not None and tool_run_id == thread_run.id and tool_outputs:
|
||||
# There's an active run and we have tool results to submit, so submit the results.
|
||||
stream = self.client.beta.threads.runs.submit_tool_outputs_stream( # type: ignore[reportDeprecated]
|
||||
stream = client.beta.threads.runs.submit_tool_outputs_stream( # type: ignore[reportDeprecated]
|
||||
run_id=tool_run_id, thread_id=thread_run.thread_id, tool_outputs=tool_outputs
|
||||
)
|
||||
final_thread_id = thread_run.thread_id
|
||||
@@ -249,7 +255,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
final_thread_id = await self._prepare_thread(thread_id, thread_run, run_options)
|
||||
|
||||
# Now create a new run and stream the results.
|
||||
stream = self.client.beta.threads.runs.stream( # type: ignore[reportDeprecated]
|
||||
stream = client.beta.threads.runs.stream( # type: ignore[reportDeprecated]
|
||||
assistant_id=assistant_id, thread_id=final_thread_id, **run_options
|
||||
)
|
||||
|
||||
@@ -257,19 +263,21 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
async def _get_active_thread_run(self, thread_id: str | None) -> Run | None:
|
||||
"""Get any active run for the given thread."""
|
||||
client = await self.ensure_client()
|
||||
if thread_id is None:
|
||||
return None
|
||||
|
||||
async for run in self.client.beta.threads.runs.list(thread_id=thread_id, limit=1, order="desc"): # type: ignore[reportDeprecated]
|
||||
async for run in client.beta.threads.runs.list(thread_id=thread_id, limit=1, order="desc"): # type: ignore[reportDeprecated]
|
||||
if run.status not in ["completed", "cancelled", "failed", "expired"]:
|
||||
return run
|
||||
return None
|
||||
|
||||
async def _prepare_thread(self, thread_id: str | None, thread_run: Run | None, run_options: dict[str, Any]) -> str:
|
||||
"""Prepare the thread for a new run, creating or cleaning up as needed."""
|
||||
client = await self.ensure_client()
|
||||
if thread_id is None:
|
||||
# No thread ID was provided, so create a new thread.
|
||||
thread = await self.client.beta.threads.create( # type: ignore[reportDeprecated]
|
||||
thread = await client.beta.threads.create( # type: ignore[reportDeprecated]
|
||||
messages=run_options["additional_messages"],
|
||||
tool_resources=run_options.get("tool_resources"),
|
||||
metadata=run_options.get("metadata"),
|
||||
@@ -280,7 +288,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
if thread_run is not None:
|
||||
# There was an active run; we need to cancel it before starting a new run.
|
||||
await self.client.beta.threads.runs.cancel(run_id=thread_run.id, thread_id=thread_id) # type: ignore[reportDeprecated]
|
||||
await client.beta.threads.runs.cancel(run_id=thread_run.id, thread_id=thread_id) # type: ignore[reportDeprecated]
|
||||
|
||||
return thread_id
|
||||
|
||||
|
||||
@@ -69,10 +69,11 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
client = await self.ensure_client()
|
||||
options_dict = self._prepare_options(messages, chat_options)
|
||||
try:
|
||||
return self._create_chat_response(
|
||||
await self.client.chat.completions.create(stream=False, **options_dict), chat_options
|
||||
await client.chat.completions.create(stream=False, **options_dict), chat_options
|
||||
)
|
||||
except BadRequestError as ex:
|
||||
if ex.code == "content_filter":
|
||||
@@ -97,10 +98,11 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
client = await self.ensure_client()
|
||||
options_dict = self._prepare_options(messages, chat_options)
|
||||
options_dict["stream_options"] = {"include_usage": True}
|
||||
try:
|
||||
async for chunk in await self.client.chat.completions.create(stream=True, **options_dict):
|
||||
async for chunk in await client.chat.completions.create(stream=True, **options_dict):
|
||||
if len(chunk.choices) == 0 and chunk.usage is None:
|
||||
continue
|
||||
yield self._create_chat_response_update(chunk)
|
||||
|
||||
@@ -89,23 +89,24 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
options_dict = self._prepare_options(messages, chat_options)
|
||||
client = await self.ensure_client()
|
||||
run_options = await self.prepare_options(messages, chat_options)
|
||||
try:
|
||||
if not chat_options.response_format:
|
||||
response = await self.client.responses.create(
|
||||
response_format = run_options.pop("response_format", None)
|
||||
if not response_format:
|
||||
response = await client.responses.create(
|
||||
stream=False,
|
||||
**options_dict,
|
||||
**run_options,
|
||||
)
|
||||
chat_options.conversation_id = response.id if chat_options.store is True else None
|
||||
chat_options.conversation_id = self.get_conversation_id(response, chat_options.store)
|
||||
return self._create_response_content(response, chat_options=chat_options)
|
||||
# create call does not support response_format, so we need to handle it via parse call
|
||||
resp_format = chat_options.response_format
|
||||
parsed_response: ParsedResponse[BaseModel] = await self.client.responses.parse(
|
||||
text_format=resp_format,
|
||||
parsed_response: ParsedResponse[BaseModel] = await client.responses.parse(
|
||||
text_format=response_format,
|
||||
stream=False,
|
||||
**options_dict,
|
||||
**run_options,
|
||||
)
|
||||
chat_options.conversation_id = parsed_response.id if chat_options.store is True else None
|
||||
chat_options.conversation_id = self.get_conversation_id(parsed_response, chat_options.store)
|
||||
return self._create_response_content(parsed_response, chat_options=chat_options)
|
||||
except BadRequestError as ex:
|
||||
if ex.code == "content_filter":
|
||||
@@ -130,13 +131,15 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
options_dict = self._prepare_options(messages, chat_options)
|
||||
client = await self.ensure_client()
|
||||
run_options = await self.prepare_options(messages, chat_options)
|
||||
function_call_ids: dict[int, tuple[str, str]] = {} # output_index: (call_id, name)
|
||||
try:
|
||||
if not chat_options.response_format:
|
||||
response = await self.client.responses.create(
|
||||
response_format = run_options.pop("response_format", None)
|
||||
if not response_format:
|
||||
response = await client.responses.create(
|
||||
stream=True,
|
||||
**options_dict,
|
||||
**run_options,
|
||||
)
|
||||
async for chunk in response:
|
||||
update = self._create_streaming_response_content(
|
||||
@@ -145,9 +148,9 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
yield update
|
||||
return
|
||||
# create call does not support response_format, so we need to handle it via stream call
|
||||
async with self.client.responses.stream(
|
||||
text_format=chat_options.response_format,
|
||||
**options_dict,
|
||||
async with client.responses.stream(
|
||||
text_format=response_format,
|
||||
**run_options,
|
||||
) as response:
|
||||
async for chunk in response:
|
||||
update = self._create_streaming_response_content(
|
||||
@@ -170,6 +173,12 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
inner_exception=ex,
|
||||
) from ex
|
||||
|
||||
def get_conversation_id(
|
||||
self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None
|
||||
) -> str | None:
|
||||
"""Get the conversation ID from the response if store is True."""
|
||||
return response.id if store else None
|
||||
|
||||
# region Prep methods
|
||||
|
||||
def _tools_to_response_tools(
|
||||
@@ -180,31 +189,7 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case HostedMCPTool():
|
||||
mcp: Mcp = {
|
||||
"type": "mcp",
|
||||
"server_label": tool.name.replace(" ", "_"),
|
||||
"server_url": str(tool.url),
|
||||
"server_description": tool.description,
|
||||
"headers": tool.headers,
|
||||
}
|
||||
if tool.allowed_tools:
|
||||
mcp["allowed_tools"] = list(tool.allowed_tools)
|
||||
if tool.approval_mode:
|
||||
match tool.approval_mode:
|
||||
case str():
|
||||
mcp["require_approval"] = (
|
||||
"always" if tool.approval_mode == "always_require" else "never"
|
||||
)
|
||||
case _:
|
||||
if always_require_approvals := tool.approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {
|
||||
"always": {"tool_names": list(always_require_approvals)}
|
||||
}
|
||||
if never_require_approvals := tool.approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {
|
||||
"never": {"tool_names": list(never_require_approvals)}
|
||||
}
|
||||
response_tools.append(mcp)
|
||||
response_tools.append(self.get_mcp_tool(tool))
|
||||
case HostedCodeInterpreterTool():
|
||||
tool_args: CodeInterpreterContainerCodeInterpreterToolAuto = {"type": "auto"}
|
||||
if tool.inputs:
|
||||
@@ -306,12 +291,36 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
response_tools.append(tool_dict)
|
||||
return response_tools
|
||||
|
||||
def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]:
|
||||
def get_mcp_tool(self, tool: HostedMCPTool) -> Any:
|
||||
"""Get MCP tool from HostedMCPTool."""
|
||||
mcp: Mcp = {
|
||||
"type": "mcp",
|
||||
"server_label": tool.name.replace(" ", "_"),
|
||||
"server_url": str(tool.url),
|
||||
"server_description": tool.description,
|
||||
"headers": tool.headers,
|
||||
}
|
||||
if tool.allowed_tools:
|
||||
mcp["allowed_tools"] = list(tool.allowed_tools)
|
||||
if tool.approval_mode:
|
||||
match tool.approval_mode:
|
||||
case str():
|
||||
mcp["require_approval"] = "always" if tool.approval_mode == "always_require" else "never"
|
||||
case _:
|
||||
if always_require_approvals := tool.approval_mode.get("always_require_approval"):
|
||||
mcp["require_approval"] = {"always": {"tool_names": list(always_require_approvals)}}
|
||||
if never_require_approvals := tool.approval_mode.get("never_require_approval"):
|
||||
mcp["require_approval"] = {"never": {"tool_names": list(never_require_approvals)}}
|
||||
|
||||
return mcp
|
||||
|
||||
async def prepare_options(
|
||||
self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions
|
||||
) -> dict[str, Any]:
|
||||
"""Take ChatOptions and create the specific options for Responses API."""
|
||||
options_dict: dict[str, Any] = chat_options.to_dict(
|
||||
run_options: dict[str, Any] = chat_options.to_dict(
|
||||
exclude={
|
||||
"type",
|
||||
"response_format", # handled in inner get methods
|
||||
"presence_penalty", # not supported
|
||||
"frequency_penalty", # not supported
|
||||
"logit_bias", # not supported
|
||||
@@ -320,6 +329,10 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
"instructions", # already added as system message
|
||||
}
|
||||
)
|
||||
|
||||
if chat_options.response_format:
|
||||
run_options["response_format"] = chat_options.response_format
|
||||
|
||||
translations = {
|
||||
"model_id": "model",
|
||||
"allow_multiple_tool_calls": "parallel_tool_calls",
|
||||
@@ -327,35 +340,37 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
"max_tokens": "max_output_tokens",
|
||||
}
|
||||
for old_key, new_key in translations.items():
|
||||
if old_key in options_dict and old_key != new_key:
|
||||
options_dict[new_key] = options_dict.pop(old_key)
|
||||
if old_key in run_options and old_key != new_key:
|
||||
run_options[new_key] = run_options.pop(old_key)
|
||||
|
||||
# tools
|
||||
if chat_options.tools is None:
|
||||
options_dict.pop("parallel_tool_calls", None)
|
||||
run_options.pop("parallel_tool_calls", None)
|
||||
else:
|
||||
options_dict["tools"] = self._tools_to_response_tools(chat_options.tools)
|
||||
run_options["tools"] = self._tools_to_response_tools(chat_options.tools)
|
||||
|
||||
# model id
|
||||
if not options_dict.get("model"):
|
||||
options_dict["model"] = self.model_id
|
||||
if not run_options.get("model"):
|
||||
if not self.model_id:
|
||||
raise ValueError("model_id must be a non-empty string")
|
||||
run_options["model"] = self.model_id
|
||||
|
||||
# messages
|
||||
request_input = self._prepare_chat_messages_for_request(messages)
|
||||
if not request_input:
|
||||
raise ServiceInvalidRequestError("Messages are required for chat completions")
|
||||
options_dict["input"] = request_input
|
||||
run_options["input"] = request_input
|
||||
|
||||
# additional provider specific settings
|
||||
if additional_properties := options_dict.pop("additional_properties", None):
|
||||
if additional_properties := run_options.pop("additional_properties", None):
|
||||
for key, value in additional_properties.items():
|
||||
if value is not None:
|
||||
options_dict[key] = value
|
||||
if "store" not in options_dict:
|
||||
options_dict["store"] = False
|
||||
if (tool_choice := options_dict.get("tool_choice")) and len(tool_choice.keys()) == 1:
|
||||
options_dict["tool_choice"] = tool_choice["mode"]
|
||||
return options_dict
|
||||
run_options[key] = value
|
||||
if "store" not in run_options:
|
||||
run_options["store"] = False
|
||||
if (tool_choice := run_options.get("tool_choice")) and len(tool_choice.keys()) == 1:
|
||||
run_options["tool_choice"] = tool_choice["mode"]
|
||||
return run_options
|
||||
|
||||
def _prepare_chat_messages_for_request(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]:
|
||||
"""Prepare the chat messages for a request.
|
||||
@@ -504,7 +519,6 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
# call_id for the result needs to be the same as the call_id for the function call
|
||||
args: dict[str, Any] = {
|
||||
"call_id": content.call_id,
|
||||
"id": call_id_to_id.get(content.call_id),
|
||||
"type": "function_call_output",
|
||||
}
|
||||
if content.result:
|
||||
@@ -734,7 +748,7 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
"raw_representation": response,
|
||||
}
|
||||
if chat_options.store:
|
||||
args["conversation_id"] = response.id
|
||||
args["conversation_id"] = self.get_conversation_id(response, chat_options.store)
|
||||
if response.usage and (usage_details := self._usage_details_from_openai(response.usage)):
|
||||
args["usage_details"] = usage_details
|
||||
if structured_response:
|
||||
@@ -834,7 +848,7 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
contents.append(TextReasoningContent(text=event.text, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
case "response.completed":
|
||||
conversation_id = event.response.id if chat_options.store is True else None
|
||||
conversation_id = self.get_conversation_id(event.response, chat_options.store)
|
||||
model = event.response.model
|
||||
if event.response.usage:
|
||||
usage = self._usage_details_from_openai(event.response.usage)
|
||||
|
||||
@@ -127,18 +127,18 @@ class OpenAIBase(SerializationMixin):
|
||||
|
||||
INJECTABLE: ClassVar[set[str]] = {"client"}
|
||||
|
||||
def __init__(self, *, client: AsyncOpenAI, model_id: str, **kwargs: Any) -> None:
|
||||
def __init__(self, *, model_id: str | None = None, client: AsyncOpenAI | None = None, **kwargs: Any) -> None:
|
||||
"""Initialize OpenAIBase.
|
||||
|
||||
Keyword Args:
|
||||
client: The AsyncOpenAI client instance.
|
||||
model_id: The AI model ID to use (non-empty, whitespace stripped).
|
||||
model_id: The AI model ID to use.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
if not model_id or not model_id.strip():
|
||||
raise ValueError("model_id must be a non-empty string")
|
||||
self.client = client
|
||||
self.model_id = model_id.strip()
|
||||
self.model_id = None
|
||||
if model_id:
|
||||
self.model_id = model_id.strip()
|
||||
|
||||
# Call super().__init__() to continue MRO chain (e.g., BaseChatClient)
|
||||
# Extract known kwargs that belong to other base classes
|
||||
@@ -162,6 +162,21 @@ class OpenAIBase(SerializationMixin):
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
async def initialize_client(self) -> None:
|
||||
"""Initialize OpenAI client asynchronously.
|
||||
|
||||
Override in subclasses to initialize the OpenAI client asynchronously.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def ensure_client(self) -> AsyncOpenAI:
|
||||
"""Ensure OpenAI client is initialized."""
|
||||
await self.initialize_client()
|
||||
if self.client is None:
|
||||
raise ServiceInitializationError("OpenAI client is not initialized")
|
||||
|
||||
return self.client
|
||||
|
||||
def _get_api_key(
|
||||
self, api_key: str | SecretStr | Callable[[], str | Awaitable[str]] | None
|
||||
) -> str | Callable[[], str | Awaitable[str]] | None:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1407,27 +1407,27 @@ def test_create_response_content_image_generation_fallback():
|
||||
assert f"data:image/png;base64,{unrecognized_base64}" == content.uri
|
||||
|
||||
|
||||
def test_prepare_options_store_parameter_handling() -> None:
|
||||
async def test_prepare_options_store_parameter_handling() -> None:
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
messages = [ChatMessage(role="user", text="Test message")]
|
||||
|
||||
test_conversation_id = "test-conversation-123"
|
||||
chat_options = ChatOptions(store=True, conversation_id=test_conversation_id)
|
||||
options = client._prepare_options(messages, chat_options) # type: ignore
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
assert options["store"] is True
|
||||
assert options["previous_response_id"] == test_conversation_id
|
||||
|
||||
chat_options = ChatOptions(store=False, conversation_id="")
|
||||
options = client._prepare_options(messages, chat_options) # type: ignore
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
assert options["store"] is False
|
||||
|
||||
chat_options = ChatOptions(store=None, conversation_id=None)
|
||||
options = client._prepare_options(messages, chat_options) # type: ignore
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
assert options["store"] is False
|
||||
assert "previous_response_id" not in options
|
||||
|
||||
chat_options = ChatOptions()
|
||||
options = client._prepare_options(messages, chat_options) # type: ignore
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
assert options["store"] is False
|
||||
assert "previous_response_id" not in options
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+13
-13
@@ -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 && (
|
||||
|
||||
+4
-3
@@ -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[];
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
+14
-14
@@ -21,20 +21,20 @@ This directory contains samples demonstrating the capabilities of Microsoft Agen
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`getting_started/agents/azure_ai/azure_ai_basic.py`](./getting_started/agents/azure_ai/azure_ai_basic.py) | Azure AI Agent Basic Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py`](./getting_started/agents/azure_ai/azure_ai_with_azure_ai_search.py) | Azure AI Agent with Azure AI Search Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py`](./getting_started/agents/azure_ai/azure_ai_with_bing_grounding.py) | Azure AI agent with Bing Grounding search for real-time web information |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py`](./getting_started/agents/azure_ai/azure_ai_with_code_interpreter.py) | Azure AI Agent with Code Interpreter Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_existing_agent.py`](./getting_started/agents/azure_ai/azure_ai_with_existing_agent.py) | Azure AI Agent with Existing Agent Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_existing_thread.py`](./getting_started/agents/azure_ai/azure_ai_with_existing_thread.py) | Azure AI Agent with Existing Thread Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py`](./getting_started/agents/azure_ai/azure_ai_with_explicit_settings.py) | Azure AI Agent with Explicit Settings Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_file_search.py`](./getting_started/agents/azure_ai/azure_ai_with_file_search.py) | Azure AI agent with File Search capabilities |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_function_tools.py`](./getting_started/agents/azure_ai/azure_ai_with_function_tools.py) | Azure AI Agent with Function Tools Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py`](./getting_started/agents/azure_ai/azure_ai_with_hosted_mcp.py) | Azure AI Agent with Hosted MCP Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_local_mcp.py`](./getting_started/agents/azure_ai/azure_ai_with_local_mcp.py) | Azure AI Agent with Local MCP Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_multiple_tools.py`](./getting_started/agents/azure_ai/azure_ai_with_multiple_tools.py) | Azure AI Agent with Multiple Tools Example |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_openapi_tools.py`](./getting_started/agents/azure_ai/azure_ai_with_openapi_tools.py) | Azure AI agent with OpenAPI tools |
|
||||
| [`getting_started/agents/azure_ai/azure_ai_with_thread.py`](./getting_started/agents/azure_ai/azure_ai_with_thread.py) | Azure AI Agent with Thread Management Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_basic.py`](./getting_started/agents/azure_ai/azure_ai_basic.py) | Azure AI Agent Basic Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_azure_ai_search.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_azure_ai_search.py) | Azure AI Agent with Azure AI Search Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_bing_grounding.py) | Azure AI agent with Bing Grounding search for real-time web information |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_code_interpreter.py) | Azure AI Agent with Code Interpreter Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_existing_agent.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_existing_agent.py) | Azure AI Agent with Existing Agent Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_existing_thread.py) | Azure AI Agent with Existing Thread Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_explicit_settings.py) | Azure AI Agent with Explicit Settings Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_file_search.py) | Azure AI agent with File Search capabilities |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_function_tools.py) | Azure AI Agent with Function Tools Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_hosted_mcp.py) | Azure AI Agent with Hosted MCP Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_local_mcp.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_local_mcp.py) | Azure AI Agent with Local MCP Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_multiple_tools.py) | Azure AI Agent with Multiple Tools Example |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_openapi_tools.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_openapi_tools.py) | Azure AI agent with OpenAPI tools |
|
||||
| [`getting_started/agents/azure_ai_agent/azure_ai_with_thread.py`](./getting_started/agents/azure_ai_agent/azure_ai_with_thread.py) | Azure AI Agent with Thread Management Example |
|
||||
|
||||
### Azure OpenAI
|
||||
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
# Azure AI Agent Examples
|
||||
|
||||
This folder contains examples demonstrating different ways to create and use agents with the Azure AI chat client from the `agent_framework.azure` package.
|
||||
This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package.
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `ChatAgent` with `AzureAIAgentClient`. It automatically handles all configuration using environment variables. |
|
||||
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates web search capabilities with proper source citations and comprehensive error handling. |
|
||||
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
|
||||
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent ID to the Azure AI chat client. This example also demonstrates proper cleanup of manually created agents. |
|
||||
| [`azure_ai_with_existing_thread.py`](azure_ai_with_existing_thread.py) | Shows how to work with a pre-existing thread by providing the thread ID to the Azure AI chat client. This example also demonstrates proper cleanup of manually created threads. |
|
||||
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIAgentClient` settings, including project endpoint, model deployment, credentials, and agent name. |
|
||||
| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Demonstrates how to use Azure AI Search with Azure AI agents to search through indexed data. Shows how to configure search parameters, query types, and integrate with existing search indexes. |
|
||||
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use the HostedFileSearchTool with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. Includes both streaming and non-streaming examples. |
|
||||
| [`azure_ai_with_function_tools.py`](azure_ai_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
|
||||
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate Azure AI agents with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. |
|
||||
| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. |
|
||||
| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools. Shows coordinated multi-tool interactions and approval workflows. |
|
||||
| [`azure_ai_with_openapi_tools.py`](azure_ai_with_openapi_tools.py) | Demonstrates how to use OpenAPI tools with Azure AI agents to integrate external REST APIs. Shows OpenAPI specification loading, anonymous authentication, thread context management, and coordinated multi-API conversations using weather and countries APIs. |
|
||||
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `AzureAIClient`. Demonstrates both streaming and non-streaming responses with function tools. Shows automatic agent creation and basic weather functionality. |
|
||||
| [`azure_ai_use_latest_version.py`](azure_ai_use_latest_version.py) | Demonstrates how to reuse the latest version of an existing agent instead of creating a new agent version on each instantiation using the `use_latest_version=True` parameter. |
|
||||
| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Shows how to use Azure AI Search with Azure AI agents to search through indexed data and answer user questions with proper citations. Requires an Azure AI Search connection and index configured in your Azure AI project. |
|
||||
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the `HostedCodeInterpreterTool` with Azure AI agents to write and execute Python code for mathematical problem solving and data analysis. |
|
||||
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent name and version to the Azure AI client. Demonstrates agent reuse patterns for production scenarios. |
|
||||
| [`azure_ai_with_existing_conversation.py`](azure_ai_with_existing_conversation.py) | Demonstrates how to use an existing conversation created on the service side with Azure AI agents. Shows two approaches: specifying conversation ID at the client level and using AgentThread with an existing conversation ID. |
|
||||
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIClient` settings, including project endpoint, model deployment, and credentials rather than relying on environment variable defaults. |
|
||||
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Shows how to use the `HostedFileSearchTool` with Azure AI agents to upload files, create vector stores, and enable agents to search through uploaded documents to answer user questions. |
|
||||
| [`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
|
||||
|
||||
@@ -28,27 +27,18 @@ Before running the examples, you need to set up your environment variables. You
|
||||
### Option 1: Using a .env file (Recommended)
|
||||
|
||||
1. Copy the `.env.example` file from the `python` directory to create a `.env` file:
|
||||
|
||||
```bash
|
||||
cp ../../.env.example ../../.env
|
||||
cp ../../../../.env.example ../../../../.env
|
||||
```
|
||||
|
||||
2. Edit the `.env` file and add your values:
|
||||
```
|
||||
|
||||
```env
|
||||
AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint"
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
|
||||
```
|
||||
|
||||
3. For samples using Bing Grounding search (like `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`), you'll also need:
|
||||
```
|
||||
BING_CONNECTION_ID="your-bing-connection-id"
|
||||
```
|
||||
|
||||
To get your Bing connection details:
|
||||
- Go to [Azure AI Foundry portal](https://ai.azure.com)
|
||||
- Navigate to your project's "Connected resources" section
|
||||
- Add a new connection for "Grounding with Bing Search"
|
||||
- Copy the ID
|
||||
|
||||
### Option 2: Using environment variables directly
|
||||
|
||||
Set the environment variables in your shell:
|
||||
@@ -56,7 +46,6 @@ Set the environment variables in your shell:
|
||||
```bash
|
||||
export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint"
|
||||
export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
|
||||
export BING_CONNECTION_ID="your-bing-connection-id"
|
||||
```
|
||||
|
||||
### Required Variables
|
||||
@@ -64,6 +53,24 @@ export BING_CONNECTION_ID="your-bing-connection-id"
|
||||
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples)
|
||||
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples)
|
||||
|
||||
### Optional Variables
|
||||
## Authentication
|
||||
|
||||
- `BING_CONNECTION_ID`: Your Bing connection ID (required for `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`)
|
||||
All examples use `AzureCliCredential` for authentication by default. Before running the examples:
|
||||
|
||||
1. Install the Azure CLI
|
||||
2. Run `az login` to authenticate with your Azure account
|
||||
3. Ensure you have appropriate permissions to the Azure AI project
|
||||
|
||||
Alternatively, you can replace `AzureCliCredential` with other authentication options like `DefaultAzureCredential` or environment-based credentials.
|
||||
|
||||
## Running the Examples
|
||||
|
||||
Each example can be run independently. Navigate to this directory and run any example:
|
||||
|
||||
```bash
|
||||
python azure_ai_basic.py
|
||||
python azure_ai_with_code_interpreter.py
|
||||
# ... etc
|
||||
```
|
||||
|
||||
The examples demonstrate various patterns for working with Azure AI agents, from basic usage to advanced scenarios like thread management and structured outputs.
|
||||
|
||||
@@ -4,15 +4,15 @@ import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Azure AI Agent Basic Example
|
||||
|
||||
This sample demonstrates basic usage of AzureAIAgentClient to create agents with automatic
|
||||
lifecycle management. Shows both streaming and non-streaming responses with function tools.
|
||||
This sample demonstrates basic usage of AzureAIClient.
|
||||
Shows both streaming and non-streaming responses with function tools.
|
||||
"""
|
||||
|
||||
|
||||
@@ -28,14 +28,13 @@ async def non_streaming_example() -> None:
|
||||
"""Example of non-streaming response (get the complete result at once)."""
|
||||
print("=== Non-streaming Response Example ===")
|
||||
|
||||
# Since no Agent ID is provided, the agent will be automatically created
|
||||
# and deleted after getting a response
|
||||
# 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,
|
||||
AzureAIAgentClient(async_credential=credential).create_agent(
|
||||
name="WeatherAgent",
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="BasicWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
@@ -50,19 +49,18 @@ async def streaming_example() -> None:
|
||||
"""Example of streaming response (get results as they are generated)."""
|
||||
print("=== Streaming Response Example ===")
|
||||
|
||||
# Since no Agent ID is provided, the agent will be automatically created
|
||||
# and deleted after getting a response
|
||||
# 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,
|
||||
AzureAIAgentClient(async_credential=credential).create_agent(
|
||||
name="WeatherAgent",
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="BasicWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
query = "What's the weather like in Portland?"
|
||||
query = "What's the weather like in Tokyo?"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run_stream(query):
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Azure AI Agent Latest Version Example
|
||||
|
||||
This sample demonstrates how to reuse the latest version of an existing agent
|
||||
instead of creating a new agent version on each instantiation. The first call creates a new agent,
|
||||
while subsequent calls with `use_latest_version=True` reuse the latest agent version.
|
||||
"""
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with AzureCliCredential() as credential:
|
||||
async with (
|
||||
AzureAIClient(
|
||||
async_credential=credential,
|
||||
).create_agent(
|
||||
name="MyWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# First query will create a new agent
|
||||
query = "What's the weather like in Seattle?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
# Create a new agent instance
|
||||
async with (
|
||||
AzureAIClient(
|
||||
async_credential=credential,
|
||||
# This parameter will allow to re-use latest agent version
|
||||
# instead of creating a new one
|
||||
use_latest_version=True,
|
||||
).create_agent(
|
||||
name="MyWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
query = "What's the weather like in Tokyo?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,120 +1,49 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent, CitationAnnotation
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import ConnectionType
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent with Azure AI Search Example
|
||||
|
||||
This sample demonstrates how to create an Azure AI agent that uses Azure AI Search
|
||||
to search through indexed hotel data and answer user questions about hotels.
|
||||
This sample demonstrates usage of AzureAIClient with Azure AI Search
|
||||
to search through indexed data and answer user questions about it.
|
||||
|
||||
Prerequisites:
|
||||
1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables
|
||||
1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables.
|
||||
2. Ensure you have an Azure AI Search connection configured in your Azure AI project
|
||||
3. The search index "hotels-sample-index" should exist in your Azure AI Search service
|
||||
(you can create this using the Azure portal with sample hotel data)
|
||||
|
||||
NOTE: To ensure consistent search tool usage:
|
||||
- Include explicit instructions for the agent to use the search tool
|
||||
- Mention the search requirement in your queries
|
||||
- Use `tool_choice="required"` to force tool usage
|
||||
|
||||
More info on `query type` can be found here:
|
||||
https://learn.microsoft.com/en-us/python/api/azure-ai-agents/azure.ai.agents.models.aisearchindexresource?view=azure-python-preview
|
||||
and set AI_SEARCH_PROJECT_CONNECTION_ID and AI_SEARCH_INDEX_NAME environment variable.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main function demonstrating Azure AI agent with raw Azure AI Search tool."""
|
||||
print("=== Azure AI Agent with Raw Azure AI Search Tool ===")
|
||||
|
||||
# Create the client and manually create an agent with Azure AI Search tool
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
|
||||
):
|
||||
ai_search_conn_id = ""
|
||||
async for connection in project_client.connections.list():
|
||||
if connection.type == ConnectionType.AZURE_AI_SEARCH:
|
||||
ai_search_conn_id = connection.id
|
||||
break
|
||||
|
||||
# 1. Create Azure AI agent with the search tool
|
||||
azure_ai_agent = await project_client.agents.create_agent(
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
name="HotelSearchAgent",
|
||||
instructions=(
|
||||
"You are a helpful agent that searches hotel information using Azure AI Search. "
|
||||
"Always use the search tool and index to find hotel data and provide accurate information."
|
||||
),
|
||||
tools=[{"type": "azure_ai_search"}],
|
||||
tool_resources={
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="MySearchAgent",
|
||||
instructions="""You are a helpful assistant. You must always provide citations for
|
||||
answers using the tool and render them as: `[message_idx:search_idx†source]`.""",
|
||||
tools={
|
||||
"type": "azure_ai_search",
|
||||
"azure_ai_search": {
|
||||
"indexes": [
|
||||
{
|
||||
"index_connection_id": ai_search_conn_id,
|
||||
"index_name": "hotels-sample-index",
|
||||
"query_type": "vector",
|
||||
"project_connection_id": os.environ["AI_SEARCH_PROJECT_CONNECTION_ID"],
|
||||
"index_name": os.environ["AI_SEARCH_INDEX_NAME"],
|
||||
# For query_type=vector, ensure your index has a field with vectorized data.
|
||||
"query_type": "simple",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# 2. Create chat client with the existing agent
|
||||
chat_client = AzureAIAgentClient(agents_client=agents_client, agent_id=azure_ai_agent.id)
|
||||
|
||||
try:
|
||||
async with ChatAgent(
|
||||
chat_client=chat_client,
|
||||
# Additional instructions for this specific conversation
|
||||
instructions=("You are a helpful agent that uses the search tool and index to find hotel information."),
|
||||
) as agent:
|
||||
print("This agent uses raw Azure AI Search tool to search hotel data.\n")
|
||||
|
||||
# 3. Simulate conversation with the agent
|
||||
user_input = (
|
||||
"Use Azure AI search knowledge tool to find detailed information about a winter hotel."
|
||||
" Use the search tool and index." # You can modify prompt to force tool usage
|
||||
)
|
||||
print(f"User: {user_input}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
|
||||
# Stream the response and collect citations
|
||||
citations: list[CitationAnnotation] = []
|
||||
async for chunk in agent.run_stream(user_input):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
|
||||
# Collect citations from Azure AI Search responses
|
||||
for content in getattr(chunk, "contents", []):
|
||||
annotations = getattr(content, "annotations", [])
|
||||
if annotations:
|
||||
citations.extend(annotations)
|
||||
|
||||
print()
|
||||
|
||||
# Display collected citations
|
||||
if citations:
|
||||
print("\n\nCitations:")
|
||||
for i, citation in enumerate(citations, 1):
|
||||
print(f"[{i}] Reference: {citation.url}")
|
||||
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
print("Hotel search conversation completed!")
|
||||
|
||||
finally:
|
||||
# Clean up the agent manually
|
||||
await project_client.agents.delete_agent(azure_ai_agent.id)
|
||||
) as agent,
|
||||
):
|
||||
query = "Tell me about insurance options"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Result: {result}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,57 +2,53 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunResponse, ChatResponseUpdate, HostedCodeInterpreterTool
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.ai.agents.models import (
|
||||
RunStepDeltaCodeInterpreterDetailItemObject,
|
||||
)
|
||||
from agent_framework import ChatResponse, HostedCodeInterpreterTool
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from openai.types.responses.response import Response as OpenAIResponse
|
||||
from openai.types.responses.response_code_interpreter_tool_call import ResponseCodeInterpreterToolCall
|
||||
|
||||
"""
|
||||
Azure AI Agent with Code Interpreter Example
|
||||
Azure AI Agent Code Interpreter Example
|
||||
|
||||
This sample demonstrates using HostedCodeInterpreterTool with Azure AI Agents
|
||||
This sample demonstrates using HostedCodeInterpreterTool with AzureAIClient
|
||||
for Python code execution and mathematical problem solving.
|
||||
"""
|
||||
|
||||
|
||||
def print_code_interpreter_inputs(response: AgentRunResponse) -> None:
|
||||
"""Helper method to access code interpreter data."""
|
||||
|
||||
print("\nCode Interpreter Inputs during the run:")
|
||||
if response.raw_representation is None:
|
||||
return
|
||||
for chunk in response.raw_representation:
|
||||
if isinstance(chunk, ChatResponseUpdate) and isinstance(
|
||||
chunk.raw_representation, RunStepDeltaCodeInterpreterDetailItemObject
|
||||
):
|
||||
print(chunk.raw_representation.input, end="")
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Example showing how to use the HostedCodeInterpreterTool with Azure AI."""
|
||||
print("=== Azure AI Agent with Code Interpreter Example ===")
|
||||
"""Example showing how to use the HostedCodeInterpreterTool with AzureAIClient."""
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIAgentClient(async_credential=credential) as chat_client,
|
||||
):
|
||||
agent = chat_client.create_agent(
|
||||
name="CodingAgent",
|
||||
instructions=("You are a helpful assistant that can write and execute Python code to solve problems."),
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
instructions="You are a helpful assistant that can write and execute Python code to solve problems.",
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
)
|
||||
query = "Generate the factorial of 100 using python code, show the code and execute it."
|
||||
) as agent,
|
||||
):
|
||||
query = "Use code to get the factorial of 100?"
|
||||
print(f"User: {query}")
|
||||
response = await AgentRunResponse.from_agent_response_generator(agent.run_stream(query))
|
||||
print(f"Agent: {response}")
|
||||
# To review the code interpreter outputs, you can access
|
||||
# them from the response raw_representations, just uncomment the next line:
|
||||
# print_code_interpreter_inputs(response)
|
||||
result = await agent.run(query)
|
||||
print(f"Result: {result}\n")
|
||||
|
||||
if (
|
||||
isinstance(result.raw_representation, ChatResponse)
|
||||
and isinstance(result.raw_representation.raw_representation, OpenAIResponse)
|
||||
and len(result.raw_representation.raw_representation.output) > 0
|
||||
):
|
||||
# Find the first ResponseCodeInterpreterToolCall item
|
||||
code_interpreter_item = next(
|
||||
(
|
||||
item
|
||||
for item in result.raw_representation.raw_representation.output
|
||||
if isinstance(item, ResponseCodeInterpreterToolCall)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if code_interpreter_item is not None:
|
||||
generated_code = code_interpreter_item.code
|
||||
print(f"Generated code:\n{generated_code}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -4,55 +4,60 @@ import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import PromptAgentDefinition
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent with Existing Agent Example
|
||||
|
||||
This sample demonstrates working with pre-existing Azure AI Agents by providing
|
||||
agent IDs, showing agent reuse patterns for production scenarios.
|
||||
agent name and version, showing agent reuse patterns for production scenarios.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Azure AI Chat Client with Existing Agent ===")
|
||||
|
||||
# Create the client
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
|
||||
):
|
||||
azure_ai_agent = await project_client.agents.create_agent(
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
# Create remote agent with default instructions
|
||||
# These instructions will persist on created agent for every run.
|
||||
instructions="End each response with [END].",
|
||||
azure_ai_agent = await project_client.agents.create_version(
|
||||
agent_name="MyNewTestAgent",
|
||||
definition=PromptAgentDefinition(
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
# Setting specific requirements to verify that this agent is used.
|
||||
instructions="End each response with [END].",
|
||||
),
|
||||
)
|
||||
|
||||
chat_client = AzureAIAgentClient(agents_client=agents_client, agent_id=azure_ai_agent.id)
|
||||
chat_client = AzureAIClient(
|
||||
project_client=project_client,
|
||||
agent_name=azure_ai_agent.name,
|
||||
# Property agent_version is required for existing agents.
|
||||
# If this property is not configured, the client will try to create a new agent using
|
||||
# provided agent_name.
|
||||
# It's also possible to leave agent_version empty but set use_latest_version=True.
|
||||
# This will pull latest available agent version and use that version for operations.
|
||||
agent_version=azure_ai_agent.version,
|
||||
)
|
||||
|
||||
try:
|
||||
async with ChatAgent(
|
||||
chat_client=chat_client,
|
||||
# Instructions here are applicable only to this ChatAgent instance
|
||||
# These instructions will be combined with instructions on existing remote agent.
|
||||
# The final instructions during the execution will look like:
|
||||
# "'End each response with [END]. Respond with 'Hello World' only'"
|
||||
instructions="Respond with 'Hello World' only",
|
||||
) as agent:
|
||||
query = "How are you?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
# Based on local and remote instructions, the result will be
|
||||
# 'Hello World [END]'.
|
||||
# Response that indicates that previously created agent was used:
|
||||
# "I'm here and ready to help you! How can I assist you today? [END]"
|
||||
print(f"Agent: {result}\n")
|
||||
finally:
|
||||
# Clean up the agent manually
|
||||
await project_client.agents.delete_agent(azure_ai_agent.id)
|
||||
await project_client.agents.delete_version(
|
||||
agent_name=azure_ai_agent.name, agent_version=azure_ai_agent.version
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
import asyncio
|
||||
import os
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Azure AI Agent Existing Conversation Example
|
||||
|
||||
This sample demonstrates usage of AzureAIClient with existing conversation created on service side.
|
||||
"""
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def example_with_client() -> None:
|
||||
"""Example shows how to specify existing conversation ID when initializing Azure AI Client."""
|
||||
print("=== Azure AI Agent With Existing Conversation and Client ===")
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
):
|
||||
# Create a conversation using OpenAI client
|
||||
openai_client = await project_client.get_openai_client()
|
||||
conversation = await openai_client.conversations.create()
|
||||
conversation_id = conversation.id
|
||||
print(f"Conversation ID: {conversation_id}")
|
||||
|
||||
async with AzureAIClient(
|
||||
project_client=project_client,
|
||||
# Specify conversation ID on client level
|
||||
conversation_id=conversation_id,
|
||||
).create_agent(
|
||||
name="BasicAgent",
|
||||
instructions="You are a helpful agent.",
|
||||
tools=get_weather,
|
||||
) as agent:
|
||||
query = "What's the weather like in Seattle?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result.text}\n")
|
||||
|
||||
query = "What was my last question?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result.text}\n")
|
||||
|
||||
|
||||
async def example_with_thread() -> None:
|
||||
"""This example shows how to specify existing conversation ID with AgentThread."""
|
||||
print("=== Azure AI Agent With Existing Conversation and Thread ===")
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
AzureAIClient(project_client=project_client).create_agent(
|
||||
name="BasicAgent",
|
||||
instructions="You are a helpful agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# Create a conversation using OpenAI client
|
||||
openai_client = await project_client.get_openai_client()
|
||||
conversation = await openai_client.conversations.create()
|
||||
conversation_id = conversation.id
|
||||
print(f"Conversation ID: {conversation_id}")
|
||||
|
||||
# Create a thread with the existing ID
|
||||
thread = agent.get_new_thread(service_thread_id=conversation_id)
|
||||
|
||||
query = "What's the weather like in Seattle?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query, thread=thread)
|
||||
print(f"Agent: {result.text}\n")
|
||||
|
||||
query = "What was my last question?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query, thread=thread)
|
||||
print(f"Agent: {result.text}\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
await example_with_client()
|
||||
await example_with_thread()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -6,7 +6,7 @@ from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
@@ -27,28 +27,26 @@ def get_weather(
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Azure AI Chat Client with Explicit Settings ===")
|
||||
|
||||
# Since no Agent ID is provided, the agent will be automatically created
|
||||
# and deleted after getting a response
|
||||
# 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,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(
|
||||
chat_client=AzureAIClient(
|
||||
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
model_deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
async_credential=credential,
|
||||
agent_name="WeatherAgent",
|
||||
should_cleanup_agent=True, # Set to False if you want to disable automatic agent cleanup
|
||||
),
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
result = await agent.run("What's the weather like in New York?")
|
||||
print(f"Result: {result}\n")
|
||||
query = "What's the weather like in New York?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import ChatAgent, HostedFileSearchTool, HostedVectorStoreContent
|
||||
from agent_framework_azure_ai import AzureAIAgentClient
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.ai.agents.models import FileInfo, VectorStore
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
@@ -24,69 +26,50 @@ USER_INPUTS = [
|
||||
|
||||
async def main() -> None:
|
||||
"""Main function demonstrating Azure AI agent with file search capabilities."""
|
||||
client = AzureAIAgentClient(async_credential=AzureCliCredential())
|
||||
file: FileInfo | None = None
|
||||
vector_store: VectorStore | None = None
|
||||
|
||||
try:
|
||||
# 1. Upload file and create vector store
|
||||
pdf_file_path = Path(__file__).parent.parent / "resources" / "employees.pdf"
|
||||
print(f"Uploading file from: {pdf_file_path}")
|
||||
|
||||
file = await client.project_client.agents.files.upload_and_poll(
|
||||
file_path=str(pdf_file_path), purpose="assistants"
|
||||
)
|
||||
print(f"Uploaded file, file ID: {file.id}")
|
||||
|
||||
vector_store = await client.project_client.agents.vector_stores.create_and_poll(
|
||||
file_ids=[file.id], name="my_vectorstore"
|
||||
)
|
||||
print(f"Created vector store, vector store ID: {vector_store.id}")
|
||||
|
||||
# 2. Create file search tool with uploaded resources
|
||||
file_search_tool = HostedFileSearchTool(inputs=[HostedVectorStoreContent(vector_store_id=vector_store.id)])
|
||||
|
||||
# 3. Create an agent with file search capabilities
|
||||
# The tool_resources are automatically extracted from HostedFileSearchTool
|
||||
async with ChatAgent(
|
||||
chat_client=client,
|
||||
name="EmployeeSearchAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant that can search through uploaded employee files "
|
||||
"to answer questions about employees."
|
||||
),
|
||||
tools=file_search_tool,
|
||||
) as agent:
|
||||
# 4. Simulate conversation with the agent
|
||||
for user_input in USER_INPUTS:
|
||||
print(f"# User: '{user_input}'")
|
||||
response = await agent.run(user_input)
|
||||
print(f"# Agent: {response.text}")
|
||||
|
||||
# 5. Cleanup: Delete the vector store and file
|
||||
try:
|
||||
if vector_store:
|
||||
await client.project_client.agents.vector_stores.delete(vector_store.id)
|
||||
if file:
|
||||
await client.project_client.agents.files.delete(file.id)
|
||||
except Exception:
|
||||
# Ignore cleanup errors to avoid masking issues
|
||||
pass
|
||||
finally:
|
||||
# 6. Cleanup: Delete the vector store and file in case of eariler failure to prevent orphaned resources.
|
||||
|
||||
# Refreshing the client is required since chat agent closes it
|
||||
client = AzureAIAgentClient(async_credential=AzureCliCredential())
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
|
||||
AzureAIClient(async_credential=credential) as client,
|
||||
):
|
||||
try:
|
||||
if vector_store:
|
||||
await client.project_client.agents.vector_stores.delete(vector_store.id)
|
||||
if file:
|
||||
await client.project_client.agents.files.delete(file.id)
|
||||
except Exception:
|
||||
# Ignore cleanup errors to avoid masking issues
|
||||
pass
|
||||
# 1. Upload file and create vector store
|
||||
pdf_file_path = Path(__file__).parent.parent / "resources" / "employees.pdf"
|
||||
print(f"Uploading file from: {pdf_file_path}")
|
||||
|
||||
file = await agents_client.files.upload_and_poll(file_path=str(pdf_file_path), purpose="assistants")
|
||||
print(f"Uploaded file, file ID: {file.id}")
|
||||
|
||||
vector_store = await agents_client.vector_stores.create_and_poll(file_ids=[file.id], name="my_vectorstore")
|
||||
print(f"Created vector store, vector store ID: {vector_store.id}")
|
||||
|
||||
# 2. Create file search tool with uploaded resources
|
||||
file_search_tool = HostedFileSearchTool(inputs=[HostedVectorStoreContent(vector_store_id=vector_store.id)])
|
||||
|
||||
# 3. Create an agent with file search capabilities
|
||||
# The tool_resources are automatically extracted from HostedFileSearchTool
|
||||
async with ChatAgent(
|
||||
chat_client=client,
|
||||
name="EmployeeSearchAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant that can search through uploaded employee files "
|
||||
"to answer questions about employees."
|
||||
),
|
||||
tools=file_search_tool,
|
||||
) as agent:
|
||||
# 4. Simulate conversation with the agent
|
||||
for user_input in USER_INPUTS:
|
||||
print(f"# User: '{user_input}'")
|
||||
response = await agent.run(user_input)
|
||||
print(f"# Agent: {response.text}")
|
||||
finally:
|
||||
await client.close()
|
||||
# 5. Cleanup: Delete the vector store and file in case of earlier failure to prevent orphaned resources.
|
||||
if vector_store:
|
||||
await agents_client.vector_stores.delete(vector_store.id)
|
||||
if file:
|
||||
await agents_client.files.delete(file.id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,23 +3,42 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import AgentProtocol, AgentThread, HostedMCPTool
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from agent_framework import AgentProtocol, AgentRunResponse, AgentThread, ChatMessage, HostedMCPTool
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent with Hosted MCP Example
|
||||
|
||||
This sample demonstrates integration of Azure AI Agents with hosted Model Context Protocol (MCP)
|
||||
servers, including user approval workflows for function call security.
|
||||
This sample demonstrates integrating hosted Model Context Protocol (MCP) tools with Azure AI Agent.
|
||||
"""
|
||||
|
||||
|
||||
async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", thread: "AgentThread"):
|
||||
"""Here we let the thread deal with the previous responses, and we just rerun with the approval."""
|
||||
from agent_framework import ChatMessage
|
||||
async def handle_approvals_without_thread(query: str, agent: "AgentProtocol") -> AgentRunResponse:
|
||||
"""When we don't have a thread, we need to ensure we return with the input, approval request and approval."""
|
||||
|
||||
result = await agent.run(query, thread=thread, store=True)
|
||||
result = await agent.run(query, store=False)
|
||||
while len(result.user_input_requests) > 0:
|
||||
new_inputs: list[Any] = [query]
|
||||
for user_input_needed in result.user_input_requests:
|
||||
print(
|
||||
f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}"
|
||||
f" with arguments: {user_input_needed.function_call.arguments}"
|
||||
)
|
||||
new_inputs.append(ChatMessage(role="assistant", contents=[user_input_needed]))
|
||||
user_approval = input("Approve function call? (y/n): ")
|
||||
new_inputs.append(
|
||||
ChatMessage(role="user", contents=[user_input_needed.create_response(user_approval.lower() == "y")])
|
||||
)
|
||||
|
||||
result = await agent.run(new_inputs, store=False)
|
||||
return result
|
||||
|
||||
|
||||
async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", thread: "AgentThread") -> AgentRunResponse:
|
||||
"""Here we let the thread deal with the previous responses, and we just rerun with the approval."""
|
||||
|
||||
result = await agent.run(query, thread=thread)
|
||||
while len(result.user_input_requests) > 0:
|
||||
new_input: list[Any] = []
|
||||
for user_input_needed in result.user_input_requests:
|
||||
@@ -34,36 +53,64 @@ async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", threa
|
||||
contents=[user_input_needed.create_response(user_approval.lower() == "y")],
|
||||
)
|
||||
)
|
||||
result = await agent.run(new_input, thread=thread, store=True)
|
||||
result = await agent.run(new_input, thread=thread)
|
||||
return result
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Example showing Hosted MCP tools for a Azure AI Agent."""
|
||||
async def run_hosted_mcp_without_approval() -> None:
|
||||
"""Example showing MCP Tools without approval."""
|
||||
# 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,
|
||||
AzureAIAgentClient(async_credential=credential) as chat_client,
|
||||
):
|
||||
agent = chat_client.create_agent(
|
||||
name="DocsAgent",
|
||||
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="MyLearnDocsAgent",
|
||||
instructions="You are a helpful assistant that can help with Microsoft documentation questions.",
|
||||
tools=HostedMCPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
approval_mode="never_require",
|
||||
),
|
||||
)
|
||||
) as agent,
|
||||
):
|
||||
query = "How to create an Azure storage account using az cli?"
|
||||
print(f"User: {query}")
|
||||
result = await handle_approvals_without_thread(query, agent)
|
||||
print(f"{agent.name}: {result}\n")
|
||||
|
||||
|
||||
async def run_hosted_mcp_with_approval_and_thread() -> None:
|
||||
"""Example showing MCP Tools with approvals using a thread."""
|
||||
print("=== MCP with approvals and with thread ===")
|
||||
|
||||
# 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="MyApiSpecsAgent",
|
||||
instructions="You are a helpful agent that can use MCP tools to assist users.",
|
||||
tools=HostedMCPTool(
|
||||
name="api-specs",
|
||||
url="https://gitmcp.io/Azure/azure-rest-api-specs",
|
||||
approval_mode="always_require",
|
||||
),
|
||||
) as agent,
|
||||
):
|
||||
thread = agent.get_new_thread()
|
||||
# First query
|
||||
query1 = "How to create an Azure storage account using az cli?"
|
||||
print(f"User: {query1}")
|
||||
result1 = await handle_approvals_with_thread(query1, agent, thread)
|
||||
print(f"{agent.name}: {result1}\n")
|
||||
print("\n=======================================\n")
|
||||
# Second query
|
||||
query2 = "What is Microsoft Agent Framework?"
|
||||
print(f"User: {query2}")
|
||||
result2 = await handle_approvals_with_thread(query2, agent, thread)
|
||||
print(f"{agent.name}: {result2}\n")
|
||||
query = "Please summarize the Azure REST API specifications Readme"
|
||||
print(f"User: {query}")
|
||||
result = await handle_approvals_with_thread(query, agent, thread)
|
||||
print(f"{agent.name}: {result}\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Azure AI Agent with Hosted MCP Tools Example ===\n")
|
||||
|
||||
await run_hosted_mcp_without_approval()
|
||||
await run_hosted_mcp_with_approval_and_thread()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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,54 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
"""
|
||||
Azure AI Agent Response Format Example
|
||||
|
||||
This sample demonstrates basic usage of AzureAIClient with response format,
|
||||
also known as structured outputs.
|
||||
"""
|
||||
|
||||
|
||||
class ReleaseBrief(BaseModel):
|
||||
feature: str
|
||||
benefit: str
|
||||
launch_date: str
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Example of using response_format property."""
|
||||
|
||||
# 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="ProductMarketerAgent",
|
||||
instructions="Return launch briefs as structured JSON.",
|
||||
) as agent,
|
||||
):
|
||||
query = "Draft a launch brief for the Contoso Note app."
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(
|
||||
query,
|
||||
# Specify type to use as response
|
||||
response_format=ReleaseBrief,
|
||||
)
|
||||
|
||||
if isinstance(result.value, ReleaseBrief):
|
||||
release_brief = result.value
|
||||
print("Agent:")
|
||||
print(f"Feature: {release_brief.feature}")
|
||||
print(f"Benefit: {release_brief.benefit}")
|
||||
print(f"Launch date: {release_brief.launch_date}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -4,16 +4,15 @@ import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import AgentThread, ChatAgent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from agent_framework.azure import AzureAIClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Azure AI Agent with Thread Management Example
|
||||
|
||||
This sample demonstrates thread management with Azure AI Agents, comparing
|
||||
automatic thread creation with explicit thread management for persistent context.
|
||||
This sample demonstrates thread management with Azure AI Agent, showing
|
||||
persistent conversation capabilities using service-managed threads as well as storing messages in-memory.
|
||||
"""
|
||||
|
||||
|
||||
@@ -26,44 +25,42 @@ def get_weather(
|
||||
|
||||
|
||||
async def example_with_automatic_thread_creation() -> None:
|
||||
"""Example showing automatic thread creation (service-managed thread)."""
|
||||
"""Example showing automatic thread creation."""
|
||||
print("=== Automatic Thread Creation Example ===")
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=credential),
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="BasicWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# First conversation - no thread provided, will be created automatically
|
||||
first_query = "What's the weather like in Seattle?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query)
|
||||
print(f"Agent: {first_result.text}")
|
||||
query1 = "What's the weather like in Seattle?"
|
||||
print(f"User: {query1}")
|
||||
result1 = await agent.run(query1)
|
||||
print(f"Agent: {result1.text}")
|
||||
|
||||
# Second conversation - still no thread provided, will create another new thread
|
||||
second_query = "What was the last city I asked about?"
|
||||
print(f"\nUser: {second_query}")
|
||||
second_result = await agent.run(second_query)
|
||||
print(f"Agent: {second_result.text}")
|
||||
query2 = "What was the last city I asked about?"
|
||||
print(f"\nUser: {query2}")
|
||||
result2 = await agent.run(query2)
|
||||
print(f"Agent: {result2.text}")
|
||||
print("Note: Each call creates a separate thread, so the agent doesn't remember previous context.\n")
|
||||
|
||||
|
||||
async def example_with_thread_persistence() -> None:
|
||||
"""Example showing thread persistence across multiple conversations."""
|
||||
print("=== Thread Persistence Example ===")
|
||||
print("Using the same thread across multiple conversations to maintain context.\n")
|
||||
async def example_with_thread_persistence_in_memory() -> None:
|
||||
"""
|
||||
Example showing thread persistence across multiple conversations.
|
||||
In this example, messages are stored in-memory.
|
||||
"""
|
||||
print("=== Thread Persistence Example (In-Memory) ===")
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=credential),
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="BasicWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
@@ -72,81 +69,80 @@ async def example_with_thread_persistence() -> None:
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
# First conversation
|
||||
first_query = "What's the weather like in Tokyo?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query, thread=thread)
|
||||
print(f"Agent: {first_result.text}")
|
||||
query1 = "What's the weather like in Tokyo?"
|
||||
print(f"User: {query1}")
|
||||
result1 = await agent.run(query1, thread=thread, store=False)
|
||||
print(f"Agent: {result1.text}")
|
||||
|
||||
# Second conversation using the same thread - maintains context
|
||||
second_query = "How about London?"
|
||||
print(f"\nUser: {second_query}")
|
||||
second_result = await agent.run(second_query, thread=thread)
|
||||
print(f"Agent: {second_result.text}")
|
||||
query2 = "How about London?"
|
||||
print(f"\nUser: {query2}")
|
||||
result2 = await agent.run(query2, thread=thread, store=False)
|
||||
print(f"Agent: {result2.text}")
|
||||
|
||||
# Third conversation - agent should remember both previous cities
|
||||
third_query = "Which of the cities I asked about has better weather?"
|
||||
print(f"\nUser: {third_query}")
|
||||
third_result = await agent.run(third_query, thread=thread)
|
||||
print(f"Agent: {third_result.text}")
|
||||
query3 = "Which of the cities I asked about has better weather?"
|
||||
print(f"\nUser: {query3}")
|
||||
result3 = await agent.run(query3, thread=thread, store=False)
|
||||
print(f"Agent: {result3.text}")
|
||||
print("Note: The agent remembers context from previous messages in the same thread.\n")
|
||||
|
||||
|
||||
async def example_with_existing_thread_id() -> None:
|
||||
"""Example showing how to work with an existing thread ID from the service."""
|
||||
"""
|
||||
Example showing how to work with an existing thread ID from the service.
|
||||
In this example, messages are stored on the server.
|
||||
"""
|
||||
print("=== Existing Thread ID Example ===")
|
||||
print("Using a specific thread ID to continue an existing conversation.\n")
|
||||
|
||||
# First, create a conversation and capture the thread ID
|
||||
existing_thread_id = None
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=credential),
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="BasicWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# Start a conversation and get the thread ID
|
||||
thread = agent.get_new_thread()
|
||||
first_query = "What's the weather in Paris?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query, thread=thread)
|
||||
print(f"Agent: {first_result.text}")
|
||||
|
||||
query1 = "What's the weather in Paris?"
|
||||
print(f"User: {query1}")
|
||||
result1 = await agent.run(query1, thread=thread)
|
||||
print(f"Agent: {result1.text}")
|
||||
|
||||
# The thread ID is set after the first response
|
||||
existing_thread_id = thread.service_thread_id
|
||||
print(f"Thread ID: {existing_thread_id}")
|
||||
|
||||
if existing_thread_id:
|
||||
print("\n--- Continuing with the same thread ID in a new agent instance ---")
|
||||
if existing_thread_id:
|
||||
print("\n--- Continuing with the same thread ID in a new agent instance ---")
|
||||
|
||||
# Create a new agent instance but use the existing thread ID
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(thread_id=existing_thread_id, async_credential=credential),
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# Create a thread with the existing ID
|
||||
thread = AgentThread(service_thread_id=existing_thread_id)
|
||||
async with (
|
||||
AzureAIClient(async_credential=credential).create_agent(
|
||||
name="BasicWeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# Create a thread with the existing ID
|
||||
thread = agent.get_new_thread(service_thread_id=existing_thread_id)
|
||||
|
||||
second_query = "What was the last city I asked about?"
|
||||
print(f"User: {second_query}")
|
||||
second_result = await agent.run(second_query, thread=thread)
|
||||
print(f"Agent: {second_result.text}")
|
||||
print("Note: The agent continues the conversation from the previous thread.\n")
|
||||
query2 = "What was the last city I asked about?"
|
||||
print(f"User: {query2}")
|
||||
result2 = await agent.run(query2, thread=thread)
|
||||
print(f"Agent: {result2.text}")
|
||||
print("Note: The agent continues the conversation from the previous thread by using thread ID.\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Azure AI Chat Client Agent Thread Management Examples ===\n")
|
||||
print("=== Azure AI Agent Thread Management Examples ===\n")
|
||||
|
||||
await example_with_automatic_thread_creation()
|
||||
await example_with_thread_persistence()
|
||||
await example_with_thread_persistence_in_memory()
|
||||
await example_with_existing_thread_id()
|
||||
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -0,0 +1,69 @@
|
||||
# Azure AI Agent Examples
|
||||
|
||||
This folder contains examples demonstrating different ways to create and use agents with the Azure AI chat client from the `agent_framework.azure` package.
|
||||
|
||||
## Examples
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| [`azure_ai_basic.py`](azure_ai_basic.py) | The simplest way to create an agent using `ChatAgent` with `AzureAIAgentClient`. It automatically handles all configuration using environment variables. |
|
||||
| [`azure_ai_with_bing_grounding.py`](azure_ai_with_bing_grounding.py) | Shows how to use Bing Grounding search with Azure AI agents to find real-time information from the web. Demonstrates web search capabilities with proper source citations and comprehensive error handling. |
|
||||
| [`azure_ai_with_code_interpreter.py`](azure_ai_with_code_interpreter.py) | Shows how to use the HostedCodeInterpreterTool with Azure AI agents to write and execute Python code. Includes helper methods for accessing code interpreter data from response chunks. |
|
||||
| [`azure_ai_with_existing_agent.py`](azure_ai_with_existing_agent.py) | Shows how to work with a pre-existing agent by providing the agent ID to the Azure AI chat client. This example also demonstrates proper cleanup of manually created agents. |
|
||||
| [`azure_ai_with_existing_thread.py`](azure_ai_with_existing_thread.py) | Shows how to work with a pre-existing thread by providing the thread ID to the Azure AI chat client. This example also demonstrates proper cleanup of manually created threads. |
|
||||
| [`azure_ai_with_explicit_settings.py`](azure_ai_with_explicit_settings.py) | Shows how to create an agent with explicitly configured `AzureAIAgentClient` settings, including project endpoint, model deployment, credentials, and agent name. |
|
||||
| [`azure_ai_with_azure_ai_search.py`](azure_ai_with_azure_ai_search.py) | Demonstrates how to use Azure AI Search with Azure AI agents to search through indexed data. Shows how to configure search parameters, query types, and integrate with existing search indexes. |
|
||||
| [`azure_ai_with_file_search.py`](azure_ai_with_file_search.py) | Demonstrates how to use the HostedFileSearchTool with Azure AI agents to search through uploaded documents. Shows file upload, vector store creation, and querying document content. Includes both streaming and non-streaming examples. |
|
||||
| [`azure_ai_with_function_tools.py`](azure_ai_with_function_tools.py) | Demonstrates how to use function tools with agents. Shows both agent-level tools (defined when creating the agent) and query-level tools (provided with specific queries). |
|
||||
| [`azure_ai_with_hosted_mcp.py`](azure_ai_with_hosted_mcp.py) | Shows how to integrate Azure AI agents with hosted Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates remote MCP server connections and tool discovery. |
|
||||
| [`azure_ai_with_local_mcp.py`](azure_ai_with_local_mcp.py) | Shows how to integrate Azure AI agents with local Model Context Protocol (MCP) servers for enhanced functionality and tool integration. Demonstrates both agent-level and run-level tool configuration. |
|
||||
| [`azure_ai_with_multiple_tools.py`](azure_ai_with_multiple_tools.py) | Demonstrates how to use multiple tools together with Azure AI agents, including web search, MCP servers, and function tools. Shows coordinated multi-tool interactions and approval workflows. |
|
||||
| [`azure_ai_with_openapi_tools.py`](azure_ai_with_openapi_tools.py) | Demonstrates how to use OpenAPI tools with Azure AI agents to integrate external REST APIs. Shows OpenAPI specification loading, anonymous authentication, thread context management, and coordinated multi-API conversations using weather and countries APIs. |
|
||||
| [`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. |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Before running the examples, you need to set up your environment variables. You can do this in one of two ways:
|
||||
|
||||
### Option 1: Using a .env file (Recommended)
|
||||
|
||||
1. Copy the `.env.example` file from the `python` directory to create a `.env` file:
|
||||
```bash
|
||||
cp ../../.env.example ../../.env
|
||||
```
|
||||
|
||||
2. Edit the `.env` file and add your values:
|
||||
```
|
||||
AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint"
|
||||
AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
|
||||
```
|
||||
|
||||
3. For samples using Bing Grounding search (like `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`), you'll also need:
|
||||
```
|
||||
BING_CONNECTION_ID="your-bing-connection-id"
|
||||
```
|
||||
|
||||
To get your Bing connection details:
|
||||
- Go to [Azure AI Foundry portal](https://ai.azure.com)
|
||||
- Navigate to your project's "Connected resources" section
|
||||
- Add a new connection for "Grounding with Bing Search"
|
||||
- Copy the ID
|
||||
|
||||
### Option 2: Using environment variables directly
|
||||
|
||||
Set the environment variables in your shell:
|
||||
|
||||
```bash
|
||||
export AZURE_AI_PROJECT_ENDPOINT="your-project-endpoint"
|
||||
export AZURE_AI_MODEL_DEPLOYMENT_NAME="your-model-deployment-name"
|
||||
export BING_CONNECTION_ID="your-bing-connection-id"
|
||||
```
|
||||
|
||||
### Required Variables
|
||||
|
||||
- `AZURE_AI_PROJECT_ENDPOINT`: Your Azure AI project endpoint (required for all examples)
|
||||
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for all examples)
|
||||
|
||||
### Optional Variables
|
||||
|
||||
- `BING_CONNECTION_ID`: Your Bing connection ID (required for `azure_ai_with_bing_grounding.py` and `azure_ai_with_multiple_tools.py`)
|
||||
@@ -0,0 +1,82 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Azure AI Agent Basic Example
|
||||
|
||||
This sample demonstrates basic usage of AzureAIAgentClient to create agents with automatic
|
||||
lifecycle management. Shows both streaming and non-streaming responses with function tools.
|
||||
"""
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def non_streaming_example() -> None:
|
||||
"""Example of non-streaming response (get the complete result at once)."""
|
||||
print("=== Non-streaming Response Example ===")
|
||||
|
||||
# Since no Agent ID is provided, the agent will be automatically created
|
||||
# and deleted after getting a response
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIAgentClient(async_credential=credential).create_agent(
|
||||
name="WeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
query = "What's the weather like in Seattle?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Agent: {result}\n")
|
||||
|
||||
|
||||
async def streaming_example() -> None:
|
||||
"""Example of streaming response (get results as they are generated)."""
|
||||
print("=== Streaming Response Example ===")
|
||||
|
||||
# Since no Agent ID is provided, the agent will be automatically created
|
||||
# and deleted after getting a response
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIAgentClient(async_credential=credential).create_agent(
|
||||
name="WeatherAgent",
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
query = "What's the weather like in Portland?"
|
||||
print(f"User: {query}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
async for chunk in agent.run_stream(query):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Basic Azure AI Chat Client Agent Example ===")
|
||||
|
||||
await non_streaming_example()
|
||||
await streaming_example()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent, CitationAnnotation
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.ai.projects.aio import AIProjectClient
|
||||
from azure.ai.projects.models import ConnectionType
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent with Azure AI Search Example
|
||||
|
||||
This sample demonstrates how to create an Azure AI agent that uses Azure AI Search
|
||||
to search through indexed hotel data and answer user questions about hotels.
|
||||
|
||||
Prerequisites:
|
||||
1. Set AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_MODEL_DEPLOYMENT_NAME environment variables
|
||||
2. Ensure you have an Azure AI Search connection configured in your Azure AI project
|
||||
3. The search index "hotels-sample-index" should exist in your Azure AI Search service
|
||||
(you can create this using the Azure portal with sample hotel data)
|
||||
|
||||
NOTE: To ensure consistent search tool usage:
|
||||
- Include explicit instructions for the agent to use the search tool
|
||||
- Mention the search requirement in your queries
|
||||
- Use `tool_choice="required"` to force tool usage
|
||||
|
||||
More info on `query type` can be found here:
|
||||
https://learn.microsoft.com/en-us/python/api/azure-ai-agents/azure.ai.agents.models.aisearchindexresource?view=azure-python-preview
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main function demonstrating Azure AI agent with raw Azure AI Search tool."""
|
||||
print("=== Azure AI Agent with Raw Azure AI Search Tool ===")
|
||||
|
||||
# Create the client and manually create an agent with Azure AI Search tool
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AIProjectClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as project_client,
|
||||
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
|
||||
):
|
||||
ai_search_conn_id = ""
|
||||
async for connection in project_client.connections.list():
|
||||
if connection.type == ConnectionType.AZURE_AI_SEARCH:
|
||||
ai_search_conn_id = connection.id
|
||||
break
|
||||
|
||||
# 1. Create Azure AI agent with the search tool
|
||||
azure_ai_agent = await agents_client.create_agent(
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
name="HotelSearchAgent",
|
||||
instructions=(
|
||||
"You are a helpful agent that searches hotel information using Azure AI Search. "
|
||||
"Always use the search tool and index to find hotel data and provide accurate information."
|
||||
),
|
||||
tools=[{"type": "azure_ai_search"}],
|
||||
tool_resources={
|
||||
"azure_ai_search": {
|
||||
"indexes": [
|
||||
{
|
||||
"index_connection_id": ai_search_conn_id,
|
||||
"index_name": "hotels-sample-index",
|
||||
"query_type": "vector",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# 2. Create chat client with the existing agent
|
||||
chat_client = AzureAIAgentClient(agents_client=agents_client, agent_id=azure_ai_agent.id)
|
||||
|
||||
try:
|
||||
async with ChatAgent(
|
||||
chat_client=chat_client,
|
||||
# Additional instructions for this specific conversation
|
||||
instructions=("You are a helpful agent that uses the search tool and index to find hotel information."),
|
||||
) as agent:
|
||||
print("This agent uses raw Azure AI Search tool to search hotel data.\n")
|
||||
|
||||
# 3. Simulate conversation with the agent
|
||||
user_input = (
|
||||
"Use Azure AI search knowledge tool to find detailed information about a winter hotel."
|
||||
" Use the search tool and index." # You can modify prompt to force tool usage
|
||||
)
|
||||
print(f"User: {user_input}")
|
||||
print("Agent: ", end="", flush=True)
|
||||
|
||||
# Stream the response and collect citations
|
||||
citations: list[CitationAnnotation] = []
|
||||
async for chunk in agent.run_stream(user_input):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="", flush=True)
|
||||
|
||||
# Collect citations from Azure AI Search responses
|
||||
for content in getattr(chunk, "contents", []):
|
||||
annotations = getattr(content, "annotations", [])
|
||||
if annotations:
|
||||
citations.extend(annotations)
|
||||
|
||||
print()
|
||||
|
||||
# Display collected citations
|
||||
if citations:
|
||||
print("\n\nCitations:")
|
||||
for i, citation in enumerate(citations, 1):
|
||||
print(f"[{i}] Reference: {citation.url}")
|
||||
|
||||
print("\n" + "=" * 50 + "\n")
|
||||
print("Hotel search conversation completed!")
|
||||
|
||||
finally:
|
||||
# Clean up the agent manually
|
||||
await agents_client.delete_agent(azure_ai_agent.id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import AgentRunResponse, ChatResponseUpdate, HostedCodeInterpreterTool
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.ai.agents.models import (
|
||||
RunStepDeltaCodeInterpreterDetailItemObject,
|
||||
)
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent with Code Interpreter Example
|
||||
|
||||
This sample demonstrates using HostedCodeInterpreterTool with Azure AI Agents
|
||||
for Python code execution and mathematical problem solving.
|
||||
"""
|
||||
|
||||
|
||||
def print_code_interpreter_inputs(response: AgentRunResponse) -> None:
|
||||
"""Helper method to access code interpreter data."""
|
||||
|
||||
print("\nCode Interpreter Inputs during the run:")
|
||||
if response.raw_representation is None:
|
||||
return
|
||||
for chunk in response.raw_representation:
|
||||
if isinstance(chunk, ChatResponseUpdate) and isinstance(
|
||||
chunk.raw_representation, RunStepDeltaCodeInterpreterDetailItemObject
|
||||
):
|
||||
print(chunk.raw_representation.input, end="")
|
||||
print("\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Example showing how to use the HostedCodeInterpreterTool with Azure AI."""
|
||||
print("=== Azure AI Agent with Code Interpreter Example ===")
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIAgentClient(async_credential=credential) as chat_client,
|
||||
):
|
||||
agent = chat_client.create_agent(
|
||||
name="CodingAgent",
|
||||
instructions=("You are a helpful assistant that can write and execute Python code to solve problems."),
|
||||
tools=HostedCodeInterpreterTool(),
|
||||
)
|
||||
query = "Generate the factorial of 100 using python code, show the code and execute it."
|
||||
print(f"User: {query}")
|
||||
response = await AgentRunResponse.from_agent_response_generator(agent.run_stream(query))
|
||||
print(f"Agent: {response}")
|
||||
# To review the code interpreter outputs, you can access
|
||||
# them from the response raw_representations, just uncomment the next line:
|
||||
# print_code_interpreter_inputs(response)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.ai.agents.aio import AgentsClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent with Existing Agent Example
|
||||
|
||||
This sample demonstrates working with pre-existing Azure AI Agents by providing
|
||||
agent IDs, showing agent reuse patterns for production scenarios.
|
||||
"""
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Azure AI Chat Client with Existing Agent ===")
|
||||
|
||||
# Create the client
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AgentsClient(endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"], credential=credential) as agents_client,
|
||||
):
|
||||
azure_ai_agent = await agents_client.create_agent(
|
||||
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
# Create remote agent with default instructions
|
||||
# These instructions will persist on created agent for every run.
|
||||
instructions="End each response with [END].",
|
||||
)
|
||||
|
||||
chat_client = AzureAIAgentClient(agents_client=agents_client, agent_id=azure_ai_agent.id)
|
||||
|
||||
try:
|
||||
async with ChatAgent(
|
||||
chat_client=chat_client,
|
||||
# Instructions here are applicable only to this ChatAgent instance
|
||||
# These instructions will be combined with instructions on existing remote agent.
|
||||
# The final instructions during the execution will look like:
|
||||
# "'End each response with [END]. Respond with 'Hello World' only'"
|
||||
instructions="Respond with 'Hello World' only",
|
||||
) as agent:
|
||||
query = "How are you?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
# Based on local and remote instructions, the result will be
|
||||
# 'Hello World [END]'.
|
||||
print(f"Agent: {result}\n")
|
||||
finally:
|
||||
# Clean up the agent manually
|
||||
await agents_client.delete_agent(azure_ai_agent.id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Azure AI Agent with Explicit Settings Example
|
||||
|
||||
This sample demonstrates creating Azure AI Agents with explicit configuration
|
||||
settings rather than relying on environment variable defaults.
|
||||
"""
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Azure AI Chat Client with Explicit Settings ===")
|
||||
|
||||
# Since no Agent ID is provided, the agent will be automatically created
|
||||
# and deleted after getting a response
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(
|
||||
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
|
||||
model_deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
|
||||
async_credential=credential,
|
||||
agent_name="WeatherAgent",
|
||||
should_cleanup_agent=True, # Set to False if you want to disable automatic agent cleanup
|
||||
),
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
result = await agent.run("What's the weather like in New York?")
|
||||
print(f"Result: {result}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,91 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from agent_framework import ChatAgent, HostedFileSearchTool, HostedVectorStoreContent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.ai.agents.models import FileInfo, VectorStore
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
The following sample demonstrates how to create a simple, Azure AI agent that
|
||||
uses a file search tool to answer user questions.
|
||||
"""
|
||||
|
||||
|
||||
# Simulate a conversation with the agent
|
||||
USER_INPUTS = [
|
||||
"Who is the youngest employee?",
|
||||
"Who works in sales?",
|
||||
"I have a customer request, who can help me?",
|
||||
]
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main function demonstrating Azure AI agent with file search capabilities."""
|
||||
client = AzureAIAgentClient(async_credential=AzureCliCredential())
|
||||
file: FileInfo | None = None
|
||||
vector_store: VectorStore | None = None
|
||||
|
||||
try:
|
||||
# 1. Upload file and create vector store
|
||||
pdf_file_path = Path(__file__).parent.parent / "resources" / "employees.pdf"
|
||||
print(f"Uploading file from: {pdf_file_path}")
|
||||
|
||||
file = await client.agents_client.files.upload_and_poll(file_path=str(pdf_file_path), purpose="assistants")
|
||||
print(f"Uploaded file, file ID: {file.id}")
|
||||
|
||||
vector_store = await client.agents_client.vector_stores.create_and_poll(
|
||||
file_ids=[file.id], name="my_vectorstore"
|
||||
)
|
||||
print(f"Created vector store, vector store ID: {vector_store.id}")
|
||||
|
||||
# 2. Create file search tool with uploaded resources
|
||||
file_search_tool = HostedFileSearchTool(inputs=[HostedVectorStoreContent(vector_store_id=vector_store.id)])
|
||||
|
||||
# 3. Create an agent with file search capabilities
|
||||
# The tool_resources are automatically extracted from HostedFileSearchTool
|
||||
async with ChatAgent(
|
||||
chat_client=client,
|
||||
name="EmployeeSearchAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant that can search through uploaded employee files "
|
||||
"to answer questions about employees."
|
||||
),
|
||||
tools=file_search_tool,
|
||||
) as agent:
|
||||
# 4. Simulate conversation with the agent
|
||||
for user_input in USER_INPUTS:
|
||||
print(f"# User: '{user_input}'")
|
||||
response = await agent.run(user_input)
|
||||
print(f"# Agent: {response.text}")
|
||||
|
||||
# 5. Cleanup: Delete the vector store and file
|
||||
try:
|
||||
if vector_store:
|
||||
await client.agents_client.vector_stores.delete(vector_store.id)
|
||||
if file:
|
||||
await client.agents_client.files.delete(file.id)
|
||||
except Exception:
|
||||
# Ignore cleanup errors to avoid masking issues
|
||||
pass
|
||||
finally:
|
||||
# 6. Cleanup: Delete the vector store and file in case of earlier failure to prevent orphaned resources.
|
||||
|
||||
# Refreshing the client is required since chat agent closes it
|
||||
client = AzureAIAgentClient(async_credential=AzureCliCredential())
|
||||
try:
|
||||
if vector_store:
|
||||
await client.agents_client.vector_stores.delete(vector_store.id)
|
||||
if file:
|
||||
await client.agents_client.files.delete(file.id)
|
||||
except Exception:
|
||||
# Ignore cleanup errors to avoid masking issues
|
||||
pass
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import AgentProtocol, AgentRunResponse, AgentThread, HostedMCPTool
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
|
||||
"""
|
||||
Azure AI Agent with Hosted MCP Example
|
||||
|
||||
This sample demonstrates integration of Azure AI Agents with hosted Model Context Protocol (MCP)
|
||||
servers, including user approval workflows for function call security.
|
||||
"""
|
||||
|
||||
|
||||
async def handle_approvals_with_thread(query: str, agent: "AgentProtocol", thread: "AgentThread") -> AgentRunResponse:
|
||||
"""Here we let the thread deal with the previous responses, and we just rerun with the approval."""
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
result = await agent.run(query, thread=thread, store=True)
|
||||
while len(result.user_input_requests) > 0:
|
||||
new_input: list[Any] = []
|
||||
for user_input_needed in result.user_input_requests:
|
||||
print(
|
||||
f"User Input Request for function from {agent.name}: {user_input_needed.function_call.name}"
|
||||
f" with arguments: {user_input_needed.function_call.arguments}"
|
||||
)
|
||||
user_approval = input("Approve function call? (y/n): ")
|
||||
new_input.append(
|
||||
ChatMessage(
|
||||
role="user",
|
||||
contents=[user_input_needed.create_response(user_approval.lower() == "y")],
|
||||
)
|
||||
)
|
||||
result = await agent.run(new_input, thread=thread, store=True)
|
||||
return result
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Example showing Hosted MCP tools for a Azure AI Agent."""
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
AzureAIAgentClient(async_credential=credential) as chat_client,
|
||||
):
|
||||
agent = chat_client.create_agent(
|
||||
name="DocsAgent",
|
||||
instructions="You are a helpful assistant that can help with microsoft documentation questions.",
|
||||
tools=HostedMCPTool(
|
||||
name="Microsoft Learn MCP",
|
||||
url="https://learn.microsoft.com/api/mcp",
|
||||
),
|
||||
)
|
||||
thread = agent.get_new_thread()
|
||||
# First query
|
||||
query1 = "How to create an Azure storage account using az cli?"
|
||||
print(f"User: {query1}")
|
||||
result1 = await handle_approvals_with_thread(query1, agent, thread)
|
||||
print(f"{agent.name}: {result1}\n")
|
||||
print("\n=======================================\n")
|
||||
# Second query
|
||||
query2 = "What is Microsoft Agent Framework?"
|
||||
print(f"User: {query2}")
|
||||
result2 = await handle_approvals_with_thread(query2, agent, thread)
|
||||
print(f"{agent.name}: {result2}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,154 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
from random import randint
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import AgentThread, ChatAgent
|
||||
from agent_framework.azure import AzureAIAgentClient
|
||||
from azure.identity.aio import AzureCliCredential
|
||||
from pydantic import Field
|
||||
|
||||
"""
|
||||
Azure AI Agent with Thread Management Example
|
||||
|
||||
This sample demonstrates thread management with Azure AI Agents, comparing
|
||||
automatic thread creation with explicit thread management for persistent context.
|
||||
"""
|
||||
|
||||
|
||||
def get_weather(
|
||||
location: Annotated[str, Field(description="The location to get the weather for.")],
|
||||
) -> str:
|
||||
"""Get the weather for a given location."""
|
||||
conditions = ["sunny", "cloudy", "rainy", "stormy"]
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def example_with_automatic_thread_creation() -> None:
|
||||
"""Example showing automatic thread creation (service-managed thread)."""
|
||||
print("=== Automatic Thread Creation Example ===")
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=credential),
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# First conversation - no thread provided, will be created automatically
|
||||
first_query = "What's the weather like in Seattle?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query)
|
||||
print(f"Agent: {first_result.text}")
|
||||
|
||||
# Second conversation - still no thread provided, will create another new thread
|
||||
second_query = "What was the last city I asked about?"
|
||||
print(f"\nUser: {second_query}")
|
||||
second_result = await agent.run(second_query)
|
||||
print(f"Agent: {second_result.text}")
|
||||
print("Note: Each call creates a separate thread, so the agent doesn't remember previous context.\n")
|
||||
|
||||
|
||||
async def example_with_thread_persistence() -> None:
|
||||
"""Example showing thread persistence across multiple conversations."""
|
||||
print("=== Thread Persistence Example ===")
|
||||
print("Using the same thread across multiple conversations to maintain context.\n")
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=credential),
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# Create a new thread that will be reused
|
||||
thread = agent.get_new_thread()
|
||||
|
||||
# First conversation
|
||||
first_query = "What's the weather like in Tokyo?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query, thread=thread)
|
||||
print(f"Agent: {first_result.text}")
|
||||
|
||||
# Second conversation using the same thread - maintains context
|
||||
second_query = "How about London?"
|
||||
print(f"\nUser: {second_query}")
|
||||
second_result = await agent.run(second_query, thread=thread)
|
||||
print(f"Agent: {second_result.text}")
|
||||
|
||||
# Third conversation - agent should remember both previous cities
|
||||
third_query = "Which of the cities I asked about has better weather?"
|
||||
print(f"\nUser: {third_query}")
|
||||
third_result = await agent.run(third_query, thread=thread)
|
||||
print(f"Agent: {third_result.text}")
|
||||
print("Note: The agent remembers context from previous messages in the same thread.\n")
|
||||
|
||||
|
||||
async def example_with_existing_thread_id() -> None:
|
||||
"""Example showing how to work with an existing thread ID from the service."""
|
||||
print("=== Existing Thread ID Example ===")
|
||||
print("Using a specific thread ID to continue an existing conversation.\n")
|
||||
|
||||
# First, create a conversation and capture the thread ID
|
||||
existing_thread_id = None
|
||||
|
||||
# For authentication, run `az login` command in terminal or replace AzureCliCredential with preferred
|
||||
# authentication option.
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(async_credential=credential),
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# Start a conversation and get the thread ID
|
||||
thread = agent.get_new_thread()
|
||||
first_query = "What's the weather in Paris?"
|
||||
print(f"User: {first_query}")
|
||||
first_result = await agent.run(first_query, thread=thread)
|
||||
print(f"Agent: {first_result.text}")
|
||||
|
||||
# The thread ID is set after the first response
|
||||
existing_thread_id = thread.service_thread_id
|
||||
print(f"Thread ID: {existing_thread_id}")
|
||||
|
||||
if existing_thread_id:
|
||||
print("\n--- Continuing with the same thread ID in a new agent instance ---")
|
||||
|
||||
# Create a new agent instance but use the existing thread ID
|
||||
async with (
|
||||
AzureCliCredential() as credential,
|
||||
ChatAgent(
|
||||
chat_client=AzureAIAgentClient(thread_id=existing_thread_id, async_credential=credential),
|
||||
instructions="You are a helpful weather agent.",
|
||||
tools=get_weather,
|
||||
) as agent,
|
||||
):
|
||||
# Create a thread with the existing ID
|
||||
thread = AgentThread(service_thread_id=existing_thread_id)
|
||||
|
||||
second_query = "What was the last city I asked about?"
|
||||
print(f"User: {second_query}")
|
||||
second_result = await agent.run(second_query, thread=thread)
|
||||
print(f"Agent: {second_result.text}")
|
||||
print("Note: The agent continues the conversation from the previous thread.\n")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
print("=== Azure AI Chat Client Agent Thread Management Examples ===\n")
|
||||
|
||||
await example_with_automatic_thread_creation()
|
||||
await example_with_thread_persistence()
|
||||
await example_with_existing_thread_id()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
+4
-6
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import ChatAgent, MCPStreamableHTTPTool
|
||||
from agent_framework.azure import AzureOpenAIResponsesClient
|
||||
@@ -18,13 +18,14 @@ servers.
|
||||
# --- Below code uses Microsoft Learn MCP server over Streamable HTTP ---
|
||||
# --- Users can set these environment variables, or just edit the values below to their desired local MCP server
|
||||
MCP_NAME = os.environ.get("MCP_NAME", "Microsoft Learn MCP") # example name
|
||||
MCP_URL = os.environ.get("MCP_URL", "https://learn.microsoft.com/api/mcp") # example endpoint
|
||||
MCP_URL = os.environ.get("MCP_URL", "https://learn.microsoft.com/api/mcp") # example endpoint
|
||||
|
||||
# Environment variables for Azure OpenAI Responses authentication
|
||||
# AZURE_OPENAI_ENDPOINT="<your-azure openai-endpoint>"
|
||||
# AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME="<your-deployment-name>"
|
||||
# AZURE_OPENAI_API_VERSION="<your-api-version>" # e.g. "2025-03-01-preview"
|
||||
|
||||
|
||||
async def main():
|
||||
"""Example showing local MCP tools for a Azure OpenAI Responses Agent."""
|
||||
# AuthN: use Azure CLI
|
||||
@@ -38,16 +39,13 @@ async def main():
|
||||
|
||||
agent: ChatAgent = responses_client.create_agent(
|
||||
name="DocsAgent",
|
||||
instructions=(
|
||||
"You are a helpful assistant that can help with Microsoft documentation questions."
|
||||
),
|
||||
instructions=("You are a helpful assistant that can help with Microsoft documentation questions."),
|
||||
)
|
||||
|
||||
# Connect to the MCP server (Streamable HTTP)
|
||||
async with MCPStreamableHTTPTool(
|
||||
name=MCP_NAME,
|
||||
url=MCP_URL,
|
||||
|
||||
) as mcp_tool:
|
||||
# First query — expect the agent to use the MCP tool if it helps
|
||||
q1 = "How to create an Azure storage account using az cli?"
|
||||
|
||||
+13
-15
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import HostedWebSearchTool
|
||||
from agent_framework import ChatAgent, HostedWebSearchTool
|
||||
from agent_framework.openai import OpenAIChatClient
|
||||
|
||||
"""
|
||||
@@ -14,34 +14,32 @@ for real-time information retrieval and current data access.
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = OpenAIChatClient(model_id="gpt-4o-search-preview")
|
||||
|
||||
message = "What is the current weather? Do not ask for my current location."
|
||||
# Test that the client will use the web search tool with location
|
||||
# Test that the agent will use the web search tool with location
|
||||
additional_properties = {
|
||||
"user_location": {
|
||||
"country": "US",
|
||||
"city": "Seattle",
|
||||
}
|
||||
}
|
||||
|
||||
agent = ChatAgent(
|
||||
chat_client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
|
||||
instructions="You are a helpful assistant that can search the web for current information.",
|
||||
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
)
|
||||
|
||||
message = "What is the current weather? Do not ask for my current location."
|
||||
stream = False
|
||||
print(f"User: {message}")
|
||||
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_streaming_response(
|
||||
message,
|
||||
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
tool_choice="auto",
|
||||
):
|
||||
async for chunk in agent.run_stream(message):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(
|
||||
message,
|
||||
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
tool_choice="auto",
|
||||
)
|
||||
response = await agent.run(message)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
|
||||
+10
-11
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import HostedFileSearchTool, HostedVectorStoreContent
|
||||
from agent_framework import ChatAgent, HostedFileSearchTool, HostedVectorStoreContent
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
"""
|
||||
@@ -46,22 +46,21 @@ async def main() -> None:
|
||||
stream = False
|
||||
print(f"User: {message}")
|
||||
file_id, vector_store = await create_vector_store(client)
|
||||
|
||||
agent = ChatAgent(
|
||||
chat_client=client,
|
||||
instructions="You are a helpful assistant that can search through files to find information.",
|
||||
tools=[HostedFileSearchTool(inputs=vector_store)],
|
||||
)
|
||||
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_streaming_response(
|
||||
message,
|
||||
tools=[HostedFileSearchTool(inputs=vector_store)],
|
||||
tool_choice="auto",
|
||||
):
|
||||
async for chunk in agent.run_stream(message):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(
|
||||
message,
|
||||
tools=[HostedFileSearchTool(inputs=vector_store)],
|
||||
tool_choice="auto",
|
||||
)
|
||||
response = await agent.run(message)
|
||||
print(f"Assistant: {response}")
|
||||
await delete_vector_store(client, file_id, vector_store.vector_store_id)
|
||||
|
||||
|
||||
+13
-15
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from agent_framework import HostedWebSearchTool
|
||||
from agent_framework import ChatAgent, HostedWebSearchTool
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
|
||||
"""
|
||||
@@ -14,34 +14,32 @@ for direct real-time information retrieval and current data access.
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
client = OpenAIResponsesClient()
|
||||
|
||||
message = "What is the current weather? Do not ask for my current location."
|
||||
# Test that the client will use the web search tool with location
|
||||
# Test that the agent will use the web search tool with location
|
||||
additional_properties = {
|
||||
"user_location": {
|
||||
"country": "US",
|
||||
"city": "Seattle",
|
||||
}
|
||||
}
|
||||
|
||||
agent = ChatAgent(
|
||||
chat_client=OpenAIResponsesClient(),
|
||||
instructions="You are a helpful assistant that can search the web for current information.",
|
||||
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
)
|
||||
|
||||
message = "What is the current weather? Do not ask for my current location."
|
||||
stream = False
|
||||
print(f"User: {message}")
|
||||
|
||||
if stream:
|
||||
print("Assistant: ", end="")
|
||||
async for chunk in client.get_streaming_response(
|
||||
message,
|
||||
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
tool_choice="auto",
|
||||
):
|
||||
async for chunk in agent.run_stream(message):
|
||||
if chunk.text:
|
||||
print(chunk.text, end="")
|
||||
print("")
|
||||
else:
|
||||
response = await client.get_response(
|
||||
message,
|
||||
tools=[HostedWebSearchTool(additional_properties=additional_properties)],
|
||||
tool_choice="auto",
|
||||
)
|
||||
response = await agent.run(message)
|
||||
print(f"Assistant: {response}")
|
||||
|
||||
|
||||
|
||||
Generated
+3460
-3460
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user