mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3854c2dbc2 | ||
|
|
2f12d87cdc | ||
|
|
95436c168d | ||
|
|
8d4029a271 | ||
|
|
15a5bbab21 | ||
|
|
1feb5efb59 | ||
|
|
ece4787df7 | ||
|
|
33f3b02d41 | ||
|
|
dfb316ee59 |
@@ -254,7 +254,7 @@ internal static class SemanticAnalyzer
|
||||
|
||||
/// <summary>
|
||||
/// Combines ClassProtocolInfo results into an AnalysisResult for classes that only have IO attributes
|
||||
/// (no [MessageHandler] methods). This generates only .SendsMessage/.YieldsMessage calls in the protocol
|
||||
/// (no [MessageHandler] methods). This generates only .SendsMessage/.YieldsOutput calls in the protocol
|
||||
/// configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
|
||||
@@ -9,17 +9,6 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
internal static class AIAgentsAbstractionsExtensions
|
||||
{
|
||||
public static ChatMessage ToChatMessage(this AgentResponseUpdate update) =>
|
||||
new()
|
||||
{
|
||||
AuthorName = update.AuthorName,
|
||||
Contents = update.Contents,
|
||||
Role = update.Role ?? ChatRole.User,
|
||||
CreatedAt = update.CreatedAt,
|
||||
MessageId = update.MessageId,
|
||||
RawRepresentation = update.RawRepresentation ?? update,
|
||||
};
|
||||
|
||||
public static ChatMessage ChatAssistantToUserIfNotFromNamed(this ChatMessage message, string agentName)
|
||||
=> message.ChatAssistantToUserIfNotFromNamed(agentName, out _, false);
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class ChatForwardingExecutor(string id, ChatForwardingExecutorOpti
|
||||
if (this._stringMessageChatRole.HasValue)
|
||||
{
|
||||
routeBuilder = routeBuilder.AddHandler<string>(
|
||||
(message, context) => context.SendMessageAsync(new ChatMessage(ChatRole.User, message)));
|
||||
(message, context) => context.SendMessageAsync(new ChatMessage(this._stringMessageChatRole.Value, message)));
|
||||
}
|
||||
|
||||
routeBuilder.AddHandler<ChatMessage>(ForwardMessageAsync)
|
||||
|
||||
@@ -73,7 +73,14 @@ public class FunctionExecutor<TInput>(string id,
|
||||
ExecutorOptions? options = null,
|
||||
IEnumerable<Type>? sentMessageTypes = null,
|
||||
IEnumerable<Type>? outputTypes = null,
|
||||
bool declareCrossRunShareable = false) : this(id, WrapAction(handlerSync, out var attributeSentTypes, out var attributeYieldTypes), options, attributeSentTypes.Concat(sentMessageTypes ?? []), attributeYieldTypes.Concat(outputTypes ?? []), declareCrossRunShareable)
|
||||
bool declareCrossRunShareable = false) : this(id,
|
||||
WrapAction(handlerSync,
|
||||
out var attributeSentTypes,
|
||||
out var attributeYieldTypes),
|
||||
options,
|
||||
attributeSentTypes.Concat(sentMessageTypes ?? []),
|
||||
attributeYieldTypes.Concat(outputTypes ?? []),
|
||||
declareCrossRunShareable)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -96,8 +103,18 @@ public class FunctionExecutor<TInput, TOutput>(string id,
|
||||
IEnumerable<Type>? outputTypes = null,
|
||||
bool declareCrossRunShareable = false) : Executor<TInput, TOutput>(id, options, declareCrossRunShareable)
|
||||
{
|
||||
internal static Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>> WrapFunc(Func<TInput, IWorkflowContext, CancellationToken, TOutput> handlerSync)
|
||||
internal static Func<TInput, IWorkflowContext, CancellationToken, ValueTask<TOutput>> WrapFunc(Func<TInput, IWorkflowContext, CancellationToken, TOutput> handlerSync, out IEnumerable<Type> sentTypes, out IEnumerable<Type> yieldedTypes)
|
||||
{
|
||||
if (handlerSync.Method != null)
|
||||
{
|
||||
MethodInfo method = handlerSync.Method;
|
||||
(sentTypes, yieldedTypes) = method.GetAttributeTypes();
|
||||
}
|
||||
else
|
||||
{
|
||||
sentTypes = yieldedTypes = [];
|
||||
}
|
||||
|
||||
return RunFuncAsync;
|
||||
|
||||
ValueTask<TOutput> RunFuncAsync(TInput input, IWorkflowContext workflowContext, CancellationToken cancellationToken)
|
||||
@@ -133,7 +150,14 @@ public class FunctionExecutor<TInput, TOutput>(string id,
|
||||
ExecutorOptions? options = null,
|
||||
IEnumerable<Type>? sentMessageTypes = null,
|
||||
IEnumerable<Type>? outputTypes = null,
|
||||
bool declareCrossRunShareable = false) : this(id, WrapFunc(handlerSync), options, sentMessageTypes, outputTypes, declareCrossRunShareable)
|
||||
bool declareCrossRunShareable = false) : this(id,
|
||||
WrapFunc(handlerSync,
|
||||
out var attributeSentTypes,
|
||||
out var attributeYieldTypes),
|
||||
options,
|
||||
attributeSentTypes.Concat(sentMessageTypes ?? []),
|
||||
attributeYieldTypes.Concat(outputTypes ?? []),
|
||||
declareCrossRunShareable)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ internal static class DiagnosticConstants
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[ExcludeFromCodeCoverage] // This is obsolete, and 1:1 equivalent to HandoffWorkflowBuilder (no "s")
|
||||
[Obsolete("Prefer HandoffWorkflowBuilder (no 's') instead, which has the same API but the preferred name. This will be removed in a future release before GA.")]
|
||||
#pragma warning disable MAAIW001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
|
||||
public sealed class HandoffsWorkflowBuilder(AIAgent initialAgent) : HandoffWorkflowBuilderCore<HandoffsWorkflowBuilder>(initialAgent)
|
||||
|
||||
+23
@@ -29,6 +29,7 @@ namespace Microsoft.Agents.AI.Workflows;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
[Obsolete("Use YieldsOutput instead. The Code Generator and the runtime attribute-based type mapping ignore this attribute.")]
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class YieldsMessageAttribute : Attribute
|
||||
{
|
||||
@@ -47,3 +48,25 @@ public sealed class YieldsMessageAttribute : Attribute
|
||||
this.Type = Throw.IfNull(type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This attribute indicates that a message handler streams messages during its execution.
|
||||
/// </summary>
|
||||
[Obsolete("This attribute does not do anything. The Code Generator and the runtime attribute-based type mapping ignore this attribute.")]
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public sealed class StreamsMessageAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the message that the handler yields.
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the message handler yields streaming messages during the course of execution.
|
||||
/// </summary>
|
||||
public StreamsMessageAttribute(Type type)
|
||||
{
|
||||
// This attribute is used to mark executors that yield messages.
|
||||
this.Type = Throw.IfNull(type);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// This attribute indicates that a message handler streams messages during its execution.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public sealed class StreamsMessageAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of the message that the handler yields.
|
||||
/// </summary>
|
||||
public Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the message handler yields streaming messages during the course of execution.
|
||||
/// </summary>
|
||||
public StreamsMessageAttribute(Type type)
|
||||
{
|
||||
// This attribute is used to mark executors that yield messages.
|
||||
this.Type = Throw.IfNull(type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Agents.AI.Workflows.Checkpointing;
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
internal enum ChatRoleType
|
||||
{
|
||||
None,
|
||||
User,
|
||||
Assistant,
|
||||
Custom
|
||||
}
|
||||
|
||||
internal static class ChatRoleTestingExtensions
|
||||
{
|
||||
public const string CustomChatRoleName = nameof(CustomChatRole);
|
||||
|
||||
public static ChatRole CustomChatRole { get; } = new(CustomChatRoleName);
|
||||
|
||||
public static ChatRole? ToChatRole(this ChatRoleType type)
|
||||
=> type switch
|
||||
{
|
||||
ChatRoleType.None => null,
|
||||
ChatRoleType.User => ChatRole.User,
|
||||
ChatRoleType.Assistant => ChatRole.Assistant,
|
||||
ChatRoleType.Custom => CustomChatRole,
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(type),
|
||||
type,
|
||||
$"Invalid ChatRoleType {type}; expecting one of {string.Join(",",
|
||||
[null,
|
||||
ChatRole.User,
|
||||
ChatRole.Assistant,
|
||||
CustomChatRole])}")
|
||||
};
|
||||
}
|
||||
|
||||
public class ChatForwardingExecutorTests
|
||||
{
|
||||
private async Task<TestWorkflowContext> RunForwardMessageTestAsync<TMessage>(ChatForwardingExecutor executor, TMessage message)
|
||||
where TMessage : notnull
|
||||
{
|
||||
// Ensure that we have constructed the Protocol (and registered the handlers)
|
||||
_ = executor.Protocol;
|
||||
|
||||
TestWorkflowContext testContext = new(executor.Id);
|
||||
object? callResult = await executor.ExecuteCoreAsync(message, new TypeId(typeof(TMessage)), testContext);
|
||||
|
||||
callResult.Should().BeNull(); // ChatForwardingExecutor's do not have a return type
|
||||
|
||||
return testContext;
|
||||
}
|
||||
|
||||
private const string TestMessageContent = nameof(TestMessageContent);
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_DoesNotForwardStringByDefaultAsync()
|
||||
{
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
|
||||
// Act
|
||||
Func<Task<TestWorkflowContext>> action = () => this.RunForwardMessageTestAsync(executor, TestMessageContent);
|
||||
await action.Should().ThrowAsync<NotSupportedException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(ChatRoleType.None)]
|
||||
[InlineData(ChatRoleType.User)]
|
||||
[InlineData(ChatRoleType.Assistant)]
|
||||
[InlineData(ChatRoleType.Custom)]
|
||||
internal async Task Test_ChatForwardingExecutor_ForwardsStringIfConfiguredAsync(ChatRoleType chatRoleType)
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutorOptions options = new()
|
||||
{
|
||||
StringMessageChatRole = chatRoleType.ToChatRole()
|
||||
};
|
||||
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor), options);
|
||||
|
||||
// Act
|
||||
Func<Task<TestWorkflowContext>> action = () => this.RunForwardMessageTestAsync(executor, TestMessageContent);
|
||||
|
||||
// Assert
|
||||
if (options.StringMessageChatRole is ChatRole chatRole)
|
||||
{
|
||||
TestWorkflowContext testContext = await action();
|
||||
|
||||
testContext.SentMessages.Should().HaveCount(1)
|
||||
.And.BeEquivalentTo([new ChatMessage(chatRole, TestMessageContent)]);
|
||||
}
|
||||
else
|
||||
{
|
||||
await action.Should().ThrowAsync<NotSupportedException>();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsChatMessageUnmodifiedAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
ChatMessage testMessage = new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent);
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testMessage);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(message => ReferenceEquals(message, testMessage));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsChatMessageListUnmodifiedAsync(bool sendAsIEnumerable)
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
List<ChatMessage> testMessages = [new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent),
|
||||
new(ChatRole.Assistant, "ResponseMessage")];
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext
|
||||
= sendAsIEnumerable
|
||||
? await this.RunForwardMessageTestAsync<IEnumerable<ChatMessage>>(executor, testMessages)
|
||||
: await this.RunForwardMessageTestAsync(executor, testMessages);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(messages => ReferenceEquals(messages, testMessages));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsChatMessageArrayUnchangedAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
ChatMessage[] testMessages = [new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent),
|
||||
new(ChatRole.Assistant, "ResponseMessage")];
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testMessages);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(messages => ReferenceEquals(messages, testMessages));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsMessageCollectionAsListAsync()
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
ConcurrentBag<ChatMessage> testMessages = [new(ChatRoleTestingExtensions.CustomChatRole, TestMessageContent),
|
||||
new(ChatRole.Assistant, "ResponseMessage")];
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testMessages);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().ContainSingle(messages => !ReferenceEquals(messages, testMessages))
|
||||
.And.Subject.Single().Should().BeEquivalentTo(testMessages);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_ChatForwardingExecutor_ForwardsTurnTokenUnmodifiedAsync(bool? emitEvents)
|
||||
{
|
||||
// Arrange
|
||||
ChatForwardingExecutor executor = new(nameof(ChatForwardingExecutor));
|
||||
TurnToken testTurnToken = new(emitEvents);
|
||||
|
||||
// Act
|
||||
TestWorkflowContext testContext = await this.RunForwardMessageTestAsync(executor, testTurnToken);
|
||||
|
||||
// Assert
|
||||
testContext.SentMessages.Should().BeEquivalentTo([testTurnToken]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.UnitTests;
|
||||
|
||||
public class ExecutorTestsBase
|
||||
{
|
||||
public sealed record TextMessage(string Text);
|
||||
|
||||
public const string TestMessageContent = nameof(TestMessage);
|
||||
public static TextMessage TestMessage { get; } = new(TestMessageContent);
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Test Object")]
|
||||
public sealed record DataMessage(string Base64Bytes)
|
||||
{
|
||||
private static string ToBase64String(string text, Encoding? expectedEncoding)
|
||||
{
|
||||
byte[] bytes = (expectedEncoding ?? Encoding.UTF8).GetBytes(text);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
public DataMessage(TextMessage textMessage, Encoding? expectedEncoding = null) : this(ToBase64String(textMessage.Text, expectedEncoding))
|
||||
{ }
|
||||
}
|
||||
|
||||
public const string DataMessageContent = nameof(DataMessage);
|
||||
public static DataMessage TestDataMessage { get; } = new(TestMessage);
|
||||
|
||||
public sealed class InvocationEvent<TMessage>(TMessage message) : WorkflowEvent(message)
|
||||
{
|
||||
public TMessage Message => message;
|
||||
}
|
||||
|
||||
internal sealed record ExecutorTestResult(TestWorkflowContext Context, object? CallResult);
|
||||
|
||||
internal async ValueTask<ExecutorTestResult> Run_FunctionExecutor_MessageHandlerTestAsync<TMessage>(Executor executor, TMessage message, CancellationToken cancellationToken = default)
|
||||
where TMessage : notnull
|
||||
{
|
||||
TestWorkflowContext workflowContext = this.CreateWorkflowContext(executor);
|
||||
_ = executor.DescribeProtocol();
|
||||
|
||||
object? result = await executor.ExecuteCoreAsync(message, new(typeof(TMessage)), workflowContext, cancellationToken);
|
||||
|
||||
return new(workflowContext, result);
|
||||
}
|
||||
|
||||
internal static void CheckInvoked<TMessage>(ExecutorTestResult result, TMessage expectedInput, object? expectedCallResult = null)
|
||||
where TMessage : class
|
||||
{
|
||||
result.CallResult.Should().Be(expectedCallResult);
|
||||
|
||||
result.Context.EmittedEvents.Should().Contain(evt => evt is ExecutorInvokedEvent
|
||||
&& ((ExecutorInvokedEvent)evt).Data as TMessage == expectedInput)
|
||||
.And.Contain(evt => evt is ExecutorCompletedEvent
|
||||
&& ((ExecutorCompletedEvent)evt).Data == expectedCallResult);
|
||||
}
|
||||
|
||||
internal static void CheckInvoked<TMessage, TOutput>(ExecutorTestResult result, TMessage expectedInput, TOutput expectedCallResult)
|
||||
where TMessage : class
|
||||
where TOutput : class
|
||||
{
|
||||
result.CallResult.Should().Be(expectedCallResult);
|
||||
|
||||
result.Context.EmittedEvents.Should().Contain(evt => evt is ExecutorInvokedEvent
|
||||
&& ((ExecutorInvokedEvent)evt).Data as TMessage == expectedInput)
|
||||
.And.Contain(evt => evt is ExecutorCompletedEvent
|
||||
&& ((ExecutorCompletedEvent)evt).Data as TOutput == expectedCallResult);
|
||||
}
|
||||
|
||||
internal TestWorkflowContext CreateWorkflowContext(Executor executor) => new(executor.Id);
|
||||
}
|
||||
|
||||
public class FunctionExecutorTests : ExecutorTestsBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_FunctionExecutor__1_InvokesDelegateSuccessfullyAsync(bool useAsync)
|
||||
{
|
||||
// Arrange
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), MessageHandlerAsync)
|
||||
: new(nameof(FunctionExecutor<>), MessageHandler);
|
||||
|
||||
// Act
|
||||
ExecutorTestResult result = await this.Run_FunctionExecutor_MessageHandlerTestAsync(executor, TestMessage);
|
||||
|
||||
// Assert
|
||||
CheckInvoked(result, TestMessage);
|
||||
|
||||
// Helpers
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> default;
|
||||
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken) { }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_FunctionExecutor__2_InvokesDelegateSuccessfullyAsync(bool useAsync)
|
||||
{
|
||||
// Arrange
|
||||
FunctionExecutor<TextMessage, DataMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<,>), MessageHandlerAsync)
|
||||
: new(nameof(FunctionExecutor<,>), MessageHandler);
|
||||
|
||||
// Act
|
||||
ExecutorTestResult result = await this.Run_FunctionExecutor_MessageHandlerTestAsync(executor, TestMessage);
|
||||
|
||||
// Assert
|
||||
CheckInvoked(result, TestMessage, TestDataMessage);
|
||||
|
||||
// Helpers
|
||||
ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(new DataMessage(message));
|
||||
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__1_SendTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
IEnumerable<Type>? sendTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(TextMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, sentMessageTypes: sendTypes)
|
||||
: new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, sentMessageTypes: sendTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Sends.Should().BeEquivalentTo([typeof(TextMessage)]);
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
ValueTask MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
void MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__2_SendTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = false,
|
||||
AutoYieldOutputHandlerResultObject = false
|
||||
};
|
||||
|
||||
IEnumerable<Type>? sendTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(TextMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage, DataMessage> executor
|
||||
= useAsync
|
||||
? new(nameof(FunctionExecutor<,>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, options, sentMessageTypes: sendTypes)
|
||||
: new(nameof(FunctionExecutor<,>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, options, sentMessageTypes: sendTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Sends.Should().BeEquivalentTo([typeof(TextMessage)]);
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
ValueTask<DataMessage> MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
async ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.SendMessageAsync(message, cancellationToken);
|
||||
return new(message);
|
||||
}
|
||||
|
||||
[SendsMessage(typeof(TextMessage))]
|
||||
DataMessage MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.SendMessageAsync(message, cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
return new(message);
|
||||
}
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__1_YieldTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
IEnumerable<Type>? yieldTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(DataMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, outputTypes: yieldTypes)
|
||||
: new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, outputTypes: yieldTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Yields.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
ValueTask MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.YieldOutputAsync(new DataMessage(message), cancellationToken);
|
||||
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
void MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.YieldOutputAsync(new DataMessage(message), cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public void Test_FunctionExecutor__2_YieldTypesAreRegistered(bool useAsync, bool useAnnotated)
|
||||
{
|
||||
// Arrange
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = false,
|
||||
AutoYieldOutputHandlerResultObject = false
|
||||
};
|
||||
|
||||
IEnumerable<Type>? yieldTypes = useAnnotated
|
||||
? null
|
||||
: [typeof(DataMessage)];
|
||||
|
||||
FunctionExecutor<TextMessage, DataMessage> executor
|
||||
= useAsync
|
||||
? new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotatedAsync
|
||||
: MessageHandlerAsync, options, outputTypes: yieldTypes)
|
||||
: new(nameof(FunctionExecutor<>), useAnnotated ? MessageHandlerAnnotated
|
||||
: MessageHandler, options, outputTypes: yieldTypes);
|
||||
|
||||
// Act
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
protocol.Yields.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
ValueTask<DataMessage> MessageHandlerAnnotatedAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandlerAsync(message, context, cancellationToken);
|
||||
|
||||
async ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
await context.YieldOutputAsync(new DataMessage(message), cancellationToken);
|
||||
return new(message);
|
||||
}
|
||||
|
||||
[YieldsOutput(typeof(DataMessage))]
|
||||
DataMessage MessageHandlerAnnotated(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> MessageHandler(message, context, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
context.YieldOutputAsync(new DataMessage(message), cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
return new(message);
|
||||
}
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, false)]
|
||||
[InlineData(false, false, true)]
|
||||
[InlineData(false, true, false)]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(true, true, false)]
|
||||
[InlineData(true, true, true)]
|
||||
public void Test_FunctionExecutor__1_ExecutorOptionsAreNoOp(bool useAsync, bool autoSendReturnValue, bool autoYieldReturnValue)
|
||||
{
|
||||
// Because FunctionExecutor<TInput> does not have a rail for a returned value, setting up options for it will
|
||||
// not register any output types
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = autoSendReturnValue,
|
||||
AutoYieldOutputHandlerResultObject = autoYieldReturnValue
|
||||
};
|
||||
|
||||
FunctionExecutor<TextMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), MessageHandlerAsync, options)
|
||||
: new(nameof(FunctionExecutor<>), MessageHandler, options);
|
||||
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
|
||||
// Helpers
|
||||
ValueTask MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken);
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
void MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> context.SendMessageAsync(message, cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false, false)]
|
||||
[InlineData(false, false, true)]
|
||||
[InlineData(false, true, false)]
|
||||
[InlineData(false, true, true)]
|
||||
[InlineData(true, false, false)]
|
||||
[InlineData(true, false, true)]
|
||||
[InlineData(true, true, false)]
|
||||
[InlineData(true, true, true)]
|
||||
public async Task Test_FunctionExecutor__2_ExecutorOptionsCauseCorrectRegistration_AndAutoBehaviorAsync(bool useAsync, bool autoSendReturnValue, bool autoYieldReturnValue)
|
||||
{
|
||||
// Arrange
|
||||
// Because FunctionExecutor<TInput> does not have a rail for a returned value, setting up options for it will
|
||||
// not register any output types
|
||||
ExecutorOptions options = new()
|
||||
{
|
||||
AutoSendMessageHandlerResultObject = autoSendReturnValue,
|
||||
AutoYieldOutputHandlerResultObject = autoYieldReturnValue
|
||||
};
|
||||
|
||||
FunctionExecutor<TextMessage, DataMessage> executor = useAsync
|
||||
? new(nameof(FunctionExecutor<>), MessageHandlerAsync, options)
|
||||
: new(nameof(FunctionExecutor<>), MessageHandler, options);
|
||||
|
||||
// Act
|
||||
ExecutorTestResult result = await this.Run_FunctionExecutor_MessageHandlerTestAsync(executor, TestMessage);
|
||||
ProtocolDescriptor protocol = executor.DescribeProtocol();
|
||||
|
||||
// Assert
|
||||
CheckInvoked(result, TestMessage, TestDataMessage);
|
||||
if (autoSendReturnValue)
|
||||
{
|
||||
protocol.Sends.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
result.Context.SentMessages.Should().ContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
protocol.Sends.Should().BeEmpty();
|
||||
result.Context.SentMessages.Should().NotContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
|
||||
if (autoYieldReturnValue)
|
||||
{
|
||||
protocol.Yields.Should().BeEquivalentTo([typeof(DataMessage)]);
|
||||
result.Context.YieldedOutputs.Should().ContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
protocol.Yields.Should().BeEmpty();
|
||||
result.Context.YieldedOutputs.Should().NotContainEquivalentOf(TestDataMessage);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
ValueTask<DataMessage> MessageHandlerAsync(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(new DataMessage(message));
|
||||
|
||||
DataMessage MessageHandler(TextMessage message, IWorkflowContext context, CancellationToken cancellationToken)
|
||||
=> new(message);
|
||||
}
|
||||
}
|
||||
@@ -206,6 +206,14 @@ internal sealed class TurnTrackingStartExecutor : ChatProtocolExecutor
|
||||
}
|
||||
}
|
||||
|
||||
public class NonChatProtocolExecutor() : Executor<string>(nameof(NonChatProtocolExecutor))
|
||||
{
|
||||
public override ValueTask HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public class WorkflowHostSmokeTests : AIAgentHostingExecutorTestsBase
|
||||
{
|
||||
private sealed class AlwaysFailsAIAgent(bool failByThrowing) : AIAgent
|
||||
@@ -732,6 +740,25 @@ public class WorkflowHostSmokeTests : AIAgentHostingExecutorTestsBase
|
||||
.BeEmpty();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Test_AsAgent_FailsWhenNotChatProtocolAsync(bool runAsync)
|
||||
{
|
||||
// Arrange
|
||||
NonChatProtocolExecutor executor = new();
|
||||
executor.DescribeProtocol().IsChatProtocol().Should().BeFalse();
|
||||
|
||||
Workflow workflow = new WorkflowBuilder(executor).Build();
|
||||
AIAgent workflowAsAgent = workflow.AsAIAgent();
|
||||
|
||||
Func<Task> action = runAsync
|
||||
? () => workflowAsAgent.RunStreamingAsync().ToAgentResponseAsync()
|
||||
: () => workflowAsAgent.RunAsync();
|
||||
|
||||
await action.Should().ThrowAsync<InvalidOperationException>();
|
||||
}
|
||||
|
||||
private async Task Run_AsAgent_OutgoingMessagesInHistoryAsync(Workflow workflow, bool runAsync)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -122,7 +122,6 @@ from ._telemetry import (
|
||||
APP_INFO,
|
||||
USER_AGENT_KEY,
|
||||
USER_AGENT_TELEMETRY_DISABLED_ENV_VAR,
|
||||
get_user_agent_extra_headers,
|
||||
prepend_agent_framework_to_user_agent,
|
||||
)
|
||||
from ._tools import (
|
||||
@@ -423,7 +422,6 @@ __all__ = [
|
||||
"evaluator",
|
||||
"executor",
|
||||
"function_middleware",
|
||||
"get_user_agent_extra_headers",
|
||||
"handler",
|
||||
"included_messages",
|
||||
"included_token_count",
|
||||
|
||||
@@ -59,24 +59,6 @@ def _get_user_agent() -> str:
|
||||
return f"{'/'.join(prefixes)}/{AGENT_FRAMEWORK_USER_AGENT}"
|
||||
|
||||
|
||||
def get_user_agent_extra_headers() -> dict[str, str]:
|
||||
"""Return extra headers containing the current User-Agent string for per-request injection.
|
||||
|
||||
This function evaluates the user agent at call time, picking up any active
|
||||
``user_agent_prefix`` context. Use it to supply ``extra_headers`` on individual
|
||||
API calls so that the User-Agent reflects the current functional area.
|
||||
|
||||
When user agent telemetry is disabled, an empty dict is returned.
|
||||
|
||||
Returns:
|
||||
A dict with ``"User-Agent"`` set to the runtime user agent string,
|
||||
or an empty dict when telemetry is disabled.
|
||||
"""
|
||||
if not IS_TELEMETRY_ENABLED:
|
||||
return {}
|
||||
return {USER_AGENT_KEY: _get_user_agent()}
|
||||
|
||||
|
||||
def prepend_agent_framework_to_user_agent(headers: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Prepend "agent-framework" to the User-Agent in the headers.
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from agent_framework import (
|
||||
AGENT_FRAMEWORK_USER_AGENT,
|
||||
USER_AGENT_KEY,
|
||||
USER_AGENT_TELEMETRY_DISABLED_ENV_VAR,
|
||||
get_user_agent_extra_headers,
|
||||
prepend_agent_framework_to_user_agent,
|
||||
)
|
||||
from agent_framework._telemetry import user_agent_prefix
|
||||
@@ -151,33 +150,3 @@ def test_user_agent_prefix_nesting():
|
||||
# Both removed
|
||||
result = prepend_agent_framework_to_user_agent()
|
||||
assert result["User-Agent"] == AGENT_FRAMEWORK_USER_AGENT
|
||||
|
||||
|
||||
# region Test get_user_agent_extra_headers
|
||||
|
||||
|
||||
def test_get_user_agent_extra_headers_returns_user_agent():
|
||||
"""Test that get_user_agent_extra_headers returns a User-Agent header."""
|
||||
result = get_user_agent_extra_headers()
|
||||
assert "User-Agent" in result
|
||||
assert result["User-Agent"] == AGENT_FRAMEWORK_USER_AGENT
|
||||
|
||||
|
||||
def test_get_user_agent_extra_headers_with_prefix():
|
||||
"""Test that get_user_agent_extra_headers respects user_agent_prefix context."""
|
||||
with user_agent_prefix("test-host"):
|
||||
result = get_user_agent_extra_headers()
|
||||
assert result["User-Agent"].startswith("test-host/")
|
||||
assert AGENT_FRAMEWORK_USER_AGENT in result["User-Agent"]
|
||||
|
||||
# After exiting context, prefix is removed
|
||||
result = get_user_agent_extra_headers()
|
||||
assert result["User-Agent"] == AGENT_FRAMEWORK_USER_AGENT
|
||||
|
||||
|
||||
def test_get_user_agent_extra_headers_with_nested_prefix():
|
||||
"""Test that get_user_agent_extra_headers picks up nested prefixes."""
|
||||
with user_agent_prefix("outer"), user_agent_prefix("inner"):
|
||||
result = get_user_agent_extra_headers()
|
||||
assert "outer" in result["User-Agent"]
|
||||
assert "inner" in result["User-Agent"]
|
||||
|
||||
@@ -915,76 +915,3 @@ class TestToMessage:
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region User Agent Prefix
|
||||
|
||||
|
||||
class TestUserAgentPrefix:
|
||||
"""Tests that the user_agent_prefix context manager is active during agent execution."""
|
||||
|
||||
async def test_user_agent_prefix_set_during_non_streaming(self) -> None:
|
||||
"""The user agent should contain the foundry-hosting prefix in non-streaming mode."""
|
||||
from agent_framework._telemetry import _get_user_agent # type: ignore
|
||||
|
||||
captured_user_agent: list[str] = []
|
||||
|
||||
async def run_and_capture(*args: Any, **kwargs: Any) -> AgentResponse:
|
||||
captured_user_agent.append(_get_user_agent())
|
||||
return AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])])
|
||||
|
||||
agent = _make_agent()
|
||||
agent.run = AsyncMock(side_effect=run_and_capture)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, input_text="Hi", stream=False)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert len(captured_user_agent) == 1
|
||||
assert "foundry-hosting" in captured_user_agent[0]
|
||||
|
||||
async def test_user_agent_prefix_set_during_streaming(self) -> None:
|
||||
"""The user agent should contain the foundry-hosting prefix in streaming mode."""
|
||||
from agent_framework._telemetry import _get_user_agent # type: ignore
|
||||
|
||||
captured_user_agent: list[str] = []
|
||||
|
||||
async def _stream_gen() -> AsyncIterator[AgentResponseUpdate]:
|
||||
captured_user_agent.append(_get_user_agent())
|
||||
yield AgentResponseUpdate(contents=[Content.from_text("hello")], role="assistant")
|
||||
|
||||
def run_streaming(*args: Any, **kwargs: Any) -> Any:
|
||||
if kwargs.get("stream"):
|
||||
return ResponseStream(_stream_gen()) # type: ignore
|
||||
raise NotImplementedError
|
||||
|
||||
agent = _make_agent()
|
||||
agent.run = MagicMock(side_effect=run_streaming)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, stream=True)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert len(captured_user_agent) == 1
|
||||
assert "foundry-hosting" in captured_user_agent[0]
|
||||
|
||||
async def test_user_agent_extra_headers_during_run(self) -> None:
|
||||
"""get_user_agent_extra_headers() should include the prefix during a request."""
|
||||
from agent_framework._telemetry import get_user_agent_extra_headers
|
||||
|
||||
captured_headers: list[dict[str, str]] = []
|
||||
|
||||
async def run_and_capture(*args: Any, **kwargs: Any) -> AgentResponse:
|
||||
captured_headers.append(get_user_agent_extra_headers())
|
||||
return AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])])
|
||||
|
||||
agent = _make_agent()
|
||||
agent.run = AsyncMock(side_effect=run_and_capture)
|
||||
server = _make_server(agent)
|
||||
resp = await _post(server, input_text="Hi", stream=False)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert len(captured_headers) == 1
|
||||
assert "User-Agent" in captured_headers[0]
|
||||
assert "foundry-hosting" in captured_headers[0]["User-Agent"]
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@@ -32,7 +32,7 @@ from agent_framework._clients import BaseChatClient
|
||||
from agent_framework._compaction import CompactionStrategy, TokenizerProtocol
|
||||
from agent_framework._middleware import ChatAndFunctionMiddlewareTypes, ChatMiddlewareLayer
|
||||
from agent_framework._settings import SecretString
|
||||
from agent_framework._telemetry import USER_AGENT_KEY, get_user_agent_extra_headers
|
||||
from agent_framework._telemetry import USER_AGENT_KEY
|
||||
from agent_framework._tools import (
|
||||
SHELL_TOOL_KIND_VALUE,
|
||||
FunctionInvocationConfiguration,
|
||||
@@ -482,13 +482,6 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
client = self.client
|
||||
validated_options = await self._validate_options(options)
|
||||
run_options = await self._prepare_options(messages, validated_options)
|
||||
ua_headers = get_user_agent_extra_headers()
|
||||
if ua_headers:
|
||||
existing = run_options.get("extra_headers")
|
||||
if existing is None:
|
||||
run_options["extra_headers"] = ua_headers
|
||||
elif USER_AGENT_KEY not in existing:
|
||||
run_options["extra_headers"] = {**existing, **ua_headers}
|
||||
return client, run_options, validated_options
|
||||
|
||||
def _handle_request_error(self, ex: Exception) -> NoReturn:
|
||||
@@ -532,7 +525,6 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
stream_response = await client.responses.retrieve(
|
||||
continuation_token["response_id"],
|
||||
stream=True,
|
||||
extra_headers=get_user_agent_extra_headers(),
|
||||
)
|
||||
async for chunk in stream_response:
|
||||
yield self._parse_chunk_from_openai(
|
||||
@@ -580,10 +572,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
|
||||
client = self.client
|
||||
validated_options = await self._validate_options(options)
|
||||
try:
|
||||
response = await client.responses.retrieve(
|
||||
continuation_token["response_id"],
|
||||
extra_headers=get_user_agent_extra_headers(),
|
||||
)
|
||||
response = await client.responses.retrieve(continuation_token["response_id"])
|
||||
except Exception as ex:
|
||||
self._handle_request_error(ex)
|
||||
return self._parse_response_from_openai(response, options=validated_options)
|
||||
|
||||
@@ -22,7 +22,7 @@ from agent_framework._compaction import CompactionStrategy, TokenizerProtocol
|
||||
from agent_framework._docstrings import apply_layered_docstring
|
||||
from agent_framework._middleware import ChatAndFunctionMiddlewareTypes, ChatMiddlewareLayer
|
||||
from agent_framework._settings import SecretString
|
||||
from agent_framework._telemetry import USER_AGENT_KEY, get_user_agent_extra_headers
|
||||
from agent_framework._telemetry import USER_AGENT_KEY
|
||||
from agent_framework._tools import (
|
||||
FunctionInvocationConfiguration,
|
||||
FunctionInvocationLayer,
|
||||
@@ -671,16 +671,6 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
|
||||
run_options["response_format"] = response_format
|
||||
else:
|
||||
run_options["response_format"] = type_to_response_format_param(response_format)
|
||||
|
||||
# runtime user-agent header
|
||||
ua_headers = get_user_agent_extra_headers()
|
||||
if ua_headers:
|
||||
existing = run_options.get("extra_headers")
|
||||
if existing is None:
|
||||
run_options["extra_headers"] = ua_headers
|
||||
elif USER_AGENT_KEY not in existing:
|
||||
run_options["extra_headers"] = {**existing, **ua_headers}
|
||||
|
||||
return run_options
|
||||
|
||||
def _parse_response_from_openai(self, response: ChatCompletion, options: Mapping[str, Any]) -> ChatResponse:
|
||||
|
||||
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypedDict, ov
|
||||
|
||||
from agent_framework._clients import BaseEmbeddingClient
|
||||
from agent_framework._settings import SecretString
|
||||
from agent_framework._telemetry import USER_AGENT_KEY, get_user_agent_extra_headers
|
||||
from agent_framework._telemetry import USER_AGENT_KEY
|
||||
from agent_framework._types import Embedding, EmbeddingGenerationOptions, GeneratedEmbeddings, UsageDetails
|
||||
from agent_framework.observability import EmbeddingTelemetryLayer
|
||||
from openai import AsyncAzureOpenAI, AsyncOpenAI
|
||||
@@ -282,13 +282,6 @@ class RawOpenAIEmbeddingClient(
|
||||
kwargs["encoding_format"] = encoding_format
|
||||
if user := opts.get("user"):
|
||||
kwargs["user"] = user
|
||||
ua_headers = get_user_agent_extra_headers()
|
||||
if ua_headers:
|
||||
existing = kwargs.get("extra_headers")
|
||||
if existing is None:
|
||||
kwargs["extra_headers"] = ua_headers
|
||||
elif USER_AGENT_KEY not in existing:
|
||||
kwargs["extra_headers"] = {**existing, **ua_headers}
|
||||
|
||||
response = await self.client.embeddings.create(**kwargs) # type: ignore[union-attr]
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from copy import copy
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union
|
||||
|
||||
from agent_framework._settings import SecretString, load_settings
|
||||
from agent_framework._telemetry import APP_INFO
|
||||
from agent_framework._telemetry import APP_INFO, prepend_agent_framework_to_user_agent
|
||||
from agent_framework.exceptions import SettingNotFoundError
|
||||
from openai import AsyncAzureOpenAI, AsyncOpenAI, AsyncStream, _legacy_response # type: ignore
|
||||
from openai.types import Completion
|
||||
@@ -174,6 +174,7 @@ def load_openai_service_settings(
|
||||
merged_headers = dict(copy(default_headers)) if default_headers else {}
|
||||
if APP_INFO:
|
||||
merged_headers.update(APP_INFO)
|
||||
merged_headers = prepend_agent_framework_to_user_agent(merged_headers)
|
||||
|
||||
api_key_callable = api_key if callable(api_key) else None
|
||||
api_key_str = api_key if not callable(api_key) else None
|
||||
|
||||
Reference in New Issue
Block a user