Compare commits

...
Author SHA1 Message Date
Jacob Alber 931204f6dd WIP: Proposal for StreamingRunWatcher API 2025-11-06 15:01:16 -05:00
westeyandGitHub 6e205445be .NET: Add tool calling sample with OpenAPI (#1968)
* Add tool calling sample with OpenAPI

* Address PR comments.

* Rename folders and moved literal to inline.

* Fix broken link.
2025-11-06 14:34:27 +00:00
Evan MattsonandGitHub 708556e4ee Python: Update changelog with ag-ui changes (#1954)
* Update changelog with ag-ui changes

* Changed -> Fixed
2025-11-06 12:21:54 +09:00
Giles OdigweandGitHub ee1661ecb7 Python: Thread Samples Fix (#1945)
* thread samples fix

* custom chat message store fix
2025-11-06 02:59:22 +00:00
Evan MattsonandGitHub ac018f700b Python: Fix ag-ui examples packaging for PyPI publish (#1953)
* Fix ag-ui examples packaging for PyPI publish

* Fix markdown links
2025-11-06 11:31:24 +09:00
Dmytro StrukandGitHub 6fec8a61e3 Updated packages configuration (#1952) 2025-11-06 01:04:48 +00:00
Evan MattsonandGitHub 99e2875fc8 Bump ag-ui package to 1.0.0b251106 for a release. Update CHANGELOG. (#1951) 2025-11-06 09:57:23 +09:00
44 changed files with 4324 additions and 3461 deletions
+5 -3
View File
@@ -68,13 +68,15 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(AspireAppHostSdkVersion)" />
<PackageVersion Include="Microsoft.Extensions.VectorData.Abstractions" Version="9.7.0" />
<!-- Vector Stores -->
<!-- Semantic Kernel -->
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.Core" Version="1.66.0" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.OpenAI" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Agents.AzureAI" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Plugins.OpenApi" Version="1.66.0" />
<!-- Vector Stores -->
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.66.0-preview" />
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.Qdrant" Version="1.66.0-preview" />
<!-- Agent SDKs -->
<PackageVersion Include="Microsoft.Agents.CopilotStudio.Client" Version="1.2.41" />
<!-- A2A -->
+4 -3
View File
@@ -47,7 +47,8 @@
<File Path="samples/GettingStarted/Agents/README.md" />
<Project Path="samples/GettingStarted/Agents/Agent_Step01_Running/Agent_Step01_Running.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step02_MultiturnConversation/Agent_Step02_MultiturnConversation.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step03_UsingFunctionTools/Agent_Step03_UsingFunctionTools.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step03.1_UsingFunctionTools/Agent_Step03.1_UsingFunctionTools.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step03.2_UsingFunctionTools_FromOpenAPI/Agent_Step03.2_UsingFunctionTools_FromOpenAPI.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step04_UsingFunctionToolsWithApprovals/Agent_Step04_UsingFunctionToolsWithApprovals.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step05_StructuredOutput/Agent_Step05_StructuredOutput.csproj" />
<Project Path="samples/GettingStarted/Agents/Agent_Step06_PersistedConversations/Agent_Step06_PersistedConversations.csproj" />
@@ -154,8 +155,8 @@
<Project Path="samples/GettingStarted/Workflows/_Foundational/08_WriterCriticWorkflow/08_WriterCriticWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/Catalog/">
<Project Path="samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
<Project Path="samples/Catalog/AgentsInWorkflows/AgentsInWorkflows.csproj" />
<Project Path="samples/Catalog/AgentWithTextSearchRag/AgentWithTextSearchRag.csproj" />
<Project Path="samples/Catalog/DeepResearchAgent/DeepResearchAgent.csproj" />
</Folder>
<Folder Name="/Solution Items/">
@@ -309,8 +310,8 @@
</Folder>
<Folder Name="/Tests/UnitTests/">
<Project Path="tests/Microsoft.Agents.AI.A2A.UnitTests/Microsoft.Agents.AI.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Abstractions.UnitTests/Microsoft.Agents.AI.Abstractions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AGUI.UnitTests/Microsoft.Agents.AI.AGUI.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests/Microsoft.Agents.AI.AzureAI.Persistent.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.Tests/Microsoft.Agents.AI.Hosting.A2A.Tests.csproj" Id="2a1c544d-237d-4436-8732-ba0c447ac06b" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.SemanticKernel.Plugins.OpenApi" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="OpenAPISpec.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,354 @@
{
"openapi": "3.0.1",
"info": {
"title": "Github Versions API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.github.com"
}
],
"components": {
"schemas": {
"basic-error": {
"title": "Basic Error",
"description": "Basic Error",
"type": "object",
"properties": {
"message": {
"type": "string"
},
"documentation_url": {
"type": "string"
},
"url": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"label": {
"title": "Label",
"description": "Color-coded labels help you categorize and filter your issues (just like labels in Gmail).",
"type": "object",
"properties": {
"id": {
"description": "Unique identifier for the label.",
"type": "integer",
"format": "int64",
"example": 208045946
},
"node_id": {
"type": "string",
"example": "MDU6TGFiZWwyMDgwNDU5NDY="
},
"url": {
"description": "URL for the label",
"example": "https://api.github.com/repositories/42/labels/bug",
"type": "string",
"format": "uri"
},
"name": {
"description": "The name of the label.",
"example": "bug",
"type": "string"
},
"description": {
"description": "Optional description of the label, such as its purpose.",
"type": "string",
"example": "Something isn't working",
"nullable": true
},
"color": {
"description": "6-character hex code, without the leading #, identifying the color",
"example": "FFFFFF",
"type": "string"
},
"default": {
"description": "Whether this label comes by default in a new repository.",
"type": "boolean",
"example": true
}
},
"required": [
"id",
"node_id",
"url",
"name",
"description",
"color",
"default"
]
},
"tag": {
"title": "Tag",
"description": "Tag",
"type": "object",
"properties": {
"name": {
"type": "string",
"example": "v0.1"
},
"commit": {
"type": "object",
"properties": {
"sha": {
"type": "string"
},
"url": {
"type": "string",
"format": "uri"
}
},
"required": [
"sha",
"url"
]
},
"zipball_url": {
"type": "string",
"format": "uri",
"example": "https://github.com/octocat/Hello-World/zipball/v0.1"
},
"tarball_url": {
"type": "string",
"format": "uri",
"example": "https://github.com/octocat/Hello-World/tarball/v0.1"
},
"node_id": {
"type": "string"
}
},
"required": [
"name",
"node_id",
"commit",
"zipball_url",
"tarball_url"
]
}
},
"examples": {
"label-items": {
"value": [
{
"id": 208045946,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDY=",
"url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
"name": "bug",
"description": "Something isn't working",
"color": "f29513",
"default": true
},
{
"id": 208045947,
"node_id": "MDU6TGFiZWwyMDgwNDU5NDc=",
"url": "https://api.github.com/repos/octocat/Hello-World/labels/enhancement",
"name": "enhancement",
"description": "New feature or request",
"color": "a2eeef",
"default": false
}
]
},
"tag-items": {
"value": [
{
"name": "v0.1",
"commit": {
"sha": "c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc",
"url": "https://api.github.com/repos/octocat/Hello-World/commits/c5b97d5ae6c19d5c5df71a34c7fbeeda2479ccbc"
},
"zipball_url": "https://github.com/octocat/Hello-World/zipball/v0.1",
"tarball_url": "https://github.com/octocat/Hello-World/tarball/v0.1",
"node_id": "MDQ6VXNlcjE="
}
]
}
},
"parameters": {
"owner": {
"name": "owner",
"description": "The account owner of the repository. The name is not case sensitive.",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
"repo": {
"name": "repo",
"description": "The name of the repository without the `.git` extension. The name is not case sensitive.",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
"per-page": {
"name": "per_page",
"description": "The number of results per page (max 100). For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
"in": "query",
"schema": {
"type": "integer",
"default": 30
}
},
"page": {
"name": "page",
"description": "The page number of the results to fetch. For more information, see \"[Using pagination in the REST API](https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api).\"",
"in": "query",
"schema": {
"type": "integer",
"default": 1
}
}
},
"responses": {
"not_found": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/basic-error"
}
}
}
}
},
"headers": {
"link": {
"example": "<https://api.github.com/resource?page=2>; rel=\"next\", <https://api.github.com/resource?page=5>; rel=\"last\"",
"schema": {
"type": "string"
}
}
}
},
"paths": {
"/repos/{owner}/{repo}/tags": {
"get": {
"summary": "List repository tags",
"description": "",
"tags": [
"repos"
],
"operationId": "repos/list-tags",
"externalDocs": {
"description": "API method documentation",
"url": "https://docs.github.com/rest/repos/repos#list-repository-tags"
},
"parameters": [
{
"$ref": "#/components/parameters/owner"
},
{
"$ref": "#/components/parameters/repo"
},
{
"$ref": "#/components/parameters/per-page"
},
{
"$ref": "#/components/parameters/page"
}
],
"responses": {
"200": {
"description": "Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/tag"
}
},
"examples": {
"default": {
"$ref": "#/components/examples/tag-items"
}
}
}
},
"headers": {
"Link": {
"$ref": "#/components/headers/link"
}
}
}
},
"x-github": {
"githubCloudOnly": false,
"enabledForGitHubApps": true,
"category": "repos",
"subcategory": "repos"
}
}
},
"/repos/{owner}/{repo}/labels": {
"get": {
"summary": "List labels for a repository",
"description": "Lists all labels for a repository.",
"tags": [
"issues"
],
"operationId": "issues/list-labels-for-repo",
"externalDocs": {
"description": "API method documentation",
"url": "https://docs.github.com/rest/issues/labels#list-labels-for-a-repository"
},
"parameters": [
{
"$ref": "#/components/parameters/owner"
},
{
"$ref": "#/components/parameters/repo"
},
{
"$ref": "#/components/parameters/per-page"
},
{
"$ref": "#/components/parameters/page"
}
],
"responses": {
"200": {
"description": "Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/label"
}
},
"examples": {
"default": {
"$ref": "#/components/examples/label-items"
}
}
}
},
"headers": {
"Link": {
"$ref": "#/components/headers/link"
}
}
},
"404": {
"$ref": "#/components/responses/not_found"
}
},
"x-github": {
"githubCloudOnly": false,
"enabledForGitHubApps": true,
"category": "issues",
"subcategory": "labels"
}
}
}
}
}
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a ChatClientAgent with function tools provided via an OpenAPI spec.
// It uses functionality from Semantic Kernel to parse the OpenAPI spec and create function tools to use with the Agent Framework Agent.
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
using OpenAI;
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
// Load the OpenAPI Spec from a file.
KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("github", "OpenAPISpec.json");
// Convert the Semantic Kernel plugin to Agent Framework function tools.
// This requires a dummy Kernel instance, since KernelFunctions cannot execute without one.
Kernel kernel = new();
List<AITool> tools = plugin.Select(x => x.WithKernel(kernel)).Cast<AITool>().ToList();
// Create the chat client and agent, and provide the OpenAPI function tools to the agent.
AIAgent agent = new AzureOpenAIClient(
new Uri(endpoint),
new AzureCliCredential())
.GetChatClient(deploymentName)
.CreateAIAgent(instructions: "You are a helpful assistant", tools: tools);
// Run the agent with the OpenAPI function tools.
Console.WriteLine(await agent.RunAsync("Please list the names, colors and descriptions of all the labels available in the microsoft/agent-framework repository on github."));
@@ -28,7 +28,8 @@ Before you begin, ensure you have the following prerequisites:
|---|---|
|[Running a simple agent](./Agent_Step01_Running/)|This sample demonstrates how to create and run a basic agent with instructions|
|[Multi-turn conversation with a simple agent](./Agent_Step02_MultiturnConversation/)|This sample demonstrates how to implement a multi-turn conversation with a simple agent|
|[Using function tools with a simple agent](./Agent_Step03_UsingFunctionTools/)|This sample demonstrates how to use function tools with a simple agent|
|[Using function tools with a simple agent](./Agent_Step03.1_UsingFunctionTools/)|This sample demonstrates how to use function tools with a simple agent|
|[Using OpenAPI function tools with a simple agent](./Agent_Step03.2_UsingFunctionTools_FromOpenAPI/)|This sample demonstrates how to create function tools from an OpenAPI spec and use them with a simple agent|
|[Using function tools with approvals](./Agent_Step04_UsingFunctionToolsWithApprovals/)|This sample demonstrates how to use function tools where approvals require human in the loop approvals before execution|
|[Structured output with a simple agent](./Agent_Step05_StructuredOutput/)|This sample demonstrates how to use structured output with a simple agent|
|[Persisted conversations with a simple agent](./Agent_Step06_PersistedConversations/)|This sample demonstrates how to persist conversations and reload them later. This is useful for cases where an agent is hosted in a stateless service|
@@ -4,11 +4,15 @@
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<VersionSuffix>preview</VersionSuffix>
<IsPackable>false</IsPackable>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- Disable packing until we are ready to release this as a nuget -->
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
</PropertyGroup>
@@ -6,15 +6,14 @@
<Nullable>enable</Nullable>
<RootNamespace>Microsoft.Agents.AI.DevUI</RootNamespace>
<OutputType>Library</OutputType>
<Title>Microsoft Agent Framework Developer UI</Title>
<Description>Provides Microsoft Agent Framework support for developer UI.</Description>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<VersionSuffix>preview</VersionSuffix>
<!-- Suppress warnings for internal DevUI implementation -->
<NoWarn>$(NoWarn);CS1591;CA1852;CA1050;RCS1037;RCS1036;RCS1124;RCS1021;RCS1146;RCS1211;CA2007;CA1308;IL2026;IL3050;CA1812</NoWarn>
</PropertyGroup>
<!-- Import nuget packaging properties -->
<Import Project="..\..\nuget\nuget-package.props" />
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<!-- Import frontend web assets build targets -->
<Import Project="Microsoft.Agents.AI.DevUI.Frontend.targets" />
@@ -28,4 +27,10 @@
<PackageReference Include="System.Net.ServerSentEvents" VersionOverride="10.0.0-rc.2.25502.107" />
</ItemGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Developer UI</Title>
<Description>Provides Microsoft Agent Framework support for developer UI.</Description>
</PropertyGroup>
</Project>
@@ -6,13 +6,17 @@
<RootNamespace>Microsoft.Agents.AI.Hosting.AGUI.AspNetCore</RootNamespace>
<VersionSuffix>preview</VersionSuffix>
<DefineConstants>$(DefineConstants);ASPNETCORE</DefineConstants>
<IsPackable>false</IsPackable>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Generated</InterceptorsNamespaces>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- Disable packing until we are ready to release this as a nuget -->
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Hosting AG-UI ASP.NET Core</Title>
@@ -4,7 +4,6 @@
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<VersionSuffix>preview</VersionSuffix>
<!-- Disable packing until we are ready to release this as a nuget -->
</PropertyGroup>
<PropertyGroup>
@@ -14,6 +13,7 @@
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- Disable packing until we are ready to release this as a nuget -->
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows;
internal class EventBinding<TEvent> where TEvent : WorkflowEvent
{
private readonly EventChain<TEvent> _eventChain;
internal EventBinding(EventChain<TEvent> eventChain)
{
this._eventChain = eventChain;
}
internal ValueTask<bool> RaiseAsync(TEvent evt, CancellationToken cancelationToken)
=> this._eventChain.RaiseAsync(evt, cancelationToken);
public event WorkflowEventHandler<TEvent>? Event
{
add => this._eventChain.AttachHandler(value);
remove => this._eventChain.DetachHandler(value);
}
public event WorkflowEventHandlerAsync<TEvent>? AsyncEvent
{
add => this._eventChain.AttachHandler(value);
remove => this._eventChain.DetachHandler(value);
}
}
@@ -0,0 +1,103 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows;
internal class EventChain<TEvent> : IWorkflowEventChain, IDisposable where TEvent : WorkflowEvent
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly List<Func<TEvent, ValueTask<bool>>> _handlers = new();
private readonly Dictionary<object, Func<TEvent, ValueTask<bool>>> _handlerMap = new();
ValueTask<bool> IWorkflowEventChain.RaiseAsync(WorkflowEvent evt, CancellationToken cancellationToken)
{
if (evt is TEvent typedEvent)
{
return this.RaiseAsync(typedEvent, cancellationToken);
}
throw new InvalidOperationException($"Sending event of type {evt.GetType().FullName} to event chain for " +
$"type {typeof(TEvent).FullName}.");
}
public async ValueTask<bool> RaiseAsync(TEvent evt, CancellationToken cancellationToken)
{
await this._semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
foreach (Func<TEvent, ValueTask<bool>> handler in this._handlers)
{
bool handled = await handler(evt).ConfigureAwait(false);
if (handled)
{
return true;
}
}
return false;
}
finally
{
this._semaphore.Release();
}
}
private bool AttachHandler(Func<TEvent, ValueTask<bool>> handlerAsync, object key)
{
this._semaphore.Wait();
bool shouldAdd = !this._handlerMap.ContainsKey(key);
if (shouldAdd)
{
this._handlerMap.Add(key, handlerAsync);
this._handlers.Add(handlerAsync);
}
this._semaphore.Release();
return shouldAdd;
}
private bool DetachHandler(object key)
{
this._semaphore.Wait();
bool removed = false;
if (this._handlerMap.TryGetValue(key, out Func<TEvent, ValueTask<bool>>? handler))
{
this._handlers.Remove(handler);
removed = true;
}
this._semaphore.Release();
return removed;
}
public bool AttachHandler(WorkflowEventHandlerAsync<TEvent>? handlerAsync)
=> handlerAsync is not null
? this.AttachHandler(handlerAsync.Invoke, handlerAsync)
: false;
public bool AttachHandler(WorkflowEventHandler<TEvent>? handler)
=> handler is not null
? this.AttachHandler(evt => new(handler.Invoke(evt)), handler)
: false;
public bool DetachHandler(WorkflowEventHandlerAsync<TEvent>? handlerAsync)
=> handlerAsync is not null
? this.DetachHandler((object)handlerAsync)
: false;
public bool DetachHandler(WorkflowEventHandler<TEvent>? handler)
=> handler is not null
? this.DetachHandler((object)handler)
: false;
public void Dispose()
{
this._semaphore.Dispose();
}
}
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows;
internal interface IWorkflowEventChain
{
ValueTask<bool> RaiseAsync(WorkflowEvent evt, CancellationToken cancellationToken);
}
@@ -0,0 +1,210 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.Agents.AI.Workflows;
internal delegate bool WorkflowEventHandler<in TEvent>(TEvent evt) where TEvent : WorkflowEvent;
internal delegate ValueTask<bool> WorkflowEventHandlerAsync<in TEvent>(TEvent evt) where TEvent : WorkflowEvent;
internal class StreamingRunWatcher
{
private readonly ConcurrentDictionary<Type, IWorkflowEventChain> _eventHandlerChains = new();
public StreamingRunWatcher()
{
this.SuperStepCompleted += evt =>
{
this.LastCheckpoint = evt.CompletionInfo?.Checkpoint;
return false;
};
}
public event WorkflowEventHandler<WorkflowStartedEvent> WorkflowStarted
{
add => this.For<WorkflowStartedEvent>().Event += value;
remove => this.For<WorkflowStartedEvent>().Event -= value;
}
public event WorkflowEventHandler<WorkflowErrorEvent> WorkflowError
{
add => this.For<WorkflowErrorEvent>().Event += value;
remove => this.For<WorkflowErrorEvent>().Event -= value;
}
public event WorkflowEventHandler<WorkflowWarningEvent> WorkflowWarning
{
add => this.For<WorkflowWarningEvent>().Event += value;
remove => this.For<WorkflowWarningEvent>().Event -= value;
}
public event WorkflowEventHandler<SuperStepStartedEvent> SuperStepStarted
{
add => this.For<SuperStepStartedEvent>().Event += value;
remove => this.For<SuperStepStartedEvent>().Event -= value;
}
public event WorkflowEventHandler<SuperStepCompletedEvent> SuperStepCompleted
{
add => this.For<SuperStepCompletedEvent>().Event += value;
remove => this.For<SuperStepCompletedEvent>().Event -= value;
}
public event WorkflowEventHandler<ExecutorInvokedEvent> ExecutorInvoked
{
add => this.For<ExecutorInvokedEvent>().Event += value;
remove => this.For<ExecutorInvokedEvent>().Event -= value;
}
public event WorkflowEventHandler<ExecutorFailedEvent> ExecutorFailed
{
add => this.For<ExecutorFailedEvent>().Event += value;
remove => this.For<ExecutorFailedEvent>().Event -= value;
}
public event WorkflowEventHandler<ExecutorCompletedEvent> ExecutorCompleted
{
add => this.For<ExecutorCompletedEvent>().Event += value;
remove => this.For<ExecutorCompletedEvent>().Event -= value;
}
public event WorkflowEventHandler<AgentRunUpdateEvent> AgentRunUpdate
{
add => this.For<AgentRunUpdateEvent>().Event += value;
remove => this.For<AgentRunUpdateEvent>().Event -= value;
}
public event WorkflowEventHandler<AgentRunResponseEvent> AgentRunResponse
{
add => this.For<AgentRunResponseEvent>().Event += value;
remove => this.For<AgentRunResponseEvent>().Event -= value;
}
public event WorkflowEventHandler<RequestInfoEvent> RequestInfo
{
add => this.For<RequestInfoEvent>().Event += value;
remove => this.For<RequestInfoEvent>().Event -= value;
}
public event WorkflowEventHandler<WorkflowOutputEvent> WorkflowOutput
{
add => this.For<WorkflowOutputEvent>().Event += value;
remove => this.For<WorkflowOutputEvent>().Event -= value;
}
public event WorkflowEventHandler<SubworkflowErrorEvent> SubworkflowError
{
add => this.For<SubworkflowErrorEvent>().Event += value;
remove => this.For<SubworkflowErrorEvent>().Event -= value;
}
public event WorkflowEventHandler<SubworkflowWarningEvent> SubworkflowWarning
{
add => this.For<SubworkflowWarningEvent>().Event += value;
remove => this.For<SubworkflowWarningEvent>().Event -= value;
}
public EventBinding<TEvent> For<TEvent>() where TEvent : WorkflowEvent
{
IWorkflowEventChain chainObj = this._eventHandlerChains.GetOrAdd(typeof(TEvent), _ => new EventChain<TEvent>());
Debug.Assert(chainObj is EventChain<TEvent>);
EventChain<TEvent> eventChain = (EventChain<TEvent>)chainObj;
return new(eventChain);
}
public async ValueTask RunToHaltAsync(StreamingRun streamingRun, CancellationToken cancellationToken = default)
{
List<RequestInfoEvent> pendingRequestInfoEvents = new();
// TODO: Should we make some kind of facility to make it easier to shunt events to run on the "UI" / "main" thread?
await foreach (WorkflowEvent evt in streamingRun.WatchStreamAsync(cancellationToken)
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
if (evt is RequestInfoEvent requestInfoEvt)
{
pendingRequestInfoEvents.Add(requestInfoEvt);
continue;
}
await TryInvokeHandlerAsync(evt).ConfigureAwait(false);
if (evt is SuperStepCompletedEvent)
{
foreach (RequestInfoEvent pendingEvt in pendingRequestInfoEvents)
{
await TryInvokeHandlerAsync(pendingEvt).ConfigureAwait(false);
}
pendingRequestInfoEvents.Clear();
}
}
async ValueTask TryInvokeHandlerAsync(WorkflowEvent evt)
{
Type? eventType = evt.GetType();
bool handled = false;
while (!handled && eventType != null)
{
// Check if there are any handlers for this event type, and see if they handle the event.
if (this._eventHandlerChains.TryGetValue(evt.GetType(), out IWorkflowEventChain? chain))
{
handled = await chain.RaiseAsync(evt, cancellationToken).ConfigureAwait(false);
}
// If we reach the base WorkflowEvent type, stop looking further.
if (eventType == typeof(WorkflowEvent))
{
break;
}
eventType = eventType.BaseType;
}
}
}
public CheckpointInfo? LastCheckpoint { get; private set; }
}
internal static class Sample
{
public static async ValueTask RunAsync()
{
WorkflowBuilder builder = new("start");
// build the workflow
Workflow workflow = builder.Build();
StreamingRunWatcher watcher = new();
watcher.RequestInfo += (evt) =>
{
// handle the request
return false;
};
watcher.AgentRunUpdate += (evt) =>
{
Console.Write(evt.Update.Text);
return true;
};
watcher.For<WorkflowEvent>().Event += (evt) =>
{
// log the event
return false;
};
StreamingRun streamingRun = await InProcessExecution.Concurrent
.StreamAsync(workflow, input: "some input")
.ConfigureAwait(false);
await watcher.RunToHaltAsync(streamingRun).ConfigureAwait(false);
}
}
+12
View File
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0b251106.post1] - 2025-11-06
### Fixed
- **agent-framework-ag-ui**: Fix ag-ui examples packaging for PyPI publish ([#1953](https://github.com/microsoft/agent-framework/pull/1953))
## [1.0.0b251106] - 2025-11-06
### Changed
- **agent-framework-ag-ui**: export sample ag-ui agents ([#1927](https://github.com/microsoft/agent-framework/pull/1927))
## [1.0.0b251105] - 2025-11-05
### Added
+2 -2
View File
@@ -36,7 +36,7 @@ add_agent_framework_fastapi_endpoint(app, agent, "/")
## Documentation
- **[Getting Started Tutorial](getting_started/)** - Step-by-step guide to building your first AG-UI server and client
- **[Examples](examples/)** - Complete examples for AG-UI features
- **[Examples](agent_framework_ag_ui_examples/)** - Complete examples for AG-UI features
## Features
@@ -64,7 +64,7 @@ The package uses a clean, orchestrator-based architecture:
## Next Steps
1. **New to AG-UI?** Start with the [Getting Started Tutorial](getting_started/)
2. **Want to see examples?** Check out the [Examples](examples/) for AG-UI features
2. **Want to see examples?** Check out the [Examples](agent_framework_ag_ui_examples/) for AG-UI features
## License
@@ -629,7 +629,7 @@ Now that you understand the basics of AG-UI, you can:
## Additional Resources
- [AG-UI Examples](../examples/README.md): Complete working examples for all 7 features
- [AG-UI Examples](../agent_framework_ag_ui_examples/README.md): Complete working examples for all 7 features
- [Agent Framework Documentation](../../core/README.md): Learn more about creating agents
- [AG-UI Protocol Spec](https://docs.ag-ui.com/): Official protocol documentation
+2 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b251105"
version = "1.0.0b251106.post1"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
@@ -40,8 +40,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["agent_framework_ag_ui"]
force-include = { "examples" = "agent_framework_ag_ui_examples" }
packages = ["agent_framework_ag_ui", "agent_framework_ag_ui_examples"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
@@ -7,5 +7,14 @@ This folder contains examples demonstrating different ways to manage conversatio
| File | Description |
|------|-------------|
| [`custom_chat_message_store_thread.py`](custom_chat_message_store_thread.py) | Demonstrates how to implement a custom `ChatMessageStore` for persisting conversation history. Shows how to create a custom store with serialization/deserialization capabilities and integrate it with agents for thread management across multiple sessions. |
| [`suspend_resume_thread.py`](suspend_resume_thread.py) | Shows how to suspend and resume conversation threads, allowing you to save the state of a conversation and continue it later. This is useful for long-running conversations or when you need to persist conversation state across application restarts. |
| [`redis_chat_message_store_thread.py`](redis_chat_message_store_thread.py) | Comprehensive examples of using the Redis-backed `RedisChatMessageStore` for persistent conversation storage. Covers basic usage, user session management, conversation persistence across app restarts, thread serialization, and automatic message trimming. Requires Redis server and demonstrates production-ready patterns for scalable chat applications. |
| [`suspend_resume_thread.py`](suspend_resume_thread.py) | Shows how to suspend and resume conversation threads, comparing service-managed threads (Azure AI) with in-memory threads (OpenAI). Demonstrates saving conversation state and continuing it later, useful for long-running conversations or persisting state across application restarts. |
## Environment Variables
Make sure to set the following environment variables before running the examples:
- `OPENAI_API_KEY`: Your OpenAI API key (required for all samples)
- `OPENAI_CHAT_MODEL_ID`: The OpenAI model to use (e.g., `gpt-4o`, `gpt-4o-mini`, `gpt-3.5-turbo`) (required for all samples)
- `AZURE_AI_PROJECT_ENDPOINT`: Azure AI Project endpoint URL (required for service-managed thread examples)
- `AZURE_AI_MODEL_DEPLOYMENT_NAME`: The name of your model deployment (required for service-managed thread examples)
@@ -8,6 +8,14 @@ from agent_framework import ChatMessage, ChatMessageStoreProtocol
from agent_framework._threads import ChatMessageStoreState
from agent_framework.openai import OpenAIChatClient
"""
Custom Chat Message Store Thread Example
This sample demonstrates how to implement and use a custom chat message store
for thread management, allowing you to persist conversation history in your
preferred storage solution (database, file system, etc.).
"""
class CustomChatMessageStore(ChatMessageStoreProtocol):
"""Implementation of custom chat message store.
@@ -24,13 +32,22 @@ class CustomChatMessageStore(ChatMessageStoreProtocol):
async def list_messages(self) -> list[ChatMessage]:
return self._messages
async def deserialize_state(self, serialized_store_state: Any, **kwargs: Any) -> None:
@classmethod
async def deserialize(cls, serialized_store_state: Any, **kwargs: Any) -> "CustomChatMessageStore":
"""Create a new instance from serialized state."""
store = cls()
await store.update_from_state(serialized_store_state, **kwargs)
return store
async def update_from_state(self, serialized_store_state: Any, **kwargs: Any) -> None:
"""Update this instance from serialized state."""
if serialized_store_state:
state = ChatMessageStoreState.from_dict(serialized_store_state, **kwargs)
if state.messages:
self._messages.extend(state.messages)
async def serialize_state(self, **kwargs: Any) -> Any:
async def serialize(self, **kwargs: Any) -> Any:
"""Serialize this store's state."""
state = ChatMessageStoreState(messages=self._messages)
return state.to_dict(**kwargs)
@@ -42,8 +59,8 @@ async def main() -> None:
# OpenAI Chat Client is used as an example here,
# other chat clients can be used as well.
agent = OpenAIChatClient().create_agent(
name="Joker",
instructions="You are good at telling jokes.",
name="CustomBot",
instructions="You are a helpful assistant that remembers our conversation.",
# Use custom chat message store.
# If not provided, the default in-memory store will be used.
chat_message_store_factory=CustomChatMessageStore,
@@ -53,7 +70,7 @@ async def main() -> None:
thread = agent.get_new_thread()
# Respond to user input.
query = "Tell me a joke about a pirate."
query = "Hello! My name is Alice and I love pizza."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=thread)}\n")
@@ -67,7 +84,7 @@ async def main() -> None:
resumed_thread = await agent.deserialize_thread(serialized_thread)
# Respond to user input.
query = "Now tell the same joke in the voice of a pirate, and add some emojis to the joke."
query = "What do you remember about me?"
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=resumed_thread)}\n")
@@ -8,6 +8,14 @@ from agent_framework import AgentThread
from agent_framework.openai import OpenAIChatClient
from agent_framework.redis import RedisChatMessageStore
"""
Redis Chat Message Store Thread Example
This sample demonstrates how to use Redis as a chat message store for thread
management, enabling persistent conversation history storage across sessions
with Redis as the backend data store.
"""
async def example_manual_memory_store() -> None:
"""Basic example of using Redis chat message store."""
@@ -2,38 +2,51 @@
import asyncio
from agent_framework.azure import AzureAIAgentClient
from agent_framework.openai import OpenAIChatClient
from azure.identity.aio import AzureCliCredential
"""
Thread Suspend and Resume Example
This sample demonstrates how to suspend and resume conversation threads, comparing
service-managed threads (Azure AI) with in-memory threads (OpenAI) for persistent
conversation state across sessions.
"""
async def suspend_resume_service_managed_thread() -> None:
"""Demonstrates how to suspend and resume a service-managed thread."""
print("=== Suspend-Resume Service-Managed Thread ===")
# OpenAI Chat Client is used as an example here,
# other chat clients can be used as well.
agent = OpenAIChatClient().create_agent(name="Joker", instructions="You are good at telling jokes.")
# AzureAIAgentClient supports service-managed threads.
async with (
AzureCliCredential() as credential,
AzureAIAgentClient(async_credential=credential).create_agent(
name="MemoryBot", instructions="You are a helpful assistant that remembers our conversation."
) as agent,
):
# Start a new thread for the agent conversation.
thread = agent.get_new_thread()
# Start a new thread for the agent conversation.
thread = agent.get_new_thread()
# Respond to user input.
query = "Hello! My name is Alice and I love pizza."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=thread)}\n")
# Respond to user input.
query = "Tell me a joke about a pirate."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=thread)}\n")
# Serialize the thread state, so it can be stored for later use.
serialized_thread = await thread.serialize()
# Serialize the thread state, so it can be stored for later use.
serialized_thread = await thread.serialize()
# The thread can now be saved to a database, file, or any other storage mechanism and loaded again later.
print(f"Serialized thread: {serialized_thread}\n")
# The thread can now be saved to a database, file, or any other storage mechanism and loaded again later.
print(f"Serialized thread: {serialized_thread}\n")
# Deserialize the thread state after loading from storage.
resumed_thread = await agent.deserialize_thread(serialized_thread)
# Deserialize the thread state after loading from storage.
resumed_thread = await agent.deserialize_thread(serialized_thread)
# Respond to user input.
query = "Now tell the same joke in the voice of a pirate, and add some emojis to the joke."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=resumed_thread)}\n")
# Respond to user input.
query = "What do you remember about me?"
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=resumed_thread)}\n")
async def suspend_resume_in_memory_thread() -> None:
@@ -42,13 +55,15 @@ async def suspend_resume_in_memory_thread() -> None:
# OpenAI Chat Client is used as an example here,
# other chat clients can be used as well.
agent = OpenAIChatClient().create_agent(name="Joker", instructions="You are good at telling jokes.")
agent = OpenAIChatClient().create_agent(
name="MemoryBot", instructions="You are a helpful assistant that remembers our conversation."
)
# Start a new thread for the agent conversation.
thread = agent.get_new_thread()
# Respond to user input.
query = "Tell me a joke about a pirate."
query = "Hello! My name is Alice and I love pizza."
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=thread)}\n")
@@ -62,7 +77,7 @@ async def suspend_resume_in_memory_thread() -> None:
resumed_thread = await agent.deserialize_thread(serialized_thread)
# Respond to user input.
query = "Now tell the same joke in the voice of a pirate, and add some emojis to the joke."
query = "What do you remember about me?"
print(f"User: {query}")
print(f"Agent: {await agent.run(query, thread=resumed_thread)}\n")
+3428 -3412
View File
File diff suppressed because it is too large Load Diff