mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
175e8e15a4 | ||
|
|
090b88a956 | ||
|
|
746c7da216 | ||
|
|
f804646239 | ||
|
|
d30103fee6 | ||
|
|
073114d2c1 | ||
|
|
00adc5bf9e | ||
|
|
55ae57c0ed | ||
|
|
d284d96a9e | ||
|
|
e94cfc6aef | ||
|
|
d1a81159de |
@@ -28,7 +28,7 @@ Welcome to Microsoft's comprehensive multi-language framework for building, orch
|
||||
Python
|
||||
|
||||
```bash
|
||||
pip install agent-framework --pre
|
||||
pip install agent-framework
|
||||
# This will install all sub-packages, see `python/packages` for individual packages.
|
||||
# It may take a minute on first install on Windows.
|
||||
```
|
||||
@@ -90,7 +90,7 @@ Still have questions? Join our [weekly office hours](./COMMUNITY.md#public-commu
|
||||
Create a simple Azure Responses Agent that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```python
|
||||
# pip install agent-framework --pre
|
||||
# pip install agent-framework
|
||||
# Use `az login` to authenticate with Azure CLI
|
||||
import os
|
||||
import asyncio
|
||||
@@ -120,38 +120,38 @@ if __name__ == "__main__":
|
||||
```
|
||||
|
||||
### Basic Agent - .NET
|
||||
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.Foundry
|
||||
// Use `az login` to authenticate with Azure CLI
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using System;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-5.4-mini";
|
||||
|
||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
Create a simple Agent, using OpenAI Responses, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
|
||||
using Microsoft.Agents.AI;
|
||||
// dotnet add package Microsoft.Agents.AI.OpenAI
|
||||
using System;
|
||||
using OpenAI;
|
||||
using OpenAI.Responses;
|
||||
|
||||
// Replace the <apikey> with your OpenAI API key.
|
||||
var agent = new OpenAIClient("<apikey>")
|
||||
.GetResponsesClient("gpt-4o-mini")
|
||||
.AsAIAgent(name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
|
||||
Create a simple Agent, using Microsoft Foundry with token-based auth, that writes a haiku about the Microsoft Agent Framework
|
||||
|
||||
```c#
|
||||
// dotnet add package Microsoft.Agents.AI.AzureAI --prerelease
|
||||
// dotnet add package Azure.Identity
|
||||
// Use `az login` to authenticate with Azure CLI
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
var agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
|
||||
.AsAIAgent(model: deploymentName, name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
.GetResponsesClient()
|
||||
.AsAIAgent(model: "gpt-5.4-mini", name: "HaikuBot", instructions: "You are an upbeat assistant that writes beautifully.");
|
||||
|
||||
Console.WriteLine(await agent.RunAsync("Write a haiku about Microsoft Agent Framework."));
|
||||
```
|
||||
@@ -207,4 +207,9 @@ The samples typically read configuration from environment variables. Common requ
|
||||
|
||||
## Important Notes
|
||||
|
||||
If you use the Microsoft Agent Framework to build applications that operate with third-party servers or agents, you do so at your own risk. We recommend reviewing all data being shared with third-party servers or agents and being cognizant of third-party practices for retention and location of data. It is your responsibility to manage whether your data will flow outside of your organization's Azure compliance and geographic boundaries and any related implications.
|
||||
> [!IMPORTANT]
|
||||
> If you use Microsoft Agent Framework to build applications that operate with any third-party servers, agents, code, or non-Azure Direct models (“Third-Party Systems”), you do so at your own risk. Third-Party Systems are Non-Microsoft Products under the Microsoft Product Terms and are governed by their own third-party license terms. You are responsible for any usage and associated costs.
|
||||
>
|
||||
>We recommend reviewing all data being shared with and received from Third-Party Systems and being cognizant of third-party practices for handling, sharing, retention and location of data. It is your responsibility to manage whether your data will flow outside of your organization’s Azure compliance and geographic boundaries and any related implications, and that appropriate permissions, boundaries and approvals are provisioned.
|
||||
>
|
||||
>You are responsible for carefully reviewing and testing applications you build using Microsoft Agent Framework in the context of your specific use cases, and making all appropriate decisions and customizations. This includes implementing your own responsible AI mitigations such as metaprompt, content filters, or other safety systems, and ensuring your applications meet appropriate quality, reliability, security, and trustworthiness standards. See also: [Transparency FAQ](./TRANSPARENCY_FAQ.md)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- Aspire.* -->
|
||||
<PackageVersion Include="Anthropic" Version="12.8.0" />
|
||||
<PackageVersion Include="Anthropic" Version="12.11.0" />
|
||||
<PackageVersion Include="Anthropic.Foundry" Version="0.4.2" />
|
||||
<PackageVersion Include="Aspire.Azure.AI.OpenAI" Version="13.0.0-preview.1.25560.3" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireAppHostSdkVersion)" />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="Azure.AI.Projects" />
|
||||
<PackageReference Include="Azure.Identity" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Azure.AI.OpenAI;
|
||||
using System.Text;
|
||||
using Azure.AI.Projects;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
@@ -31,22 +32,26 @@ public static class Program
|
||||
{
|
||||
private static async Task Main()
|
||||
{
|
||||
// Set up the Azure OpenAI client
|
||||
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";
|
||||
var chatClient = new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential()).GetChatClient(deploymentName).AsIChatClient();
|
||||
// Set up the Azure AI Project client
|
||||
var endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
|
||||
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
|
||||
var deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
var chatClient = new AIProjectClient(new Uri(endpoint), new AzureCliCredential())
|
||||
.ProjectOpenAIClient.GetChatClient(deploymentName).AsIChatClient();
|
||||
|
||||
// Create the executors
|
||||
ChatClientAgent physicist = new(
|
||||
var physicist = new ChatClientAgent(
|
||||
chatClient,
|
||||
name: "Physicist",
|
||||
instructions: "You are an expert in physics. You answer questions from a physics perspective."
|
||||
);
|
||||
ChatClientAgent chemist = new(
|
||||
).BindAsExecutor(new AIAgentHostOptions { ForwardIncomingMessages = false });
|
||||
|
||||
var chemist = new ChatClientAgent(
|
||||
chatClient,
|
||||
name: "Chemist",
|
||||
instructions: "You are an expert in chemistry. You answer questions from a chemistry perspective."
|
||||
);
|
||||
).BindAsExecutor(new AIAgentHostOptions { ForwardIncomingMessages = false });
|
||||
|
||||
var startExecutor = new ConcurrentStartExecutor();
|
||||
var aggregationExecutor = new ConcurrentAggregationExecutor();
|
||||
|
||||
@@ -61,11 +66,30 @@ public static class Program
|
||||
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, input: "What is temperature?");
|
||||
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
|
||||
{
|
||||
if (evt is WorkflowOutputEvent output)
|
||||
switch (evt)
|
||||
{
|
||||
Console.WriteLine($"Workflow completed with results:\n{output.Data}");
|
||||
case WorkflowOutputEvent workflowOutput:
|
||||
Console.WriteLine($"Workflow completed with results:\n{workflowOutput.Data}");
|
||||
break;
|
||||
|
||||
case WorkflowErrorEvent workflowError:
|
||||
WriteError(workflowError.Exception?.ToString() ?? "Unknown workflow error occurred");
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailed:
|
||||
WriteError($"Executor '{executorFailed.ExecutorId}' failed with {(
|
||||
executorFailed.Data == null ? "unknown error" : $"exception {executorFailed.Data}"
|
||||
)}.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteError(string error)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Write(error);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +116,7 @@ internal sealed partial class ConcurrentStartExecutor() :
|
||||
// the message but will not start processing until they receive a turn token.
|
||||
await context.SendMessageAsync(new ChatMessage(ChatRole.User, message), cancellationToken: cancellationToken);
|
||||
// Broadcast the turn token to kick off the agents.
|
||||
await context.SendMessageAsync(new TurnToken(emitEvents: true), cancellationToken: cancellationToken);
|
||||
await context.SendMessageAsync(new TurnToken(emitEvents: false), cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,11 +140,19 @@ internal sealed partial class ConcurrentAggregationExecutor() :
|
||||
public override async ValueTask HandleAsync(List<ChatMessage> message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this._messages.AddRange(message);
|
||||
}
|
||||
|
||||
if (this._messages.Count == 2)
|
||||
protected override ValueTask OnMessageDeliveryFinishedAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
StringBuilder resultBuilder = new();
|
||||
foreach (ChatMessage m in this._messages)
|
||||
{
|
||||
var formattedMessages = string.Join(Environment.NewLine, this._messages.Select(m => $"{m.AuthorName}: {m.Text}"));
|
||||
await context.YieldOutputAsync(formattedMessages, cancellationToken);
|
||||
resultBuilder.AppendLine($"{m.AuthorName}: {m.Text}");
|
||||
resultBuilder.AppendLine();
|
||||
}
|
||||
|
||||
this._messages.Clear();
|
||||
|
||||
return context.YieldOutputAsync(resultBuilder.ToString(), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Anthropic" />
|
||||
<PackageReference Include="AWSSDK.Extensions.Bedrock.MEAI" />
|
||||
<PackageReference Include="Google.GenAI" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Amazon.BedrockRuntime;
|
||||
using Google.GenAI;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Agents.AI.Workflows;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -9,22 +9,20 @@ using Microsoft.Extensions.AI;
|
||||
const string Topic = "Goldendoodles make the best pets.";
|
||||
|
||||
// Create the IChatClients to talk to different services.
|
||||
IChatClient aws = new AmazonBedrockRuntimeClient(
|
||||
Environment.GetEnvironmentVariable("BEDROCK_ACCESS_KEY"!),
|
||||
Environment.GetEnvironmentVariable("BEDROCK_SECRET_KEY")!,
|
||||
Amazon.RegionEndpoint.USEast1)
|
||||
.AsIChatClient("amazon.nova-pro-v1:0");
|
||||
IChatClient google = new Client(vertexAI: false, apiKey: Environment.GetEnvironmentVariable("GOOGLE_GENAI_API_KEY"))
|
||||
.AsIChatClient("gemini-2.5-flash");
|
||||
|
||||
IChatClient anthropic = new Anthropic.AnthropicClient(
|
||||
new() { ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY") })
|
||||
.AsIChatClient("claude-sonnet-4-20250514");
|
||||
|
||||
IChatClient openai = new OpenAI.OpenAIClient(
|
||||
Environment.GetEnvironmentVariable("OPENAI_API_KEY")!).GetChatClient("gpt-4o-mini")
|
||||
.AsIChatClient();
|
||||
Environment.GetEnvironmentVariable("OPENAI_API_KEY"))
|
||||
.GetResponsesClient()
|
||||
.AsIChatClient("gpt-4o-mini");
|
||||
|
||||
// Define our agents.
|
||||
AIAgent researcher = new ChatClientAgent(aws,
|
||||
AIAgent researcher = new ChatClientAgent(google,
|
||||
instructions: """
|
||||
Write a short essay on topic specified by the user. The essay should be three to five paragraphs, written at a
|
||||
high school reading level, and include relevant background information, key claims, and notable perspectives.
|
||||
@@ -60,6 +58,12 @@ AIAgent workflowAgent = AgentWorkflowBuilder.BuildSequential(researcher, factChe
|
||||
string? lastAuthor = null;
|
||||
await foreach (var update in workflowAgent.RunStreamingAsync(Topic))
|
||||
{
|
||||
// Skip WorkflowEvent-only updates
|
||||
if ((update.Contents == null || update.Contents.Count == 0) && update.RawRepresentation is WorkflowEvent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastAuthor != update.AuthorName)
|
||||
{
|
||||
lastAuthor = update.AuthorName;
|
||||
|
||||
@@ -287,10 +287,6 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
{
|
||||
// Discard each event (including InternalCompletionSignals)
|
||||
}
|
||||
|
||||
// After clearing, signal the run loop to continue if needed
|
||||
// The run loop will send a new completion signal when it finishes processing from the restored state
|
||||
this.SignalInput();
|
||||
}
|
||||
|
||||
public async ValueTask StopAsync()
|
||||
|
||||
@@ -310,22 +310,40 @@ public abstract class Executor : IIdentified
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked once per superstep before any messages are delivered to the Executor.
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow context.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
protected internal virtual ValueTask OnMessageDeliveryStartingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked once per superstep after all messages have been delivered to the Executor.
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow context.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
protected internal virtual ValueTask OnMessageDeliveryFinishedAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked before a checkpoint is saved, allowing custom pre-save logic in derived classes.
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow context.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
protected internal virtual ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after a checkpoint is loaded, allowing custom post-load logic in derived classes.
|
||||
/// </summary>
|
||||
/// <param name="context">The workflow context.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
|
||||
/// The default is <see cref="CancellationToken.None"/>.</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation.</returns>
|
||||
protected internal virtual ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -182,6 +182,9 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
|
||||
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, fromCheckpoint, [], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new(runHandle);
|
||||
Run run = new(runHandle);
|
||||
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return run;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,17 +249,33 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
this.StepTracer.TraceActivated(receiverId);
|
||||
while (envelopes.TryDequeue(out var envelope))
|
||||
{
|
||||
(object message, TypeId messageType) = await TranslateMessageAsync(envelope).ConfigureAwait(false);
|
||||
|
||||
await executor.ExecuteCoreAsync(
|
||||
message,
|
||||
messageType,
|
||||
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
|
||||
this.TelemetryContext,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
// TODO: #5084 - Add delivery-level activity (max one per step per executor) to capture non-message
|
||||
// specific invocations of executor logic.
|
||||
IWorkflowContext tracelessContext = this.RunContext.BindWorkflowContext(receiverId);
|
||||
|
||||
try
|
||||
{
|
||||
await executor.OnMessageDeliveryStartingAsync(tracelessContext, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
while (envelopes.TryDequeue(out var envelope))
|
||||
{
|
||||
(object message, TypeId messageType) = await TranslateMessageAsync(envelope).ConfigureAwait(false);
|
||||
|
||||
await executor.ExecuteCoreAsync(
|
||||
message,
|
||||
messageType,
|
||||
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
|
||||
this.TelemetryContext,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await executor.OnMessageDeliveryFinishedAsync(tracelessContext, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
async ValueTask<(object, TypeId)> TranslateMessageAsync(MessageEnvelope envelope)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.InProc;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
public class InProcessExecutorEventsTests
|
||||
{
|
||||
[SendsMessage(typeof(string[]))]
|
||||
private sealed class EventTrackingExecutor(bool forwardMessages, string id) : Executor<IEnumerable<string>>(id)
|
||||
{
|
||||
public List<IEnumerable<string>> ReceivedMessages { get; } = [];
|
||||
|
||||
private int _checkpointingCalls;
|
||||
public int CheckpointingCalls => this._checkpointingCalls;
|
||||
|
||||
private int _checkpointRestoredCalls;
|
||||
public int CheckpointRestoredCalls => this._checkpointRestoredCalls;
|
||||
|
||||
private int _deliveryStartingCalls;
|
||||
public int DeliveryStartingCalls => this._deliveryStartingCalls;
|
||||
|
||||
private int _deliveryFinishedAsyncCalls;
|
||||
public int DeliveryFinishedCalls => this._deliveryFinishedAsyncCalls;
|
||||
|
||||
protected internal override ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref this._checkpointingCalls);
|
||||
return base.OnCheckpointingAsync(context, cancellationToken);
|
||||
}
|
||||
|
||||
protected internal override ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref this._checkpointRestoredCalls);
|
||||
return base.OnCheckpointRestoredAsync(context, cancellationToken);
|
||||
}
|
||||
|
||||
protected internal override ValueTask OnMessageDeliveryStartingAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref this._deliveryStartingCalls);
|
||||
return base.OnMessageDeliveryStartingAsync(context, cancellationToken);
|
||||
}
|
||||
|
||||
protected internal override ValueTask OnMessageDeliveryFinishedAsync(IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Interlocked.Increment(ref this._deliveryFinishedAsyncCalls);
|
||||
return base.OnMessageDeliveryFinishedAsync(context, cancellationToken);
|
||||
}
|
||||
|
||||
public override async ValueTask HandleAsync(IEnumerable<string> message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ReceivedMessages.Add(message);
|
||||
|
||||
if (forwardMessages)
|
||||
{
|
||||
foreach (string packedMessage in message)
|
||||
{
|
||||
await context.SendMessageAsync(new[] { packedMessage }, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestFixture
|
||||
{
|
||||
public EventTrackingExecutor StartingExecutor { get; } = new(true, nameof(StartingExecutor));
|
||||
public EventTrackingExecutor ReceivesMessage { get; } = new(false, nameof(ReceivesMessage));
|
||||
public EventTrackingExecutor UninvokedExecutor { get; } = new(false, nameof(UninvokedExecutor));
|
||||
|
||||
public Workflow Workflow { get; }
|
||||
|
||||
public TestFixture()
|
||||
{
|
||||
this.Workflow = new WorkflowBuilder(this.StartingExecutor)
|
||||
.AddEdge(this.StartingExecutor, this.ReceivesMessage)
|
||||
// The uninvoked executor remains uninvoked because ReceivesMessage does not forward its incoming message
|
||||
.AddEdge(this.ReceivesMessage, this.UninvokedExecutor)
|
||||
.Build();
|
||||
}
|
||||
|
||||
public const int StepsPerInputBatch = 2;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, ExecutionEnvironment.InProcess_Lockstep)]
|
||||
[InlineData(1, ExecutionEnvironment.InProcess_OffThread)]
|
||||
internal async Task Test_InProcessExecution_InvokesDeliveryEventsOnceAsync(int messageCount, ExecutionEnvironment environment)
|
||||
{
|
||||
// Arrange
|
||||
TestFixture fixture = new();
|
||||
InProcessExecutionEnvironment executionEnvironment = environment.ToWorkflowExecutionEnvironment();
|
||||
|
||||
// Act
|
||||
IEnumerable<string> batch = Enumerable.Range(1, messageCount).Select(i => $"Message_{i}");
|
||||
await using StreamingRun streamingRun = await executionEnvironment.OpenStreamingAsync(fixture.Workflow);
|
||||
|
||||
await streamingRun.TrySendMessageAsync(batch);
|
||||
await streamingRun.RunToCompletionAsync(ThrowOnError);
|
||||
|
||||
// Assert
|
||||
fixture.StartingExecutor.DeliveryStartingCalls.Should().Be(1);
|
||||
fixture.StartingExecutor.DeliveryFinishedCalls.Should().Be(1);
|
||||
|
||||
fixture.ReceivesMessage.DeliveryStartingCalls.Should().Be(1);
|
||||
fixture.ReceivesMessage.DeliveryFinishedCalls.Should().Be(1);
|
||||
|
||||
fixture.UninvokedExecutor.DeliveryStartingCalls.Should().Be(0);
|
||||
fixture.UninvokedExecutor.DeliveryFinishedCalls.Should().Be(0);
|
||||
|
||||
ExternalResponse? ThrowOnError(WorkflowEvent workflowEvent)
|
||||
{
|
||||
switch (workflowEvent)
|
||||
{
|
||||
case WorkflowErrorEvent workflowError:
|
||||
Assert.Fail(workflowError.Exception?.ToString() ?? "Unknown error occurred while executing workflow.");
|
||||
break;
|
||||
|
||||
case ExecutorFailedEvent executorFailed:
|
||||
Assert.Fail(executorFailed.Data != null
|
||||
? $"Executor {executorFailed.ExecutorId} failed with exception: {executorFailed.Data}"
|
||||
: $"Executor {executorFailed.ExecutorId} failed with unknown error");
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Test_InProcessExecution_InvokesCheckpointingEventIFFCheckpointingEnabledAsync(bool useCheckpointing)
|
||||
{
|
||||
// Arrange
|
||||
TestFixture fixture = new();
|
||||
|
||||
InProcessExecutionEnvironment executionEnvironment = InProcessExecution.Default;
|
||||
|
||||
if (useCheckpointing)
|
||||
{
|
||||
executionEnvironment = executionEnvironment.WithCheckpointing(CheckpointManager.CreateInMemory());
|
||||
}
|
||||
|
||||
// Act
|
||||
string sessionId = Guid.NewGuid().ToString();
|
||||
await using Run run = await executionEnvironment.RunAsync<string[]>(fixture.Workflow, ["Message"], sessionId);
|
||||
|
||||
// Assert
|
||||
run.OutgoingEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty();
|
||||
run.OutgoingEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty();
|
||||
|
||||
const int ExpectedSteps = TestFixture.StepsPerInputBatch;
|
||||
run.OutgoingEvents.OfType<SuperStepCompletedEvent>().Should().HaveCount(ExpectedSteps);
|
||||
|
||||
int expectedCheckpoints = useCheckpointing ? ExpectedSteps : 0;
|
||||
run.Checkpoints.Should().HaveCount(expectedCheckpoints);
|
||||
|
||||
fixture.StartingExecutor.CheckpointingCalls.Should().Be(expectedCheckpoints);
|
||||
fixture.StartingExecutor.CheckpointRestoredCalls.Should().Be(0);
|
||||
|
||||
fixture.ReceivesMessage.CheckpointingCalls.Should().Be(expectedCheckpoints);
|
||||
fixture.ReceivesMessage.CheckpointRestoredCalls.Should().Be(0);
|
||||
|
||||
fixture.UninvokedExecutor.CheckpointingCalls.Should().Be(0); // Uninvoked executors don't get "instantiated" in the workflow context
|
||||
fixture.UninvokedExecutor.CheckpointRestoredCalls.Should().Be(0);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
//[InlineData(false, true)] - impossible to restore checkpoint with checkpointing disabled, will throw
|
||||
public async Task Test_InProcessExecution_InvokesRestoredEventIFFRestoringCheckpointAsync(bool restoreCheckpoint)
|
||||
{
|
||||
// Arrange
|
||||
TestFixture runFixture = new();
|
||||
InProcessExecutionEnvironment executionEnvironment = InProcessExecution.Default.WithCheckpointing(CheckpointManager.CreateInMemory());
|
||||
|
||||
// Act
|
||||
string sessionId = Guid.NewGuid().ToString();
|
||||
Run run = await executionEnvironment.RunAsync<string[]>(runFixture.Workflow, ["Message"], sessionId);
|
||||
|
||||
// Assert
|
||||
run.OutgoingEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty();
|
||||
run.OutgoingEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty();
|
||||
|
||||
TestFixture validateFixture = runFixture;
|
||||
|
||||
// Act 2
|
||||
int expectedCheckpoints = TestFixture.StepsPerInputBatch;
|
||||
|
||||
if (restoreCheckpoint)
|
||||
{
|
||||
expectedCheckpoints--; // We are restoring from the first one, so skip one
|
||||
|
||||
validateFixture = new();
|
||||
run.Checkpoints.Should().HaveCount(TestFixture.StepsPerInputBatch);
|
||||
|
||||
CheckpointInfo firstCheckpoint = run.Checkpoints[0];
|
||||
|
||||
await run.DisposeAsync();
|
||||
run = await executionEnvironment.ResumeAsync(validateFixture.Workflow, firstCheckpoint);
|
||||
}
|
||||
|
||||
// Assert 2
|
||||
if (restoreCheckpoint)
|
||||
{
|
||||
// Make sure the second run did not have failures
|
||||
run.OutgoingEvents.OfType<WorkflowErrorEvent>().Should().BeEmpty();
|
||||
run.OutgoingEvents.OfType<ExecutorFailedEvent>().Should().BeEmpty();
|
||||
}
|
||||
|
||||
int expectedRestoreCalls = restoreCheckpoint ? 1 : 0;
|
||||
|
||||
validateFixture.StartingExecutor.CheckpointingCalls.Should().Be(expectedCheckpoints);
|
||||
validateFixture.StartingExecutor.CheckpointRestoredCalls.Should().Be(expectedRestoreCalls);
|
||||
|
||||
validateFixture.ReceivesMessage.CheckpointingCalls.Should().Be(expectedCheckpoints);
|
||||
validateFixture.ReceivesMessage.CheckpointRestoredCalls.Should().Be(expectedRestoreCalls);
|
||||
|
||||
validateFixture.UninvokedExecutor.CheckpointingCalls.Should().Be(0); // Uninvoked executors don't get "instantiated" in the workflow context
|
||||
validateFixture.UninvokedExecutor.CheckpointRestoredCalls.Should().Be(0);
|
||||
|
||||
// Cleanup
|
||||
await run.DisposeAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Neo4j Context Providers
|
||||
|
||||
Neo4j offers two context providers for the Agent Framework, each serving a different purpose:
|
||||
|
||||
| | [Neo4j Memory](../neo4j_memory/README.md) | [Neo4j GraphRAG](../../../05-end-to-end/neo4j_graphrag/README.md) |
|
||||
|---|---|---|
|
||||
| **What it does** | Read-write memory — stores conversations, builds knowledge graphs, learns from interactions | Read-only retrieval from a pre-existing knowledge base with optional graph traversal |
|
||||
| **Data source** | Agent interactions (grows over time) | Pre-loaded documents and indexes |
|
||||
| **Python package** | [`neo4j-agent-memory`](https://pypi.org/project/neo4j-agent-memory/) | [`agent-framework-neo4j`](https://pypi.org/project/agent-framework-neo4j/) |
|
||||
| **Database setup** | Empty — creates its own schema | Requires pre-indexed documents with vector or fulltext indexes |
|
||||
| **Example use case** | "Remember my preferences", "What did we discuss last time?" | "Search our documents", "What risks does Acme Corp face?" |
|
||||
|
||||
## Which should I use?
|
||||
|
||||
**Use [Neo4j Memory](../neo4j_memory/README.md)** when your agent needs to remember things across sessions — user preferences, past conversations, extracted entities, and reasoning traces. The memory provider writes to the database on every interaction, building a knowledge graph that grows over time.
|
||||
|
||||
**Use [Neo4j GraphRAG](../../../05-end-to-end/neo4j_graphrag/README.md)** when your agent needs to search an existing knowledge base — documents, articles, product catalogs — and optionally enrich results by traversing graph relationships. The GraphRAG provider is read-only and does not modify your data.
|
||||
|
||||
You can use both together: GraphRAG for domain knowledge retrieval, Memory for personalization and learning.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Neo4j Memory Context Provider
|
||||
|
||||
[Neo4j Agent Memory](https://github.com/neo4j-labs/agent-memory) is a graph-native memory system for AI agents that stores conversations, builds knowledge graphs from interactions, and lets agents learn from their own reasoning — all backed by Neo4j.
|
||||
|
||||
For full documentation, installation instructions, code examples, and configuration details, see the [Neo4j Memory integration guide on Microsoft Learn](https://learn.microsoft.com/agent-framework/integrations/neo4j-memory).
|
||||
|
||||
For a runnable example, see the [retail assistant sample](https://github.com/neo4j-labs/agent-memory/tree/main/examples/microsoft_agent_retail_assistant).
|
||||
|
||||
For help choosing between the Memory and GraphRAG providers, see the [Neo4j Context Providers overview](../neo4j/README.md).
|
||||
@@ -4,6 +4,8 @@ The [Neo4j GraphRAG context provider](https://github.com/neo4j-labs/neo4j-maf-pr
|
||||
|
||||
This sample keeps setup lightweight by using a pre-built Neo4j fulltext index plus a graph-enrichment query.
|
||||
|
||||
For full documentation, see the [Neo4j GraphRAG integration guide on Microsoft Learn](https://learn.microsoft.com/agent-framework/integrations/neo4j-graphrag).
|
||||
|
||||
## Example
|
||||
|
||||
| File | Description |
|
||||
|
||||
Reference in New Issue
Block a user