mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'features/3768-devui-aspire-integration' of github.com:tommasodotNET/agent-framework into features/3768-devui-aspire-integration
This commit is contained in:
@@ -12,13 +12,13 @@ env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
name: Checks
|
||||
pre-commit-hooks:
|
||||
name: Pre-commit Hooks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.14"]
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
@@ -37,16 +37,106 @@ jobs:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
# Configure a constant location for the uv cache
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
name: Run Pre-Commit Hooks
|
||||
path: ~/.cache/prek
|
||||
key: prek|${{ matrix.python-version }}|${{ hashFiles('python/.pre-commit-config.yaml') }}
|
||||
- uses: j178/prek-action@v1
|
||||
name: Run Pre-commit Hooks (excluding poe-check)
|
||||
env:
|
||||
SKIP: poe-check
|
||||
with:
|
||||
extra_args: --config python/.pre-commit-config.yaml --all-files
|
||||
extra-args: --cd python --all-files
|
||||
|
||||
package-checks:
|
||||
name: Package Checks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run fmt, lint, pyright in parallel across packages
|
||||
run: uv run poe check-packages
|
||||
|
||||
samples-markdown:
|
||||
name: Samples & Markdown
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run samples lint
|
||||
run: uv run poe samples-lint
|
||||
- name: Run samples syntax check
|
||||
run: uv run poe samples-syntax
|
||||
- name: Run markdown code lint
|
||||
run: uv run poe markdown-code-lint
|
||||
|
||||
mypy:
|
||||
name: Mypy Checks
|
||||
if: "!cancelled()"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./python
|
||||
env:
|
||||
UV_PYTHON: ${{ matrix.python-version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up python and install the project
|
||||
id: python-setup
|
||||
uses: ./.github/actions/python-setup
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
os: ${{ runner.os }}
|
||||
env:
|
||||
UV_CACHE_DIR: /tmp/.uv-cache
|
||||
- name: Run Mypy
|
||||
env:
|
||||
GITHUB_BASE_REF: ${{ github.event.pull_request.base.ref || github.base_ref || 'main' }}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260205.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260205.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260205.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260209.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260209.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260209.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -6,7 +6,6 @@ using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
+7
-1
@@ -35,8 +35,10 @@ public static class Program
|
||||
|
||||
using var traceProvider = Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(resourceBuilder)
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddSource(SourceName)
|
||||
// The following source is only required if not specifying
|
||||
// the `activitySource` in the WithOpenTelemetry call below
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString)
|
||||
.Build();
|
||||
|
||||
@@ -51,6 +53,10 @@ public static class Program
|
||||
// Build the workflow by connecting executors sequentially
|
||||
var workflow = new WorkflowBuilder(uppercase)
|
||||
.AddEdge(uppercase, reverse)
|
||||
.WithOpenTelemetry(
|
||||
// Set `EnableSensitiveData` to true to include message content in traces
|
||||
configure: cfg => cfg.EnableSensitiveData = true,
|
||||
activitySource: s_activitySource)
|
||||
.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
|
||||
@@ -37,8 +37,10 @@ public static class Program
|
||||
|
||||
using var traceProvider = Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(resourceBuilder)
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddSource(SourceName)
|
||||
// The following source is only required if not specifying
|
||||
// the `activitySource` in the WithOpenTelemetry call below
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
|
||||
.Build();
|
||||
|
||||
@@ -53,6 +55,10 @@ public static class Program
|
||||
// Build the workflow by connecting executors sequentially
|
||||
var workflow = new WorkflowBuilder(uppercase)
|
||||
.AddEdge(uppercase, reverse)
|
||||
.WithOpenTelemetry(
|
||||
// Set `EnableSensitiveData` to true to include message content in traces
|
||||
configure: cfg => cfg.EnableSensitiveData = true,
|
||||
activitySource: s_activitySource)
|
||||
.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -41,4 +44,23 @@ public sealed class DeclarativeWorkflowOptions(WorkflowAgentProvider agentProvid
|
||||
/// Gets the <see cref="ILoggerFactory"/> used to create loggers for workflow components.
|
||||
/// </summary>
|
||||
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the callback to configure telemetry options.
|
||||
/// </summary>
|
||||
public Action<WorkflowTelemetryOptions>? ConfigureTelemetry { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional <see cref="ActivitySource"/> for telemetry.
|
||||
/// If provided, the caller retains ownership and is responsible for disposal.
|
||||
/// If <see langword="null"/> but <see cref="ConfigureTelemetry"/> is set, a shared default
|
||||
/// activity source named "Microsoft.Agents.AI.Workflows" will be used.
|
||||
/// </summary>
|
||||
public ActivitySource? TelemetryActivitySource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether telemetry is enabled.
|
||||
/// Telemetry is enabled when either <see cref="ConfigureTelemetry"/> or <see cref="TelemetryActivitySource"/> is set.
|
||||
/// </summary>
|
||||
internal bool IsTelemetryEnabled => this.ConfigureTelemetry is not null || this.TelemetryActivitySource is not null;
|
||||
}
|
||||
|
||||
+8
@@ -51,6 +51,14 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
this._workflowModel.Build(builder);
|
||||
|
||||
// Apply telemetry if configured
|
||||
if (this._workflowOptions.IsTelemetryEnabled)
|
||||
{
|
||||
builder.WorkflowBuilder.WithOpenTelemetry(
|
||||
this._workflowOptions.ConfigureTelemetry,
|
||||
this._workflowOptions.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
// Build final workflow
|
||||
return builder.WorkflowBuilder.Build(validateOrphans: false);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ internal sealed class DirectEdgeRunner(IRunnerContext runContext, DirectEdgeData
|
||||
|
||||
protected internal override async ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer)
|
||||
{
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(DirectEdgeRunner))
|
||||
.SetTag(Tags.MessageSourceId, this.EdgeData.SourceId)
|
||||
|
||||
@@ -14,9 +14,6 @@ internal interface IStatefulEdgeRunner
|
||||
|
||||
internal abstract class EdgeRunner
|
||||
{
|
||||
protected static readonly string s_namespace = typeof(EdgeRunner).Namespace!;
|
||||
protected static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
// TODO: Can this be sync?
|
||||
protected internal abstract ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer);
|
||||
}
|
||||
@@ -26,4 +23,6 @@ internal abstract class EdgeRunner<TEdgeData>(
|
||||
{
|
||||
protected IRunnerContext RunContext { get; } = Throw.IfNull(runContext);
|
||||
protected TEdgeData EdgeData { get; } = Throw.IfNull(edgeData);
|
||||
|
||||
protected Activity? StartActivity() => this.RunContext.TelemetryContext.StartEdgeGroupProcessActivity();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ internal sealed class FanInEdgeRunner(IRunnerContext runContext, FanInEdgeData e
|
||||
{
|
||||
Debug.Assert(!envelope.IsExternal, "FanIn edges should never be chased from external input");
|
||||
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(FanInEdgeRunner))
|
||||
.SetTag(Tags.MessageTargetId, this.EdgeData.SinkId);
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class FanOutEdgeRunner(IRunnerContext runContext, FanOutEdgeData
|
||||
{
|
||||
protected internal override async ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer)
|
||||
{
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(FanOutEdgeRunner))
|
||||
.SetTag(Tags.MessageSourceId, this.EdgeData.SourceId);
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal interface IRunnerContext : IExternalRequestSink, ISuperStepJoinContext
|
||||
{
|
||||
WorkflowTelemetryContext TelemetryContext { get; }
|
||||
|
||||
ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
|
||||
ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
@@ -12,6 +13,8 @@ internal interface ISuperStepRunner
|
||||
|
||||
string StartExecutorId { get; }
|
||||
|
||||
WorkflowTelemetryContext TelemetryContext { get; }
|
||||
|
||||
bool HasUnservicedRequests { get; }
|
||||
bool HasUnprocessedMessages { get; }
|
||||
|
||||
|
||||
@@ -13,9 +13,6 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
{
|
||||
private static readonly string s_namespace = typeof(LockstepRunEventStream).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
private readonly CancellationTokenSource _stopCancellation = new();
|
||||
private readonly InputWaiter _inputWaiter = new();
|
||||
private int _isDisposed;
|
||||
@@ -53,7 +50,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
|
||||
this._stepRunner.OutgoingEvents.EventRaised += OnWorkflowEventAsync;
|
||||
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowRun);
|
||||
using Activity? activity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
|
||||
activity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.RunId, this._stepRunner.RunId);
|
||||
|
||||
try
|
||||
|
||||
@@ -25,7 +25,7 @@ internal sealed class ResponseEdgeRunner(IRunnerContext runContext, string execu
|
||||
{
|
||||
Debug.Assert(envelope.IsExternal, "Input edges should only be chased from external input");
|
||||
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(ResponseEdgeRunner))
|
||||
.SetTag(Tags.MessageSourceId, envelope.SourceId)
|
||||
|
||||
@@ -17,9 +17,6 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
/// </summary>
|
||||
internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
{
|
||||
private static readonly string s_namespace = typeof(StreamingRunEventStream).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
private readonly Channel<WorkflowEvent> _eventChannel;
|
||||
private readonly ISuperStepRunner _stepRunner;
|
||||
private readonly InputWaiter _inputWaiter;
|
||||
@@ -63,7 +60,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
// Subscribe to events - they will flow directly to the channel as they're raised
|
||||
this._stepRunner.OutgoingEvents.EventRaised += OnEventRaisedAsync;
|
||||
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowRun);
|
||||
using Activity? activity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
|
||||
activity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.RunId, this._stepRunner.RunId);
|
||||
|
||||
try
|
||||
|
||||
@@ -26,9 +26,6 @@ public abstract class Executor : IIdentified
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
|
||||
private static readonly string s_namespace = typeof(Executor).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
// TODO: Add overloads for binding with a configuration/options object once the Configured<T> hierarchy goes away.
|
||||
|
||||
/// <summary>
|
||||
@@ -142,13 +139,13 @@ public abstract class Executor : IIdentified
|
||||
/// <returns>A ValueTask representing the asynchronous operation, wrapping the output from the executor.</returns>
|
||||
/// <exception cref="NotSupportedException">No handler found for the message type.</exception>
|
||||
/// <exception cref="TargetInvocationException">An exception is generated while handling the message.</exception>
|
||||
public async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
public ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
=> this.ExecuteAsync(message, messageType, context, WorkflowTelemetryContext.Disabled, cancellationToken);
|
||||
|
||||
internal async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, WorkflowTelemetryContext telemetryContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.ExecutorProcess, ActivityKind.Internal);
|
||||
activity?.SetTag(Tags.ExecutorId, this.Id)
|
||||
.SetTag(Tags.ExecutorType, this.GetType().FullName)
|
||||
.SetTag(Tags.MessageType, messageType.TypeName)
|
||||
.CreateSourceLinks(context.TraceContext);
|
||||
using var activity = telemetryContext.StartExecutorProcessActivity(this.Id, this.GetType().FullName, messageType.TypeName, message);
|
||||
activity?.CreateSourceLinks(context.TraceContext);
|
||||
|
||||
await context.AddEventAsync(new ExecutorInvokedEvent(this.Id, message), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -183,6 +180,11 @@ public abstract class Executor : IIdentified
|
||||
return null; // Void result.
|
||||
}
|
||||
|
||||
// Output is not available if executor does not return anything, in which case
|
||||
// messages sent in the handlers of this executor will be set in the message
|
||||
// send activities.
|
||||
telemetryContext.SetExecutorOutput(activity, result.Result);
|
||||
|
||||
// If we had a real return type, raise it as a SendMessage; TODO: Should we have a way to disable this behaviour?
|
||||
if (result.Result is not null && this.Options.AutoSendMessageHandlerResultObject)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.InProc;
|
||||
@@ -70,6 +71,9 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
/// <inheritdoc cref="ISuperStepRunner.StartExecutorId"/>
|
||||
public string StartExecutorId { get; }
|
||||
|
||||
/// <inheritdoc cref="ISuperStepRunner.TelemetryContext"/>
|
||||
public WorkflowTelemetryContext TelemetryContext => this.Workflow.TelemetryContext;
|
||||
|
||||
private readonly HashSet<Type> _knownValidInputTypes;
|
||||
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -201,6 +205,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
envelope.Message,
|
||||
envelope.MessageType,
|
||||
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
|
||||
this.TelemetryContext,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
this.ConcurrentRunsEnabled = enableConcurrentRuns;
|
||||
this.OutgoingEvents = outgoingEvents;
|
||||
}
|
||||
public WorkflowTelemetryContext TelemetryContext => this._workflow.TelemetryContext;
|
||||
|
||||
public IExternalRequestSink RegisterPort(string executorId, RequestPort port)
|
||||
{
|
||||
@@ -195,12 +196,10 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
return this.OutgoingEvents.EnqueueAsync(workflowEvent);
|
||||
}
|
||||
|
||||
private static readonly string s_namespace = typeof(IWorkflowContext).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
public async ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.MessageSend, ActivityKind.Producer);
|
||||
using Activity? activity = this._workflow.TelemetryContext.StartMessageSendActivity(sourceId, targetId, message);
|
||||
|
||||
// Create a carrier for trace context propagation
|
||||
var traceContext = activity is null ? null : new Dictionary<string, string>();
|
||||
if (traceContext is not null)
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Microsoft.Agents.AI.Workflows.Observability;
|
||||
internal static class ActivityNames
|
||||
{
|
||||
public const string WorkflowBuild = "workflow.build";
|
||||
public const string WorkflowRun = "workflow.run";
|
||||
public const string WorkflowRun = "workflow_invoke";
|
||||
public const string MessageSend = "message.send";
|
||||
public const string ExecutorProcess = "executor.process";
|
||||
public const string EdgeGroupProcess = "edge_group.process";
|
||||
|
||||
@@ -14,7 +14,10 @@ internal static class Tags
|
||||
public const string RunId = "run.id";
|
||||
public const string ExecutorId = "executor.id";
|
||||
public const string ExecutorType = "executor.type";
|
||||
public const string ExecutorInput = "executor.input";
|
||||
public const string ExecutorOutput = "executor.output";
|
||||
public const string MessageType = "message.type";
|
||||
public const string MessageContent = "message.content";
|
||||
public const string EdgeGroupType = "edge_group.type";
|
||||
public const string MessageSourceId = "message.source_id";
|
||||
public const string MessageTargetId = "message.target_id";
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Internal context for workflow telemetry, holding the enabled state and configuration options.
|
||||
/// </summary>
|
||||
internal sealed class WorkflowTelemetryContext
|
||||
{
|
||||
private const string DefaultSourceName = "Microsoft.Agents.AI.Workflows";
|
||||
private static readonly ActivitySource s_defaultActivitySource = new(DefaultSourceName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a shared instance representing disabled telemetry.
|
||||
/// </summary>
|
||||
public static WorkflowTelemetryContext Disabled { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether telemetry is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the telemetry options.
|
||||
/// </summary>
|
||||
public WorkflowTelemetryOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the activity source used for creating telemetry spans.
|
||||
/// </summary>
|
||||
public ActivitySource ActivitySource { get; }
|
||||
|
||||
private WorkflowTelemetryContext()
|
||||
{
|
||||
this.IsEnabled = false;
|
||||
this.Options = new WorkflowTelemetryOptions();
|
||||
this.ActivitySource = s_defaultActivitySource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkflowTelemetryContext"/> class with telemetry enabled.
|
||||
/// </summary>
|
||||
/// <param name="options">The telemetry options.</param>
|
||||
/// <param name="activitySource">
|
||||
/// An optional activity source to use. If provided, this activity source will be used directly
|
||||
/// and the caller retains ownership (responsible for disposal). If <see langword="null"/>, the
|
||||
/// shared default activity source will be used.
|
||||
/// </param>
|
||||
public WorkflowTelemetryContext(WorkflowTelemetryOptions options, ActivitySource? activitySource = null)
|
||||
{
|
||||
this.IsEnabled = true;
|
||||
this.Options = options;
|
||||
this.ActivitySource = activitySource ?? s_defaultActivitySource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts an activity if telemetry is enabled, otherwise returns null.
|
||||
/// </summary>
|
||||
/// <param name="name">The activity name.</param>
|
||||
/// <param name="kind">The activity kind.</param>
|
||||
/// <returns>An activity if telemetry is enabled and the activity is sampled, otherwise null.</returns>
|
||||
public Activity? StartActivity(string name, ActivityKind kind = ActivityKind.Internal)
|
||||
{
|
||||
if (!this.IsEnabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(name, kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a workflow build activity if enabled.
|
||||
/// </summary>
|
||||
/// <returns>An activity if workflow build telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartWorkflowBuildActivity()
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableWorkflowBuild)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(ActivityNames.WorkflowBuild);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a workflow run activity if enabled.
|
||||
/// </summary>
|
||||
/// <returns>An activity if workflow run telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartWorkflowRunActivity()
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableWorkflowRun)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(ActivityNames.WorkflowRun);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts an executor process activity if enabled, with all standard tags set.
|
||||
/// </summary>
|
||||
/// <param name="executorId">The executor identifier.</param>
|
||||
/// <param name="executorType">The executor type name.</param>
|
||||
/// <param name="messageType">The message type name.</param>
|
||||
/// <param name="message">The input message. Logged only when <see cref="WorkflowTelemetryOptions.EnableSensitiveData"/> is true.</param>
|
||||
/// <returns>An activity if executor process telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartExecutorProcessActivity(string executorId, string? executorType, string messageType, object? message)
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableExecutorProcess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Activity? activity = this.ActivitySource.StartActivity(ActivityNames.ExecutorProcess + " " + executorId);
|
||||
if (activity is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
activity.SetTag(Tags.ExecutorId, executorId)
|
||||
.SetTag(Tags.ExecutorType, executorType)
|
||||
.SetTag(Tags.MessageType, messageType);
|
||||
|
||||
if (this.Options.EnableSensitiveData)
|
||||
{
|
||||
activity.SetTag(Tags.ExecutorInput, SerializeForTelemetry(message));
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the executor output tag on an activity when sensitive data logging is enabled.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set the output on.</param>
|
||||
/// <param name="output">The output value to log.</param>
|
||||
public void SetExecutorOutput(Activity? activity, object? output)
|
||||
{
|
||||
if (activity is not null && this.Options.EnableSensitiveData)
|
||||
{
|
||||
activity.SetTag(Tags.ExecutorOutput, SerializeForTelemetry(output));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts an edge group process activity if enabled.
|
||||
/// </summary>
|
||||
/// <returns>An activity if edge group process telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartEdgeGroupProcessActivity()
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableEdgeGroupProcess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a message send activity if enabled, with all standard tags set.
|
||||
/// </summary>
|
||||
/// <param name="sourceId">The source executor identifier.</param>
|
||||
/// <param name="targetId">The target executor identifier, if any.</param>
|
||||
/// <param name="message">The message being sent. Logged only when <see cref="WorkflowTelemetryOptions.EnableSensitiveData"/> is true.</param>
|
||||
/// <returns>An activity if message send telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartMessageSendActivity(string sourceId, string? targetId, object? message)
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableMessageSend)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Activity? activity = this.ActivitySource.StartActivity(ActivityNames.MessageSend, ActivityKind.Producer);
|
||||
if (activity is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
activity.SetTag(Tags.MessageSourceId, sourceId);
|
||||
if (targetId is not null)
|
||||
{
|
||||
activity.SetTag(Tags.MessageTargetId, targetId);
|
||||
}
|
||||
|
||||
if (this.Options.EnableSensitiveData)
|
||||
{
|
||||
activity.SetTag(Tags.MessageContent, SerializeForTelemetry(message));
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Telemetry serialization is optional and only used when explicitly enabled.")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Telemetry serialization is optional and only used when explicitly enabled.")]
|
||||
private static string? SerializeForTelemetry(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Serialize(value, value.GetType());
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return $"[Unserializable: {value.GetType().FullName}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for workflow telemetry.
|
||||
/// </summary>
|
||||
public sealed class WorkflowTelemetryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether potentially sensitive information should be included in telemetry.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if potentially sensitive information should be included in telemetry;
|
||||
/// <see langword="false"/> if telemetry shouldn't include raw inputs and outputs.
|
||||
/// The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// By default, telemetry includes metadata but not raw inputs and outputs,
|
||||
/// such as message content and executor data.
|
||||
/// </remarks>
|
||||
public bool EnableSensitiveData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether workflow build activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>workflow.build</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableWorkflowBuild { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether workflow run activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>workflow_invoke</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableWorkflowRun { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether executor process activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>executor.process</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableExecutorProcess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether edge group process activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>edge_group.process</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableEdgeGroupProcess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether message send activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>message.send</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableMessageSend { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for adding OpenTelemetry instrumentation to <see cref="WorkflowBuilder"/> instances.
|
||||
/// </summary>
|
||||
public static class OpenTelemetryWorkflowBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables OpenTelemetry instrumentation for the workflow, providing comprehensive observability for workflow operations.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WorkflowBuilder"/> to which OpenTelemetry support will be added.</param>
|
||||
/// <param name="configure">
|
||||
/// An optional callback that provides additional configuration of the <see cref="WorkflowTelemetryOptions"/> instance.
|
||||
/// This allows for fine-tuning telemetry behavior such as enabling sensitive data collection.
|
||||
/// </param>
|
||||
/// <param name="activitySource">
|
||||
/// An optional <see cref="ActivitySource"/> to use for telemetry. If provided, this activity source will be used
|
||||
/// directly and the caller retains ownership (responsible for disposal). If <see langword="null"/>, a shared
|
||||
/// default activity source named "Microsoft.Agents.AI.Workflows" will be used.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="WorkflowBuilder"/> with OpenTelemetry instrumentation enabled, enabling method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This extension adds comprehensive telemetry capabilities to workflows, including:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Distributed tracing of workflow execution</description></item>
|
||||
/// <item><description>Executor invocation and processing spans</description></item>
|
||||
/// <item><description>Edge routing and message delivery spans</description></item>
|
||||
/// <item><description>Workflow build and validation spans</description></item>
|
||||
/// <item><description>Error tracking and exception details</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// By default, workflow telemetry is disabled. Call this method to enable telemetry collection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var workflow = new WorkflowBuilder(startExecutor)
|
||||
/// .AddEdge(executor1, executor2)
|
||||
/// .WithOpenTelemetry(cfg => cfg.EnableSensitiveData = true)
|
||||
/// .Build();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static WorkflowBuilder WithOpenTelemetry(
|
||||
this WorkflowBuilder builder,
|
||||
Action<WorkflowTelemetryOptions>? configure = null,
|
||||
ActivitySource? activitySource = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
WorkflowTelemetryOptions options = new();
|
||||
configure?.Invoke(options);
|
||||
|
||||
WorkflowTelemetryContext context = new(options, activitySource);
|
||||
|
||||
builder.SetTelemetryContext(context);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
@@ -76,6 +77,11 @@ public class Workflow
|
||||
/// </summary>
|
||||
public string? Description { get; internal init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the telemetry context for the workflow.
|
||||
/// </summary>
|
||||
internal WorkflowTelemetryContext TelemetryContext { get; }
|
||||
|
||||
internal bool AllowConcurrent => this.ExecutorBindings.Values.All(registration => registration.SupportsConcurrentSharedExecution);
|
||||
|
||||
internal IEnumerable<string> NonConcurrentExecutorIds =>
|
||||
@@ -88,11 +94,13 @@ public class Workflow
|
||||
/// <param name="startExecutorId">The unique identifier of the starting executor for the workflow. Cannot be <c>null</c>.</param>
|
||||
/// <param name="name">Optional human-readable name for the workflow.</param>
|
||||
/// <param name="description">Optional description of what the workflow does.</param>
|
||||
internal Workflow(string startExecutorId, string? name = null, string? description = null)
|
||||
/// <param name="telemetryContext">Optional telemetry context for the workflow.</param>
|
||||
internal Workflow(string startExecutorId, string? name = null, string? description = null, WorkflowTelemetryContext? telemetryContext = null)
|
||||
{
|
||||
this.StartExecutorId = Throw.IfNull(startExecutorId);
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.TelemetryContext = telemetryContext ?? WorkflowTelemetryContext.Disabled;
|
||||
}
|
||||
|
||||
private bool _needsReset;
|
||||
|
||||
@@ -38,9 +38,7 @@ public class WorkflowBuilder
|
||||
private readonly string _startExecutorId;
|
||||
private string? _name;
|
||||
private string? _description;
|
||||
|
||||
private static readonly string s_namespace = typeof(WorkflowBuilder).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
private WorkflowTelemetryContext _telemetryContext = WorkflowTelemetryContext.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the WorkflowBuilder class with the specified starting executor.
|
||||
@@ -137,6 +135,15 @@ public class WorkflowBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the telemetry context for the workflow.
|
||||
/// </summary>
|
||||
/// <param name="context">The telemetry context to use.</param>
|
||||
internal void SetTelemetryContext(WorkflowTelemetryContext context)
|
||||
{
|
||||
this._telemetryContext = Throw.IfNull(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the specified executor (via registration) to the workflow, allowing it to participate in workflow execution.
|
||||
/// </summary>
|
||||
@@ -563,7 +570,7 @@ public class WorkflowBuilder
|
||||
|
||||
activity?.AddEvent(new ActivityEvent(EventNames.BuildValidationCompleted));
|
||||
|
||||
var workflow = new Workflow(this._startExecutorId, this._name, this._description)
|
||||
var workflow = new Workflow(this._startExecutorId, this._name, this._description, this._telemetryContext)
|
||||
{
|
||||
ExecutorBindings = this._executorBindings,
|
||||
Edges = this._edges,
|
||||
@@ -601,7 +608,7 @@ public class WorkflowBuilder
|
||||
/// or if the start executor is not bound.</exception>
|
||||
public Workflow Build(bool validateOrphans = true)
|
||||
{
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowBuild);
|
||||
using Activity? activity = this._telemetryContext.StartWorkflowBuildActivity();
|
||||
|
||||
var workflow = this.BuildInternal(validateOrphans, activity);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DeclarativeWorkflowOptions"/> telemetry configuration.
|
||||
/// </summary>
|
||||
[Collection("DeclarativeWorkflowOptionsTest")]
|
||||
public sealed class DeclarativeWorkflowOptionsTest : IDisposable
|
||||
{
|
||||
// These constants mirror Microsoft.Agents.AI.Workflows.Observability.ActivityNames
|
||||
// which is internal and not accessible from this test project.
|
||||
private const string WorkflowBuildActivityName = "workflow.build";
|
||||
private const string WorkflowRunActivityName = "workflow_invoke";
|
||||
|
||||
// The default activity source name used by the workflow telemetry context.
|
||||
private const string DefaultTelemetrySourceName = "Microsoft.Agents.AI.Workflows";
|
||||
|
||||
private const string SimpleWorkflowYaml = """
|
||||
kind: Workflow
|
||||
trigger:
|
||||
kind: OnConversationStart
|
||||
id: test_workflow
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
""";
|
||||
|
||||
private readonly ActivitySource _activitySource = new("TestSource");
|
||||
private readonly ActivityListener _activityListener;
|
||||
private readonly ConcurrentBag<Activity> _capturedActivities = [];
|
||||
|
||||
public DeclarativeWorkflowOptionsTest()
|
||||
{
|
||||
this._activityListener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = source =>
|
||||
source.Name == DefaultTelemetrySourceName ||
|
||||
source.Name == "TestSource",
|
||||
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
|
||||
ActivityStarted = activity => this._capturedActivities.Add(activity),
|
||||
};
|
||||
ActivitySource.AddActivityListener(this._activityListener);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._activityListener.Dispose();
|
||||
this._activitySource.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.ConfigureTelemetry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool callbackInvoked = false;
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(options.ConfigureTelemetry);
|
||||
WorkflowTelemetryOptions telemetryOptions = new();
|
||||
options.ConfigureTelemetry(telemetryOptions);
|
||||
Assert.True(callbackInvoked);
|
||||
Assert.True(telemetryOptions.EnableSensitiveData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Same(this._activitySource, options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithDefaultTelemetry_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("DefaultTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = _ => { },
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithTelemetryActivitySource_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("TelemetryActivitySourceTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource,
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == "TestSource")
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.All(capturedActivities, a => Assert.Equal("TestSource", a.Source.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithConfigureTelemetry_AppliesConfigurationAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("ConfigureTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool configureInvoked = false;
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
configureInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
},
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Assert.True(configureInvoked);
|
||||
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithoutTelemetry_DoesNotCreateActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("NoTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert - No workflow activities should be created when telemetry is disabled
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId &&
|
||||
(a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal) ||
|
||||
a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal)))
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(capturedActivities);
|
||||
}
|
||||
|
||||
private static Mock<WorkflowAgentProvider> CreateMockProvider()
|
||||
{
|
||||
Mock<WorkflowAgentProvider> mockAgentProvider = new(MockBehavior.Strict);
|
||||
mockAgentProvider
|
||||
.Setup(provider => provider.CreateConversationAsync(It.IsAny<CancellationToken>()))
|
||||
.Returns(() => Task.FromResult(Guid.NewGuid().ToString("N")));
|
||||
mockAgentProvider
|
||||
.Setup(provider => provider.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.FromResult(new ChatMessage(ChatRole.Assistant, "Test response")));
|
||||
return mockAgentProvider;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.Build();
|
||||
return builder.WithOpenTelemetry().Build();
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> GetExpectedActivityNameCounts() =>
|
||||
@@ -111,8 +111,6 @@ public sealed class ObservabilityTests : IDisposable
|
||||
Run run = await executionEnvironment.RunAsync(workflow, "Hello, World!");
|
||||
await run.DisposeAsync();
|
||||
|
||||
await Task.Delay(100); // Allow time for activities to be captured
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().HaveCount(8, "Exactly 8 activities should be created.");
|
||||
@@ -122,12 +120,12 @@ public sealed class ObservabilityTests : IDisposable
|
||||
{
|
||||
var activityName = kvp.Key;
|
||||
var expectedCount = kvp.Value;
|
||||
var actualCount = capturedActivities.Count(a => a.OperationName == activityName);
|
||||
var actualCount = capturedActivities.Count(a => a.OperationName.StartsWith(activityName, StringComparison.Ordinal));
|
||||
actualCount.Should().Be(expectedCount, $"Activity '{activityName}' should occur {expectedCount} times.");
|
||||
}
|
||||
|
||||
// Verify WorkflowRun activity events include workflow lifecycle events
|
||||
var workflowRunActivity = capturedActivities.First(a => a.OperationName == ActivityNames.WorkflowRun);
|
||||
var workflowRunActivity = capturedActivities.First(a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal));
|
||||
var activityEvents = workflowRunActivity.Events.ToList();
|
||||
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowStarted, "activity should have workflow started event");
|
||||
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event");
|
||||
@@ -166,8 +164,6 @@ public sealed class ObservabilityTests : IDisposable
|
||||
|
||||
// Act
|
||||
CreateWorkflow();
|
||||
await Task.Delay(100); // Allow time for activities to be captured
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().HaveCount(1, "Exactly 1 activity should be created.");
|
||||
@@ -183,4 +179,325 @@ public sealed class ObservabilityTests : IDisposable
|
||||
tags.Should().ContainKey(Tags.WorkflowId);
|
||||
tags.Should().ContainKey(Tags.WorkflowDefinition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TelemetryDisabledByDefault_CreatesNoActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
// Create a test activity to correlate captured activities
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
// Act - Build workflow WITHOUT calling WithOpenTelemetry()
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.Build(); // No WithOpenTelemetry() call
|
||||
// Assert - No activities should be created
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().BeEmpty("No activities should be created when telemetry is disabled (default).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithOpenTelemetry_UsesProvidedActivitySourceAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
using var userActivitySource = new ActivitySource("UserProvidedSource");
|
||||
|
||||
// Set up a separate listener for the user-provided source
|
||||
ConcurrentBag<Activity> userActivities = [];
|
||||
using var userListener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = source => source.Name == "UserProvidedSource",
|
||||
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
|
||||
ActivityStarted = activity => userActivities.Add(activity),
|
||||
};
|
||||
ActivitySource.AddActivityListener(userListener);
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(activitySource: userActivitySource).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = userActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotBeEmpty("Activities should be created with user-provided ActivitySource.");
|
||||
capturedActivities.Should().OnlyContain(
|
||||
a => a.Source.Name == "UserProvidedSource",
|
||||
"All activities should come from the user-provided ActivitySource.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableWorkflowBuild_PreventsWorkflowBuildActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOpenTelemetry(configure: opts => opts.DisableWorkflowBuild = true).Build();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowBuild, StringComparison.Ordinal),
|
||||
"WorkflowBuild activity should be disabled.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableWorkflowRun_PreventsWorkflowRunActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.DisableWorkflowRun = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
|
||||
"WorkflowRun activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowBuild, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableExecutorProcess_PreventsExecutorProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.DisableExecutorProcess = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"ExecutorProcess activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableEdgeGroupProcess_PreventsEdgeGroupProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
var workflow = CreateWorkflowWithDisabledEdges();
|
||||
|
||||
// Act
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.EdgeGroupProcess, StringComparison.Ordinal),
|
||||
"EdgeGroupProcess activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableMessageSend_PreventsMessageSendActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
var workflow = CreateWorkflowWithDisabledMessages();
|
||||
|
||||
// Act
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal),
|
||||
"MessageSend activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
private static Workflow CreateWorkflowWithDisabledEdges()
|
||||
{
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.WithOpenTelemetry(configure: opts => opts.DisableEdgeGroupProcess = true).Build();
|
||||
}
|
||||
|
||||
private static Workflow CreateWorkflowWithDisabledMessages()
|
||||
{
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.WithOpenTelemetry(configure: opts => opts.DisableMessageSend = true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_LogsExecutorInputAndOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.EnableSensitiveData = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var executorActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal));
|
||||
|
||||
executorActivity.Should().NotBeNull("ExecutorProcess activity should be created.");
|
||||
|
||||
var tags = executorActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().ContainKey(Tags.ExecutorInput, "Input should be logged when EnableSensitiveData is true.");
|
||||
tags.Should().ContainKey(Tags.ExecutorOutput, "Output should be logged when EnableSensitiveData is true.");
|
||||
tags[Tags.ExecutorInput].Should().Contain("hello", "Input should contain the input value.");
|
||||
tags[Tags.ExecutorOutput].Should().Contain("HELLO", "Output should contain the transformed value.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogInputOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act - EnableSensitiveData is false by default
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry().Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var executorActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal));
|
||||
|
||||
executorActivity.Should().NotBeNull("ExecutorProcess activity should be created.");
|
||||
|
||||
var tags = executorActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().NotContainKey(Tags.ExecutorInput, "Input should NOT be logged when EnableSensitiveData is false.");
|
||||
tags.Should().NotContainKey(Tags.ExecutorOutput, "Output should NOT be logged when EnableSensitiveData is false.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_LogsMessageSendContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.EnableSensitiveData = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var messageSendActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal));
|
||||
|
||||
messageSendActivity.Should().NotBeNull("MessageSend activity should be created.");
|
||||
|
||||
var tags = messageSendActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().ContainKey(Tags.MessageContent, "Message content should be logged when EnableSensitiveData is true.");
|
||||
tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should be logged.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogMessageContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
// Act - EnableSensitiveData is false by default
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
var workflow = builder.WithOpenTelemetry().Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var messageSendActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal));
|
||||
|
||||
messageSendActivity.Should().NotBeNull("MessageSend activity should be created.");
|
||||
|
||||
var tags = messageSendActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().NotContainKey(Tags.MessageContent, "Message content should NOT be logged when EnableSensitiveData is false.");
|
||||
tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should still be logged.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
@@ -133,6 +134,8 @@ public class TestRunContext : IRunnerContext
|
||||
public bool WithCheckpointing => false;
|
||||
public bool ConcurrentRunsEnabled => false;
|
||||
|
||||
WorkflowTelemetryContext IRunnerContext.TelemetryContext => WorkflowTelemetryContext.Disabled;
|
||||
|
||||
ValueTask<Executor> IRunnerContext.EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken) =>
|
||||
new(this.Executors[executorId]);
|
||||
|
||||
|
||||
@@ -1,59 +1,68 @@
|
||||
files: ^python/
|
||||
fail_fast: true
|
||||
exclude: ^scripts/
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
- repo: builtin
|
||||
hooks:
|
||||
- id: check-toml
|
||||
name: Check TOML files
|
||||
files: \.toml$
|
||||
exclude: ^python/packages/lab/cookiecutter-agent-framework-lab/
|
||||
exclude: ^packages/lab/cookiecutter-agent-framework-lab/
|
||||
- id: check-yaml
|
||||
name: Check YAML files
|
||||
files: \.yaml$
|
||||
- id: check-json
|
||||
name: Check JSON files
|
||||
files: \.json$
|
||||
exclude: ^.*\.vscode\/.*|^python/demos/samples/chatkit-integration/frontend/(tsconfig.*\.json|package-lock\.json)$
|
||||
exclude: ^.*\.vscode\/.*|^demos/samples/chatkit-integration/frontend/(tsconfig.*\.json|package-lock\.json)$
|
||||
- id: end-of-file-fixer
|
||||
name: Fix End of File
|
||||
files: \.py$
|
||||
exclude: ^python/packages/lab/cookiecutter-agent-framework-lab/
|
||||
exclude: ^packages/lab/cookiecutter-agent-framework-lab/
|
||||
- id: mixed-line-ending
|
||||
name: Check Mixed Line Endings
|
||||
files: \.py$
|
||||
exclude: ^python/packages/lab/cookiecutter-agent-framework-lab/
|
||||
exclude: ^packages/lab/cookiecutter-agent-framework-lab/
|
||||
- id: trailing-whitespace
|
||||
name: Trim Trailing Whitespace
|
||||
exclude: ^packages/lab/cookiecutter-agent-framework-lab/
|
||||
- id: check-merge-conflict
|
||||
name: Check Merge Conflicts
|
||||
- id: detect-private-key
|
||||
name: Detect Private Keys
|
||||
- id: check-added-large-files
|
||||
name: Check Added Large Files
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
name: Check Valid Python Samples
|
||||
types: ["python"]
|
||||
exclude: ^python/packages/lab/cookiecutter-agent-framework-lab/
|
||||
exclude: ^packages/lab/cookiecutter-agent-framework-lab/
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.20.0
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
name: Upgrade Python syntax
|
||||
args: [--py310-plus]
|
||||
exclude: ^python/packages/lab/cookiecutter-agent-framework-lab/
|
||||
exclude: ^packages/lab/cookiecutter-agent-framework-lab/
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: poe-check
|
||||
name: Run checks through Poe
|
||||
entry: uv --directory ./python run poe pre-commit-check
|
||||
entry: uv run poe prek-check
|
||||
language: system
|
||||
files: ^python/
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.8.5
|
||||
rev: 1.9.3
|
||||
hooks:
|
||||
- id: bandit
|
||||
name: Bandit Security Checks
|
||||
args: ["-c", "python/pyproject.toml"]
|
||||
args: ["-c", "pyproject.toml"]
|
||||
additional_dependencies: ["bandit[toml]"]
|
||||
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||
# uv version.
|
||||
rev: 0.7.18
|
||||
rev: 0.10.0
|
||||
hooks:
|
||||
# Update the uv lockfile
|
||||
- id: uv-lock
|
||||
name: Update uv lockfile
|
||||
files: python/pyproject.toml
|
||||
args: [--project, python]
|
||||
files: pyproject.toml
|
||||
|
||||
Vendored
+1
-1
@@ -9,7 +9,7 @@
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"run",
|
||||
"pre-commit",
|
||||
"prek",
|
||||
"run",
|
||||
"-a"
|
||||
],
|
||||
|
||||
+2
-15
@@ -61,22 +61,9 @@ from agent_framework.azure import AzureOpenAIChatClient, AzureAIAgentClient
|
||||
- **Comments**: Avoid excessive comments; prefer clear code
|
||||
- **Formatting**: Format only files you changed, not the entire codebase
|
||||
|
||||
## Sample Structure
|
||||
## Samples
|
||||
|
||||
1. Copyright header: `# Copyright (c) Microsoft. All rights reserved.`
|
||||
2. Required imports
|
||||
3. Module docstring: `"""This sample demonstrates..."""`
|
||||
4. Helper functions
|
||||
5. Main function(s) demonstrating functionality
|
||||
6. Entry point: `if __name__ == "__main__": asyncio.run(main())`
|
||||
|
||||
When modifying samples, update associated README files in the same or parent folders.
|
||||
|
||||
### Samples Syntax Checking
|
||||
|
||||
Run `uv run poe samples-syntax` to check samples for syntax errors and missing imports from `agent_framework`. This uses a relaxed pyright configuration that validates imports without strict type checking.
|
||||
|
||||
Some samples depend on external packages (e.g., `azure.ai.agentserver.agentframework`, `microsoft_agents`) that are not installed in the dev environment. These are excluded in `pyrightconfig.samples.json`. When adding or modifying these excluded samples, add them to the exclude list and manually verify they have no import errors from `agent_framework` packages by temporarily removing them from the exclude list and running the check.
|
||||
See [samples/SAMPLE_GUIDELINES.md](samples/SAMPLE_GUIDELINES.md) for sample structure, external dependency handling (PEP 723), and syntax checking instructions.
|
||||
|
||||
## Package Documentation
|
||||
|
||||
|
||||
@@ -264,6 +264,13 @@ After the package has been released and gained a measure of confidence:
|
||||
2. Add the package to the `[all]` extra in `packages/core/pyproject.toml`
|
||||
3. Create a provider folder in `agent_framework/` with lazy loading `__init__.py`
|
||||
|
||||
### Versioning and Core Dependency
|
||||
|
||||
All non-core packages declare a lower bound on `agent-framework-core` (e.g., `"agent-framework-core>=1.0.0b260130"`). Follow these rules when bumping versions:
|
||||
|
||||
- **Core version changes**: When `agent-framework-core` is updated with breaking or significant changes and its version is bumped, update the `agent-framework-core>=...` lower bound in every other package's `pyproject.toml` to match the new core version.
|
||||
- **Non-core version changes**: Non-core packages (connectors, extensions) can have their own versions incremented independently while keeping the existing core lower bound pinned. Only raise the core lower bound if the non-core package actually depends on new core APIs.
|
||||
|
||||
### Installation Options
|
||||
|
||||
Connectors are distributed as separate packages and are not imported by default in the core package. Users install the specific connectors they need:
|
||||
|
||||
+25
-19
@@ -64,11 +64,11 @@ uv venv --python $PYTHON_VERSION
|
||||
uv sync --dev
|
||||
# Install all the tools and dependencies
|
||||
uv run poe install
|
||||
# Install pre-commit hooks
|
||||
uv run poe pre-commit-install
|
||||
# Install prek hooks
|
||||
uv run poe prek-install
|
||||
```
|
||||
|
||||
Alternatively, you can reinstall the venv, pacakges, dependencies and pre-commit hooks with a single command (but this requires poe in the current env), this is especially useful if you want to switch python versions:
|
||||
Alternatively, you can reinstall the venv, pacakges, dependencies and prek hooks with a single command (but this requires poe in the current env), this is especially useful if you want to switch python versions:
|
||||
|
||||
```bash
|
||||
uv run poe setup -p 3.13
|
||||
@@ -144,7 +144,7 @@ To run the same checks that run during a commit and the GitHub Action `Python Co
|
||||
uv run poe check
|
||||
```
|
||||
|
||||
Ideally you should run these checks before committing any changes, when you install using the instructions above the pre-commit hooks should be installed already.
|
||||
Ideally you should run these checks before committing any changes, when you install using the instructions above the prek hooks should be installed already.
|
||||
|
||||
## Code Coverage
|
||||
|
||||
@@ -196,10 +196,10 @@ and then you can run the following tasks:
|
||||
uv sync --all-extras --dev
|
||||
```
|
||||
|
||||
After this initial setup, you can use the following tasks to manage your development environment. It is advised to use the following setup command since that also installs the pre-commit hooks.
|
||||
After this initial setup, you can use the following tasks to manage your development environment. It is advised to use the following setup command since that also installs the prek hooks.
|
||||
|
||||
#### `setup`
|
||||
Set up the development environment with a virtual environment, install dependencies and pre-commit hooks:
|
||||
Set up the development environment with a virtual environment, install dependencies and prek hooks:
|
||||
```bash
|
||||
uv run poe setup
|
||||
# or with specific Python version
|
||||
@@ -220,36 +220,36 @@ uv run poe venv
|
||||
uv run poe venv --python 3.12
|
||||
```
|
||||
|
||||
#### `pre-commit-install`
|
||||
Install pre-commit hooks:
|
||||
#### `prek-install`
|
||||
Install prek hooks:
|
||||
```bash
|
||||
uv run poe pre-commit-install
|
||||
uv run poe prek-install
|
||||
```
|
||||
|
||||
### Code Quality and Formatting
|
||||
|
||||
Each of the following tasks are designed to run against both the main `agent-framework` package and the extension packages, ensuring consistent code quality across the project.
|
||||
Each of the following tasks run against both the main `agent-framework` package and the extension packages in parallel, ensuring consistent code quality across the project.
|
||||
|
||||
#### `fmt` (format)
|
||||
Format code using ruff:
|
||||
Format code using ruff (runs in parallel across all packages):
|
||||
```bash
|
||||
uv run poe fmt
|
||||
```
|
||||
|
||||
#### `lint`
|
||||
Run linting checks and fix issues:
|
||||
Run linting checks and fix issues (runs in parallel across all packages):
|
||||
```bash
|
||||
uv run poe lint
|
||||
```
|
||||
|
||||
#### `pyright`
|
||||
Run Pyright type checking:
|
||||
Run Pyright type checking (runs in parallel across all packages):
|
||||
```bash
|
||||
uv run poe pyright
|
||||
```
|
||||
|
||||
#### `mypy`
|
||||
Run MyPy type checking:
|
||||
Run MyPy type checking (runs in parallel across all packages):
|
||||
```bash
|
||||
uv run poe mypy
|
||||
```
|
||||
@@ -270,8 +270,14 @@ uv run poe markdown-code-lint
|
||||
|
||||
### Comprehensive Checks
|
||||
|
||||
#### `check-packages`
|
||||
Run all package-level quality checks (format, lint, pyright, mypy) in parallel across all packages. This runs the full cross-product of (package × check) concurrently:
|
||||
```bash
|
||||
uv run poe check-packages
|
||||
```
|
||||
|
||||
#### `check`
|
||||
Run all quality checks (format, lint, pyright, mypy, test, markdown lint):
|
||||
Run all quality checks including package checks, samples, tests and markdown lint:
|
||||
```bash
|
||||
uv run poe check
|
||||
```
|
||||
@@ -279,7 +285,7 @@ uv run poe check
|
||||
### Testing
|
||||
|
||||
#### `test`
|
||||
Run unit tests with coverage by invoking the `test` task in each package sequentially:
|
||||
Run unit tests with coverage by invoking the `test` task in each package in parallel:
|
||||
```bash
|
||||
uv run poe test
|
||||
```
|
||||
@@ -325,10 +331,10 @@ Publish packages to PyPI:
|
||||
uv run poe publish
|
||||
```
|
||||
|
||||
## Pre-commit Hooks
|
||||
## Prek Hooks
|
||||
|
||||
Pre-commit hooks run automatically on commit and execute a subset of the checks on changed files only. You can also run all checks using pre-commit directly:
|
||||
Prek hooks run automatically on commit and execute a subset of the checks on changed files only. Package-level checks (fmt, lint, pyright) run in parallel but only for packages with changed files. Markdown and sample checks are skipped when no relevant files were changed. If the `core` package is changed, all packages are checked. You can also run all checks using prek directly:
|
||||
|
||||
```bash
|
||||
uv run pre-commit run -a
|
||||
uv run prek run -a
|
||||
```
|
||||
|
||||
+2
-2
@@ -6,5 +6,5 @@ uv venv --python $PYTHON_VERSION
|
||||
uv sync --dev
|
||||
# Install all the tools and dependencies
|
||||
uv run poe install
|
||||
# Install pre-commit hooks
|
||||
uv run poe pre-commit-install
|
||||
# Install prek hooks
|
||||
uv run poe prek-install
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
@@ -169,7 +171,7 @@ class A2AAgent(AgentTelemetryLayer, BaseAgent):
|
||||
msg = f"Invalid timeout type: {type(timeout)}. Expected float, httpx.Timeout, or None."
|
||||
raise TypeError(msg)
|
||||
|
||||
async def __aenter__(self) -> "A2AAgent":
|
||||
async def __aenter__(self) -> A2AAgent:
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"a2a-sdk>=0.3.5",
|
||||
]
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ add_agent_framework_fastapi_endpoint(app, agent, "/")
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from agent_framework import TextContent
|
||||
from agent_framework.ag_ui import AGUIChatClient
|
||||
|
||||
async def main():
|
||||
@@ -48,7 +47,7 @@ async def main():
|
||||
# Stream responses
|
||||
async for update in client.get_response("Hello!", stream=True):
|
||||
for content in update.contents:
|
||||
if isinstance(content, TextContent):
|
||||
if content.type == "text" and content.text:
|
||||
print(content.text, end="", flush=True)
|
||||
print()
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""AG-UI Chat Client implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
@@ -216,7 +218,7 @@ class AGUIChatClient(
|
||||
http_client: httpx.AsyncClient | None = None,
|
||||
timeout: float = 60.0,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
middleware: Sequence["ChatAndFunctionMiddlewareTypes"] | None = None,
|
||||
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""FastAPI endpoint creation for AG-UI agents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
from collections.abc import AsyncGenerator, Sequence
|
||||
@@ -77,7 +79,7 @@ def add_agent_framework_fastapi_endpoint(
|
||||
)
|
||||
logger.info(f"Received request at {path}: {input_data.get('run_id', 'no-run-id')}")
|
||||
|
||||
async def event_generator() -> AsyncGenerator[str, None]:
|
||||
async def event_generator() -> AsyncGenerator[str]:
|
||||
encoder = EventEncoder()
|
||||
event_count = 0
|
||||
async for event in wrapped_agent.run_agent(input_data):
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Event converter for AG-UI protocol events to Agent Framework types."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import (
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""HTTP service for AG-UI protocol communication."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import AsyncIterable
|
||||
@@ -148,7 +150,7 @@ class AGUIHttpService:
|
||||
if self._owns_client and self.http_client:
|
||||
await self.http_client.aclose()
|
||||
|
||||
async def __aenter__(self) -> "AGUIHttpService":
|
||||
async def __aenter__(self) -> AGUIHttpService:
|
||||
"""Enter async context manager."""
|
||||
return self
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Message format conversion between AG-UI and Agent Framework."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -6,6 +6,8 @@ Most orchestration helpers have been moved inline to _run.py.
|
||||
This module retains utilities that may be useful for testing or extensions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Predictive state handling utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Tool handling helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -29,7 +31,7 @@ def _collect_mcp_tool_functions(mcp_tools: list[Any]) -> list[Any]:
|
||||
return functions
|
||||
|
||||
|
||||
def collect_server_tools(agent: "SupportsAgentRun") -> list[Any]:
|
||||
def collect_server_tools(agent: SupportsAgentRun) -> list[Any]:
|
||||
"""Collect server tools from an agent.
|
||||
|
||||
This includes both regular tools from default_options and MCP tools.
|
||||
@@ -64,7 +66,7 @@ def collect_server_tools(agent: "SupportsAgentRun") -> list[Any]:
|
||||
return server_tools
|
||||
|
||||
|
||||
def register_additional_client_tools(agent: "SupportsAgentRun", client_tools: list[Any] | None) -> None:
|
||||
def register_additional_client_tools(agent: SupportsAgentRun, client_tools: list[Any] | None) -> None:
|
||||
"""Register client tools as additional declaration-only tools to avoid server execution.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Simplified AG-UI orchestration - single linear flow."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
@@ -742,8 +744,8 @@ def _build_messages_snapshot(
|
||||
async def run_agent_stream(
|
||||
input_data: dict[str, Any],
|
||||
agent: SupportsAgentRun,
|
||||
config: "AgentConfig",
|
||||
) -> "AsyncGenerator[BaseEvent, None]":
|
||||
config: AgentConfig,
|
||||
) -> AsyncGenerator[BaseEvent]:
|
||||
"""Run agent and yield AG-UI events.
|
||||
|
||||
This is the single entry point for all AG-UI agent runs. It follows a simple
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Utility functions for AG-UI integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import uuid
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Example agent demonstrating predictive state updates with document writing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
from agent_framework.ag_ui import AgentFrameworkAgent
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Recipe agent example demonstrating shared state management (Feature 3)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Task steps agent demonstrating agentic generative UI (Feature 6)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
from enum import Enum
|
||||
@@ -128,7 +130,7 @@ class TaskStepsAgentWithExecution:
|
||||
"""Delegate all other attribute access to base agent."""
|
||||
return getattr(self._base_agent, name)
|
||||
|
||||
async def run_agent(self, input_data: dict[str, Any]) -> AsyncGenerator[Any, None]:
|
||||
async def run_agent(self, input_data: dict[str, Any]) -> AsyncGenerator[Any]:
|
||||
"""Run the agent and then simulate step execution."""
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Example agent demonstrating Tool-based Generative UI (Feature 5)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Weather agent example demonstrating backend tool rendering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import ChatAgent, ChatClientProtocol, tool
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Example FastAPI server with AG-UI endpoints."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
@@ -292,7 +292,6 @@ Create a file named `client.py`:
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from agent_framework import TextContent
|
||||
from agent_framework.ag_ui import AGUIChatClient
|
||||
|
||||
|
||||
@@ -333,7 +332,7 @@ async def main():
|
||||
|
||||
# Stream text content as it arrives
|
||||
for content in update.contents:
|
||||
if isinstance(content, TextContent) and content.text:
|
||||
if content.type == "text" and content.text:
|
||||
print(content.text, end="", flush=True)
|
||||
|
||||
print() # New line after response
|
||||
|
||||
@@ -9,6 +9,8 @@ This example demonstrates advanced AGUIChatClient features including:
|
||||
- Error handling
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
@@ -18,6 +18,8 @@ This demonstrates the HYBRID pattern matching .NET AGUIClient implementation:
|
||||
This matches .NET pattern: thread maintains state, tools execute on appropriate side.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""AG-UI server example with server-side tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"ag-ui-protocol>=0.1.9",
|
||||
"fastapi>=0.115.0",
|
||||
"uvicorn>=0.30.0"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Awaitable, Mapping, MutableMapping, Sequence
|
||||
from typing import Any, ClassVar, Final, Generic, Literal, TypedDict
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"anthropic>=0.70.0,<1",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable, MutableSequence
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"azure-search-documents==11.7.0b2",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable, MutableMapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Generic, cast
|
||||
@@ -141,7 +143,7 @@ class AzureAIAgentsProvider(Generic[TOptions_co]):
|
||||
)
|
||||
self._should_close_client = True
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
@@ -177,7 +179,7 @@ class AzureAIAgentsProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Create a new agent on the Azure AI service and return a ChatAgent.
|
||||
|
||||
This method creates a persistent agent on the Azure AI service with the specified
|
||||
@@ -274,7 +276,7 @@ class AzureAIAgentsProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Retrieve an existing agent from the service and return a ChatAgent.
|
||||
|
||||
This method fetches an agent by ID from the Azure AI service
|
||||
@@ -330,7 +332,7 @@ class AzureAIAgentsProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Wrap an existing Agent SDK object as a ChatAgent without making HTTP calls.
|
||||
|
||||
Use this method when you already have an Agent object from a previous
|
||||
@@ -383,7 +385,7 @@ class AzureAIAgentsProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Create a ChatAgent from an Agent SDK object.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
@@ -346,7 +348,7 @@ class AzureAIAgentClient(
|
||||
self._should_close_client = should_close_client # Track whether we should close client connection
|
||||
self._agent_definition: Agent | None = None # Cached definition for existing agent
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
@@ -1047,9 +1049,7 @@ class AzureAIAgentClient(
|
||||
|
||||
return tool_definitions
|
||||
|
||||
def _prepare_mcp_resources(
|
||||
self, tools: Sequence["ToolProtocol | MutableMapping[str, Any]"]
|
||||
) -> list[dict[str, Any]]:
|
||||
def _prepare_mcp_resources(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Prepare MCP tool resources for approval mode configuration."""
|
||||
mcp_tools = [tool for tool in tools if isinstance(tool, HostedMCPTool)]
|
||||
if not mcp_tools:
|
||||
@@ -1142,7 +1142,7 @@ class AzureAIAgentClient(
|
||||
return additional_messages, instructions, required_action_results
|
||||
|
||||
async def _prepare_tools_for_azure_ai(
|
||||
self, tools: Sequence["ToolProtocol | MutableMapping[str, Any]"], run_options: dict[str, Any] | None = None
|
||||
self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]], run_options: dict[str, Any] | None = None
|
||||
) -> list[ToolDefinition | dict[str, Any]]:
|
||||
"""Prepare tool definitions for the Azure AI Agents API."""
|
||||
tool_definitions: list[ToolDefinition | dict[str, Any]] = []
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable, Mapping, MutableMapping, Sequence
|
||||
from typing import Any, ClassVar, Generic, TypedDict, TypeVar, cast
|
||||
@@ -295,7 +297,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[TAzureAIClientOptions], Generic[
|
||||
# Complete setup with core observability
|
||||
enable_instrumentation(enable_sensitive_data=enable_sensitive_data)
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Callable, MutableMapping, Sequence
|
||||
from typing import Any, Generic
|
||||
@@ -168,7 +170,7 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Create a new agent on the Azure AI service and return a local ChatAgent wrapper.
|
||||
|
||||
Args:
|
||||
@@ -270,7 +272,7 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Retrieve an existing agent from the Azure AI service and return a local ChatAgent wrapper.
|
||||
|
||||
You must provide either name or reference. Use `as_agent()` if you already have
|
||||
@@ -330,7 +332,7 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Wrap an SDK agent version object into a ChatAgent without making HTTP calls.
|
||||
|
||||
Use this when you already have an AgentVersionDetails from a previous API call.
|
||||
@@ -370,7 +372,7 @@ class AzureAIProjectAgentProvider(Generic[TOptions_co]):
|
||||
default_options: TOptions_co | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Create a ChatAgent from an AgentVersionDetails.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from collections.abc import Mapping, MutableMapping, Sequence
|
||||
from typing import Any, ClassVar, Literal, cast
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"azure-ai-projects >= 2.0.0b3",
|
||||
"azure-ai-agents == 1.2.0b5",
|
||||
"aiohttp",
|
||||
|
||||
@@ -6,6 +6,8 @@ This module provides the AgentFunctionApp class that integrates Microsoft Agent
|
||||
with Azure Durable Entities, enabling stateful and durable AI agent execution.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
|
||||
@@ -7,6 +7,8 @@ Using entities instead of orchestrations provides better state management and
|
||||
allows for long-running agent conversations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"agent-framework-durabletask",
|
||||
"azure-functions",
|
||||
"azure-functions-durable",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"boto3>=1.35.0,<2.0.0",
|
||||
"botocore>=1.35.0,<2.0.0",
|
||||
]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
"""Converter utilities for converting ChatKit thread items to Agent Framework messages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
|
||||
@@ -22,7 +22,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"openai-chatkit>=1.4.0,<2.0.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable, MutableMapping, Sequence
|
||||
@@ -100,13 +102,13 @@ class ClaudeAgentOptions(TypedDict, total=False):
|
||||
disallowed_tools: list[str]
|
||||
"""Blocklist of tools. Claude cannot use these tools."""
|
||||
|
||||
mcp_servers: dict[str, "McpServerConfig"]
|
||||
mcp_servers: dict[str, McpServerConfig]
|
||||
"""MCP server configurations for external tools."""
|
||||
|
||||
permission_mode: "PermissionMode"
|
||||
permission_mode: PermissionMode
|
||||
"""Permission handling mode ("default", "acceptEdits", "plan", "bypassPermissions")."""
|
||||
|
||||
can_use_tool: "CanUseTool"
|
||||
can_use_tool: CanUseTool
|
||||
"""Permission callback for tool use."""
|
||||
|
||||
max_turns: int
|
||||
@@ -115,16 +117,16 @@ class ClaudeAgentOptions(TypedDict, total=False):
|
||||
max_budget_usd: float
|
||||
"""Budget limit in USD."""
|
||||
|
||||
hooks: dict[str, list["HookMatcher"]]
|
||||
hooks: dict[str, list[HookMatcher]]
|
||||
"""Pre/post tool hooks."""
|
||||
|
||||
add_dirs: list[str | Path]
|
||||
"""Additional directories to add to context."""
|
||||
|
||||
sandbox: "SandboxSettings"
|
||||
sandbox: SandboxSettings
|
||||
"""Sandbox configuration for bash isolation."""
|
||||
|
||||
agents: dict[str, "AgentDefinition"]
|
||||
agents: dict[str, AgentDefinition]
|
||||
"""Custom agent definitions."""
|
||||
|
||||
output_format: dict[str, Any]
|
||||
@@ -133,7 +135,7 @@ class ClaudeAgentOptions(TypedDict, total=False):
|
||||
enable_file_checkpointing: bool
|
||||
"""Enable file checkpointing for rewind."""
|
||||
|
||||
betas: list["SdkBeta"]
|
||||
betas: list[SdkBeta]
|
||||
"""Beta features to enable."""
|
||||
|
||||
|
||||
@@ -328,7 +330,7 @@ class ClaudeAgent(BaseAgent, Generic[TOptions]):
|
||||
normalized = normalize_tools(tool)
|
||||
self._custom_tools.extend(normalized)
|
||||
|
||||
async def __aenter__(self) -> "ClaudeAgent[TOptions]":
|
||||
async def __aenter__(self) -> ClaudeAgent[TOptions]:
|
||||
"""Start the agent when entering async context."""
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"claude-agent-sdk>=0.1.25",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterable, Awaitable, Sequence
|
||||
from typing import Any, ClassVar, Literal, overload
|
||||
|
||||
@@ -213,7 +215,7 @@ class CopilotStudioAgent(BaseAgent):
|
||||
stream: Literal[False] = False,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "Awaitable[AgentResponse]": ...
|
||||
) -> Awaitable[AgentResponse]: ...
|
||||
|
||||
@overload
|
||||
def run(
|
||||
@@ -232,7 +234,7 @@ class CopilotStudioAgent(BaseAgent):
|
||||
stream: bool = False,
|
||||
thread: AgentThread | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]":
|
||||
) -> Awaitable[AgentResponse] | ResponseStream[AgentResponseUpdate, AgentResponse]:
|
||||
"""Get a response from the agent.
|
||||
|
||||
This method returns the final result of the agent's execution
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"agent-framework-core",
|
||||
"agent-framework-core>=1.0.0b260130",
|
||||
"microsoft-agents-copilotstudio-client>=0.3.1",
|
||||
]
|
||||
|
||||
|
||||
@@ -117,11 +117,10 @@ agent = OpenAIChatClient().as_agent(
|
||||
from agent_framework import ChatAgent, AgentMiddleware, AgentContext
|
||||
|
||||
class LoggingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next) -> AgentResponse:
|
||||
async def process(self, context: AgentContext, call_next) -> None:
|
||||
print(f"Input: {context.messages}")
|
||||
response = await next(context)
|
||||
print(f"Output: {response}")
|
||||
return response
|
||||
await call_next(context)
|
||||
print(f"Output: {context.result}")
|
||||
|
||||
agent = ChatAgent(..., middleware=[LoggingMiddleware()])
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
@@ -729,7 +731,7 @@ class RawChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
self._async_exit_stack = AsyncExitStack()
|
||||
self._update_agent_name_and_description()
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Enter the async context manager.
|
||||
|
||||
If any of the chat_client or local_mcp_tools are context managers,
|
||||
@@ -787,7 +789,7 @@ class RawChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
options: "ChatOptions[TResponseModelT]",
|
||||
options: ChatOptions[TResponseModelT],
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[TResponseModelT]]: ...
|
||||
|
||||
@@ -803,7 +805,7 @@ class RawChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
options: "TOptions_co | ChatOptions[None] | None" = None,
|
||||
options: TOptions_co | ChatOptions[None] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]]: ...
|
||||
|
||||
@@ -819,7 +821,7 @@ class RawChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
options: "TOptions_co | ChatOptions[Any] | None" = None,
|
||||
options: TOptions_co | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[AgentResponseUpdate, AgentResponse[Any]]: ...
|
||||
|
||||
@@ -834,7 +836,7 @@ class RawChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
| MutableMapping[str, Any]
|
||||
| list[ToolProtocol | Callable[..., Any] | MutableMapping[str, Any]]
|
||||
| None = None,
|
||||
options: "TOptions_co | ChatOptions[Any] | None" = None,
|
||||
options: TOptions_co | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[AgentResponse[Any]] | ResponseStream[AgentResponseUpdate, AgentResponse[Any]]:
|
||||
"""Run the agent with the given messages and options.
|
||||
@@ -1149,9 +1151,9 @@ class RawChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
server_name: str = "Agent",
|
||||
version: str | None = None,
|
||||
instructions: str | None = None,
|
||||
lifespan: Callable[["Server[Any]"], AbstractAsyncContextManager[Any]] | None = None,
|
||||
lifespan: Callable[[Server[Any]], AbstractAsyncContextManager[Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "Server[Any]":
|
||||
) -> Server[Any]:
|
||||
"""Create an MCP server from an agent instance.
|
||||
|
||||
This function automatically creates a MCP server from an agent instance, it uses the provided arguments to
|
||||
@@ -1177,7 +1179,7 @@ class RawChatAgent(BaseAgent, Generic[TOptions_co]): # type: ignore[misc]
|
||||
if kwargs:
|
||||
server_args.update(kwargs)
|
||||
|
||||
server: "Server[Any]" = Server(**server_args) # type: ignore[call-arg]
|
||||
server: Server[Any] = Server(**server_args) # type: ignore[call-arg]
|
||||
|
||||
agent_tool = self.as_tool(name=self._get_agent_name())
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import (
|
||||
@@ -137,7 +139,7 @@ class ChatClientProtocol(Protocol[TOptions_contra]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
options: "ChatOptions[TResponseModelT]",
|
||||
options: ChatOptions[TResponseModelT],
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[TResponseModelT]]: ...
|
||||
|
||||
@@ -147,7 +149,7 @@ class ChatClientProtocol(Protocol[TOptions_contra]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
options: "TOptions_contra | ChatOptions[None] | None" = None,
|
||||
options: TOptions_contra | ChatOptions[None] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@@ -157,7 +159,7 @@ class ChatClientProtocol(Protocol[TOptions_contra]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: Literal[True],
|
||||
options: "TOptions_contra | ChatOptions[Any] | None" = None,
|
||||
options: TOptions_contra | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
@@ -166,7 +168,7 @@ class ChatClientProtocol(Protocol[TOptions_contra]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: bool = False,
|
||||
options: "TOptions_contra | ChatOptions[Any] | None" = None,
|
||||
options: TOptions_contra | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
"""Send input and return the response.
|
||||
@@ -366,7 +368,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
options: "ChatOptions[TResponseModelT]",
|
||||
options: ChatOptions[TResponseModelT],
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[TResponseModelT]]: ...
|
||||
|
||||
@@ -376,7 +378,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: Literal[False] = ...,
|
||||
options: "TOptions_co | ChatOptions[None] | None" = None,
|
||||
options: TOptions_co | ChatOptions[None] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]]: ...
|
||||
|
||||
@@ -386,7 +388,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: Literal[True],
|
||||
options: "TOptions_co | ChatOptions[Any] | None" = None,
|
||||
options: TOptions_co | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ResponseStream[ChatResponseUpdate, ChatResponse[Any]]: ...
|
||||
|
||||
@@ -395,7 +397,7 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]):
|
||||
messages: str | ChatMessage | Sequence[str | ChatMessage],
|
||||
*,
|
||||
stream: bool = False,
|
||||
options: "TOptions_co | ChatOptions[Any] | None" = None,
|
||||
options: TOptions_co | ChatOptions[Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse[Any]] | ResponseStream[ChatResponseUpdate, ChatResponse[Any]]:
|
||||
"""Get a response from a chat client.
|
||||
@@ -443,10 +445,10 @@ class BaseChatClient(SerializationMixin, ABC, Generic[TOptions_co]):
|
||||
default_options: TOptions_co | Mapping[str, Any] | None = None,
|
||||
chat_message_store_factory: Callable[[], ChatMessageStoreProtocol] | None = None,
|
||||
context_provider: ContextProvider | None = None,
|
||||
middleware: Sequence["MiddlewareTypes"] | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
**kwargs: Any,
|
||||
) -> "ChatAgent[TOptions_co]":
|
||||
) -> ChatAgent[TOptions_co]:
|
||||
"""Create a ChatAgent with this client.
|
||||
|
||||
This is a convenience method that creates a ChatAgent instance with this
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import logging
|
||||
@@ -333,7 +335,7 @@ class MCPTool:
|
||||
parse_prompt_results: Literal[True] | Callable[[types.GetPromptResult], Any] | None = True,
|
||||
session: ClientSession | None = None,
|
||||
request_timeout: int | None = None,
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
chat_client: ChatClientProtocol | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the MCP Tool base.
|
||||
@@ -940,7 +942,7 @@ class MCPStdioTool(MCPTool):
|
||||
args: list[str] | None = None,
|
||||
env: dict[str, str] | None = None,
|
||||
encoding: str | None = None,
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
chat_client: ChatClientProtocol | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
@@ -1059,7 +1061,7 @@ class MCPStreamableHTTPTool(MCPTool):
|
||||
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
|
||||
allowed_tools: Collection[str] | None = None,
|
||||
terminate_on_close: bool | None = None,
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
chat_client: ChatClientProtocol | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
http_client: httpx.AsyncClient | None = None,
|
||||
**kwargs: Any,
|
||||
@@ -1173,7 +1175,7 @@ class MCPWebsocketTool(MCPTool):
|
||||
description: str | None = None,
|
||||
approval_mode: (Literal["always_require", "never_require"] | HostedMCPSpecificApproval | None) = None,
|
||||
allowed_tools: Collection[str] | None = None,
|
||||
chat_client: "ChatClientProtocol | None" = None,
|
||||
chat_client: ChatClientProtocol | None = None,
|
||||
additional_properties: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import MutableSequence, Sequence
|
||||
@@ -50,7 +52,7 @@ class Context:
|
||||
self,
|
||||
instructions: str | None = None,
|
||||
messages: Sequence[ChatMessage] | None = None,
|
||||
tools: Sequence["ToolProtocol"] | None = None,
|
||||
tools: Sequence[ToolProtocol] | None = None,
|
||||
):
|
||||
"""Create a new Context object.
|
||||
|
||||
@@ -61,7 +63,7 @@ class Context:
|
||||
"""
|
||||
self.instructions = instructions
|
||||
self.messages: Sequence[ChatMessage] = messages or []
|
||||
self.tools: Sequence["ToolProtocol"] = tools or []
|
||||
self.tools: Sequence[ToolProtocol] = tools or []
|
||||
|
||||
|
||||
# region ContextProvider
|
||||
@@ -151,7 +153,7 @@ class ContextProvider(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
async def __aenter__(self) -> "Self":
|
||||
async def __aenter__(self) -> Self:
|
||||
"""Enter the async context manager.
|
||||
|
||||
Override this method to perform any setup operations when the context provider is entered.
|
||||
|
||||
@@ -122,7 +122,7 @@ class AgentContext:
|
||||
options: The options for the agent invocation as a dict.
|
||||
stream: Whether this is a streaming invocation.
|
||||
metadata: Metadata dictionary for sharing data between agent middleware.
|
||||
result: Agent execution result. Can be observed after calling ``next()``
|
||||
result: Agent execution result. Can be observed after calling ``call_next()``
|
||||
to see the actual execution result or can be set to override the execution result.
|
||||
For non-streaming: should be AgentResponse.
|
||||
For streaming: should be ResponseStream[AgentResponseUpdate, AgentResponse].
|
||||
@@ -135,7 +135,7 @@ class AgentContext:
|
||||
|
||||
|
||||
class LoggingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next):
|
||||
async def process(self, context: AgentContext, call_next):
|
||||
print(f"Agent: {context.agent.name}")
|
||||
print(f"Messages: {len(context.messages)}")
|
||||
print(f"Thread: {context.thread}")
|
||||
@@ -145,7 +145,7 @@ class AgentContext:
|
||||
context.metadata["start_time"] = time.time()
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Access result after execution
|
||||
print(f"Result: {context.result}")
|
||||
@@ -208,7 +208,7 @@ class FunctionInvocationContext:
|
||||
function: The function being invoked.
|
||||
arguments: The validated arguments for the function.
|
||||
metadata: Metadata dictionary for sharing data between function middleware.
|
||||
result: Function execution result. Can be observed after calling ``next()``
|
||||
result: Function execution result. Can be observed after calling ``call_next()``
|
||||
to see the actual execution result or can be set to override the execution result.
|
||||
|
||||
kwargs: Additional keyword arguments passed to the chat method that invoked this function.
|
||||
@@ -220,7 +220,7 @@ class FunctionInvocationContext:
|
||||
|
||||
|
||||
class ValidationMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
async def process(self, context: FunctionInvocationContext, call_next):
|
||||
print(f"Function: {context.function.name}")
|
||||
print(f"Arguments: {context.arguments}")
|
||||
|
||||
@@ -229,7 +229,7 @@ class FunctionInvocationContext:
|
||||
raise MiddlewareTermination("Validation failed")
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -268,7 +268,7 @@ class ChatContext:
|
||||
options: The options for the chat request as a dict.
|
||||
stream: Whether this is a streaming invocation.
|
||||
metadata: Metadata dictionary for sharing data between chat middleware.
|
||||
result: Chat execution result. Can be observed after calling ``next()``
|
||||
result: Chat execution result. Can be observed after calling ``call_next()``
|
||||
to see the actual execution result or can be set to override the execution result.
|
||||
For non-streaming: should be ChatResponse.
|
||||
For streaming: should be ResponseStream[ChatResponseUpdate, ChatResponse].
|
||||
@@ -284,7 +284,7 @@ class ChatContext:
|
||||
|
||||
|
||||
class TokenCounterMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next):
|
||||
async def process(self, context: ChatContext, call_next):
|
||||
print(f"Chat client: {context.chat_client.__class__.__name__}")
|
||||
print(f"Messages: {len(context.messages)}")
|
||||
print(f"Model: {context.options.get('model_id')}")
|
||||
@@ -293,7 +293,7 @@ class ChatContext:
|
||||
context.metadata["input_tokens"] = self.count_tokens(context.messages)
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Access result and count output tokens
|
||||
if context.result:
|
||||
@@ -363,9 +363,9 @@ class AgentMiddleware(ABC):
|
||||
def __init__(self, max_retries: int = 3):
|
||||
self.max_retries = max_retries
|
||||
|
||||
async def process(self, context: AgentContext, next):
|
||||
async def process(self, context: AgentContext, call_next):
|
||||
for attempt in range(self.max_retries):
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
if context.result and not context.result.is_error:
|
||||
break
|
||||
print(f"Retry {attempt + 1}/{self.max_retries}")
|
||||
@@ -379,7 +379,7 @@ class AgentMiddleware(ABC):
|
||||
async def process(
|
||||
self,
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Process an agent invocation.
|
||||
|
||||
@@ -387,16 +387,16 @@ class AgentMiddleware(ABC):
|
||||
context: Agent invocation context containing agent, messages, and metadata.
|
||||
Use context.stream to determine if this is a streaming call.
|
||||
MiddlewareTypes can set context.result to override execution, or observe
|
||||
the actual execution result after calling next().
|
||||
the actual execution result after calling call_next().
|
||||
For non-streaming: AgentResponse
|
||||
For streaming: AsyncIterable[AgentResponseUpdate]
|
||||
next: Function to call the next middleware or final agent execution.
|
||||
call_next: Function to call the next middleware or final agent execution.
|
||||
Does not return anything - all data flows through the context.
|
||||
|
||||
Note:
|
||||
MiddlewareTypes should not return anything. All data manipulation should happen
|
||||
within the context object. Set context.result to override execution,
|
||||
or observe context.result after calling next() for actual results.
|
||||
or observe context.result after calling call_next() for actual results.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -422,7 +422,7 @@ class FunctionMiddleware(ABC):
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
async def process(self, context: FunctionInvocationContext, call_next):
|
||||
cache_key = f"{context.function.name}:{context.arguments}"
|
||||
|
||||
# Check cache
|
||||
@@ -431,7 +431,7 @@ class FunctionMiddleware(ABC):
|
||||
raise MiddlewareTermination()
|
||||
|
||||
# Execute function
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Cache result
|
||||
if context.result:
|
||||
@@ -446,21 +446,21 @@ class FunctionMiddleware(ABC):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Process a function invocation.
|
||||
|
||||
Args:
|
||||
context: Function invocation context containing function, arguments, and metadata.
|
||||
MiddlewareTypes can set context.result to override execution, or observe
|
||||
the actual execution result after calling next().
|
||||
next: Function to call the next middleware or final function execution.
|
||||
the actual execution result after calling call_next().
|
||||
call_next: Function to call the next middleware or final function execution.
|
||||
Does not return anything - all data flows through the context.
|
||||
|
||||
Note:
|
||||
MiddlewareTypes should not return anything. All data manipulation should happen
|
||||
within the context object. Set context.result to override execution,
|
||||
or observe context.result after calling next() for actual results.
|
||||
or observe context.result after calling call_next() for actual results.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -486,14 +486,14 @@ class ChatMiddleware(ABC):
|
||||
def __init__(self, system_prompt: str):
|
||||
self.system_prompt = system_prompt
|
||||
|
||||
async def process(self, context: ChatContext, next):
|
||||
async def process(self, context: ChatContext, call_next):
|
||||
# Add system prompt to messages
|
||||
from agent_framework import ChatMessage
|
||||
|
||||
context.messages.insert(0, ChatMessage(role="system", text=self.system_prompt))
|
||||
|
||||
# Continue execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
# Use with an agent
|
||||
@@ -508,7 +508,7 @@ class ChatMiddleware(ABC):
|
||||
async def process(
|
||||
self,
|
||||
context: ChatContext,
|
||||
next: Callable[[ChatContext], Awaitable[None]],
|
||||
call_next: Callable[[ChatContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Process a chat client request.
|
||||
|
||||
@@ -516,16 +516,16 @@ class ChatMiddleware(ABC):
|
||||
context: Chat invocation context containing chat client, messages, options, and metadata.
|
||||
Use context.stream to determine if this is a streaming call.
|
||||
MiddlewareTypes can set context.result to override execution, or observe
|
||||
the actual execution result after calling next().
|
||||
the actual execution result after calling call_next().
|
||||
For non-streaming: ChatResponse
|
||||
For streaming: ResponseStream[ChatResponseUpdate, ChatResponse]
|
||||
next: Function to call the next middleware or final chat execution.
|
||||
call_next: Function to call the next middleware or final chat execution.
|
||||
Does not return anything - all data flows through the context.
|
||||
|
||||
Note:
|
||||
MiddlewareTypes should not return anything. All data manipulation should happen
|
||||
within the context object. Set context.result to override execution,
|
||||
or observe context.result after calling next() for actual results.
|
||||
or observe context.result after calling call_next() for actual results.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -576,9 +576,9 @@ def agent_middleware(func: AgentMiddlewareCallable) -> AgentMiddlewareCallable:
|
||||
|
||||
|
||||
@agent_middleware
|
||||
async def logging_middleware(context: AgentContext, next):
|
||||
async def logging_middleware(context: AgentContext, call_next):
|
||||
print(f"Before: {context.agent.name}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print(f"After: {context.result}")
|
||||
|
||||
|
||||
@@ -609,9 +609,9 @@ def function_middleware(func: FunctionMiddlewareCallable) -> FunctionMiddlewareC
|
||||
|
||||
|
||||
@function_middleware
|
||||
async def logging_middleware(context: FunctionInvocationContext, next):
|
||||
async def logging_middleware(context: FunctionInvocationContext, call_next):
|
||||
print(f"Calling: {context.function.name}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print(f"Result: {context.result}")
|
||||
|
||||
|
||||
@@ -642,9 +642,9 @@ def chat_middleware(func: ChatMiddlewareCallable) -> ChatMiddlewareCallable:
|
||||
|
||||
|
||||
@chat_middleware
|
||||
async def logging_middleware(context: ChatContext, next):
|
||||
async def logging_middleware(context: ChatContext, call_next):
|
||||
print(f"Messages: {len(context.messages)}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print(f"Response: {context.result}")
|
||||
|
||||
|
||||
@@ -669,8 +669,8 @@ class MiddlewareWrapper(Generic[TContext]):
|
||||
def __init__(self, func: Callable[[TContext, Callable[[TContext], Awaitable[None]]], Awaitable[None]]) -> None:
|
||||
self.func = func
|
||||
|
||||
async def process(self, context: TContext, next: Callable[[TContext], Awaitable[None]]) -> None:
|
||||
await self.func(context, next)
|
||||
async def process(self, context: TContext, call_next: Callable[[TContext], Awaitable[None]]) -> None:
|
||||
await self.func(context, call_next)
|
||||
|
||||
|
||||
class BaseMiddlewarePipeline(ABC):
|
||||
@@ -1226,7 +1226,7 @@ def _determine_middleware_type(middleware: Any) -> MiddlewareType:
|
||||
sig = inspect.signature(middleware)
|
||||
params = list(sig.parameters.values())
|
||||
|
||||
# Must have at least 2 parameters (context and next)
|
||||
# Must have at least 2 parameters (context and call_next)
|
||||
if len(params) >= 2:
|
||||
first_param = params[0]
|
||||
if hasattr(first_param.annotation, "__name__"):
|
||||
@@ -1240,7 +1240,7 @@ def _determine_middleware_type(middleware: Any) -> MiddlewareType:
|
||||
else:
|
||||
# Not enough parameters - can't be valid middleware
|
||||
raise MiddlewareException(
|
||||
f"MiddlewareTypes function must have at least 2 parameters (context, next), "
|
||||
f"Middleware function must have at least 2 parameters (context, call_next), "
|
||||
f"but {middleware.__name__} has {len(params)}"
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any, ClassVar, TypeVar
|
||||
|
||||
from pydantic import Field, UrlConstraints
|
||||
@@ -48,7 +50,7 @@ class AFBaseSettings(BaseSettings):
|
||||
kwargs = {k: v for k, v in kwargs.items() if v is not None}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def __new__(cls: type["TSettings"], *args: Any, **kwargs: Any) -> "TSettings":
|
||||
def __new__(cls: type[TSettings], *args: Any, **kwargs: Any) -> TSettings:
|
||||
"""Override the __new__ method to set the env_prefix."""
|
||||
# for both, if supplied but None, set to default
|
||||
if "env_file_encoding" in kwargs and kwargs["env_file_encoding"] is not None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Mapping, MutableMapping
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Final
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import MutableMapping, Sequence
|
||||
from typing import Any, Protocol, TypeVar
|
||||
|
||||
@@ -74,7 +76,7 @@ class ChatMessageStoreProtocol(Protocol):
|
||||
@classmethod
|
||||
async def deserialize(
|
||||
cls, serialized_store_state: MutableMapping[str, Any], **kwargs: Any
|
||||
) -> "ChatMessageStoreProtocol":
|
||||
) -> ChatMessageStoreProtocol:
|
||||
"""Creates a new instance of the store from previously serialized state.
|
||||
|
||||
This method, together with ``serialize()`` can be used to save and load messages from a persistent store
|
||||
|
||||
@@ -1239,6 +1239,7 @@ def tool(
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
schema: type[BaseModel] | Mapping[str, Any] | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | None = None,
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
@@ -1252,6 +1253,7 @@ def tool(
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
schema: type[BaseModel] | Mapping[str, Any] | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | None = None,
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
@@ -1264,6 +1266,7 @@ def tool(
|
||||
*,
|
||||
name: str | None = None,
|
||||
description: str | None = None,
|
||||
schema: type[BaseModel] | Mapping[str, Any] | None = None,
|
||||
approval_mode: Literal["always_require", "never_require"] | None = None,
|
||||
max_invocations: int | None = None,
|
||||
max_invocation_exceptions: int | None = None,
|
||||
@@ -1279,6 +1282,9 @@ def tool(
|
||||
with a string description as the second argument. You can also use Pydantic's
|
||||
``Field`` class for more advanced configuration.
|
||||
|
||||
Alternatively, you can provide an explicit schema via the ``schema`` parameter
|
||||
to bypass automatic inference from the function signature.
|
||||
|
||||
Args:
|
||||
func: The function to decorate.
|
||||
|
||||
@@ -1287,6 +1293,13 @@ def tool(
|
||||
attribute will be used.
|
||||
description: A description of the function. If not provided, the function's
|
||||
docstring will be used.
|
||||
schema: An explicit input schema for the function. This can be a Pydantic
|
||||
``BaseModel`` subclass or a JSON schema dictionary (``Mapping[str, Any]``).
|
||||
When a dictionary is provided, it must be a flat object schema with a
|
||||
``properties`` key (complex JSON Schema features such as ``oneOf``,
|
||||
``$ref``, or nested compositions are not supported).
|
||||
When provided, the schema is used instead of inferring one from the
|
||||
function's signature. Defaults to ``None`` (infer from signature).
|
||||
approval_mode: Whether or not approval is required to run this tool.
|
||||
Default is that approval is required.
|
||||
max_invocations: The maximum number of times this function can be invoked.
|
||||
@@ -1341,6 +1354,21 @@ def tool(
|
||||
# Simulate async operation
|
||||
return f"Weather in {location}"
|
||||
|
||||
|
||||
# With an explicit Pydantic model schema
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class WeatherInput(BaseModel):
|
||||
location: Annotated[str, Field(description="City name")]
|
||||
unit: str = "celsius"
|
||||
|
||||
|
||||
@tool(schema=WeatherInput)
|
||||
def get_weather(location: str, unit: str = "celsius") -> str:
|
||||
'''Get weather for a location.'''
|
||||
return f"Weather in {location}: 22 {unit}"
|
||||
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., ReturnT | Awaitable[ReturnT]]) -> FunctionTool[Any, ReturnT]:
|
||||
@@ -1356,6 +1384,7 @@ def tool(
|
||||
max_invocation_exceptions=max_invocation_exceptions,
|
||||
additional_properties=additional_properties or {},
|
||||
func=f,
|
||||
input_model=schema,
|
||||
)
|
||||
|
||||
return wrapper(func)
|
||||
@@ -1674,7 +1703,14 @@ async def _try_execute_function_calls(
|
||||
)
|
||||
if declaration_only_flag:
|
||||
# return the declaration only tools to the user, since we cannot execute them.
|
||||
return ([fcc for fcc in function_calls if fcc.type == "function_call"], False)
|
||||
# Mark as user_input_request so AgentExecutor emits request_info events and pauses the workflow.
|
||||
declaration_only_calls = []
|
||||
for fcc in function_calls:
|
||||
if fcc.type == "function_call":
|
||||
fcc.user_input_request = True
|
||||
fcc.id = fcc.call_id
|
||||
declaration_only_calls.append(fcc)
|
||||
return (declaration_only_calls, False)
|
||||
|
||||
# Run all function calls concurrently, handling MiddlewareTermination
|
||||
from ._middleware import MiddlewareTermination
|
||||
@@ -1915,10 +1951,14 @@ def _handle_function_call_results(
|
||||
from ._types import ChatMessage
|
||||
|
||||
if any(fccr.type in {"function_approval_request", "function_call"} for fccr in function_call_results):
|
||||
if response.messages and response.messages[0].role == "assistant":
|
||||
response.messages[0].contents.extend(function_call_results)
|
||||
else:
|
||||
response.messages.append(ChatMessage(role="assistant", contents=function_call_results))
|
||||
# Only add items that aren't already in the message (e.g. function_approval_request wrappers).
|
||||
# Declaration-only function_call items are already present from the LLM response.
|
||||
new_items = [fccr for fccr in function_call_results if fccr.type != "function_call"]
|
||||
if new_items:
|
||||
if response.messages and response.messages[0].role == "assistant":
|
||||
response.messages[0].contents.extend(new_items)
|
||||
else:
|
||||
response.messages.append(ChatMessage(role="assistant", contents=new_items))
|
||||
return {
|
||||
"action": "return",
|
||||
"errors_in_a_row": errors_in_a_row,
|
||||
|
||||
@@ -194,8 +194,11 @@ class AgentExecutor(Executor):
|
||||
self._pending_agent_requests.pop(original_request.id, None) # type: ignore[arg-type]
|
||||
|
||||
if not self._pending_agent_requests:
|
||||
# All pending requests have been resolved; resume agent execution
|
||||
self._cache = normalize_messages_input(ChatMessage(role="user", contents=self._pending_responses_to_agent))
|
||||
# All pending requests have been resolved; resume agent execution.
|
||||
# Use role="tool" for function_result responses (from declaration-only tools)
|
||||
# so the LLM receives proper tool results instead of orphaned tool_calls.
|
||||
role = "tool" if all(r.type == "function_result" for r in self._pending_responses_to_agent) else "user"
|
||||
self._cache = normalize_messages_input(ChatMessage(role=role, contents=self._pending_responses_to_agent))
|
||||
self._pending_responses_to_agent.clear()
|
||||
await self._run_agent_and_emit(ctx)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Generic
|
||||
@@ -63,7 +65,7 @@ class AzureOpenAIAssistantsClient(
|
||||
ad_token: str | None = None,
|
||||
ad_token_provider: AsyncAzureADTokenProvider | None = None,
|
||||
token_endpoint: str | None = None,
|
||||
credential: "TokenCredential | None" = None,
|
||||
credential: TokenCredential | None = None,
|
||||
default_headers: Mapping[str, str] | None = None,
|
||||
async_client: AsyncAzureOpenAI | None = None,
|
||||
env_file_path: str | None = None,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
@@ -175,7 +177,7 @@ class AzureOpenAIChatClient( # type: ignore[misc]
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
instruction_role: str | None = None,
|
||||
middleware: Sequence["MiddlewareTypes"] | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -15,7 +17,7 @@ logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_entra_auth_token(
|
||||
credential: "TokenCredential",
|
||||
credential: TokenCredential,
|
||||
token_endpoint: str,
|
||||
**kwargs: Any,
|
||||
) -> str | None:
|
||||
@@ -49,7 +51,7 @@ def get_entra_auth_token(
|
||||
|
||||
|
||||
async def get_entra_auth_token_async(
|
||||
credential: "AsyncTokenCredential", token_endpoint: str, **kwargs: Any
|
||||
credential: AsyncTokenCredential, token_endpoint: str, **kwargs: Any
|
||||
) -> str | None:
|
||||
"""Retrieve a async Microsoft Entra Auth Token for a given token endpoint.
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Generic
|
||||
@@ -74,7 +76,7 @@ class AzureOpenAIResponsesClient( # type: ignore[misc]
|
||||
env_file_path: str | None = None,
|
||||
env_file_encoding: str | None = None,
|
||||
instruction_role: str | None = None,
|
||||
middleware: Sequence["MiddlewareTypes"] | None = None,
|
||||
middleware: Sequence[MiddlewareTypes] | None = None,
|
||||
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable, Mapping
|
||||
@@ -110,7 +112,7 @@ class AzureOpenAISettings(AFBaseSettings):
|
||||
default_token_endpoint: str = DEFAULT_AZURE_TOKEN_ENDPOINT
|
||||
|
||||
def get_azure_auth_token(
|
||||
self, credential: "TokenCredential", token_endpoint: str | None = None, **kwargs: Any
|
||||
self, credential: TokenCredential, token_endpoint: str | None = None, **kwargs: Any
|
||||
) -> str | None:
|
||||
"""Retrieve a Microsoft Entra Auth Token for a given token endpoint for the use with Azure OpenAI.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user