mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Merge branch 'main' into feature-session-statebag
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@
|
||||
<PropertyGroup>
|
||||
<!-- Central version prefix - applies to all nuget packages. -->
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260205.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260205.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260205.1</GitTag>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix).260209.1</PackageVersion>
|
||||
<PackageVersion Condition="'$(VersionSuffix)' == ''">$(VersionPrefix)-preview.260209.1</PackageVersion>
|
||||
<GitTag>1.0.0-preview.260209.1</GitTag>
|
||||
|
||||
<Configurations>Debug;Release;Publish</Configurations>
|
||||
<IsPackable>true</IsPackable>
|
||||
|
||||
@@ -6,7 +6,6 @@ using Azure.AI.Projects;
|
||||
using Azure.AI.Projects.OpenAI;
|
||||
using Azure.Identity;
|
||||
using Microsoft.Agents.AI;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
string endpoint = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("AZURE_FOUNDRY_PROJECT_ENDPOINT is not set.");
|
||||
string deploymentName = Environment.GetEnvironmentVariable("AZURE_FOUNDRY_PROJECT_DEPLOYMENT_NAME") ?? "gpt-4o-mini";
|
||||
|
||||
+7
-1
@@ -35,8 +35,10 @@ public static class Program
|
||||
|
||||
using var traceProvider = Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(resourceBuilder)
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddSource(SourceName)
|
||||
// The following source is only required if not specifying
|
||||
// the `activitySource` in the WithOpenTelemetry call below
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddAzureMonitorTraceExporter(options => options.ConnectionString = applicationInsightsConnectionString)
|
||||
.Build();
|
||||
|
||||
@@ -51,6 +53,10 @@ public static class Program
|
||||
// Build the workflow by connecting executors sequentially
|
||||
var workflow = new WorkflowBuilder(uppercase)
|
||||
.AddEdge(uppercase, reverse)
|
||||
.WithOpenTelemetry(
|
||||
// Set `EnableSensitiveData` to true to include message content in traces
|
||||
configure: cfg => cfg.EnableSensitiveData = true,
|
||||
activitySource: s_activitySource)
|
||||
.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
|
||||
@@ -37,8 +37,10 @@ public static class Program
|
||||
|
||||
using var traceProvider = Sdk.CreateTracerProviderBuilder()
|
||||
.SetResourceBuilder(resourceBuilder)
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddSource(SourceName)
|
||||
// The following source is only required if not specifying
|
||||
// the `activitySource` in the WithOpenTelemetry call below
|
||||
.AddSource("Microsoft.Agents.AI.Workflows*")
|
||||
.AddOtlpExporter(options => options.Endpoint = new Uri(otlpEndpoint))
|
||||
.Build();
|
||||
|
||||
@@ -53,6 +55,10 @@ public static class Program
|
||||
// Build the workflow by connecting executors sequentially
|
||||
var workflow = new WorkflowBuilder(uppercase)
|
||||
.AddEdge(uppercase, reverse)
|
||||
.WithOpenTelemetry(
|
||||
// Set `EnableSensitiveData` to true to include message content in traces
|
||||
configure: cfg => cfg.EnableSensitiveData = true,
|
||||
activitySource: s_activitySource)
|
||||
.Build();
|
||||
|
||||
// Execute the workflow with input data
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
@@ -41,4 +44,23 @@ public sealed class DeclarativeWorkflowOptions(WorkflowAgentProvider agentProvid
|
||||
/// Gets the <see cref="ILoggerFactory"/> used to create loggers for workflow components.
|
||||
/// </summary>
|
||||
public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the callback to configure telemetry options.
|
||||
/// </summary>
|
||||
public Action<WorkflowTelemetryOptions>? ConfigureTelemetry { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional <see cref="ActivitySource"/> for telemetry.
|
||||
/// If provided, the caller retains ownership and is responsible for disposal.
|
||||
/// If <see langword="null"/> but <see cref="ConfigureTelemetry"/> is set, a shared default
|
||||
/// activity source named "Microsoft.Agents.AI.Workflows" will be used.
|
||||
/// </summary>
|
||||
public ActivitySource? TelemetryActivitySource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether telemetry is enabled.
|
||||
/// Telemetry is enabled when either <see cref="ConfigureTelemetry"/> or <see cref="TelemetryActivitySource"/> is set.
|
||||
/// </summary>
|
||||
internal bool IsTelemetryEnabled => this.ConfigureTelemetry is not null || this.TelemetryActivitySource is not null;
|
||||
}
|
||||
|
||||
+8
@@ -51,6 +51,14 @@ internal sealed class WorkflowActionVisitor : DialogActionVisitor
|
||||
|
||||
this._workflowModel.Build(builder);
|
||||
|
||||
// Apply telemetry if configured
|
||||
if (this._workflowOptions.IsTelemetryEnabled)
|
||||
{
|
||||
builder.WorkflowBuilder.WithOpenTelemetry(
|
||||
this._workflowOptions.ConfigureTelemetry,
|
||||
this._workflowOptions.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
// Build final workflow
|
||||
return builder.WorkflowBuilder.Build(validateOrphans: false);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ internal sealed class DirectEdgeRunner(IRunnerContext runContext, DirectEdgeData
|
||||
|
||||
protected internal override async ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer)
|
||||
{
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(DirectEdgeRunner))
|
||||
.SetTag(Tags.MessageSourceId, this.EdgeData.SourceId)
|
||||
|
||||
@@ -14,9 +14,6 @@ internal interface IStatefulEdgeRunner
|
||||
|
||||
internal abstract class EdgeRunner
|
||||
{
|
||||
protected static readonly string s_namespace = typeof(EdgeRunner).Namespace!;
|
||||
protected static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
// TODO: Can this be sync?
|
||||
protected internal abstract ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer);
|
||||
}
|
||||
@@ -26,4 +23,6 @@ internal abstract class EdgeRunner<TEdgeData>(
|
||||
{
|
||||
protected IRunnerContext RunContext { get; } = Throw.IfNull(runContext);
|
||||
protected TEdgeData EdgeData { get; } = Throw.IfNull(edgeData);
|
||||
|
||||
protected Activity? StartActivity() => this.RunContext.TelemetryContext.StartEdgeGroupProcessActivity();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ internal sealed class FanInEdgeRunner(IRunnerContext runContext, FanInEdgeData e
|
||||
{
|
||||
Debug.Assert(!envelope.IsExternal, "FanIn edges should never be chased from external input");
|
||||
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(FanInEdgeRunner))
|
||||
.SetTag(Tags.MessageTargetId, this.EdgeData.SinkId);
|
||||
|
||||
@@ -13,7 +13,7 @@ internal sealed class FanOutEdgeRunner(IRunnerContext runContext, FanOutEdgeData
|
||||
{
|
||||
protected internal override async ValueTask<DeliveryMapping?> ChaseEdgeAsync(MessageEnvelope envelope, IStepTracer? stepTracer)
|
||||
{
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(FanOutEdgeRunner))
|
||||
.SetTag(Tags.MessageSourceId, this.EdgeData.SourceId);
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal interface IRunnerContext : IExternalRequestSink, ISuperStepJoinContext
|
||||
{
|
||||
WorkflowTelemetryContext TelemetryContext { get; }
|
||||
|
||||
ValueTask AddEventAsync(WorkflowEvent workflowEvent, CancellationToken cancellationToken = default);
|
||||
ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
@@ -12,6 +13,8 @@ internal interface ISuperStepRunner
|
||||
|
||||
string StartExecutorId { get; }
|
||||
|
||||
WorkflowTelemetryContext TelemetryContext { get; }
|
||||
|
||||
bool HasUnservicedRequests { get; }
|
||||
bool HasUnprocessedMessages { get; }
|
||||
|
||||
|
||||
@@ -13,9 +13,6 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
|
||||
internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
{
|
||||
private static readonly string s_namespace = typeof(LockstepRunEventStream).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
private readonly CancellationTokenSource _stopCancellation = new();
|
||||
private readonly InputWaiter _inputWaiter = new();
|
||||
private int _isDisposed;
|
||||
@@ -53,7 +50,7 @@ internal sealed class LockstepRunEventStream : IRunEventStream
|
||||
|
||||
this._stepRunner.OutgoingEvents.EventRaised += OnWorkflowEventAsync;
|
||||
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowRun);
|
||||
using Activity? activity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
|
||||
activity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.RunId, this._stepRunner.RunId);
|
||||
|
||||
try
|
||||
|
||||
@@ -25,7 +25,7 @@ internal sealed class ResponseEdgeRunner(IRunnerContext runContext, string execu
|
||||
{
|
||||
Debug.Assert(envelope.IsExternal, "Input edges should only be chased from external input");
|
||||
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
using var activity = this.StartActivity();
|
||||
activity?
|
||||
.SetTag(Tags.EdgeGroupType, nameof(ResponseEdgeRunner))
|
||||
.SetTag(Tags.MessageSourceId, envelope.SourceId)
|
||||
|
||||
@@ -17,9 +17,6 @@ namespace Microsoft.Agents.AI.Workflows.Execution;
|
||||
/// </summary>
|
||||
internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
{
|
||||
private static readonly string s_namespace = typeof(StreamingRunEventStream).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
private readonly Channel<WorkflowEvent> _eventChannel;
|
||||
private readonly ISuperStepRunner _stepRunner;
|
||||
private readonly InputWaiter _inputWaiter;
|
||||
@@ -63,7 +60,7 @@ internal sealed class StreamingRunEventStream : IRunEventStream
|
||||
// Subscribe to events - they will flow directly to the channel as they're raised
|
||||
this._stepRunner.OutgoingEvents.EventRaised += OnEventRaisedAsync;
|
||||
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowRun);
|
||||
using Activity? activity = this._stepRunner.TelemetryContext.StartWorkflowRunActivity();
|
||||
activity?.SetTag(Tags.WorkflowId, this._stepRunner.StartExecutorId).SetTag(Tags.RunId, this._stepRunner.RunId);
|
||||
|
||||
try
|
||||
|
||||
@@ -26,9 +26,6 @@ public abstract class Executor : IIdentified
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
|
||||
private static readonly string s_namespace = typeof(Executor).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
// TODO: Add overloads for binding with a configuration/options object once the Configured<T> hierarchy goes away.
|
||||
|
||||
/// <summary>
|
||||
@@ -142,13 +139,13 @@ public abstract class Executor : IIdentified
|
||||
/// <returns>A ValueTask representing the asynchronous operation, wrapping the output from the executor.</returns>
|
||||
/// <exception cref="NotSupportedException">No handler found for the message type.</exception>
|
||||
/// <exception cref="TargetInvocationException">An exception is generated while handling the message.</exception>
|
||||
public async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
public ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
=> this.ExecuteAsync(message, messageType, context, WorkflowTelemetryContext.Disabled, cancellationToken);
|
||||
|
||||
internal async ValueTask<object?> ExecuteAsync(object message, TypeId messageType, IWorkflowContext context, WorkflowTelemetryContext telemetryContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var activity = s_activitySource.StartActivity(ActivityNames.ExecutorProcess, ActivityKind.Internal);
|
||||
activity?.SetTag(Tags.ExecutorId, this.Id)
|
||||
.SetTag(Tags.ExecutorType, this.GetType().FullName)
|
||||
.SetTag(Tags.MessageType, messageType.TypeName)
|
||||
.CreateSourceLinks(context.TraceContext);
|
||||
using var activity = telemetryContext.StartExecutorProcessActivity(this.Id, this.GetType().FullName, messageType.TypeName, message);
|
||||
activity?.CreateSourceLinks(context.TraceContext);
|
||||
|
||||
await context.AddEventAsync(new ExecutorInvokedEvent(this.Id, message), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -183,6 +180,11 @@ public abstract class Executor : IIdentified
|
||||
return null; // Void result.
|
||||
}
|
||||
|
||||
// Output is not available if executor does not return anything, in which case
|
||||
// messages sent in the handlers of this executor will be set in the message
|
||||
// send activities.
|
||||
telemetryContext.SetExecutorOutput(activity, result.Result);
|
||||
|
||||
// If we had a real return type, raise it as a SendMessage; TODO: Should we have a way to disable this behaviour?
|
||||
if (result.Result is not null && this.Options.AutoSendMessageHandlerResultObject)
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.InProc;
|
||||
@@ -70,6 +71,9 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
/// <inheritdoc cref="ISuperStepRunner.StartExecutorId"/>
|
||||
public string StartExecutorId { get; }
|
||||
|
||||
/// <inheritdoc cref="ISuperStepRunner.TelemetryContext"/>
|
||||
public WorkflowTelemetryContext TelemetryContext => this.Workflow.TelemetryContext;
|
||||
|
||||
private readonly HashSet<Type> _knownValidInputTypes;
|
||||
public async ValueTask<bool> IsValidInputTypeAsync(Type messageType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -201,6 +205,7 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
|
||||
envelope.Message,
|
||||
envelope.MessageType,
|
||||
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
|
||||
this.TelemetryContext,
|
||||
cancellationToken
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
this.ConcurrentRunsEnabled = enableConcurrentRuns;
|
||||
this.OutgoingEvents = outgoingEvents;
|
||||
}
|
||||
public WorkflowTelemetryContext TelemetryContext => this._workflow.TelemetryContext;
|
||||
|
||||
public IExternalRequestSink RegisterPort(string executorId, RequestPort port)
|
||||
{
|
||||
@@ -195,12 +196,10 @@ internal sealed class InProcessRunnerContext : IRunnerContext
|
||||
return this.OutgoingEvents.EnqueueAsync(workflowEvent);
|
||||
}
|
||||
|
||||
private static readonly string s_namespace = typeof(IWorkflowContext).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
|
||||
public async ValueTask SendMessageAsync(string sourceId, object message, string? targetId = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.MessageSend, ActivityKind.Producer);
|
||||
using Activity? activity = this._workflow.TelemetryContext.StartMessageSendActivity(sourceId, targetId, message);
|
||||
|
||||
// Create a carrier for trace context propagation
|
||||
var traceContext = activity is null ? null : new Dictionary<string, string>();
|
||||
if (traceContext is not null)
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Microsoft.Agents.AI.Workflows.Observability;
|
||||
internal static class ActivityNames
|
||||
{
|
||||
public const string WorkflowBuild = "workflow.build";
|
||||
public const string WorkflowRun = "workflow.run";
|
||||
public const string WorkflowRun = "workflow_invoke";
|
||||
public const string MessageSend = "message.send";
|
||||
public const string ExecutorProcess = "executor.process";
|
||||
public const string EdgeGroupProcess = "edge_group.process";
|
||||
|
||||
@@ -14,7 +14,10 @@ internal static class Tags
|
||||
public const string RunId = "run.id";
|
||||
public const string ExecutorId = "executor.id";
|
||||
public const string ExecutorType = "executor.type";
|
||||
public const string ExecutorInput = "executor.input";
|
||||
public const string ExecutorOutput = "executor.output";
|
||||
public const string MessageType = "message.type";
|
||||
public const string MessageContent = "message.content";
|
||||
public const string EdgeGroupType = "edge_group.type";
|
||||
public const string MessageSourceId = "message.source_id";
|
||||
public const string MessageTargetId = "message.target_id";
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Internal context for workflow telemetry, holding the enabled state and configuration options.
|
||||
/// </summary>
|
||||
internal sealed class WorkflowTelemetryContext
|
||||
{
|
||||
private const string DefaultSourceName = "Microsoft.Agents.AI.Workflows";
|
||||
private static readonly ActivitySource s_defaultActivitySource = new(DefaultSourceName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a shared instance representing disabled telemetry.
|
||||
/// </summary>
|
||||
public static WorkflowTelemetryContext Disabled { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether telemetry is enabled.
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the telemetry options.
|
||||
/// </summary>
|
||||
public WorkflowTelemetryOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the activity source used for creating telemetry spans.
|
||||
/// </summary>
|
||||
public ActivitySource ActivitySource { get; }
|
||||
|
||||
private WorkflowTelemetryContext()
|
||||
{
|
||||
this.IsEnabled = false;
|
||||
this.Options = new WorkflowTelemetryOptions();
|
||||
this.ActivitySource = s_defaultActivitySource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="WorkflowTelemetryContext"/> class with telemetry enabled.
|
||||
/// </summary>
|
||||
/// <param name="options">The telemetry options.</param>
|
||||
/// <param name="activitySource">
|
||||
/// An optional activity source to use. If provided, this activity source will be used directly
|
||||
/// and the caller retains ownership (responsible for disposal). If <see langword="null"/>, the
|
||||
/// shared default activity source will be used.
|
||||
/// </param>
|
||||
public WorkflowTelemetryContext(WorkflowTelemetryOptions options, ActivitySource? activitySource = null)
|
||||
{
|
||||
this.IsEnabled = true;
|
||||
this.Options = options;
|
||||
this.ActivitySource = activitySource ?? s_defaultActivitySource;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts an activity if telemetry is enabled, otherwise returns null.
|
||||
/// </summary>
|
||||
/// <param name="name">The activity name.</param>
|
||||
/// <param name="kind">The activity kind.</param>
|
||||
/// <returns>An activity if telemetry is enabled and the activity is sampled, otherwise null.</returns>
|
||||
public Activity? StartActivity(string name, ActivityKind kind = ActivityKind.Internal)
|
||||
{
|
||||
if (!this.IsEnabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(name, kind);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a workflow build activity if enabled.
|
||||
/// </summary>
|
||||
/// <returns>An activity if workflow build telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartWorkflowBuildActivity()
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableWorkflowBuild)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(ActivityNames.WorkflowBuild);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a workflow run activity if enabled.
|
||||
/// </summary>
|
||||
/// <returns>An activity if workflow run telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartWorkflowRunActivity()
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableWorkflowRun)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(ActivityNames.WorkflowRun);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts an executor process activity if enabled, with all standard tags set.
|
||||
/// </summary>
|
||||
/// <param name="executorId">The executor identifier.</param>
|
||||
/// <param name="executorType">The executor type name.</param>
|
||||
/// <param name="messageType">The message type name.</param>
|
||||
/// <param name="message">The input message. Logged only when <see cref="WorkflowTelemetryOptions.EnableSensitiveData"/> is true.</param>
|
||||
/// <returns>An activity if executor process telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartExecutorProcessActivity(string executorId, string? executorType, string messageType, object? message)
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableExecutorProcess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Activity? activity = this.ActivitySource.StartActivity(ActivityNames.ExecutorProcess + " " + executorId);
|
||||
if (activity is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
activity.SetTag(Tags.ExecutorId, executorId)
|
||||
.SetTag(Tags.ExecutorType, executorType)
|
||||
.SetTag(Tags.MessageType, messageType);
|
||||
|
||||
if (this.Options.EnableSensitiveData)
|
||||
{
|
||||
activity.SetTag(Tags.ExecutorInput, SerializeForTelemetry(message));
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the executor output tag on an activity when sensitive data logging is enabled.
|
||||
/// </summary>
|
||||
/// <param name="activity">The activity to set the output on.</param>
|
||||
/// <param name="output">The output value to log.</param>
|
||||
public void SetExecutorOutput(Activity? activity, object? output)
|
||||
{
|
||||
if (activity is not null && this.Options.EnableSensitiveData)
|
||||
{
|
||||
activity.SetTag(Tags.ExecutorOutput, SerializeForTelemetry(output));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts an edge group process activity if enabled.
|
||||
/// </summary>
|
||||
/// <returns>An activity if edge group process telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartEdgeGroupProcessActivity()
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableEdgeGroupProcess)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.ActivitySource.StartActivity(ActivityNames.EdgeGroupProcess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a message send activity if enabled, with all standard tags set.
|
||||
/// </summary>
|
||||
/// <param name="sourceId">The source executor identifier.</param>
|
||||
/// <param name="targetId">The target executor identifier, if any.</param>
|
||||
/// <param name="message">The message being sent. Logged only when <see cref="WorkflowTelemetryOptions.EnableSensitiveData"/> is true.</param>
|
||||
/// <returns>An activity if message send telemetry is enabled, otherwise null.</returns>
|
||||
public Activity? StartMessageSendActivity(string sourceId, string? targetId, object? message)
|
||||
{
|
||||
if (!this.IsEnabled || this.Options.DisableMessageSend)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Activity? activity = this.ActivitySource.StartActivity(ActivityNames.MessageSend, ActivityKind.Producer);
|
||||
if (activity is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
activity.SetTag(Tags.MessageSourceId, sourceId);
|
||||
if (targetId is not null)
|
||||
{
|
||||
activity.SetTag(Tags.MessageTargetId, targetId);
|
||||
}
|
||||
|
||||
if (this.Options.EnableSensitiveData)
|
||||
{
|
||||
activity.SetTag(Tags.MessageContent, SerializeForTelemetry(message));
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050:RequiresDynamicCode", Justification = "Telemetry serialization is optional and only used when explicitly enabled.")]
|
||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access", Justification = "Telemetry serialization is optional and only used when explicitly enabled.")]
|
||||
private static string? SerializeForTelemetry(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Serialize(value, value.GetType());
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return $"[Unserializable: {value.GetType().FullName}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration options for workflow telemetry.
|
||||
/// </summary>
|
||||
public sealed class WorkflowTelemetryOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether potentially sensitive information should be included in telemetry.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> if potentially sensitive information should be included in telemetry;
|
||||
/// <see langword="false"/> if telemetry shouldn't include raw inputs and outputs.
|
||||
/// The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// By default, telemetry includes metadata but not raw inputs and outputs,
|
||||
/// such as message content and executor data.
|
||||
/// </remarks>
|
||||
public bool EnableSensitiveData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether workflow build activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>workflow.build</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableWorkflowBuild { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether workflow run activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>workflow_invoke</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableWorkflowRun { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether executor process activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>executor.process</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableExecutorProcess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether edge group process activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>edge_group.process</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableEdgeGroupProcess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether message send activities should be disabled.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true"/> to disable <c>message.send</c> activities;
|
||||
/// <see langword="false"/> to enable them. The default value is <see langword="false"/>.
|
||||
/// </value>
|
||||
public bool DisableMessageSend { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for adding OpenTelemetry instrumentation to <see cref="WorkflowBuilder"/> instances.
|
||||
/// </summary>
|
||||
public static class OpenTelemetryWorkflowBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Enables OpenTelemetry instrumentation for the workflow, providing comprehensive observability for workflow operations.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="WorkflowBuilder"/> to which OpenTelemetry support will be added.</param>
|
||||
/// <param name="configure">
|
||||
/// An optional callback that provides additional configuration of the <see cref="WorkflowTelemetryOptions"/> instance.
|
||||
/// This allows for fine-tuning telemetry behavior such as enabling sensitive data collection.
|
||||
/// </param>
|
||||
/// <param name="activitySource">
|
||||
/// An optional <see cref="ActivitySource"/> to use for telemetry. If provided, this activity source will be used
|
||||
/// directly and the caller retains ownership (responsible for disposal). If <see langword="null"/>, a shared
|
||||
/// default activity source named "Microsoft.Agents.AI.Workflows" will be used.
|
||||
/// </param>
|
||||
/// <returns>The <see cref="WorkflowBuilder"/> with OpenTelemetry instrumentation enabled, enabling method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="builder"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This extension adds comprehensive telemetry capabilities to workflows, including:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Distributed tracing of workflow execution</description></item>
|
||||
/// <item><description>Executor invocation and processing spans</description></item>
|
||||
/// <item><description>Edge routing and message delivery spans</description></item>
|
||||
/// <item><description>Workflow build and validation spans</description></item>
|
||||
/// <item><description>Error tracking and exception details</description></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// By default, workflow telemetry is disabled. Call this method to enable telemetry collection.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var workflow = new WorkflowBuilder(startExecutor)
|
||||
/// .AddEdge(executor1, executor2)
|
||||
/// .WithOpenTelemetry(cfg => cfg.EnableSensitiveData = true)
|
||||
/// .Build();
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static WorkflowBuilder WithOpenTelemetry(
|
||||
this WorkflowBuilder builder,
|
||||
Action<WorkflowTelemetryOptions>? configure = null,
|
||||
ActivitySource? activitySource = null)
|
||||
{
|
||||
Throw.IfNull(builder);
|
||||
|
||||
WorkflowTelemetryOptions options = new();
|
||||
configure?.Invoke(options);
|
||||
|
||||
WorkflowTelemetryContext context = new(options, activitySource);
|
||||
|
||||
builder.SetTelemetryContext(context);
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
@@ -76,6 +77,11 @@ public class Workflow
|
||||
/// </summary>
|
||||
public string? Description { get; internal init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the telemetry context for the workflow.
|
||||
/// </summary>
|
||||
internal WorkflowTelemetryContext TelemetryContext { get; }
|
||||
|
||||
internal bool AllowConcurrent => this.ExecutorBindings.Values.All(registration => registration.SupportsConcurrentSharedExecution);
|
||||
|
||||
internal IEnumerable<string> NonConcurrentExecutorIds =>
|
||||
@@ -88,11 +94,13 @@ public class Workflow
|
||||
/// <param name="startExecutorId">The unique identifier of the starting executor for the workflow. Cannot be <c>null</c>.</param>
|
||||
/// <param name="name">Optional human-readable name for the workflow.</param>
|
||||
/// <param name="description">Optional description of what the workflow does.</param>
|
||||
internal Workflow(string startExecutorId, string? name = null, string? description = null)
|
||||
/// <param name="telemetryContext">Optional telemetry context for the workflow.</param>
|
||||
internal Workflow(string startExecutorId, string? name = null, string? description = null, WorkflowTelemetryContext? telemetryContext = null)
|
||||
{
|
||||
this.StartExecutorId = Throw.IfNull(startExecutorId);
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
this.TelemetryContext = telemetryContext ?? WorkflowTelemetryContext.Disabled;
|
||||
}
|
||||
|
||||
private bool _needsReset;
|
||||
|
||||
@@ -38,9 +38,7 @@ public class WorkflowBuilder
|
||||
private readonly string _startExecutorId;
|
||||
private string? _name;
|
||||
private string? _description;
|
||||
|
||||
private static readonly string s_namespace = typeof(WorkflowBuilder).Namespace!;
|
||||
private static readonly ActivitySource s_activitySource = new(s_namespace);
|
||||
private WorkflowTelemetryContext _telemetryContext = WorkflowTelemetryContext.Disabled;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the WorkflowBuilder class with the specified starting executor.
|
||||
@@ -137,6 +135,15 @@ public class WorkflowBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the telemetry context for the workflow.
|
||||
/// </summary>
|
||||
/// <param name="context">The telemetry context to use.</param>
|
||||
internal void SetTelemetryContext(WorkflowTelemetryContext context)
|
||||
{
|
||||
this._telemetryContext = Throw.IfNull(context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds the specified executor (via registration) to the workflow, allowing it to participate in workflow execution.
|
||||
/// </summary>
|
||||
@@ -563,7 +570,7 @@ public class WorkflowBuilder
|
||||
|
||||
activity?.AddEvent(new ActivityEvent(EventNames.BuildValidationCompleted));
|
||||
|
||||
var workflow = new Workflow(this._startExecutorId, this._name, this._description)
|
||||
var workflow = new Workflow(this._startExecutorId, this._name, this._description, this._telemetryContext)
|
||||
{
|
||||
ExecutorBindings = this._executorBindings,
|
||||
Edges = this._edges,
|
||||
@@ -601,7 +608,7 @@ public class WorkflowBuilder
|
||||
/// or if the start executor is not bound.</exception>
|
||||
public Workflow Build(bool validateOrphans = true)
|
||||
{
|
||||
using Activity? activity = s_activitySource.StartActivity(ActivityNames.WorkflowBuild);
|
||||
using Activity? activity = this._telemetryContext.StartWorkflowBuildActivity();
|
||||
|
||||
var workflow = this.BuildInternal(validateOrphans, activity);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Declarative.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="DeclarativeWorkflowOptions"/> telemetry configuration.
|
||||
/// </summary>
|
||||
[Collection("DeclarativeWorkflowOptionsTest")]
|
||||
public sealed class DeclarativeWorkflowOptionsTest : IDisposable
|
||||
{
|
||||
// These constants mirror Microsoft.Agents.AI.Workflows.Observability.ActivityNames
|
||||
// which is internal and not accessible from this test project.
|
||||
private const string WorkflowBuildActivityName = "workflow.build";
|
||||
private const string WorkflowRunActivityName = "workflow_invoke";
|
||||
|
||||
// The default activity source name used by the workflow telemetry context.
|
||||
private const string DefaultTelemetrySourceName = "Microsoft.Agents.AI.Workflows";
|
||||
|
||||
private const string SimpleWorkflowYaml = """
|
||||
kind: Workflow
|
||||
trigger:
|
||||
kind: OnConversationStart
|
||||
id: test_workflow
|
||||
actions:
|
||||
- kind: EndConversation
|
||||
id: end_all
|
||||
""";
|
||||
|
||||
private readonly ActivitySource _activitySource = new("TestSource");
|
||||
private readonly ActivityListener _activityListener;
|
||||
private readonly ConcurrentBag<Activity> _capturedActivities = [];
|
||||
|
||||
public DeclarativeWorkflowOptionsTest()
|
||||
{
|
||||
this._activityListener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = source =>
|
||||
source.Name == DefaultTelemetrySourceName ||
|
||||
source.Name == "TestSource",
|
||||
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
|
||||
ActivityStarted = activity => this._capturedActivities.Add(activity),
|
||||
};
|
||||
ActivitySource.AddActivityListener(this._activityListener);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this._activityListener.Dispose();
|
||||
this._activitySource.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.ConfigureTelemetry);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureTelemetry_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool callbackInvoked = false;
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
callbackInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(options.ConfigureTelemetry);
|
||||
WorkflowTelemetryOptions telemetryOptions = new();
|
||||
options.ConfigureTelemetry(telemetryOptions);
|
||||
Assert.True(callbackInvoked);
|
||||
Assert.True(telemetryOptions.EnableSensitiveData);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_DefaultIsNull()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object);
|
||||
|
||||
// Assert
|
||||
Assert.Null(options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TelemetryActivitySource_CanBeSet()
|
||||
{
|
||||
// Arrange
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
|
||||
// Act
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Same(this._activitySource, options.TelemetryActivitySource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithDefaultTelemetry_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("DefaultTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = _ => { },
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithTelemetryActivitySource_AppliesTelemetryAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("TelemetryActivitySourceTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
TelemetryActivitySource = this._activitySource,
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == "TestSource")
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.All(capturedActivities, a => Assert.Equal("TestSource", a.Source.Name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithConfigureTelemetry_AppliesConfigurationAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("ConfigureTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
bool configureInvoked = false;
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
ConfigureTelemetry = opt =>
|
||||
{
|
||||
configureInvoked = true;
|
||||
opt.EnableSensitiveData = true;
|
||||
},
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert
|
||||
Assert.True(configureInvoked);
|
||||
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId && a.Source.Name == DefaultTelemetrySourceName)
|
||||
.ToArray();
|
||||
|
||||
Assert.NotEmpty(capturedActivities);
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal));
|
||||
Assert.Contains(capturedActivities, a => a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BuildWorkflow_WithoutTelemetry_DoesNotCreateActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
using Activity testActivity = new Activity("NoTelemetryTest").Start()!;
|
||||
Mock<WorkflowAgentProvider> mockProvider = CreateMockProvider();
|
||||
DeclarativeWorkflowOptions options = new(mockProvider.Object)
|
||||
{
|
||||
LoggerFactory = NullLoggerFactory.Instance
|
||||
};
|
||||
|
||||
// Act
|
||||
using StringReader reader = new(SimpleWorkflowYaml);
|
||||
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(reader, options);
|
||||
|
||||
await using Run run = await InProcessExecution.RunAsync(workflow, "test input");
|
||||
|
||||
// Assert - No workflow activities should be created when telemetry is disabled
|
||||
Activity[] capturedActivities = this._capturedActivities
|
||||
.Where(a => a.RootId == testActivity.RootId &&
|
||||
(a.OperationName.StartsWith(WorkflowBuildActivityName, StringComparison.Ordinal) ||
|
||||
a.OperationName.StartsWith(WorkflowRunActivityName, StringComparison.Ordinal)))
|
||||
.ToArray();
|
||||
|
||||
Assert.Empty(capturedActivities);
|
||||
}
|
||||
|
||||
private static Mock<WorkflowAgentProvider> CreateMockProvider()
|
||||
{
|
||||
Mock<WorkflowAgentProvider> mockAgentProvider = new(MockBehavior.Strict);
|
||||
mockAgentProvider
|
||||
.Setup(provider => provider.CreateConversationAsync(It.IsAny<CancellationToken>()))
|
||||
.Returns(() => Task.FromResult(Guid.NewGuid().ToString("N")));
|
||||
mockAgentProvider
|
||||
.Setup(provider => provider.CreateMessageAsync(It.IsAny<string>(), It.IsAny<ChatMessage>(), It.IsAny<CancellationToken>()))
|
||||
.Returns(Task.FromResult(new ChatMessage(ChatRole.Assistant, "Test response")));
|
||||
return mockAgentProvider;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public sealed class ObservabilityTests : IDisposable
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.Build();
|
||||
return builder.WithOpenTelemetry().Build();
|
||||
}
|
||||
|
||||
private static Dictionary<string, int> GetExpectedActivityNameCounts() =>
|
||||
@@ -111,8 +111,6 @@ public sealed class ObservabilityTests : IDisposable
|
||||
Run run = await executionEnvironment.RunAsync(workflow, "Hello, World!");
|
||||
await run.DisposeAsync();
|
||||
|
||||
await Task.Delay(100); // Allow time for activities to be captured
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().HaveCount(8, "Exactly 8 activities should be created.");
|
||||
@@ -122,12 +120,12 @@ public sealed class ObservabilityTests : IDisposable
|
||||
{
|
||||
var activityName = kvp.Key;
|
||||
var expectedCount = kvp.Value;
|
||||
var actualCount = capturedActivities.Count(a => a.OperationName == activityName);
|
||||
var actualCount = capturedActivities.Count(a => a.OperationName.StartsWith(activityName, StringComparison.Ordinal));
|
||||
actualCount.Should().Be(expectedCount, $"Activity '{activityName}' should occur {expectedCount} times.");
|
||||
}
|
||||
|
||||
// Verify WorkflowRun activity events include workflow lifecycle events
|
||||
var workflowRunActivity = capturedActivities.First(a => a.OperationName == ActivityNames.WorkflowRun);
|
||||
var workflowRunActivity = capturedActivities.First(a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal));
|
||||
var activityEvents = workflowRunActivity.Events.ToList();
|
||||
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowStarted, "activity should have workflow started event");
|
||||
activityEvents.Should().Contain(e => e.Name == EventNames.WorkflowCompleted, "activity should have workflow completed event");
|
||||
@@ -166,8 +164,6 @@ public sealed class ObservabilityTests : IDisposable
|
||||
|
||||
// Act
|
||||
CreateWorkflow();
|
||||
await Task.Delay(100); // Allow time for activities to be captured
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().HaveCount(1, "Exactly 1 activity should be created.");
|
||||
@@ -183,4 +179,325 @@ public sealed class ObservabilityTests : IDisposable
|
||||
tags.Should().ContainKey(Tags.WorkflowId);
|
||||
tags.Should().ContainKey(Tags.WorkflowDefinition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TelemetryDisabledByDefault_CreatesNoActivitiesAsync()
|
||||
{
|
||||
// Arrange
|
||||
// Create a test activity to correlate captured activities
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
// Act - Build workflow WITHOUT calling WithOpenTelemetry()
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.Build(); // No WithOpenTelemetry() call
|
||||
// Assert - No activities should be created
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().BeEmpty("No activities should be created when telemetry is disabled (default).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WithOpenTelemetry_UsesProvidedActivitySourceAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
using var userActivitySource = new ActivitySource("UserProvidedSource");
|
||||
|
||||
// Set up a separate listener for the user-provided source
|
||||
ConcurrentBag<Activity> userActivities = [];
|
||||
using var userListener = new ActivityListener
|
||||
{
|
||||
ShouldListenTo = source => source.Name == "UserProvidedSource",
|
||||
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
|
||||
ActivityStarted = activity => userActivities.Add(activity),
|
||||
};
|
||||
ActivitySource.AddActivityListener(userListener);
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(activitySource: userActivitySource).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = userActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotBeEmpty("Activities should be created with user-provided ActivitySource.");
|
||||
capturedActivities.Should().OnlyContain(
|
||||
a => a.Source.Name == "UserProvidedSource",
|
||||
"All activities should come from the user-provided ActivitySource.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableWorkflowBuild_PreventsWorkflowBuildActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOpenTelemetry(configure: opts => opts.DisableWorkflowBuild = true).Build();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowBuild, StringComparison.Ordinal),
|
||||
"WorkflowBuild activity should be disabled.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableWorkflowRun_PreventsWorkflowRunActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.DisableWorkflowRun = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
|
||||
"WorkflowRun activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowBuild, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableExecutorProcess_PreventsExecutorProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.DisableExecutorProcess = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"ExecutorProcess activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.WorkflowRun, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableEdgeGroupProcess_PreventsEdgeGroupProcessActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
var workflow = CreateWorkflowWithDisabledEdges();
|
||||
|
||||
// Act
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.EdgeGroupProcess, StringComparison.Ordinal),
|
||||
"EdgeGroupProcess activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DisableMessageSend_PreventsMessageSendActivityAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
var workflow = CreateWorkflowWithDisabledMessages();
|
||||
|
||||
// Act
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "Hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
capturedActivities.Should().NotContain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal),
|
||||
"MessageSend activity should be disabled.");
|
||||
capturedActivities.Should().Contain(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal),
|
||||
"Other activities should still be created.");
|
||||
}
|
||||
|
||||
private static Workflow CreateWorkflowWithDisabledEdges()
|
||||
{
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.WithOpenTelemetry(configure: opts => opts.DisableEdgeGroupProcess = true).Build();
|
||||
}
|
||||
|
||||
private static Workflow CreateWorkflowWithDisabledMessages()
|
||||
{
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
|
||||
return builder.WithOpenTelemetry(configure: opts => opts.DisableMessageSend = true).Build();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_LogsExecutorInputAndOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.EnableSensitiveData = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var executorActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal));
|
||||
|
||||
executorActivity.Should().NotBeNull("ExecutorProcess activity should be created.");
|
||||
|
||||
var tags = executorActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().ContainKey(Tags.ExecutorInput, "Input should be logged when EnableSensitiveData is true.");
|
||||
tags.Should().ContainKey(Tags.ExecutorOutput, "Output should be logged when EnableSensitiveData is true.");
|
||||
tags[Tags.ExecutorInput].Should().Contain("hello", "Input should contain the input value.");
|
||||
tags[Tags.ExecutorOutput].Should().Contain("HELLO", "Output should contain the transformed value.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogInputOutputAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
// Act - EnableSensitiveData is false by default
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.WithOutputFrom(uppercase);
|
||||
var workflow = builder.WithOpenTelemetry().Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var executorActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.ExecutorProcess, StringComparison.Ordinal));
|
||||
|
||||
executorActivity.Should().NotBeNull("ExecutorProcess activity should be created.");
|
||||
|
||||
var tags = executorActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().NotContainKey(Tags.ExecutorInput, "Input should NOT be logged when EnableSensitiveData is false.");
|
||||
tags.Should().NotContainKey(Tags.ExecutorOutput, "Output should NOT be logged when EnableSensitiveData is false.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_LogsMessageSendContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
// Act
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
var workflow = builder.WithOpenTelemetry(configure: opts => opts.EnableSensitiveData = true).Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var messageSendActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal));
|
||||
|
||||
messageSendActivity.Should().NotBeNull("MessageSend activity should be created.");
|
||||
|
||||
var tags = messageSendActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().ContainKey(Tags.MessageContent, "Message content should be logged when EnableSensitiveData is true.");
|
||||
tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should be logged.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnableSensitiveData_Disabled_DoesNotLogMessageContentAsync()
|
||||
{
|
||||
// Arrange
|
||||
using var testActivity = new Activity("ObservabilityTest").Start();
|
||||
|
||||
Func<string, string> uppercaseFunc = s => s.ToUpperInvariant();
|
||||
var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
|
||||
|
||||
Func<string, string> reverseFunc = s => new string(s.Reverse().ToArray());
|
||||
var reverse = reverseFunc.BindAsExecutor("ReverseTextExecutor");
|
||||
|
||||
// Act - EnableSensitiveData is false by default
|
||||
WorkflowBuilder builder = new(uppercase);
|
||||
builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
|
||||
var workflow = builder.WithOpenTelemetry().Build();
|
||||
|
||||
Run run = await InProcessExecution.Default.RunAsync(workflow, "hello");
|
||||
await run.DisposeAsync();
|
||||
|
||||
// Assert
|
||||
var capturedActivities = this._capturedActivities.Where(a => a.RootId == testActivity.RootId).ToList();
|
||||
var messageSendActivity = capturedActivities.FirstOrDefault(
|
||||
a => a.OperationName.StartsWith(ActivityNames.MessageSend, StringComparison.Ordinal));
|
||||
|
||||
messageSendActivity.Should().NotBeNull("MessageSend activity should be created.");
|
||||
|
||||
var tags = messageSendActivity!.Tags.ToDictionary(t => t.Key, t => t.Value);
|
||||
tags.Should().NotContainKey(Tags.MessageContent, "Message content should NOT be logged when EnableSensitiveData is false.");
|
||||
tags.Should().ContainKey(Tags.MessageSourceId, "Source ID should still be logged.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Agents.AI.Workflows.Execution;
|
||||
using Microsoft.Agents.AI.Workflows.Observability;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
@@ -133,6 +134,8 @@ public class TestRunContext : IRunnerContext
|
||||
public bool WithCheckpointing => false;
|
||||
public bool ConcurrentRunsEnabled => false;
|
||||
|
||||
WorkflowTelemetryContext IRunnerContext.TelemetryContext => WorkflowTelemetryContext.Disabled;
|
||||
|
||||
ValueTask<Executor> IRunnerContext.EnsureExecutorAsync(string executorId, IStepTracer? tracer, CancellationToken cancellationToken) =>
|
||||
new(this.Executors[executorId]);
|
||||
|
||||
|
||||
@@ -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()])
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -19,10 +19,12 @@ class TestAsToolKwargsPropagation:
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@agent_middleware
|
||||
async def capture_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def capture_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Capture kwargs passed to the sub-agent
|
||||
captured_kwargs.update(context.kwargs)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Setup mock response
|
||||
chat_client.responses = [
|
||||
@@ -60,9 +62,11 @@ class TestAsToolKwargsPropagation:
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@agent_middleware
|
||||
async def capture_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def capture_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
captured_kwargs.update(context.kwargs)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Setup mock response
|
||||
chat_client.responses = [
|
||||
@@ -95,10 +99,12 @@ class TestAsToolKwargsPropagation:
|
||||
captured_kwargs_list: list[dict[str, Any]] = []
|
||||
|
||||
@agent_middleware
|
||||
async def capture_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def capture_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Capture kwargs at each level
|
||||
captured_kwargs_list.append(dict(context.kwargs))
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Setup mock responses to trigger nested tool invocation: B calls tool C, then completes.
|
||||
chat_client.responses = [
|
||||
@@ -156,9 +162,11 @@ class TestAsToolKwargsPropagation:
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@agent_middleware
|
||||
async def capture_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def capture_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
captured_kwargs.update(context.kwargs)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Setup mock streaming responses
|
||||
from agent_framework import ChatResponseUpdate
|
||||
@@ -216,9 +224,11 @@ class TestAsToolKwargsPropagation:
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@agent_middleware
|
||||
async def capture_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def capture_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
captured_kwargs.update(context.kwargs)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Setup mock response
|
||||
chat_client.responses = [
|
||||
@@ -256,14 +266,16 @@ class TestAsToolKwargsPropagation:
|
||||
call_count = 0
|
||||
|
||||
@agent_middleware
|
||||
async def capture_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def capture_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
first_call_kwargs.update(context.kwargs)
|
||||
elif call_count == 2:
|
||||
second_call_kwargs.update(context.kwargs)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Setup mock responses for both calls
|
||||
chat_client.responses = [
|
||||
@@ -306,9 +318,11 @@ class TestAsToolKwargsPropagation:
|
||||
captured_kwargs: dict[str, Any] = {}
|
||||
|
||||
@agent_middleware
|
||||
async def capture_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def capture_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
captured_kwargs.update(context.kwargs)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Setup mock response
|
||||
chat_client.responses = [
|
||||
|
||||
@@ -135,12 +135,12 @@ class TestAgentMiddlewarePipeline:
|
||||
"""Test cases for AgentMiddlewarePipeline."""
|
||||
|
||||
class PreNextTerminateMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
raise MiddlewareTermination
|
||||
|
||||
class PostNextTerminateMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Any) -> None:
|
||||
await next(context)
|
||||
async def process(self, context: AgentContext, call_next: Any) -> None:
|
||||
await call_next(context)
|
||||
raise MiddlewareTermination
|
||||
|
||||
def test_init_empty(self) -> None:
|
||||
@@ -157,8 +157,8 @@ class TestAgentMiddlewarePipeline:
|
||||
def test_init_with_function_middleware(self) -> None:
|
||||
"""Test AgentMiddlewarePipeline initialization with function-based middleware."""
|
||||
|
||||
async def test_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
await next(context)
|
||||
async def test_middleware(context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
await call_next(context)
|
||||
|
||||
pipeline = AgentMiddlewarePipeline(test_middleware)
|
||||
assert pipeline.has_middlewares
|
||||
@@ -185,9 +185,11 @@ class TestAgentMiddlewarePipeline:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
middleware = OrderTrackingMiddleware("test")
|
||||
@@ -236,9 +238,11 @@ class TestAgentMiddlewarePipeline:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
middleware = StreamOrderTrackingMiddleware("test")
|
||||
@@ -363,10 +367,12 @@ class TestAgentMiddlewarePipeline:
|
||||
captured_thread = None
|
||||
|
||||
class ThreadCapturingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
nonlocal captured_thread
|
||||
captured_thread = context.thread
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = ThreadCapturingMiddleware()
|
||||
pipeline = AgentMiddlewarePipeline(middleware)
|
||||
@@ -388,10 +394,12 @@ class TestAgentMiddlewarePipeline:
|
||||
captured_thread = "not_none" # Use string to distinguish from None
|
||||
|
||||
class ThreadCapturingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
nonlocal captured_thread
|
||||
captured_thread = context.thread
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = ThreadCapturingMiddleware()
|
||||
pipeline = AgentMiddlewarePipeline(middleware)
|
||||
@@ -412,12 +420,12 @@ class TestFunctionMiddlewarePipeline:
|
||||
"""Test cases for FunctionMiddlewarePipeline."""
|
||||
|
||||
class PreNextTerminateFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next: Any) -> None:
|
||||
async def process(self, context: FunctionInvocationContext, call_next: Any) -> None:
|
||||
raise MiddlewareTermination
|
||||
|
||||
class PostNextTerminateFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next: Any) -> None:
|
||||
await next(context)
|
||||
async def process(self, context: FunctionInvocationContext, call_next: Any) -> None:
|
||||
await call_next(context)
|
||||
raise MiddlewareTermination
|
||||
|
||||
async def test_execute_with_pre_next_termination(self, mock_function: FunctionTool[Any, Any]) -> None:
|
||||
@@ -475,9 +483,9 @@ class TestFunctionMiddlewarePipeline:
|
||||
"""Test FunctionMiddlewarePipeline initialization with function-based middleware."""
|
||||
|
||||
async def test_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
pipeline = FunctionMiddlewarePipeline(test_middleware)
|
||||
assert pipeline.has_middlewares
|
||||
@@ -507,10 +515,10 @@ class TestFunctionMiddlewarePipeline:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
middleware = OrderTrackingFunctionMiddleware("test")
|
||||
@@ -533,12 +541,12 @@ class TestChatMiddlewarePipeline:
|
||||
"""Test cases for ChatMiddlewarePipeline."""
|
||||
|
||||
class PreNextTerminateChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
raise MiddlewareTermination
|
||||
|
||||
class PostNextTerminateChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
await next(context)
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
await call_next(context)
|
||||
raise MiddlewareTermination
|
||||
|
||||
def test_init_empty(self) -> None:
|
||||
@@ -555,8 +563,8 @@ class TestChatMiddlewarePipeline:
|
||||
def test_init_with_function_middleware(self) -> None:
|
||||
"""Test ChatMiddlewarePipeline initialization with function-based middleware."""
|
||||
|
||||
async def test_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
await next(context)
|
||||
async def test_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
await call_next(context)
|
||||
|
||||
pipeline = ChatMiddlewarePipeline(test_middleware)
|
||||
assert pipeline.has_middlewares
|
||||
@@ -584,9 +592,9 @@ class TestChatMiddlewarePipeline:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
middleware = OrderTrackingChatMiddleware("test")
|
||||
@@ -636,9 +644,9 @@ class TestChatMiddlewarePipeline:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
middleware = StreamOrderTrackingChatMiddleware("test")
|
||||
@@ -766,10 +774,12 @@ class TestClassBasedMiddleware:
|
||||
metadata_updates: list[str] = []
|
||||
|
||||
class MetadataAgentMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
context.metadata["before"] = True
|
||||
metadata_updates.append("before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
context.metadata["after"] = True
|
||||
metadata_updates.append("after")
|
||||
|
||||
@@ -797,11 +807,11 @@ class TestClassBasedMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
context.metadata["before"] = True
|
||||
metadata_updates.append("before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
context.metadata["after"] = True
|
||||
metadata_updates.append("after")
|
||||
|
||||
@@ -829,10 +839,12 @@ class TestFunctionBasedMiddleware:
|
||||
"""Test function-based agent middleware."""
|
||||
execution_order: list[str] = []
|
||||
|
||||
async def test_agent_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def test_agent_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_before")
|
||||
context.metadata["function_middleware"] = True
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_after")
|
||||
|
||||
pipeline = AgentMiddlewarePipeline(test_agent_middleware)
|
||||
@@ -854,11 +866,11 @@ class TestFunctionBasedMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
async def test_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_before")
|
||||
context.metadata["function_middleware"] = True
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_after")
|
||||
|
||||
pipeline = FunctionMiddlewarePipeline(test_function_middleware)
|
||||
@@ -884,14 +896,18 @@ class TestMixedMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class ClassMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("class_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("class_after")
|
||||
|
||||
async def function_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def function_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_after")
|
||||
|
||||
pipeline = AgentMiddlewarePipeline(ClassMiddleware(), function_middleware)
|
||||
@@ -915,17 +931,17 @@ class TestMixedMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("class_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("class_after")
|
||||
|
||||
async def function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_after")
|
||||
|
||||
pipeline = FunctionMiddlewarePipeline(ClassMiddleware(), function_middleware)
|
||||
@@ -946,16 +962,16 @@ class TestMixedMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class ClassChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("class_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("class_after")
|
||||
|
||||
async def function_chat_middleware(
|
||||
context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_after")
|
||||
|
||||
pipeline = ChatMiddlewarePipeline(ClassChatMiddleware(), function_chat_middleware)
|
||||
@@ -981,21 +997,27 @@ class TestMultipleMiddlewareOrdering:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class FirstMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("first_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("first_after")
|
||||
|
||||
class SecondMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("second_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("second_after")
|
||||
|
||||
class ThirdMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("third_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("third_after")
|
||||
|
||||
middleware = [FirstMiddleware(), SecondMiddleware(), ThirdMiddleware()]
|
||||
@@ -1029,20 +1051,20 @@ class TestMultipleMiddlewareOrdering:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("first_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("first_after")
|
||||
|
||||
class SecondMiddleware(FunctionMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("second_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("second_after")
|
||||
|
||||
middleware = [FirstMiddleware(), SecondMiddleware()]
|
||||
@@ -1065,21 +1087,21 @@ class TestMultipleMiddlewareOrdering:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class FirstChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("first_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("first_after")
|
||||
|
||||
class SecondChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("second_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("second_after")
|
||||
|
||||
class ThirdChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("third_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("third_after")
|
||||
|
||||
middleware = [FirstChatMiddleware(), SecondChatMiddleware(), ThirdChatMiddleware()]
|
||||
@@ -1114,7 +1136,9 @@ class TestContextContentValidation:
|
||||
"""Test that agent context contains expected data."""
|
||||
|
||||
class ContextValidationMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Verify context has all expected attributes
|
||||
assert hasattr(context, "agent")
|
||||
assert hasattr(context, "messages")
|
||||
@@ -1132,7 +1156,7 @@ class TestContextContentValidation:
|
||||
# Add custom metadata
|
||||
context.metadata["validated"] = True
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = ContextValidationMiddleware()
|
||||
pipeline = AgentMiddlewarePipeline(middleware)
|
||||
@@ -1154,7 +1178,7 @@ class TestContextContentValidation:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Verify context has all expected attributes
|
||||
assert hasattr(context, "function")
|
||||
@@ -1170,7 +1194,7 @@ class TestContextContentValidation:
|
||||
# Add custom metadata
|
||||
context.metadata["validated"] = True
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = ContextValidationMiddleware()
|
||||
pipeline = FunctionMiddlewarePipeline(middleware)
|
||||
@@ -1189,7 +1213,7 @@ class TestContextContentValidation:
|
||||
"""Test that chat context contains expected data."""
|
||||
|
||||
class ChatContextValidationMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
# Verify context has all expected attributes
|
||||
assert hasattr(context, "chat_client")
|
||||
assert hasattr(context, "messages")
|
||||
@@ -1211,7 +1235,7 @@ class TestContextContentValidation:
|
||||
# Add custom metadata
|
||||
context.metadata["validated"] = True
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = ChatContextValidationMiddleware()
|
||||
pipeline = ChatMiddlewarePipeline(middleware)
|
||||
@@ -1236,9 +1260,11 @@ class TestStreamingScenarios:
|
||||
streaming_flags: list[bool] = []
|
||||
|
||||
class StreamingFlagMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
streaming_flags.append(context.stream)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = StreamingFlagMiddleware()
|
||||
pipeline = AgentMiddlewarePipeline(middleware)
|
||||
@@ -1276,9 +1302,11 @@ class TestStreamingScenarios:
|
||||
chunks_processed: list[str] = []
|
||||
|
||||
class StreamProcessingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
chunks_processed.append("before_stream")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
chunks_processed.append("after_stream")
|
||||
|
||||
middleware = StreamProcessingMiddleware()
|
||||
@@ -1317,9 +1345,9 @@ class TestStreamingScenarios:
|
||||
streaming_flags: list[bool] = []
|
||||
|
||||
class ChatStreamingFlagMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
streaming_flags.append(context.stream)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = ChatStreamingFlagMiddleware()
|
||||
pipeline = ChatMiddlewarePipeline(middleware)
|
||||
@@ -1358,9 +1386,9 @@ class TestStreamingScenarios:
|
||||
chunks_processed: list[str] = []
|
||||
|
||||
class ChatStreamProcessingMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
chunks_processed.append("before_stream")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
chunks_processed.append("after_stream")
|
||||
|
||||
middleware = ChatStreamProcessingMiddleware()
|
||||
@@ -1408,24 +1436,24 @@ class FunctionTestArgs(BaseModel):
|
||||
class TestAgentMiddleware(AgentMiddleware):
|
||||
"""Test implementation of AgentMiddleware."""
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
await next(context)
|
||||
async def process(self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
await call_next(context)
|
||||
|
||||
|
||||
class TestFunctionMiddleware(FunctionMiddleware):
|
||||
"""Test implementation of FunctionMiddleware."""
|
||||
|
||||
async def process(
|
||||
self, context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
self, context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
class TestChatMiddleware(ChatMiddleware):
|
||||
"""Test implementation of ChatMiddleware."""
|
||||
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
await next(context)
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
await call_next(context)
|
||||
|
||||
|
||||
class MockFunctionArgs(BaseModel):
|
||||
@@ -1441,7 +1469,9 @@ class TestMiddlewareExecutionControl:
|
||||
"""Test that when agent middleware doesn't call next(), no execution happens."""
|
||||
|
||||
class NoNextMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Don't call next() - this should prevent any execution
|
||||
pass
|
||||
|
||||
@@ -1468,7 +1498,9 @@ class TestMiddlewareExecutionControl:
|
||||
"""Test that when agent middleware doesn't call next(), no streaming execution happens."""
|
||||
|
||||
class NoNextStreamingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Don't call next() - this should prevent any execution
|
||||
pass
|
||||
|
||||
@@ -1505,7 +1537,7 @@ class TestMiddlewareExecutionControl:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Don't call next() - this should prevent any execution
|
||||
pass
|
||||
@@ -1534,14 +1566,18 @@ class TestMiddlewareExecutionControl:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class FirstMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("first")
|
||||
# Don't call next() - this should stop the pipeline
|
||||
|
||||
class SecondMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("second")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
pipeline = AgentMiddlewarePipeline(FirstMiddleware(), SecondMiddleware())
|
||||
messages = [ChatMessage(role="user", text="test")]
|
||||
@@ -1565,7 +1601,7 @@ class TestMiddlewareExecutionControl:
|
||||
"""Test that when chat middleware doesn't call next(), no execution happens."""
|
||||
|
||||
class NoNextChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
# Don't call next() - this should prevent any execution
|
||||
pass
|
||||
|
||||
@@ -1593,7 +1629,7 @@ class TestMiddlewareExecutionControl:
|
||||
"""Test that when chat middleware doesn't call next(), no streaming execution happens."""
|
||||
|
||||
class NoNextStreamingChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
# Don't call next() - this should prevent any execution
|
||||
pass
|
||||
|
||||
@@ -1634,14 +1670,14 @@ class TestMiddlewareExecutionControl:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class FirstChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("first")
|
||||
# Don't call next() - this should stop the pipeline
|
||||
|
||||
class SecondChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("second")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
pipeline = ChatMiddlewarePipeline(FirstChatMiddleware(), SecondChatMiddleware())
|
||||
messages = [ChatMessage(role="user", text="test")]
|
||||
|
||||
@@ -43,9 +43,11 @@ class TestResultOverrideMiddleware:
|
||||
override_response = AgentResponse(messages=[ChatMessage(role="assistant", text="overridden response")])
|
||||
|
||||
class ResponseOverrideMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Execute the pipeline first, then override the response
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
context.result = override_response
|
||||
|
||||
middleware = ResponseOverrideMiddleware()
|
||||
@@ -77,9 +79,11 @@ class TestResultOverrideMiddleware:
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=" stream")])
|
||||
|
||||
class StreamResponseOverrideMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Execute the pipeline first, then override the response stream
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
context.result = ResponseStream(override_stream())
|
||||
|
||||
middleware = StreamResponseOverrideMiddleware()
|
||||
@@ -111,10 +115,10 @@ class TestResultOverrideMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Execute the pipeline first, then override the result
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
context.result = override_result
|
||||
|
||||
middleware = ResultOverrideMiddleware()
|
||||
@@ -141,9 +145,11 @@ class TestResultOverrideMiddleware:
|
||||
mock_chat_client = MockChatClient()
|
||||
|
||||
class ChatAgentResponseOverrideMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Always call next() first to allow execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
# Then conditionally override based on content
|
||||
if any("special" in msg.text for msg in context.messages if msg.text):
|
||||
context.result = AgentResponse(
|
||||
@@ -178,13 +184,15 @@ class TestResultOverrideMiddleware:
|
||||
yield AgentResponseUpdate(contents=[Content.from_text(text=" response!")])
|
||||
|
||||
class ChatAgentStreamOverrideMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Check if we want to override BEFORE calling next to avoid creating unused streams
|
||||
if any("custom stream" in msg.text for msg in context.messages if msg.text):
|
||||
context.result = ResponseStream(custom_stream())
|
||||
return # Don't call next() - we're overriding the entire result
|
||||
# Normal case - let the agent handle it
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Create ChatAgent with override middleware
|
||||
middleware = ChatAgentStreamOverrideMiddleware()
|
||||
@@ -215,10 +223,12 @@ class TestResultOverrideMiddleware:
|
||||
"""Test that when agent middleware conditionally doesn't call next(), no execution happens."""
|
||||
|
||||
class ConditionalNoNextMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Only call next() if message contains "execute"
|
||||
if any("execute" in msg.text for msg in context.messages if msg.text):
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
# Otherwise, don't call next() - no execution should happen
|
||||
|
||||
middleware = ConditionalNoNextMiddleware()
|
||||
@@ -259,13 +269,13 @@ class TestResultOverrideMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Only call next() if argument name contains "execute"
|
||||
args = context.arguments
|
||||
assert isinstance(args, FunctionTestArgs)
|
||||
if "execute" in args.name:
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
# Otherwise, don't call next() - no execution should happen
|
||||
|
||||
middleware = ConditionalNoNextFunctionMiddleware()
|
||||
@@ -308,12 +318,14 @@ class TestResultObservability:
|
||||
observed_responses: list[AgentResponse] = []
|
||||
|
||||
class ObservabilityMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Context should be empty before next()
|
||||
assert context.result is None
|
||||
|
||||
# Call next to execute
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Context should now contain the response for observability
|
||||
assert context.result is not None
|
||||
@@ -343,13 +355,13 @@ class TestResultObservability:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Context should be empty before next()
|
||||
assert context.result is None
|
||||
|
||||
# Call next to execute
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Context should now contain the result for observability
|
||||
assert context.result is not None
|
||||
@@ -374,9 +386,11 @@ class TestResultObservability:
|
||||
"""Test that middleware can override response after observing execution."""
|
||||
|
||||
class PostExecutionOverrideMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Call next to execute first
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Now observe and conditionally override
|
||||
assert context.result is not None
|
||||
@@ -409,10 +423,10 @@ class TestResultObservability:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Call next to execute first
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Now observe and conditionally override
|
||||
assert context.result is not None
|
||||
|
||||
@@ -44,9 +44,11 @@ class TestChatAgentClassBasedMiddleware:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
# Create ChatAgent with middleware
|
||||
@@ -74,9 +76,9 @@ class TestChatAgentClassBasedMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
middleware = TrackingFunctionMiddleware()
|
||||
ChatAgent(chat_client=chat_client, middleware=[middleware])
|
||||
@@ -94,10 +96,10 @@ class TestChatAgentClassBasedMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
middleware = TrackingFunctionMiddleware("function_middleware")
|
||||
@@ -120,11 +122,13 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class PreTerminationMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("middleware_before")
|
||||
raise MiddlewareTermination
|
||||
# Code after raise is unreachable
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("middleware_after")
|
||||
|
||||
# Create ChatAgent with terminating middleware
|
||||
@@ -149,9 +153,11 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class PostTerminationMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("middleware_after")
|
||||
context.terminate = True
|
||||
|
||||
@@ -187,12 +193,12 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("middleware_before")
|
||||
context.terminate = True
|
||||
# We call next() but since terminate=True, subsequent middleware and handler should not execute
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("middleware_after")
|
||||
|
||||
ChatAgent(chat_client=chat_client, middleware=[PreTerminationFunctionMiddleware()], tools=[])
|
||||
@@ -205,10 +211,10 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("middleware_after")
|
||||
context.terminate = True
|
||||
|
||||
@@ -219,10 +225,10 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
async def tracking_agent_middleware(
|
||||
context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("agent_function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("agent_function_after")
|
||||
|
||||
# Create ChatAgent with function middleware
|
||||
@@ -246,9 +252,9 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
"""Test function-based function middleware with ChatAgent."""
|
||||
|
||||
async def tracking_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
ChatAgent(chat_client=chat_client, middleware=[tracking_function_middleware])
|
||||
|
||||
@@ -259,10 +265,10 @@ class TestChatAgentFunctionBasedMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
async def tracking_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_function_after")
|
||||
|
||||
agent = ChatAgent(chat_client=chat_client_base, middleware=[tracking_function_middleware])
|
||||
@@ -284,10 +290,12 @@ class TestChatAgentStreamingMiddleware:
|
||||
streaming_flags: list[bool] = []
|
||||
|
||||
class StreamingTrackingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("middleware_before")
|
||||
streaming_flags.append(context.stream)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("middleware_after")
|
||||
|
||||
# Create ChatAgent with middleware
|
||||
@@ -326,9 +334,11 @@ class TestChatAgentStreamingMiddleware:
|
||||
streaming_flags: list[bool] = []
|
||||
|
||||
class FlagTrackingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
streaming_flags.append(context.stream)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Create ChatAgent with middleware
|
||||
middleware = FlagTrackingMiddleware()
|
||||
@@ -358,9 +368,11 @@ class TestChatAgentMultipleMiddlewareOrdering:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
# Create multiple middleware
|
||||
@@ -388,33 +400,35 @@ class TestChatAgentMultipleMiddlewareOrdering:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class ClassAgentMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("class_agent_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("class_agent_after")
|
||||
|
||||
async def function_agent_middleware(
|
||||
context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_agent_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_agent_after")
|
||||
|
||||
class ClassFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("class_function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("class_function_after")
|
||||
|
||||
async def function_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_function_after")
|
||||
|
||||
agent = ChatAgent(
|
||||
@@ -433,23 +447,25 @@ class TestChatAgentMultipleMiddlewareOrdering:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class ClassAgentMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("class_agent_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("class_agent_after")
|
||||
|
||||
async def function_agent_middleware(
|
||||
context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_agent_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_agent_after")
|
||||
|
||||
async def function_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_function_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_function_after")
|
||||
|
||||
agent = ChatAgent(
|
||||
@@ -505,10 +521,10 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append(f"{self.name}_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"{self.name}_after")
|
||||
|
||||
# Set up mock to return a function call first, then a regular response
|
||||
@@ -567,10 +583,10 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
execution_order: list[str] = []
|
||||
|
||||
async def tracking_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_middleware_after")
|
||||
|
||||
# Set up mock to return a function call first, then a regular response
|
||||
@@ -631,20 +647,20 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
async def process(
|
||||
self,
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("agent_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("agent_middleware_after")
|
||||
|
||||
class TrackingFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("function_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_middleware_after")
|
||||
|
||||
# Set up mock to return a function call first, then a regular response
|
||||
@@ -712,7 +728,7 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
|
||||
@function_middleware
|
||||
async def kwargs_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
nonlocal middleware_called
|
||||
middleware_called = True
|
||||
@@ -732,7 +748,7 @@ class TestChatAgentFunctionMiddlewareWithTools:
|
||||
modified_kwargs["new_param"] = context.kwargs.get("new_param")
|
||||
modified_kwargs["custom_param"] = context.kwargs.get("custom_param")
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
chat_client_base.run_responses = [
|
||||
ChatResponse(
|
||||
@@ -785,9 +801,9 @@ class TestMiddlewareDynamicRebuild:
|
||||
self.name = name
|
||||
self.execution_log = execution_log
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
self.execution_log.append(f"{self.name}_start")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
self.execution_log.append(f"{self.name}_end")
|
||||
|
||||
async def test_middleware_dynamic_rebuild_non_streaming(self, chat_client: "MockChatClient") -> None:
|
||||
@@ -908,9 +924,9 @@ class TestRunLevelMiddleware:
|
||||
self.name = name
|
||||
self.execution_log = execution_log
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
self.execution_log.append(f"{self.name}_start")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
self.execution_log.append(f"{self.name}_end")
|
||||
|
||||
async def test_run_level_middleware_isolation(self, chat_client: "MockChatClient") -> None:
|
||||
@@ -960,25 +976,29 @@ class TestRunLevelMiddleware:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_log.append(f"{self.name}_start")
|
||||
# Set metadata to pass information to run middleware
|
||||
context.metadata[f"{self.name}_key"] = f"{self.name}_value"
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_log.append(f"{self.name}_end")
|
||||
|
||||
class MetadataRunMiddleware(AgentMiddleware):
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_log.append(f"{self.name}_start")
|
||||
# Read metadata set by agent middleware
|
||||
for key, value in context.metadata.items():
|
||||
metadata_log.append(f"{self.name}_reads_{key}:{value}")
|
||||
# Set run-level metadata
|
||||
context.metadata[f"{self.name}_key"] = f"{self.name}_value"
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_log.append(f"{self.name}_end")
|
||||
|
||||
# Create agent with agent-level middleware
|
||||
@@ -1029,10 +1049,12 @@ class TestRunLevelMiddleware:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_log.append(f"{self.name}_start")
|
||||
streaming_flags.append(context.stream)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_log.append(f"{self.name}_end")
|
||||
|
||||
# Create agent without agent-level middleware
|
||||
@@ -1071,44 +1093,48 @@ class TestRunLevelMiddleware:
|
||||
|
||||
# Agent-level middleware
|
||||
class AgentLevelAgentMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_log.append("agent_level_agent_start")
|
||||
context.metadata["agent_level_agent"] = "processed"
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_log.append("agent_level_agent_end")
|
||||
|
||||
class AgentLevelFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_log.append("agent_level_function_start")
|
||||
context.metadata["agent_level_function"] = "processed"
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_log.append("agent_level_function_end")
|
||||
|
||||
# Run-level middleware
|
||||
class RunLevelAgentMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_log.append("run_level_agent_start")
|
||||
# Verify agent-level middleware metadata is available
|
||||
assert "agent_level_agent" in context.metadata
|
||||
context.metadata["run_level_agent"] = "processed"
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_log.append("run_level_agent_end")
|
||||
|
||||
class RunLevelFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_log.append("run_level_function_start")
|
||||
# Verify agent-level function middleware metadata is available
|
||||
assert "agent_level_function" in context.metadata
|
||||
context.metadata["run_level_function"] = "processed"
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_log.append("run_level_function_end")
|
||||
|
||||
# Create tool function for testing function middleware
|
||||
@@ -1192,17 +1218,17 @@ class TestMiddlewareDecoratorLogic:
|
||||
|
||||
@agent_middleware
|
||||
async def matching_agent_middleware(
|
||||
context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("decorator_type_match_agent")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
@function_middleware
|
||||
async def matching_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("decorator_type_match_function")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Create tool function for testing function middleware
|
||||
def custom_tool(message: str) -> str:
|
||||
@@ -1254,9 +1280,9 @@ class TestMiddlewareDecoratorLogic:
|
||||
@agent_middleware # type: ignore[arg-type]
|
||||
async def mismatched_middleware(
|
||||
context: FunctionInvocationContext, # Wrong type for @agent_middleware
|
||||
next: Any,
|
||||
call_next: Any,
|
||||
) -> None:
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
agent = ChatAgent(chat_client=chat_client, middleware=[mismatched_middleware])
|
||||
await agent.run([ChatMessage(role="user", text="test")])
|
||||
@@ -1266,14 +1292,14 @@ class TestMiddlewareDecoratorLogic:
|
||||
execution_order: list[str] = []
|
||||
|
||||
@agent_middleware
|
||||
async def decorator_only_agent(context: Any, next: Any) -> None: # No type annotation
|
||||
async def decorator_only_agent(context: Any, call_next: Any) -> None: # No type annotation
|
||||
execution_order.append("decorator_only_agent")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
@function_middleware
|
||||
async def decorator_only_function(context: Any, next: Any) -> None: # No type annotation
|
||||
async def decorator_only_function(context: Any, call_next: Any) -> None: # No type annotation
|
||||
execution_order.append("decorator_only_function")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Create tool function for testing function middleware
|
||||
def custom_tool(message: str) -> str:
|
||||
@@ -1320,16 +1346,16 @@ class TestMiddlewareDecoratorLogic:
|
||||
execution_order: list[str] = []
|
||||
|
||||
# No decorator
|
||||
async def type_only_agent(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def type_only_agent(context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("type_only_agent")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# No decorator
|
||||
async def type_only_function(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("type_only_function")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Create tool function for testing function middleware
|
||||
def custom_tool(message: str) -> str:
|
||||
@@ -1372,8 +1398,8 @@ class TestMiddlewareDecoratorLogic:
|
||||
async def test_neither_decorator_nor_type(self, chat_client: Any) -> None:
|
||||
"""Neither decorator nor parameter type specified - should throw exception."""
|
||||
|
||||
async def no_info_middleware(context: Any, next: Any) -> None: # No decorator, no type
|
||||
await next(context)
|
||||
async def no_info_middleware(context: Any, call_next: Any) -> None: # No decorator, no type
|
||||
await call_next(context)
|
||||
|
||||
# Should raise MiddlewareException
|
||||
with pytest.raises(MiddlewareException, match="Cannot determine middleware type"):
|
||||
@@ -1398,11 +1424,11 @@ class TestMiddlewareDecoratorLogic:
|
||||
"""Test that decorator markers are properly set on functions."""
|
||||
|
||||
@agent_middleware
|
||||
async def test_agent_middleware(context: Any, next: Any) -> None:
|
||||
async def test_agent_middleware(context: Any, call_next: Any) -> None:
|
||||
pass
|
||||
|
||||
@function_middleware
|
||||
async def test_function_middleware(context: Any, next: Any) -> None:
|
||||
async def test_function_middleware(context: Any, call_next: Any) -> None:
|
||||
pass
|
||||
|
||||
# Check that decorator markers were set
|
||||
@@ -1421,7 +1447,9 @@ class TestChatAgentThreadBehavior:
|
||||
thread_states: list[dict[str, Any]] = []
|
||||
|
||||
class ThreadTrackingMiddleware(AgentMiddleware):
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(
|
||||
self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Capture state before next() call
|
||||
thread_messages = []
|
||||
if context.thread and context.thread.message_store:
|
||||
@@ -1436,7 +1464,7 @@ class TestChatAgentThreadBehavior:
|
||||
}
|
||||
thread_states.append(before_state)
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Capture state after next() call
|
||||
thread_messages_after = []
|
||||
@@ -1532,9 +1560,9 @@ class TestChatAgentChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class TrackingChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("chat_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("chat_middleware_after")
|
||||
|
||||
# Create ChatAgent with chat middleware
|
||||
@@ -1561,10 +1589,10 @@ class TestChatAgentChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
async def tracking_chat_middleware(
|
||||
context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("chat_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("chat_middleware_after")
|
||||
|
||||
# Create ChatAgent with function-based chat middleware
|
||||
@@ -1590,7 +1618,7 @@ class TestChatAgentChatMiddleware:
|
||||
|
||||
@chat_middleware
|
||||
async def message_modifier_middleware(
|
||||
context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Modify the first message by adding a prefix
|
||||
if context.messages:
|
||||
@@ -1600,7 +1628,7 @@ class TestChatAgentChatMiddleware:
|
||||
original_text = msg.text or ""
|
||||
context.messages[idx] = ChatMessage(role=msg.role, text=f"MODIFIED: {original_text}")
|
||||
break
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Create ChatAgent with message-modifying middleware
|
||||
chat_client = MockBaseChatClient()
|
||||
@@ -1619,7 +1647,7 @@ class TestChatAgentChatMiddleware:
|
||||
|
||||
@chat_middleware
|
||||
async def response_override_middleware(
|
||||
context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Override the response without calling next()
|
||||
context.result = ChatResponse(
|
||||
@@ -1647,15 +1675,15 @@ class TestChatAgentChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
@chat_middleware
|
||||
async def first_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def first_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("first_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("first_after")
|
||||
|
||||
@chat_middleware
|
||||
async def second_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def second_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("second_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("second_after")
|
||||
|
||||
# Create ChatAgent with multiple chat middleware
|
||||
@@ -1681,10 +1709,10 @@ class TestChatAgentChatMiddleware:
|
||||
streaming_flags: list[bool] = []
|
||||
|
||||
class StreamingTrackingChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("streaming_chat_before")
|
||||
streaming_flags.append(context.stream)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("streaming_chat_after")
|
||||
|
||||
# Create ChatAgent with chat middleware
|
||||
@@ -1721,13 +1749,13 @@ class TestChatAgentChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class PreTerminationChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("middleware_before")
|
||||
# Set a custom response since we're terminating
|
||||
context.result = ChatResponse(messages=[ChatMessage(role="assistant", text="Terminated by middleware")])
|
||||
raise MiddlewareTermination
|
||||
# We call next() but since terminate=True, execution should stop
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("middleware_after")
|
||||
|
||||
# Create ChatAgent with terminating middleware
|
||||
@@ -1749,9 +1777,9 @@ class TestChatAgentChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
class PostTerminationChatMiddleware(ChatMiddleware):
|
||||
async def process(self, context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("middleware_after")
|
||||
context.terminate = True
|
||||
|
||||
@@ -1776,21 +1804,21 @@ class TestChatAgentChatMiddleware:
|
||||
"""Test ChatAgent with combined middleware types."""
|
||||
execution_order: list[str] = []
|
||||
|
||||
async def agent_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def agent_middleware(context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("agent_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("agent_middleware_after")
|
||||
|
||||
async def chat_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def chat_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("chat_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("chat_middleware_after")
|
||||
|
||||
async def function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_middleware_after")
|
||||
|
||||
# Create ChatAgent with function middleware and tools
|
||||
@@ -1814,7 +1842,9 @@ class TestChatAgentChatMiddleware:
|
||||
modified_kwargs: dict[str, Any] = {}
|
||||
|
||||
@agent_middleware
|
||||
async def kwargs_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def kwargs_middleware(
|
||||
context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Capture the original kwargs
|
||||
captured_kwargs.update(context.kwargs)
|
||||
|
||||
@@ -1826,7 +1856,7 @@ class TestChatAgentChatMiddleware:
|
||||
# Store modified kwargs for verification
|
||||
modified_kwargs.update(context.kwargs)
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Create ChatAgent with agent middleware
|
||||
chat_client = MockBaseChatClient()
|
||||
@@ -1865,10 +1895,10 @@ class TestChatAgentChatMiddleware:
|
||||
|
||||
# class TrackingMiddleware(AgentMiddleware):
|
||||
# async def process(
|
||||
# self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]
|
||||
# self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]
|
||||
# ) -> None:
|
||||
# execution_order.append("before")
|
||||
# await next(context)
|
||||
# await call_next(context)
|
||||
# execution_order.append("after")
|
||||
|
||||
# @use_agent_middleware
|
||||
|
||||
@@ -32,10 +32,10 @@ class TestChatMiddleware:
|
||||
async def process(
|
||||
self,
|
||||
context: ChatContext,
|
||||
next: Callable[[ChatContext], Awaitable[None]],
|
||||
call_next: Callable[[ChatContext], Awaitable[None]],
|
||||
) -> None:
|
||||
execution_order.append("chat_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("chat_middleware_after")
|
||||
|
||||
# Add middleware to chat client
|
||||
@@ -58,9 +58,11 @@ class TestChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
@chat_middleware
|
||||
async def logging_chat_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def logging_chat_middleware(
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("function_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("function_middleware_after")
|
||||
|
||||
# Add middleware to chat client
|
||||
@@ -83,13 +85,13 @@ class TestChatMiddleware:
|
||||
|
||||
@chat_middleware
|
||||
async def message_modifier_middleware(
|
||||
context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Modify the first message by adding a prefix
|
||||
if context.messages and len(context.messages) > 0:
|
||||
original_text = context.messages[0].text or ""
|
||||
context.messages[0] = ChatMessage(role=context.messages[0].role, text=f"MODIFIED: {original_text}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Add middleware to chat client
|
||||
chat_client_base.chat_middleware = [message_modifier_middleware]
|
||||
@@ -109,7 +111,7 @@ class TestChatMiddleware:
|
||||
|
||||
@chat_middleware
|
||||
async def response_override_middleware(
|
||||
context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
# Override the response without calling next()
|
||||
context.result = ChatResponse(
|
||||
@@ -136,15 +138,15 @@ class TestChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
@chat_middleware
|
||||
async def first_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def first_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("first_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("first_after")
|
||||
|
||||
@chat_middleware
|
||||
async def second_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def second_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("second_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("second_after")
|
||||
|
||||
# Add middleware to chat client (order should be preserved)
|
||||
@@ -172,10 +174,10 @@ class TestChatMiddleware:
|
||||
|
||||
@chat_middleware
|
||||
async def agent_level_chat_middleware(
|
||||
context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("agent_chat_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("agent_chat_middleware_after")
|
||||
|
||||
chat_client = MockBaseChatClient()
|
||||
@@ -203,15 +205,15 @@ class TestChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
@chat_middleware
|
||||
async def first_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def first_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("first_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("first_after")
|
||||
|
||||
@chat_middleware
|
||||
async def second_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def second_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
execution_order.append("second_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("second_after")
|
||||
|
||||
# Create ChatAgent with multiple chat middleware
|
||||
@@ -238,7 +240,9 @@ class TestChatMiddleware:
|
||||
execution_order: list[str] = []
|
||||
|
||||
@chat_middleware
|
||||
async def streaming_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def streaming_middleware(
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("streaming_before")
|
||||
# Verify it's a streaming context
|
||||
assert context.stream is True
|
||||
@@ -250,7 +254,7 @@ class TestChatMiddleware:
|
||||
return update
|
||||
|
||||
context.stream_transform_hooks.append(upper_case_update)
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("streaming_after")
|
||||
|
||||
# Add middleware to chat client
|
||||
@@ -274,9 +278,11 @@ class TestChatMiddleware:
|
||||
execution_count = {"count": 0}
|
||||
|
||||
@chat_middleware
|
||||
async def counting_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def counting_middleware(
|
||||
context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_count["count"] += 1
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# First call with run-level middleware
|
||||
messages = [ChatMessage(role="user", text="first message")]
|
||||
@@ -304,7 +310,7 @@ class TestChatMiddleware:
|
||||
modified_kwargs: dict[str, Any] = {}
|
||||
|
||||
@chat_middleware
|
||||
async def kwargs_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def kwargs_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
# Capture the original kwargs
|
||||
captured_kwargs.update(context.kwargs)
|
||||
|
||||
@@ -316,7 +322,7 @@ class TestChatMiddleware:
|
||||
# Store modified kwargs for verification
|
||||
modified_kwargs.update(context.kwargs)
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Add middleware to chat client
|
||||
chat_client_base.chat_middleware = [kwargs_middleware]
|
||||
@@ -349,11 +355,11 @@ class TestChatMiddleware:
|
||||
|
||||
@function_middleware
|
||||
async def test_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
nonlocal execution_order
|
||||
execution_order.append(f"function_middleware_before_{context.function.name}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append(f"function_middleware_after_{context.function.name}")
|
||||
|
||||
# Define a simple tool function
|
||||
@@ -415,10 +421,10 @@ class TestChatMiddleware:
|
||||
|
||||
@function_middleware
|
||||
async def run_level_function_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
execution_order.append("run_level_function_middleware_before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
execution_order.append("run_level_function_middleware_after")
|
||||
|
||||
# Define a simple tool function
|
||||
|
||||
@@ -70,6 +70,102 @@ def test_tool_decorator_without_args():
|
||||
assert test_tool.approval_mode == "never_require"
|
||||
|
||||
|
||||
def test_tool_decorator_with_pydantic_schema():
|
||||
"""Test that the tool decorator accepts an explicit Pydantic model schema."""
|
||||
from pydantic import Field
|
||||
|
||||
class MyInput(BaseModel):
|
||||
location: Annotated[str, Field(description="City name")]
|
||||
unit: str = "celsius"
|
||||
|
||||
@tool(name="weather", description="Get weather", schema=MyInput)
|
||||
def get_weather(location: str, unit: str = "celsius") -> str:
|
||||
return f"{location}: {unit}"
|
||||
|
||||
assert isinstance(get_weather, FunctionTool)
|
||||
assert get_weather.name == "weather"
|
||||
params = get_weather.parameters()
|
||||
assert "location" in params["properties"]
|
||||
assert params["properties"]["location"].get("description") == "City name"
|
||||
assert get_weather("Seattle") == "Seattle: celsius"
|
||||
assert get_weather("Seattle", "fahrenheit") == "Seattle: fahrenheit"
|
||||
|
||||
|
||||
def test_tool_decorator_with_json_schema_dict():
|
||||
"""Test that the tool decorator accepts an explicit JSON schema dict."""
|
||||
|
||||
json_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"},
|
||||
"max_results": {"type": "integer", "default": 10},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
|
||||
@tool(name="search", description="Search tool", schema=json_schema)
|
||||
def search(query: str, max_results: int = 10) -> str:
|
||||
return f"Searching for: {query} (max {max_results})"
|
||||
|
||||
assert isinstance(search, FunctionTool)
|
||||
params = search.parameters()
|
||||
assert params["properties"]["query"]["type"] == "string"
|
||||
assert params["properties"]["query"]["description"] == "Search query"
|
||||
assert "max_results" in params["properties"]
|
||||
assert search("hello") == "Searching for: hello (max 10)"
|
||||
|
||||
|
||||
def test_tool_decorator_schema_none_default():
|
||||
"""Test that schema=None (default) still infers from function signature."""
|
||||
|
||||
@tool(name="adder", schema=None)
|
||||
def add(x: int, y: int) -> int:
|
||||
return x + y
|
||||
|
||||
assert isinstance(add, FunctionTool)
|
||||
params = add.parameters()
|
||||
assert params == {
|
||||
"properties": {"x": {"title": "X", "type": "integer"}, "y": {"title": "Y", "type": "integer"}},
|
||||
"required": ["x", "y"],
|
||||
"title": "adder_input",
|
||||
"type": "object",
|
||||
}
|
||||
assert add(1, 2) == 3
|
||||
|
||||
|
||||
async def test_tool_decorator_with_schema_invoke():
|
||||
"""Test that invoke works correctly with explicit schema."""
|
||||
|
||||
class CalcInput(BaseModel):
|
||||
a: int
|
||||
b: int
|
||||
|
||||
@tool(name="calc", description="Calculator", schema=CalcInput)
|
||||
def calculate(a: int, b: int) -> int:
|
||||
return a + b
|
||||
|
||||
result = await calculate.invoke(arguments=CalcInput(a=3, b=7))
|
||||
assert result == 10
|
||||
|
||||
|
||||
def test_tool_decorator_with_schema_overrides_annotations():
|
||||
"""Test that explicit schema completely overrides function signature inference."""
|
||||
from pydantic import Field
|
||||
|
||||
class DetailedInput(BaseModel):
|
||||
location: Annotated[str, Field(description="The city and state")]
|
||||
unit: Annotated[str, Field(description="Temperature unit")] = "celsius"
|
||||
|
||||
@tool(schema=DetailedInput)
|
||||
def get_weather(location: str, unit: str = "celsius") -> str:
|
||||
"""Get weather for a location."""
|
||||
return f"{location}: {unit}"
|
||||
|
||||
params = get_weather.parameters()
|
||||
assert params["properties"]["location"].get("description") == "The city and state"
|
||||
assert params["properties"]["unit"].get("description") == "Temperature unit"
|
||||
|
||||
|
||||
def test_tool_without_args():
|
||||
"""Test the tool decorator."""
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from agent_framework import (
|
||||
ChatResponse,
|
||||
ChatResponseUpdate,
|
||||
Content,
|
||||
FunctionTool,
|
||||
ResponseStream,
|
||||
WorkflowBuilder,
|
||||
WorkflowContext,
|
||||
@@ -384,3 +385,207 @@ async def test_agent_executor_parallel_tool_call_with_approval_streaming() -> No
|
||||
# Assert
|
||||
assert output is not None
|
||||
assert output == "Tool executed successfully."
|
||||
|
||||
|
||||
# --- Declaration-only tool tests ---
|
||||
|
||||
declaration_only_tool = FunctionTool(
|
||||
name="client_side_tool",
|
||||
func=None,
|
||||
description="A client-side tool that the framework cannot execute.",
|
||||
input_model={"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
|
||||
)
|
||||
|
||||
|
||||
class DeclarationOnlyMockChatClient(FunctionInvocationLayer[Any], BaseChatClient[Any]):
|
||||
"""Mock chat client that calls a declaration-only tool on first iteration."""
|
||||
|
||||
def __init__(self, parallel_request: bool = False) -> None:
|
||||
FunctionInvocationLayer.__init__(self)
|
||||
BaseChatClient.__init__(self)
|
||||
self._iteration: int = 0
|
||||
self._parallel_request: bool = parallel_request
|
||||
|
||||
def _inner_get_response(
|
||||
self,
|
||||
*,
|
||||
messages: Sequence[ChatMessage],
|
||||
stream: bool,
|
||||
options: Mapping[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
|
||||
if stream:
|
||||
return self._build_response_stream(self._stream_response())
|
||||
|
||||
async def _get_response() -> ChatResponse:
|
||||
return self._create_response()
|
||||
|
||||
return _get_response()
|
||||
|
||||
def _create_response(self) -> ChatResponse:
|
||||
if self._iteration == 0:
|
||||
if self._parallel_request:
|
||||
response = ChatResponse(
|
||||
messages=ChatMessage(
|
||||
"assistant",
|
||||
[
|
||||
Content.from_function_call(
|
||||
call_id="1", name="client_side_tool", arguments='{"query": "test"}'
|
||||
),
|
||||
Content.from_function_call(
|
||||
call_id="2", name="client_side_tool", arguments='{"query": "test2"}'
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
response = ChatResponse(
|
||||
messages=ChatMessage(
|
||||
"assistant",
|
||||
[
|
||||
Content.from_function_call(
|
||||
call_id="1", name="client_side_tool", arguments='{"query": "test"}'
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
else:
|
||||
response = ChatResponse(messages=ChatMessage("assistant", ["Tool executed successfully."]))
|
||||
|
||||
self._iteration += 1
|
||||
return response
|
||||
|
||||
async def _stream_response(self) -> AsyncIterable[ChatResponseUpdate]:
|
||||
if self._iteration == 0:
|
||||
if self._parallel_request:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[
|
||||
Content.from_function_call(call_id="1", name="client_side_tool", arguments='{"query": "test"}'),
|
||||
Content.from_function_call(
|
||||
call_id="2", name="client_side_tool", arguments='{"query": "test2"}'
|
||||
),
|
||||
],
|
||||
role="assistant",
|
||||
)
|
||||
else:
|
||||
yield ChatResponseUpdate(
|
||||
contents=[
|
||||
Content.from_function_call(call_id="1", name="client_side_tool", arguments='{"query": "test"}')
|
||||
],
|
||||
role="assistant",
|
||||
)
|
||||
else:
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="Tool executed ")], role="assistant")
|
||||
yield ChatResponseUpdate(contents=[Content.from_text(text="successfully.")], role="assistant")
|
||||
|
||||
self._iteration += 1
|
||||
|
||||
|
||||
async def test_agent_executor_declaration_only_tool_emits_request_info() -> None:
|
||||
"""Test that AgentExecutor emits request_info when agent calls a declaration-only tool."""
|
||||
agent = ChatAgent(
|
||||
chat_client=DeclarationOnlyMockChatClient(),
|
||||
name="DeclarationOnlyAgent",
|
||||
tools=[declaration_only_tool],
|
||||
)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build()
|
||||
)
|
||||
|
||||
# Act
|
||||
events = await workflow.run("Use the client side tool")
|
||||
|
||||
# Assert - workflow should pause with a request_info event
|
||||
request_info_events = events.get_request_info_events()
|
||||
assert len(request_info_events) == 1
|
||||
request = request_info_events[0]
|
||||
assert request.data.type == "function_call"
|
||||
assert request.data.name == "client_side_tool"
|
||||
assert request.data.call_id == "1"
|
||||
|
||||
# Act - provide the function result to resume the workflow
|
||||
events = await workflow.run(
|
||||
responses={
|
||||
request.request_id: Content.from_function_result(call_id=request.data.call_id, result="client result")
|
||||
}
|
||||
)
|
||||
|
||||
# Assert - workflow should complete
|
||||
final_response = events.get_outputs()
|
||||
assert len(final_response) == 1
|
||||
assert final_response[0] == "Tool executed successfully."
|
||||
|
||||
|
||||
async def test_agent_executor_declaration_only_tool_emits_request_info_streaming() -> None:
|
||||
"""Test that AgentExecutor emits request_info for declaration-only tools in streaming mode."""
|
||||
agent = ChatAgent(
|
||||
chat_client=DeclarationOnlyMockChatClient(),
|
||||
name="DeclarationOnlyAgent",
|
||||
tools=[declaration_only_tool],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder(start_executor=agent).add_edge(agent, test_executor).build()
|
||||
|
||||
# Act
|
||||
request_info_events: list[WorkflowEvent] = []
|
||||
async for event in workflow.run("Use the client side tool", stream=True):
|
||||
if event.type == "request_info":
|
||||
request_info_events.append(event)
|
||||
|
||||
# Assert
|
||||
assert len(request_info_events) == 1
|
||||
request = request_info_events[0]
|
||||
assert request.data.type == "function_call"
|
||||
assert request.data.name == "client_side_tool"
|
||||
assert request.data.call_id == "1"
|
||||
|
||||
# Act - provide the function result
|
||||
output: str | None = None
|
||||
async for event in workflow.run(
|
||||
stream=True,
|
||||
responses={
|
||||
request.request_id: Content.from_function_result(call_id=request.data.call_id, result="client result")
|
||||
},
|
||||
):
|
||||
if event.type == "output":
|
||||
output = event.data
|
||||
|
||||
# Assert
|
||||
assert output is not None
|
||||
assert output == "Tool executed successfully."
|
||||
|
||||
|
||||
async def test_agent_executor_parallel_declaration_only_tool_emits_request_info() -> None:
|
||||
"""Test that AgentExecutor emits request_info for parallel declaration-only tool calls."""
|
||||
agent = ChatAgent(
|
||||
chat_client=DeclarationOnlyMockChatClient(parallel_request=True),
|
||||
name="DeclarationOnlyAgent",
|
||||
tools=[declaration_only_tool],
|
||||
)
|
||||
|
||||
workflow = (
|
||||
WorkflowBuilder(start_executor=agent, output_executors=[test_executor]).add_edge(agent, test_executor).build()
|
||||
)
|
||||
|
||||
# Act
|
||||
events = await workflow.run("Use the client side tool")
|
||||
|
||||
# Assert - should get 2 request_info events
|
||||
request_info_events = events.get_request_info_events()
|
||||
assert len(request_info_events) == 2
|
||||
for req in request_info_events:
|
||||
assert req.data.type == "function_call"
|
||||
assert req.data.name == "client_side_tool"
|
||||
|
||||
# Act - provide both function results
|
||||
responses = {
|
||||
req.request_id: Content.from_function_result(call_id=req.data.call_id, result=f"result for {req.data.call_id}")
|
||||
for req in request_info_events
|
||||
}
|
||||
events = await workflow.run(responses=responses)
|
||||
|
||||
# Assert - workflow should complete
|
||||
final_response = events.get_outputs()
|
||||
assert len(final_response) == 1
|
||||
assert final_response[0] == "Tool executed successfully."
|
||||
|
||||
@@ -20,8 +20,13 @@ YAML/JSON-based declarative agent and workflow definitions.
|
||||
```python
|
||||
from agent_framework.declarative import AgentFactory, WorkflowFactory
|
||||
|
||||
agent = AgentFactory.create_from_file("agent.yaml")
|
||||
workflow = WorkflowFactory.create_from_file("workflow.yaml")
|
||||
# Create agent from YAML file
|
||||
agent_factory = AgentFactory()
|
||||
agent = agent_factory.create_agent_from_yaml_path("agent.yaml")
|
||||
|
||||
# Create workflow from YAML file
|
||||
workflow_factory = WorkflowFactory()
|
||||
workflow = workflow_factory.create_workflow_from_yaml_path("workflow.yaml")
|
||||
```
|
||||
|
||||
## Import Path
|
||||
|
||||
@@ -29,14 +29,29 @@ Durable execution support for long-running agent workflows using Azure Durable F
|
||||
## Usage
|
||||
|
||||
```python
|
||||
from durabletask.client import TaskHubGrpcClient
|
||||
from durabletask.worker import TaskHubGrpcWorker
|
||||
from agent_framework import ChatAgent
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from agent_framework_durabletask import DurableAIAgentClient, DurableAIAgentWorker
|
||||
|
||||
# Client side
|
||||
client = DurableAIAgentClient(endpoint="https://your-functions.azurewebsites.net")
|
||||
response = await client.run("Hello")
|
||||
dt_client = TaskHubGrpcClient(host_address="localhost:4001")
|
||||
agent_client = DurableAIAgentClient(dt_client)
|
||||
agent = agent_client.get_agent("assistant")
|
||||
response = agent.run("Hello, how are you?")
|
||||
print(response.text)
|
||||
|
||||
# Worker side
|
||||
worker = DurableAIAgentWorker(agent=my_agent)
|
||||
dt_worker = TaskHubGrpcWorker(host_address="localhost:4001")
|
||||
agent_worker = DurableAIAgentWorker(dt_worker)
|
||||
|
||||
# Create a chat client for the agent
|
||||
chat_client = AzureOpenAIChatClient()
|
||||
my_agent = ChatAgent(chat_client=chat_client, name="assistant")
|
||||
agent_worker.add_agent(my_agent)
|
||||
|
||||
dt_worker.start()
|
||||
```
|
||||
|
||||
## Import Path
|
||||
|
||||
@@ -207,8 +207,8 @@ def test_serialize(ollama_unit_test_env: dict[str, str]) -> None:
|
||||
|
||||
def test_chat_middleware(ollama_unit_test_env: dict[str, str]) -> None:
|
||||
@chat_middleware
|
||||
async def sample_middleware(context, next):
|
||||
await next(context)
|
||||
async def sample_middleware(context, call_next):
|
||||
await call_next(context)
|
||||
|
||||
ollama_chat_client = OllamaChatClient(middleware=[sample_middleware])
|
||||
assert len(ollama_chat_client.middleware) == 1
|
||||
|
||||
@@ -129,11 +129,11 @@ class _AutoHandoffMiddleware(FunctionMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Intercept matching handoff tool calls and inject synthetic results."""
|
||||
if context.function.name not in self._handoff_functions:
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
return
|
||||
|
||||
from agent_framework._middleware import MiddlewareTermination
|
||||
|
||||
@@ -48,7 +48,7 @@ class PurviewPolicyMiddleware(AgentMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None: # type: ignore[override]
|
||||
resolved_user_id: str | None = None
|
||||
try:
|
||||
@@ -74,7 +74,7 @@ class PurviewPolicyMiddleware(AgentMiddleware):
|
||||
if not self._settings.ignore_exceptions:
|
||||
raise
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
try:
|
||||
# Post (response) check only if we have a normal AgentResponse
|
||||
@@ -140,7 +140,7 @@ class PurviewChatPolicyMiddleware(ChatMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: ChatContext,
|
||||
next: Callable[[ChatContext], Awaitable[None]],
|
||||
call_next: Callable[[ChatContext], Awaitable[None]],
|
||||
) -> None: # type: ignore[override]
|
||||
resolved_user_id: str | None = None
|
||||
try:
|
||||
@@ -164,7 +164,7 @@ class PurviewChatPolicyMiddleware(ChatMiddleware):
|
||||
if not self._settings.ignore_exceptions:
|
||||
raise
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
try:
|
||||
# Post (response) evaluation only if non-streaming and we have messages result shape
|
||||
|
||||
@@ -37,7 +37,7 @@ sequenceDiagram
|
||||
AML->>AMP: execute(AgentContext)
|
||||
|
||||
loop Agent Middleware Chain
|
||||
AMP->>AMP: middleware[i].process(context, next)
|
||||
AMP->>AMP: middleware[i].process(context, call_next)
|
||||
Note right of AMP: Can modify: messages, options, thread
|
||||
end
|
||||
|
||||
@@ -60,7 +60,7 @@ sequenceDiagram
|
||||
CML->>CMP: execute(ChatContext)
|
||||
|
||||
loop Chat Middleware Chain
|
||||
CMP->>CMP: middleware[i].process(context, next)
|
||||
CMP->>CMP: middleware[i].process(context, call_next)
|
||||
Note right of CMP: Can modify: messages, options
|
||||
end
|
||||
|
||||
@@ -81,7 +81,7 @@ sequenceDiagram
|
||||
loop For each function_call
|
||||
FIL->>FMP: execute(FunctionInvocationContext)
|
||||
loop Function Middleware Chain
|
||||
FMP->>FMP: middleware[i].process(context, next)
|
||||
FMP->>FMP: middleware[i].process(context, call_next)
|
||||
Note right of FMP: Can modify: arguments
|
||||
end
|
||||
FMP->>Tool: invoke(arguments)
|
||||
@@ -137,7 +137,7 @@ sequenceDiagram
|
||||
| `options` | `Mapping[str, Any]` | Chat options dict |
|
||||
| `stream` | `bool` | Whether streaming is enabled |
|
||||
| `metadata` | `dict` | Shared data between middleware |
|
||||
| `result` | `AgentResponse \| None` | Set after `next()` is called |
|
||||
| `result` | `AgentResponse \| None` | Set after `call_next()` is called |
|
||||
| `kwargs` | `Mapping[str, Any]` | Additional run arguments |
|
||||
|
||||
**Key Operations:**
|
||||
@@ -150,7 +150,7 @@ sequenceDiagram
|
||||
- `context.messages` - Add, remove, or modify input messages
|
||||
- `context.options` - Change model parameters, temperature, etc.
|
||||
- `context.thread` - Replace or modify the thread
|
||||
- `context.result` - Override the final response (after `next()`)
|
||||
- `context.result` - Override the final response (after `call_next()`)
|
||||
|
||||
### 2. Chat Middleware Layer (`ChatMiddlewareLayer`)
|
||||
|
||||
@@ -165,7 +165,7 @@ sequenceDiagram
|
||||
| `options` | `Mapping[str, Any]` | Chat options |
|
||||
| `stream` | `bool` | Whether streaming |
|
||||
| `metadata` | `dict` | Shared data between middleware |
|
||||
| `result` | `ChatResponse \| None` | Set after `next()` is called |
|
||||
| `result` | `ChatResponse \| None` | Set after `call_next()` is called |
|
||||
| `kwargs` | `Mapping[str, Any]` | Additional arguments |
|
||||
|
||||
**Key Operations:**
|
||||
@@ -176,7 +176,7 @@ sequenceDiagram
|
||||
**What Can Be Modified:**
|
||||
- `context.messages` - Inject system prompts, filter content
|
||||
- `context.options` - Change model, temperature, tool_choice
|
||||
- `context.result` - Override the response (after `next()`)
|
||||
- `context.result` - Override the response (after `call_next()`)
|
||||
|
||||
### 3. Function Invocation Layer (`FunctionInvocationLayer`)
|
||||
|
||||
@@ -251,23 +251,23 @@ response = await client.get_response(
|
||||
| `function` | `FunctionTool` | The function being invoked |
|
||||
| `arguments` | `BaseModel` | Validated Pydantic arguments |
|
||||
| `metadata` | `dict` | Shared data between middleware |
|
||||
| `result` | `Any` | Set after `next()` is called |
|
||||
| `result` | `Any` | Set after `call_next()` is called |
|
||||
| `kwargs` | `Mapping[str, Any]` | Runtime kwargs |
|
||||
|
||||
**What Can Be Modified:**
|
||||
- `context.arguments` - Modify validated arguments before execution
|
||||
- `context.result` - Override the function result (after `next()`)
|
||||
- `context.result` - Override the function result (after `call_next()`)
|
||||
- Raise `MiddlewareTermination` to skip execution and terminate the function invocation loop
|
||||
|
||||
**Special Behavior:** When `MiddlewareTermination` is raised in function middleware, it signals that the function invocation loop should exit **without making another LLM call**. This is useful when middleware determines that no further processing is needed (e.g., a termination condition is met).
|
||||
|
||||
```python
|
||||
class TerminatingMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
async def process(self, context: FunctionInvocationContext, call_next):
|
||||
if self.should_terminate(context):
|
||||
context.result = "terminated by middleware"
|
||||
raise MiddlewareTermination # Exit function invocation loop
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
```
|
||||
|
||||
## Arguments Added/Altered at Each Layer
|
||||
@@ -334,20 +334,20 @@ class TerminatingMiddleware(FunctionMiddleware):
|
||||
|
||||
There are three ways to exit a middleware's `process()` method:
|
||||
|
||||
### 1. Return Normally (with or without calling `next`)
|
||||
### 1. Return Normally (with or without calling `call_next`)
|
||||
|
||||
Returns control to the upstream middleware, allowing its post-processing code to run.
|
||||
|
||||
```python
|
||||
class CachingMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
# Option A: Return early WITHOUT calling next (skip downstream)
|
||||
async def process(self, context: FunctionInvocationContext, call_next):
|
||||
# Option A: Return early WITHOUT calling call_next (skip downstream)
|
||||
if cached := self.cache.get(context.function.name):
|
||||
context.result = cached
|
||||
return # Upstream post-processing still runs
|
||||
|
||||
# Option B: Call next, then return normally
|
||||
await next(context)
|
||||
# Option B: Call call_next, then return normally
|
||||
await call_next(context)
|
||||
self.cache[context.function.name] = context.result
|
||||
return # Normal completion
|
||||
```
|
||||
@@ -358,11 +358,11 @@ Immediately exits the entire middleware chain. Upstream middleware's post-proces
|
||||
|
||||
```python
|
||||
class BlockedFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
async def process(self, context: FunctionInvocationContext, call_next):
|
||||
if context.function.name in self.blocked_functions:
|
||||
context.result = "Function blocked by policy"
|
||||
raise MiddlewareTermination("Blocked") # Skips ALL post-processing
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
```
|
||||
|
||||
### 3. Raise Any Other Exception
|
||||
@@ -371,10 +371,10 @@ Bubbles up to the caller. The middleware chain is aborted and the exception prop
|
||||
|
||||
```python
|
||||
class ValidationMiddleware(FunctionMiddleware):
|
||||
async def process(self, context: FunctionInvocationContext, next):
|
||||
async def process(self, context: FunctionInvocationContext, call_next):
|
||||
if not self.is_valid(context.arguments):
|
||||
raise ValueError("Invalid arguments") # Bubbles up to user
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
```
|
||||
|
||||
## `return` vs `raise MiddlewareTermination`
|
||||
@@ -383,13 +383,13 @@ The key difference is what happens to **upstream middleware's post-processing**:
|
||||
|
||||
```python
|
||||
class MiddlewareA(AgentMiddleware):
|
||||
async def process(self, context, next):
|
||||
async def process(self, context, call_next):
|
||||
print("A: before")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print("A: after") # Does this run?
|
||||
|
||||
class MiddlewareB(AgentMiddleware):
|
||||
async def process(self, context, next):
|
||||
async def process(self, context, call_next):
|
||||
print("B: before")
|
||||
context.result = "early result"
|
||||
# Choose one:
|
||||
@@ -408,14 +408,14 @@ With middleware registered as `[MiddlewareA, MiddlewareB]`:
|
||||
|
||||
**Use `raise MiddlewareTermination`** when you want to completely bypass all remaining processing (e.g., blocking a request, returning cached response without any modification).
|
||||
|
||||
## Calling `next()` or Not
|
||||
## Calling `call_next()` or Not
|
||||
|
||||
The decision to call `next(context)` determines whether downstream middleware and the actual operation execute:
|
||||
The decision to call `call_next(context)` determines whether downstream middleware and the actual operation execute:
|
||||
|
||||
### Without calling `next()` - Skip downstream
|
||||
### Without calling `call_next()` - Skip downstream
|
||||
|
||||
```python
|
||||
async def process(self, context, next):
|
||||
async def process(self, context, call_next):
|
||||
context.result = "replacement result"
|
||||
return # Downstream middleware and actual execution are SKIPPED
|
||||
```
|
||||
@@ -425,12 +425,12 @@ async def process(self, context, next):
|
||||
- Upstream middleware post-processing: ✅ Still runs (unless `MiddlewareTermination` raised)
|
||||
- Result: Whatever you set in `context.result`
|
||||
|
||||
### With calling `next()` - Full execution
|
||||
### With calling `call_next()` - Full execution
|
||||
|
||||
```python
|
||||
async def process(self, context, next):
|
||||
async def process(self, context, call_next):
|
||||
# Pre-processing
|
||||
await next(context) # Execute downstream + actual operation
|
||||
await call_next(context) # Execute downstream + actual operation
|
||||
# Post-processing (context.result now contains real result)
|
||||
return
|
||||
```
|
||||
@@ -442,7 +442,7 @@ async def process(self, context, next):
|
||||
|
||||
### Summary Table
|
||||
|
||||
| Exit Method | Call `next()`? | Downstream Executes? | Actual Op Executes? | Upstream Post-Processing? |
|
||||
| Exit Method | Call `call_next()`? | Downstream Executes? | Actual Op Executes? | Upstream Post-Processing? |
|
||||
|-------------|----------------|---------------------|---------------------|--------------------------|
|
||||
| `return` (or implicit) | Yes | ✅ | ✅ | ✅ Yes |
|
||||
| `return` | No | ❌ | ❌ | ✅ Yes |
|
||||
@@ -450,7 +450,7 @@ async def process(self, context, next):
|
||||
| `raise MiddlewareTermination` | Yes | ✅ | ✅ | ❌ No |
|
||||
| `raise OtherException` | Either | Depends | Depends | ❌ No (exception propagates) |
|
||||
|
||||
> **Note:** The first row (`return` after calling `next()`) is the default behavior. Python functions implicitly return `None` at the end, so simply calling `await next(context)` without an explicit `return` statement achieves this pattern.
|
||||
> **Note:** The first row (`return` after calling `call_next()`) is the default behavior. Python functions implicitly return `None` at the end, so simply calling `await call_next(context)` without an explicit `return` statement achieves this pattern.
|
||||
|
||||
## Streaming vs Non-Streaming
|
||||
|
||||
|
||||
@@ -20,13 +20,13 @@ multiple specialized agents, each focusing on specific tasks.
|
||||
|
||||
async def logging_middleware(
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""MiddlewareTypes that logs tool invocations to show the delegation flow."""
|
||||
print(f"[Calling tool: {context.function.name}]")
|
||||
print(f"[Request: {context.arguments}]")
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
print(f"[Response: {context.result}]")
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ response generation, showing both streaming and non-streaming responses.
|
||||
@chat_middleware
|
||||
async def security_and_override_middleware(
|
||||
context: ChatContext,
|
||||
next: Callable[[ChatContext], Awaitable[None]],
|
||||
call_next: Callable[[ChatContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Function-based middleware that implements security filtering and response override."""
|
||||
print("[SecurityMiddleware] Processing input...")
|
||||
@@ -59,7 +59,7 @@ async def security_and_override_middleware(
|
||||
raise MiddlewareTermination
|
||||
|
||||
# Continue to next middleware or AI execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
print("[SecurityMiddleware] Response generated.")
|
||||
print(type(context.result))
|
||||
|
||||
+2
-2
@@ -19,13 +19,13 @@ multiple specialized agents, each focusing on specific tasks.
|
||||
|
||||
async def logging_middleware(
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""MiddlewareTypes that logs tool invocations to show the delegation flow."""
|
||||
print(f"[Calling tool: {context.function.name}]")
|
||||
print(f"[Request: {context.arguments}]")
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
print(f"[Response: {context.result}]")
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ def cleanup_resources():
|
||||
@chat_middleware
|
||||
async def security_filter_middleware(
|
||||
context: ChatContext,
|
||||
next: Callable[[ChatContext], Awaitable[None]],
|
||||
call_next: Callable[[ChatContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Chat middleware that blocks requests containing sensitive information."""
|
||||
blocked_terms = ["password", "secret", "api_key", "token"]
|
||||
@@ -76,13 +76,13 @@ async def security_filter_middleware(
|
||||
|
||||
raise MiddlewareTermination
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
@function_middleware
|
||||
async def atlantis_location_filter_middleware(
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Function middleware that blocks weather requests for Atlantis."""
|
||||
# Check if location parameter is "atlantis"
|
||||
@@ -94,7 +94,7 @@ async def atlantis_location_filter_middleware(
|
||||
)
|
||||
raise MiddlewareTermination
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/getting_started/tools/function_tool_with_approval.py and samples/getting_started/tools/function_tool_with_approval_and_threads.py.
|
||||
|
||||
@@ -49,7 +49,7 @@ def get_weather(
|
||||
class SecurityAgentMiddleware(AgentMiddleware):
|
||||
"""Agent-level security middleware that validates all requests."""
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
print("[SecurityMiddleware] Checking security for all requests...")
|
||||
|
||||
# Check for security violations in the last user message
|
||||
@@ -58,22 +58,22 @@ class SecurityAgentMiddleware(AgentMiddleware):
|
||||
query = last_message.text.lower()
|
||||
if any(word in query for word in ["password", "secret", "credentials"]):
|
||||
print("[SecurityMiddleware] Security violation detected! Blocking request.")
|
||||
return # Don't call next() to prevent execution
|
||||
return # Don't call call_next() to prevent execution
|
||||
|
||||
print("[SecurityMiddleware] Security check passed.")
|
||||
context.metadata["security_validated"] = True
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
async def performance_monitor_middleware(
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Agent-level performance monitoring for all runs."""
|
||||
print("[PerformanceMonitor] Starting performance monitoring...")
|
||||
start_time = time.time()
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
@@ -85,7 +85,7 @@ async def performance_monitor_middleware(
|
||||
class HighPriorityMiddleware(AgentMiddleware):
|
||||
"""Run-level middleware for high priority requests."""
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
print("[HighPriority] Processing high priority request with expedited handling...")
|
||||
|
||||
# Read metadata set by agent-level middleware
|
||||
@@ -96,13 +96,13 @@ class HighPriorityMiddleware(AgentMiddleware):
|
||||
context.metadata["priority"] = "high"
|
||||
context.metadata["expedited"] = True
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print("[HighPriority] High priority processing completed")
|
||||
|
||||
|
||||
async def debugging_middleware(
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Run-level debugging middleware for troubleshooting specific runs."""
|
||||
print("[Debug] Debug mode enabled for this run")
|
||||
@@ -115,7 +115,7 @@ async def debugging_middleware(
|
||||
|
||||
context.metadata["debug_enabled"] = True
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
print("[Debug] Debug information collected")
|
||||
|
||||
@@ -126,7 +126,7 @@ class CachingMiddleware(AgentMiddleware):
|
||||
def __init__(self) -> None:
|
||||
self.cache: dict[str, AgentResponse] = {}
|
||||
|
||||
async def process(self, context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def process(self, context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
# Create a simple cache key from the last message
|
||||
last_message = context.messages[-1] if context.messages else None
|
||||
cache_key: str = last_message.text if last_message and last_message.text else "no_message"
|
||||
@@ -134,12 +134,12 @@ class CachingMiddleware(AgentMiddleware):
|
||||
if cache_key in self.cache:
|
||||
print(f"[Cache] Cache HIT for: '{cache_key[:30]}...'")
|
||||
context.result = self.cache[cache_key] # type: ignore
|
||||
return # Don't call next(), return cached result
|
||||
return # Don't call call_next(), return cached result
|
||||
|
||||
print(f"[Cache] Cache MISS for: '{cache_key[:30]}...'")
|
||||
context.metadata["cache_key"] = cache_key
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Cache the result if we have one
|
||||
if context.result:
|
||||
@@ -149,14 +149,14 @@ class CachingMiddleware(AgentMiddleware):
|
||||
|
||||
async def function_logging_middleware(
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Function middleware that logs all function calls."""
|
||||
function_name = context.function.name
|
||||
args = context.arguments
|
||||
print(f"[FunctionLog] Calling function: {function_name} with args: {args}")
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
print(f"[FunctionLog] Function {function_name} completed")
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class InputObserverMiddleware(ChatMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: ChatContext,
|
||||
next: Callable[[ChatContext], Awaitable[None]],
|
||||
call_next: Callable[[ChatContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Observe and modify input messages before they are sent to AI."""
|
||||
print("[InputObserverMiddleware] Observing input messages:")
|
||||
@@ -91,7 +91,7 @@ class InputObserverMiddleware(ChatMiddleware):
|
||||
context.messages[:] = modified_messages
|
||||
|
||||
# Continue to next middleware or AI execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Observe that processing is complete
|
||||
print("[InputObserverMiddleware] Processing completed")
|
||||
@@ -100,7 +100,7 @@ class InputObserverMiddleware(ChatMiddleware):
|
||||
@chat_middleware
|
||||
async def security_and_override_middleware(
|
||||
context: ChatContext,
|
||||
next: Callable[[ChatContext], Awaitable[None]],
|
||||
call_next: Callable[[ChatContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Function-based middleware that implements security filtering and response override."""
|
||||
print("[SecurityMiddleware] Processing input...")
|
||||
@@ -131,7 +131,7 @@ async def security_and_override_middleware(
|
||||
raise MiddlewareTermination
|
||||
|
||||
# Continue to next middleware or AI execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
async def class_based_chat_middleware() -> None:
|
||||
|
||||
@@ -50,7 +50,7 @@ class SecurityAgentMiddleware(AgentMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Check for potential security violations in the query
|
||||
# Look at the last user message
|
||||
@@ -63,11 +63,11 @@ class SecurityAgentMiddleware(AgentMiddleware):
|
||||
context.result = AgentResponse(
|
||||
messages=[ChatMessage("assistant", ["Detected sensitive information, the request is blocked."])]
|
||||
)
|
||||
# Simply don't call next() to prevent execution
|
||||
# Simply don't call call_next() to prevent execution
|
||||
return
|
||||
|
||||
print("[SecurityAgentMiddleware] Security check passed.")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
class LoggingFunctionMiddleware(FunctionMiddleware):
|
||||
@@ -76,14 +76,14 @@ class LoggingFunctionMiddleware(FunctionMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
function_name = context.function.name
|
||||
print(f"[LoggingFunctionMiddleware] About to call function: {function_name}.")
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
@@ -50,18 +50,18 @@ def get_current_time() -> str:
|
||||
|
||||
|
||||
@agent_middleware # Decorator marks this as agent middleware - no type annotations needed
|
||||
async def simple_agent_middleware(context, next): # type: ignore - parameters intentionally untyped to demonstrate decorator functionality
|
||||
async def simple_agent_middleware(context, call_next): # type: ignore - parameters intentionally untyped to demonstrate decorator functionality
|
||||
"""Agent middleware that runs before and after agent execution."""
|
||||
print("[Agent MiddlewareTypes] Before agent execution")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print("[Agent MiddlewareTypes] After agent execution")
|
||||
|
||||
|
||||
@function_middleware # Decorator marks this as function middleware - no type annotations needed
|
||||
async def simple_function_middleware(context, next): # type: ignore - parameters intentionally untyped to demonstrate decorator functionality
|
||||
async def simple_function_middleware(context, call_next): # type: ignore - parameters intentionally untyped to demonstrate decorator functionality
|
||||
"""Function middleware that runs before and after function calls."""
|
||||
print(f"[Function MiddlewareTypes] Before calling: {context.function.name}") # type: ignore
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print(f"[Function MiddlewareTypes] After calling: {context.function.name}") # type: ignore
|
||||
|
||||
|
||||
|
||||
@@ -35,13 +35,13 @@ def unstable_data_service(
|
||||
|
||||
|
||||
async def exception_handling_middleware(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
function_name = context.function.name
|
||||
|
||||
try:
|
||||
print(f"[ExceptionHandlingMiddleware] Executing function: {function_name}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
print(f"[ExceptionHandlingMiddleware] Function {function_name} completed successfully.")
|
||||
except TimeoutError as e:
|
||||
print(f"[ExceptionHandlingMiddleware] Caught TimeoutError: {e}")
|
||||
|
||||
@@ -27,7 +27,7 @@ The example includes:
|
||||
|
||||
Function-based middleware is ideal for simple, stateless operations and provides a more
|
||||
lightweight approach compared to class-based middleware. Both agent and function middleware
|
||||
can be implemented as async functions that accept context and next parameters.
|
||||
can be implemented as async functions that accept context and call_next parameters.
|
||||
"""
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def get_weather(
|
||||
|
||||
async def security_agent_middleware(
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Agent middleware that checks for security violations."""
|
||||
# Check for potential security violations in the query
|
||||
@@ -53,16 +53,16 @@ async def security_agent_middleware(
|
||||
query = last_message.text
|
||||
if "password" in query.lower() or "secret" in query.lower():
|
||||
print("[SecurityAgentMiddleware] Security Warning: Detected sensitive information, blocking request.")
|
||||
# Simply don't call next() to prevent execution
|
||||
# Simply don't call call_next() to prevent execution
|
||||
return
|
||||
|
||||
print("[SecurityAgentMiddleware] Security check passed.")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
async def logging_function_middleware(
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Function middleware that logs function calls."""
|
||||
function_name = context.function.name
|
||||
@@ -70,7 +70,7 @@ async def logging_function_middleware(
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
@@ -23,7 +23,7 @@ MiddlewareTypes Termination Example
|
||||
This sample demonstrates how middleware can terminate execution using the `context.terminate` flag.
|
||||
The example includes:
|
||||
|
||||
- PreTerminationMiddleware: Terminates execution before calling next() to prevent agent processing
|
||||
- PreTerminationMiddleware: Terminates execution before calling call_next() to prevent agent processing
|
||||
- PostTerminationMiddleware: Allows processing to complete but terminates further execution
|
||||
|
||||
This is useful for implementing security checks, rate limiting, or early exit conditions.
|
||||
@@ -49,7 +49,7 @@ class PreTerminationMiddleware(AgentMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
# Check if the user message contains any blocked words
|
||||
last_message = context.messages[-1] if context.messages else None
|
||||
@@ -75,7 +75,7 @@ class PreTerminationMiddleware(AgentMiddleware):
|
||||
# Set terminate flag to prevent further processing
|
||||
raise MiddlewareTermination
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
class PostTerminationMiddleware(AgentMiddleware):
|
||||
@@ -88,7 +88,7 @@ class PostTerminationMiddleware(AgentMiddleware):
|
||||
async def process(
|
||||
self,
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
print(f"[PostTerminationMiddleware] Processing request (response count: {self.response_count})")
|
||||
|
||||
@@ -101,7 +101,7 @@ class PostTerminationMiddleware(AgentMiddleware):
|
||||
raise MiddlewareTermination
|
||||
|
||||
# Allow the agent to process normally
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Increment response count after processing
|
||||
self.response_count += 1
|
||||
|
||||
@@ -48,11 +48,11 @@ def get_weather(
|
||||
return f"The weather in {location} is {conditions[randint(0, 3)]} with a high of {randint(10, 30)}°C."
|
||||
|
||||
|
||||
async def weather_override_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def weather_override_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
"""Chat middleware that overrides weather results for both streaming and non-streaming cases."""
|
||||
|
||||
# Let the original agent execution complete first
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# Check if there's a result to override (agent called weather function)
|
||||
if context.result is not None:
|
||||
@@ -83,9 +83,9 @@ async def weather_override_middleware(context: ChatContext, next: Callable[[Chat
|
||||
context.result = ChatResponse(messages=[ChatMessage(role="assistant", text=custom_message)])
|
||||
|
||||
|
||||
async def validate_weather_middleware(context: ChatContext, next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
async def validate_weather_middleware(context: ChatContext, call_next: Callable[[ChatContext], Awaitable[None]]) -> None:
|
||||
"""Chat middleware that simulates result validation for both streaming and non-streaming cases."""
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
validation_note = "Validation: weather data verified."
|
||||
|
||||
@@ -103,9 +103,9 @@ async def validate_weather_middleware(context: ChatContext, next: Callable[[Chat
|
||||
context.result.messages.append(ChatMessage(role="assistant", text=validation_note))
|
||||
|
||||
|
||||
async def agent_cleanup_middleware(context: AgentContext, next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
async def agent_cleanup_middleware(context: AgentContext, call_next: Callable[[AgentContext], Awaitable[None]]) -> None:
|
||||
"""Agent middleware that validates chat middleware effects and cleans the result."""
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
if context.result is None:
|
||||
return
|
||||
|
||||
@@ -54,7 +54,7 @@ class SessionContextContainer:
|
||||
async def inject_context_middleware(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""MiddlewareTypes that extracts runtime context from kwargs and stores in container.
|
||||
|
||||
@@ -74,7 +74,7 @@ class SessionContextContainer:
|
||||
print(f" - Session Metadata Keys: {list(self.session_metadata.keys())}")
|
||||
|
||||
# Continue to tool execution
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
# Create a container instance that will be shared via closure
|
||||
@@ -278,19 +278,19 @@ async def pattern_2_hierarchical_with_kwargs_propagation() -> None:
|
||||
|
||||
@function_middleware
|
||||
async def email_kwargs_tracker(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
email_agent_kwargs.update(context.kwargs)
|
||||
print(f"[EmailAgent] Received runtime context: {list(context.kwargs.keys())}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
@function_middleware
|
||||
async def sms_kwargs_tracker(
|
||||
context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
sms_agent_kwargs.update(context.kwargs)
|
||||
print(f"[SMSAgent] Received runtime context: {list(context.kwargs.keys())}")
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
client = OpenAIChatClient(model_id="gpt-4o-mini")
|
||||
|
||||
@@ -359,7 +359,7 @@ class AuthContextMiddleware:
|
||||
self.validated_tokens: list[str] = []
|
||||
|
||||
async def validate_and_track(
|
||||
self, context: FunctionInvocationContext, next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
self, context: FunctionInvocationContext, call_next: Callable[[FunctionInvocationContext], Awaitable[None]]
|
||||
) -> None:
|
||||
"""Validate API token and track usage."""
|
||||
api_token = context.kwargs.get("api_token")
|
||||
@@ -375,7 +375,7 @@ class AuthContextMiddleware:
|
||||
else:
|
||||
print("[AuthMiddleware] No API token provided")
|
||||
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
|
||||
@tool(approval_mode="never_require")
|
||||
|
||||
@@ -57,7 +57,7 @@ class MiddlewareContainer:
|
||||
async def call_counter_middleware(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""First middleware: increments call count in shared state."""
|
||||
# Increment the shared call count
|
||||
@@ -66,18 +66,18 @@ class MiddlewareContainer:
|
||||
print(f"[CallCounter] This is function call #{self.call_count}")
|
||||
|
||||
# Call the next middleware/function
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
async def result_enhancer_middleware(
|
||||
self,
|
||||
context: FunctionInvocationContext,
|
||||
next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
call_next: Callable[[FunctionInvocationContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""Second middleware: uses shared call count to enhance function results."""
|
||||
print(f"[ResultEnhancer] Current total calls so far: {self.call_count}")
|
||||
|
||||
# Call the next middleware/function
|
||||
await next(context)
|
||||
await call_next(context)
|
||||
|
||||
# After function execution, enhance the result using shared state
|
||||
if context.result:
|
||||
|
||||
@@ -21,14 +21,14 @@ The example shows:
|
||||
|
||||
- How AgentContext.thread property behaves across multiple runs
|
||||
- How middleware can access conversation history through the thread
|
||||
- The timing of when thread messages are populated (before vs after next() call)
|
||||
- The timing of when thread messages are populated (before vs after call_next() call)
|
||||
- How to track thread state changes across runs
|
||||
|
||||
Key behaviors demonstrated:
|
||||
1. First run: context.messages is populated, context.thread is initially empty (before next())
|
||||
2. After next(): thread contains input message + response from agent
|
||||
1. First run: context.messages is populated, context.thread is initially empty (before call_next())
|
||||
2. After call_next(): thread contains input message + response from agent
|
||||
3. Second run: context.messages contains only current input, thread contains previous history
|
||||
4. After next(): thread contains full conversation history (all previous + current messages)
|
||||
4. After call_next(): thread contains full conversation history (all previous + current messages)
|
||||
"""
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def get_weather(
|
||||
|
||||
async def thread_tracking_middleware(
|
||||
context: AgentContext,
|
||||
next: Callable[[AgentContext], Awaitable[None]],
|
||||
call_next: Callable[[AgentContext], Awaitable[None]],
|
||||
) -> None:
|
||||
"""MiddlewareTypes that tracks and logs thread behavior across runs."""
|
||||
thread_messages = []
|
||||
@@ -56,8 +56,8 @@ async def thread_tracking_middleware(
|
||||
print(f"[MiddlewareTypes pre-execution] Current input messages: {len(context.messages)}")
|
||||
print(f"[MiddlewareTypes pre-execution] Thread history messages: {len(thread_messages)}")
|
||||
|
||||
# Call next to execute the agent
|
||||
await next(context)
|
||||
# Call call_next to execute the agent
|
||||
await call_next(context)
|
||||
|
||||
# Check thread state after agent execution
|
||||
updated_thread_messages = []
|
||||
|
||||
@@ -19,6 +19,7 @@ keep `approval_mode="always_require"` unless you are confident in the tool behav
|
||||
| [`function_tool_with_thread_injection.py`](function_tool_with_thread_injection.py) | Shows how to access the current `thread` object inside a local tool via `**kwargs`. |
|
||||
| [`function_tool_with_max_exceptions.py`](function_tool_with_max_exceptions.py) | Shows how to limit the number of times a tool can fail with exceptions using `max_invocation_exceptions`. Useful for preventing expensive tools from being called repeatedly when they keep failing. |
|
||||
| [`function_tool_with_max_invocations.py`](function_tool_with_max_invocations.py) | Demonstrates limiting the total number of times a tool can be invoked using `max_invocations`. Useful for rate-limiting expensive operations or ensuring tools are only called a specific number of times per conversation. |
|
||||
| [`function_tool_with_explicit_schema.py`](function_tool_with_explicit_schema.py) | Demonstrates how to provide an explicit Pydantic model or JSON schema dictionary to the `@tool` decorator via the `schema` parameter, bypassing automatic inference from the function signature. |
|
||||
| [`tool_in_class.py`](tool_in_class.py) | Shows how to use the `tool` decorator with class methods to create stateful tools. Demonstrates how class state can control tool behavior dynamically, allowing you to adjust tool functionality at runtime by modifying class properties. |
|
||||
|
||||
## Key Concepts
|
||||
@@ -26,6 +27,7 @@ keep `approval_mode="always_require"` unless you are confident in the tool behav
|
||||
### Local Tool Features
|
||||
|
||||
- **Function Declarations**: Define tool schemas without implementations for testing or external tools
|
||||
- **Explicit Schema**: Provide a Pydantic model or JSON schema dict to control the tool's parameter schema directly
|
||||
- **Dependency Injection**: Create tools from configurations with runtime-injected implementations
|
||||
- **Error Handling**: Gracefully handle and recover from tool execution failures
|
||||
- **Approval Workflows**: Require user approval before executing sensitive or important operations
|
||||
@@ -55,6 +57,23 @@ def sensitive_operation(data: Annotated[str, "Data to process"]) -> str:
|
||||
return f"Processed: {data}"
|
||||
```
|
||||
|
||||
#### Tool with Explicit Schema
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, Field
|
||||
from agent_framework import tool
|
||||
from typing import Annotated
|
||||
|
||||
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 the weather for a location."""
|
||||
return f"Weather in {location}: 22 {unit}"
|
||||
```
|
||||
|
||||
#### Tool with Invocation Limits
|
||||
|
||||
```python
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Function Tool with Explicit Schema Example
|
||||
|
||||
This example demonstrates how to provide an explicit schema to the @tool decorator
|
||||
using the `schema` parameter, bypassing the automatic inference from the function
|
||||
signature. This is useful when you want full control over the tool's parameter
|
||||
schema that the AI model sees, or when the function signature does not accurately
|
||||
represent the desired schema.
|
||||
|
||||
Two approaches are shown:
|
||||
1. Using a Pydantic BaseModel subclass as the schema
|
||||
2. Using a raw JSON schema dictionary as the schema
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Annotated
|
||||
|
||||
from agent_framework import tool
|
||||
from agent_framework.openai import OpenAIResponsesClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# Approach 1: Pydantic model as explicit schema
|
||||
class WeatherInput(BaseModel):
|
||||
"""Input schema for the weather tool."""
|
||||
|
||||
location: Annotated[str, Field(description="The city name to get weather for")]
|
||||
unit: Annotated[str, Field(description="Temperature unit: celsius or fahrenheit")] = "celsius"
|
||||
|
||||
|
||||
@tool(
|
||||
name="get_weather",
|
||||
description="Get the current weather for a given location.",
|
||||
schema=WeatherInput,
|
||||
approval_mode="never_require",
|
||||
)
|
||||
def get_weather(location: str, unit: str = "celsius") -> str:
|
||||
"""Get the current weather for a location."""
|
||||
return f"The weather in {location} is 22 degrees {unit}."
|
||||
|
||||
|
||||
# Approach 2: JSON schema dictionary as explicit schema
|
||||
get_current_time_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"timezone": {"type": "string", "description": "The timezone to get the current time for", "default": "UTC"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@tool(
|
||||
name="get_current_time",
|
||||
description="Get the current time in a given timezone.",
|
||||
schema=get_current_time_schema,
|
||||
approval_mode="never_require",
|
||||
)
|
||||
def get_current_time(timezone: str = "UTC") -> str:
|
||||
"""Get the current time."""
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
return f"The current time in {timezone} is {datetime.now(ZoneInfo(timezone)).isoformat()}"
|
||||
|
||||
|
||||
async def main():
|
||||
agent = OpenAIResponsesClient().as_agent(
|
||||
name="AssistantAgent",
|
||||
instructions="You are a helpful assistant. Use the available tools to answer questions.",
|
||||
tools=[get_weather, get_current_time],
|
||||
)
|
||||
|
||||
query = "What is the weather in Seattle and what time is it?"
|
||||
print(f"User: {query}")
|
||||
result = await agent.run(query)
|
||||
print(f"Result: {result.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -84,6 +84,7 @@ Once comfortable with these, explore the rest of the samples below.
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| Human-In-The-Loop (Guessing Game) | [human-in-the-loop/guessing_game_with_human_input.py](./human-in-the-loop/guessing_game_with_human_input.py) | Interactive request/response prompts with a human via `ctx.request_info()` |
|
||||
| Agents with Approval Requests in Workflows | [human-in-the-loop/agents_with_approval_requests.py](./human-in-the-loop/agents_with_approval_requests.py) | Agents that create approval requests during workflow execution and wait for human approval to proceed |
|
||||
| Agents with Declaration-Only Tools | [human-in-the-loop/agents_with_declaration_only_tools.py](./human-in-the-loop/agents_with_declaration_only_tools.py) | Workflow pauses when agent calls a client-side tool (`func=None`), caller supplies the result |
|
||||
| SequentialBuilder Request Info | [human-in-the-loop/sequential_request_info.py](./human-in-the-loop/sequential_request_info.py) | Request info for agent responses mid-workflow using `.with_request_info()` on SequentialBuilder |
|
||||
| ConcurrentBuilder Request Info | [human-in-the-loop/concurrent_request_info.py](./human-in-the-loop/concurrent_request_info.py) | Review concurrent agent outputs before aggregation using `.with_request_info()` on ConcurrentBuilder |
|
||||
| GroupChatBuilder Request Info | [human-in-the-loop/group_chat_request_info.py](./human-in-the-loop/group_chat_request_info.py) | Steer group discussions with periodic guidance using `.with_request_info()` on GroupChatBuilder |
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
"""
|
||||
Sample: Declaration-only tools in a workflow (issue #3425)
|
||||
|
||||
A declaration-only tool (func=None) represents a client-side tool that the
|
||||
framework cannot execute — the LLM can call it, but the workflow must pause
|
||||
so the caller can supply the result.
|
||||
|
||||
Flow:
|
||||
1. The agent is given a declaration-only tool ("get_user_location").
|
||||
2. When the LLM decides to call it, the workflow pauses and emits a
|
||||
request_info event containing the FunctionCallContent.
|
||||
3. The caller inspects the tool name/args, runs the tool however it wants,
|
||||
and feeds the result back via workflow.run(responses={...}).
|
||||
4. The workflow resumes — the agent sees the tool result and finishes.
|
||||
|
||||
Prerequisites:
|
||||
- Azure OpenAI endpoint configured via environment variables.
|
||||
- `az login` for AzureCliCredential.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from agent_framework import Content, FunctionTool, WorkflowBuilder
|
||||
from agent_framework.azure import AzureOpenAIChatClient
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
# A declaration-only tool: the schema is sent to the LLM, but the framework
|
||||
# has no implementation to execute. The caller must supply the result.
|
||||
get_user_location = FunctionTool(
|
||||
name="get_user_location",
|
||||
func=None,
|
||||
description="Get the user's current city. Only the client application can resolve this.",
|
||||
input_model={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {"type": "string", "description": "Why the location is needed"},
|
||||
},
|
||||
"required": ["reason"],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
agent = AzureOpenAIChatClient(credential=AzureCliCredential()).as_agent(
|
||||
name="WeatherBot",
|
||||
instructions=(
|
||||
"You are a helpful weather assistant. "
|
||||
"When the user asks about weather, call get_user_location first, "
|
||||
"then make up a plausible forecast for that city."
|
||||
),
|
||||
tools=[get_user_location],
|
||||
)
|
||||
|
||||
workflow = WorkflowBuilder(start_executor=agent).build()
|
||||
|
||||
# --- First run: the agent should call the declaration-only tool ---
|
||||
print(">>> Sending: 'What's the weather like today?'")
|
||||
result = await workflow.run("What's the weather like today?")
|
||||
|
||||
requests = result.get_request_info_events()
|
||||
if not requests:
|
||||
# The LLM chose not to call the tool — print whatever it said and exit
|
||||
print(f"Agent replied without calling the tool: {result.get_outputs()}")
|
||||
return
|
||||
|
||||
# --- Inspect what the agent wants ---
|
||||
for req in requests:
|
||||
data = req.data
|
||||
args = json.loads(data.arguments) if isinstance(data.arguments, str) else data.arguments
|
||||
print(f"Workflow paused — agent called: {data.name}({args})")
|
||||
|
||||
# --- "Execute" the tool on the client side and send results back ---
|
||||
responses: dict[str, Any] = {}
|
||||
for req in requests:
|
||||
# In a real app this could be a GPS lookup, browser API, user prompt, etc.
|
||||
client_result = "Seattle, WA"
|
||||
print(f"Client provides result for {req.data.name}: {client_result!r}")
|
||||
responses[req.request_id] = Content.from_function_result(
|
||||
call_id=req.data.call_id,
|
||||
result=client_result,
|
||||
)
|
||||
|
||||
result = await workflow.run(responses=responses)
|
||||
|
||||
# --- Final answer ---
|
||||
for output in result.get_outputs():
|
||||
print(f"\nAgent: {output.text}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Generated
+131
-117
@@ -1324,19 +1324,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "claude-agent-sdk"
|
||||
version = "0.1.33"
|
||||
version = "0.1.34"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "mcp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/aa/5c417ef464d3fa712d830cd56a9a79aef8dfb5bc3414aae4bae136cf4e73/claude_agent_sdk-0.1.33.tar.gz", hash = "sha256:134bf403bb7553d829dadec42c30ecef340f5d4ad1595c1bdef933a9ca3129cf", size = 61196, upload-time = "2026-02-07T19:19:53.372Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/69/faeb64e9c8f0962cbf12bee1b959acc41f87c82947ec7074a6780b417001/claude_agent_sdk-0.1.34.tar.gz", hash = "sha256:db9e4023a754d9a58a0793666fe9174ead277197cd896156d2f8784cc73c5006", size = 61196, upload-time = "2026-02-10T01:04:00.585Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/31/1ac5d536013b1e38b37d71928a0db214cbd47e7bb815c21141dbc6dd93b6/claude_agent_sdk-0.1.33-py3-none-macosx_11_0_arm64.whl", hash = "sha256:57886a2dd124e5b3c9e12ec3e4841742ab3444d1e428b45ceaec8841c96698fa", size = 54323456, upload-time = "2026-02-07T19:19:39.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/36/79c3feb3f2c95591b80de39a1d3097d30bc3a9a84fcff6422f5434f1187a/claude_agent_sdk-0.1.33-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:ea0f1e4fadeec766000122723c406a6f47c6210ea11bb5cc0c88af11ef7c940c", size = 69106772, upload-time = "2026-02-07T19:19:42.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/a8/64d22ae767154da4629004a80e9f59f71b5070d55fcfade4efdfb06b1f7a/claude_agent_sdk-0.1.33-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:0ecd822c577b4ea2a52e51146a24dcea73eb69ff366bdb875785dadb116d593b", size = 69688592, upload-time = "2026-02-07T19:19:46.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/aa/83677a3d42b047bcacf4dbe730bf5189a106b5b6746ee83f6920e5d9729a/claude_agent_sdk-0.1.33-py3-none-win_amd64.whl", hash = "sha256:a9fbd09d8f947005e087340ecd0706ed35639c946b4bd49429d3132db4cb3751", size = 72211078, upload-time = "2026-02-07T19:19:50.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/7b/2ccdc12b553a61b59b0470f1cf3b0a864c79ab8a4ac8013ea22fa3e6c461/claude_agent_sdk-0.1.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:18569ab4bfb5451c4aacb51c0d44eb9802d18d8442d30c29f32b6e8a2479d210", size = 54604881, upload-time = "2026-02-10T01:03:44.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/59/335b213fb3342c4405fa992cc9e45e52e18a543068f0574ae84010dc4c08/claude_agent_sdk-0.1.34-py3-none-manylinux_2_17_aarch64.whl", hash = "sha256:d7ecd7421066e405376d3feca21ccb3e9245506ba7c219858f7a7f0129877cdb", size = 69359030, upload-time = "2026-02-10T01:03:49.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/3d/28c715efdad7b5c413e046f5f914d9ab888d25f2c3bb9233f1164d58d2be/claude_agent_sdk-0.1.34-py3-none-manylinux_2_17_x86_64.whl", hash = "sha256:82e9148410ec98ff4061e43e85601d8f0a2e8568d897ab82c324ccf11c297fc5", size = 69949555, upload-time = "2026-02-10T01:03:53.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/3d/843159343b20d6c9b44cf4a7fe46b568d5b448276ba8ba4c49178d26ba4c/claude_agent_sdk-0.1.34-py3-none-win_amd64.whl", hash = "sha256:a64031a9bf5c70388a6a84368d350d68586d9854a1539f494b46ec6d0b6acf93", size = 72493949, upload-time = "2026-02-10T01:03:57.839Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1542,101 +1542,115 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.13.3"
|
||||
version = "7.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -1839,7 +1853,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.128.5"
|
||||
version = "0.128.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -1848,9 +1862,9 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/d4/811e7283aaaa84f1e7bd55fb642b58f8c01895e4884a9b7628cb55e00d63/fastapi-0.128.5.tar.gz", hash = "sha256:a7173579fc162d6471e3c6fbd9a4b7610c7a3b367bcacf6c4f90d5d022cab711", size = 374636, upload-time = "2026-02-08T10:22:30.493Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/d1/195005b5e45b443e305136df47ee7df4493d782e0c039dd0d97065580324/fastapi-0.128.6.tar.gz", hash = "sha256:0cb3946557e792d731b26a42b04912f16367e3c3135ea8290f620e234f2b604f", size = 374757, upload-time = "2026-02-09T17:27:03.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/e0/511972dba23ee76c0e9d09d1ae95e916fc8ebce5322b2b8b65a481428b10/fastapi-0.128.5-py3-none-any.whl", hash = "sha256:bceec0de8aa6564599c5bcc0593b0d287703562c848271fca8546fd2c87bf4dd", size = 103677, upload-time = "2026-02-08T10:22:28.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/58/a2c4f6b240eeb148fb88cdac48f50a194aba760c1ca4988c6031c66a20ee/fastapi-0.128.6-py3-none-any.whl", hash = "sha256:bb1c1ef87d6086a7132d0ab60869d6f1ee67283b20fbf84ec0003bd335099509", size = 103674, upload-time = "2026-02-09T17:27:02.355Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2945,7 +2959,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langfuse"
|
||||
version = "3.13.0"
|
||||
version = "3.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -2959,9 +2973,9 @@ dependencies = [
|
||||
{ name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/d0/744e5613c728427330ac2049da0f54fc313e8bf84622f71b025bfba65496/langfuse-3.13.0.tar.gz", hash = "sha256:dacea8111ca4442e97dbfec4f8d676cf9709b35357a26e468f8887b95de0012f", size = 233420, upload-time = "2026-02-06T19:54:14.415Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/7a/ddd8df7f2c5d8c2112a8b20fa88b2513917e2c25d2ef87034c0927f87596/langfuse-3.14.1.tar.gz", hash = "sha256:404a6104cd29353d7829aa417ec46565b04917e5599afdda96c5b0865f4bc991", size = 234530, upload-time = "2026-02-09T15:37:45.994Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/63/148382e8e79948f7e5c9c137288e504bb88117574eb7e7c886b4fb470b4b/langfuse-3.13.0-py3-none-any.whl", hash = "sha256:71912ddac1cc831a65df895eae538a556f564c094ae51473e747426e9ded1a9d", size = 417626, upload-time = "2026-02-06T19:54:12.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/b9/e8ac3072469737358975da66ec4218dc1cee0051555dd4665b3e34a28420/langfuse-3.14.1-py3-none-any.whl", hash = "sha256:17bed605dbfc9947cbd1738a715f6d27c1b80b6da9f2946586171958fa5820d0", size = 420336, upload-time = "2026-02-09T15:37:44.381Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3864,7 +3878,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.17.0"
|
||||
version = "2.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -3876,14 +3890,14 @@ dependencies = [
|
||||
{ name = "tqdm", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/a2/677f22c4b487effb8a09439fb6134034b5f0a39ca27df8b95fac23a93720/openai-2.17.0.tar.gz", hash = "sha256:47224b74bd20f30c6b0a6a329505243cb2f26d5cf84d9f8d0825ff8b35e9c999", size = 631445, upload-time = "2026-02-05T16:27:40.953Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/cb/f2c9f988a06d1fcdd18ddc010f43ac384219a399eb01765493d6b34b1461/openai-2.18.0.tar.gz", hash = "sha256:5018d3bcb6651c5aac90e6d0bf9da5cde1bdd23749f67b45b37c522b6e6353af", size = 632124, upload-time = "2026-02-09T21:42:18.017Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/5f/8940e0641c223eaf972732b3154f2178a968290f8cb99e8c88582cde60ed/openai-2.18.0-py3-none-any.whl", hash = "sha256:538f97e1c77a00e3a99507688c878cda7e9e63031807ba425c68478854d48b30", size = 1069897, upload-time = "2026-02-09T21:42:16.4Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai-agents"
|
||||
version = "0.8.1"
|
||||
version = "0.8.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "griffe", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -3894,9 +3908,9 @@ dependencies = [
|
||||
{ name = "types-requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/43/ccea6b70e3c4399eea24a7e0c0cde9e05727781e5b7dd2c00e2cebe09961/openai_agents-0.8.1.tar.gz", hash = "sha256:32dc6124359397e5775e936e621892576a0b2f5c88b3fc548a084334f6918541", size = 2373798, upload-time = "2026-02-06T22:44:24.24Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/00/6b/f86002a00f16b387b0570860e461475660d81eb00e2817391926d3947933/openai_agents-0.8.3.tar.gz", hash = "sha256:07a6e900b0fe4b7fd8f91a06ed9ab4fec9df335ed676f1c9e1125f60cb57919b", size = 2378346, upload-time = "2026-02-10T00:11:07.048Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/3f/49ff704c933cf2a3467c040b13231258bb1f2fa66d995c3b62b3a13c2eb4/openai_agents-0.8.1-py3-none-any.whl", hash = "sha256:a29916690f4ca2d67c0d782abbff99350ce2a7cee0067b8dd2c2297e38a3714a", size = 376922, upload-time = "2026-02-06T22:44:21.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/38/d77602daf5308395ee067954ffa7e96cb9ecf9292ad3b5f398f1c77e0b36/openai_agents-0.8.3-py3-none-any.whl", hash = "sha256:e562ec1a70177abaa34ca6f0428241a9dbeb6b3d73f88a7f4ba3ee3d72b3b98d", size = 378042, upload-time = "2026-02-10T00:11:04.967Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4525,7 +4539,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "posthog"
|
||||
version = "7.8.3"
|
||||
version = "7.8.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backoff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
@@ -4535,9 +4549,9 @@ dependencies = [
|
||||
{ name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/ad/2f116cd9b83dc83ece4328a4efe0bcb80e5c2993837f89a788467d261da8/posthog-7.8.3.tar.gz", hash = "sha256:2b85e818bf818ac2768a890b772b7c12d4f909797226acd9327d66a319dbcf83", size = 167083, upload-time = "2026-02-06T13:16:22.938Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/10/8e74a5e997c8286f0b63c69da522e503b1ab11627217ab76a06c7b62d647/posthog-7.8.5.tar.gz", hash = "sha256:e4f3796ce18323d8e05139bf419a04d318ccc4ad77b210f4d9d7c7546aea4f35", size = 169117, upload-time = "2026-02-09T22:59:49.207Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e5/5a4b060cbb9aa9defb8bfd55d15899b3146fece14147f4d66be80e81955a/posthog-7.8.3-py3-none-any.whl", hash = "sha256:1840796e4f7e14dd91ec5fdeb939712c3383fe9e758cfcdeb0317d8f30f7b901", size = 192528, upload-time = "2026-02-06T13:16:21.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/b3/59b61d4b90e2efd138abaa34d98c7a89a4a352850cc3a079a60a46780655/posthog-7.8.5-py3-none-any.whl", hash = "sha256:979d306f07e61a8e837746e5dc432aafc49827fecac91bd6c624dcf3a1967448", size = 194647, upload-time = "2026-02-09T22:59:47.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5319,14 +5333,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "7.1.0"
|
||||
version = "7.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "async-timeout", marker = "(python_full_version < '3.11.3' and sys_platform == 'darwin') or (python_full_version < '3.11.3' and sys_platform == 'linux') or (python_full_version < '3.11.3' and sys_platform == 'win32')" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f7/80/2971931d27651affa88a44c0ad7b8c4a19dc29c998abb20b23868d319b59/redis-7.1.1.tar.gz", hash = "sha256:a2814b2bda15b39dad11391cc48edac4697214a8a5a4bd10abe936ab4892eb43", size = 4800064, upload-time = "2026-02-09T18:39:40.292Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/55/1de1d812ba1481fa4b37fb03b4eec0fcb71b6a0d44c04ea3482eb017600f/redis-7.1.1-py3-none-any.whl", hash = "sha256:f77817f16071c2950492c67d40b771fa493eb3fccc630a424a10976dbb794b7a", size = 356057, upload-time = "2026-02-09T18:39:38.602Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user