Merge branch 'features/3768-devui-aspire-integration' of github.com:tommasodotNET/agent-framework into features/3768-devui-aspire-integration

This commit is contained in:
tommasodotnet
2026-02-10 12:53:32 +01:00
Unverified
242 changed files with 6209 additions and 1604 deletions
+99 -9
View File
@@ -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
+3 -3
View File
@@ -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";
@@ -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;
}
@@ -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>
@@ -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]);
+26 -17
View File
@@ -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
+1 -1
View File
@@ -9,7 +9,7 @@
"command": "uv",
"args": [
"run",
"pre-commit",
"prek",
"run",
"-a"
],
+2 -15
View File
@@ -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
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core",
"agent-framework-core>=1.0.0b260130",
"a2a-sdk>=0.3.5",
]
+1 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core",
"agent-framework-core>=1.0.0b260130",
"microsoft-agents-copilotstudio-client>=0.3.1",
]
+3 -4
View File
@@ -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
+6 -4
View File
@@ -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
+45 -5
View File
@@ -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