Skip to content

Commit 0392247

Browse files
authored
Add AITool.GetService (#6830)
Following the same pattern as elsewhere in M.E.AI, this enables a consumer to reach through layers of delegating tools to grab information from inner ones, such as whether they were marked as requiring approval.
1 parent d649cbb commit 0392247

File tree

12 files changed

+115
-9
lines changed

12 files changed

+115
-9
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## NOT YET RELEASED
44

55
- Added new `ChatResponseFormat.ForJsonSchema` overloads that export a JSON schema from a .NET type.
6+
- Added new `AITool.GetService` virtual method.
67
- Updated `TextReasoningContent` to include `ProtectedData` for representing encrypted/redacted content.
78
- Fixed `MinLength`/`MaxLength`/`Length` attribute mapping in nullable string properties during schema export.
89

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunction.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,14 @@ protected DelegatingAIFunction(AIFunction innerFunction)
5858
/// <inheritdoc />
5959
protected override ValueTask<object?> InvokeCoreAsync(AIFunctionArguments arguments, CancellationToken cancellationToken) =>
6060
InnerFunction.InvokeAsync(arguments, cancellationToken);
61+
62+
/// <inheritdoc />
63+
public override object? GetService(Type serviceType, object? serviceKey = null)
64+
{
65+
_ = Throw.IfNull(serviceType);
66+
67+
return
68+
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
69+
InnerFunction.GetService(serviceType, serviceKey);
70+
}
6171
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Functions/DelegatingAIFunctionDeclaration.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,14 @@ protected DelegatingAIFunctionDeclaration(AIFunctionDeclaration innerFunction)
4545

4646
/// <inheritdoc />
4747
public override string ToString() => InnerFunction.ToString();
48+
49+
/// <inheritdoc />
50+
public override object? GetService(Type serviceType, object? serviceKey = null)
51+
{
52+
_ = Throw.IfNull(serviceType);
53+
54+
return
55+
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
56+
InnerFunction.GetService(serviceType, serviceKey);
57+
}
4858
}

src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,14 @@
685685
"Member": "Microsoft.Extensions.AI.AITool.AITool();",
686686
"Stage": "Stable"
687687
},
688+
{
689+
"Member": "virtual object? Microsoft.Extensions.AI.AITool.GetService(System.Type serviceType, object? serviceKey = null);",
690+
"Stage": "Stable"
691+
},
692+
{
693+
"Member": "TService? Microsoft.Extensions.AI.AITool.GetService<TService>(object? serviceKey = null);",
694+
"Stage": "Stable"
695+
},
688696
{
689697
"Member": "override string Microsoft.Extensions.AI.AITool.ToString();",
690698
"Stage": "Stable"
@@ -1477,6 +1485,10 @@
14771485
"Member": "Microsoft.Extensions.AI.DelegatingAIFunction.DelegatingAIFunction(Microsoft.Extensions.AI.AIFunction innerFunction);",
14781486
"Stage": "Stable"
14791487
},
1488+
{
1489+
"Member": "override object? Microsoft.Extensions.AI.DelegatingAIFunction.GetService(System.Type serviceType, object? serviceKey = null);",
1490+
"Stage": "Stable"
1491+
},
14801492
{
14811493
"Member": "override System.Threading.Tasks.ValueTask<object?> Microsoft.Extensions.AI.DelegatingAIFunction.InvokeCoreAsync(Microsoft.Extensions.AI.AIFunctionArguments arguments, System.Threading.CancellationToken cancellationToken);",
14821494
"Stage": "Stable"

src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Diagnostics;
67
using System.Text;
78
using Microsoft.Shared.Collections;
9+
using Microsoft.Shared.Diagnostics;
810

911
namespace Microsoft.Extensions.AI;
1012

@@ -31,6 +33,35 @@ protected AITool()
3133
/// <inheritdoc/>
3234
public override string ToString() => Name;
3335

36+
/// <summary>Asks the <see cref="AITool"/> for an object of the specified type <paramref name="serviceType"/>.</summary>
37+
/// <param name="serviceType">The type of object being requested.</param>
38+
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
39+
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
40+
/// <exception cref="ArgumentNullException"><paramref name="serviceType"/> is <see langword="null"/>.</exception>
41+
/// <remarks>
42+
/// The purpose of this method is to allow for the retrieval of strongly-typed services that might be provided by the <see cref="AITool"/>,
43+
/// including itself or any services it might be wrapping.
44+
/// </remarks>
45+
public virtual object? GetService(Type serviceType, object? serviceKey = null)
46+
{
47+
_ = Throw.IfNull(serviceType);
48+
49+
return
50+
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
51+
null;
52+
}
53+
54+
/// <summary>Asks the <see cref="AITool"/> for an object of type <typeparamref name="TService"/>.</summary>
55+
/// <typeparam name="TService">The type of the object to be retrieved.</typeparam>
56+
/// <param name="serviceKey">An optional key that can be used to help identify the target service.</param>
57+
/// <returns>The found object, otherwise <see langword="null"/>.</returns>
58+
/// <remarks>
59+
/// The purpose of this method is to allow for the retrieval of strongly typed services that may be provided by the <see cref="AITool"/>,
60+
/// including itself or any services it might be wrapping.
61+
/// </remarks>
62+
public TService? GetService<TService>(object? serviceKey = null) =>
63+
GetService(typeof(TService), serviceKey) is TService service ? service : default;
64+
3465
/// <summary>Gets the string to display in the debugger for this instance.</summary>
3566
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
3667
private string DebuggerDisplay

src/Libraries/Microsoft.Extensions.AI.AzureAIInference/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## NOT YET RELEASED
44

5+
- Updated to accommodate the additions in `Microsoft.Extensions.AI.Abstractions`.
6+
57
## 9.9.0-preview.1.25458.4
68

79
- Updated tool mapping to recognize any `AIFunctionDeclaration`.

src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,5 +897,15 @@ internal sealed class ResponseToolAITool(ResponseTool tool) : AITool
897897
{
898898
public ResponseTool Tool => tool;
899899
public override string Name => Tool.GetType().Name;
900+
901+
/// <inheritdoc />
902+
public override object? GetService(Type serviceType, object? serviceKey = null)
903+
{
904+
_ = Throw.IfNull(serviceType);
905+
906+
return
907+
serviceKey is null && serviceType.IsInstanceOfType(Tool) ? Tool :
908+
base.GetService(serviceType, serviceKey);
909+
}
900910
}
901911
}

src/Libraries/Microsoft.Extensions.AI/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
## NOT YET RELEASED
44

5-
- Updated the EnableSensitiveData properties on OpenTelemetryChatClient/EmbeddingGenerator to respect a OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT environment variable.
6-
- Added OpenTelemetryImageGenerator to provide OpenTelemetry instrumentation for IImageGenerator implementations.
5+
- Updated the `EnableSensitiveData` properties on `OpenTelemetryChatClient/EmbeddingGenerator` to respect a `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` environment variable.
6+
- Updated `OpenTelemetryChatClient/EmbeddingGenerator` to emit recent additions to the OpenTelemetry Semantic Conventions for Generative AI systems.
7+
- Added `OpenTelemetryImageGenerator` to provide OpenTelemetry instrumentation for `IImageGenerator` implementations.
78

89
## 9.9.0
910

src/Libraries/Microsoft.Extensions.AI/ChatCompletion/FunctionInvokingChatClient.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
430430
List<ChatMessage> originalMessages = [.. messages];
431431
messages = originalMessages;
432432

433-
ApprovalRequiredAIFunction[]? approvalRequiredFunctions = null; // available tools that require approval
433+
AITool[]? approvalRequiredFunctions = null; // available tools that require approval
434434
List<ChatMessage>? augmentedHistory = null; // the actual history of messages sent on turns other than the first
435435
List<FunctionCallContent>? functionCallContents = null; // function call contents that need responding to in the current turn
436436
List<ChatMessage>? responseMessages = null; // tracked list of messages, across multiple turns, to be used in fallback cases to reconstitute history
@@ -539,7 +539,7 @@ public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseA
539539
approvalRequiredFunctions =
540540
(options?.Tools ?? Enumerable.Empty<AITool>())
541541
.Concat(AdditionalTools ?? Enumerable.Empty<AITool>())
542-
.OfType<ApprovalRequiredAIFunction>()
542+
.Where(t => t.GetService<ApprovalRequiredAIFunction>() is not null)
543543
.ToArray();
544544
}
545545

@@ -741,7 +741,7 @@ private static (Dictionary<string, AITool>? ToolMap, bool AnyRequireApproval) Cr
741741
for (int i = 0; i < count; i++)
742742
{
743743
AITool tool = toolList[i];
744-
anyRequireApproval |= tool is ApprovalRequiredAIFunction;
744+
anyRequireApproval |= tool.GetService<ApprovalRequiredAIFunction>() is not null;
745745
map[tool.Name] = tool;
746746
}
747747
}
@@ -1475,7 +1475,7 @@ private static ChatMessage ConvertToFunctionCallContentMessage(ApprovalResultWit
14751475
/// </summary>
14761476
private static (bool hasApprovalRequiringFcc, int lastApprovalCheckedFCCIndex) CheckForApprovalRequiringFCC(
14771477
List<FunctionCallContent>? functionCallContents,
1478-
ApprovalRequiredAIFunction[] approvalRequiredFunctions,
1478+
AITool[] approvalRequiredFunctions,
14791479
bool hasApprovalRequiringFcc,
14801480
int lastApprovalCheckedFCCIndex)
14811481
{
@@ -1556,7 +1556,7 @@ private static IList<ChatMessage> ReplaceFunctionCallsWithApprovalRequests(
15561556
{
15571557
foreach (var t in toolMap)
15581558
{
1559-
if (t.Value is ApprovalRequiredAIFunction araf && araf.Name == functionCall.Name)
1559+
if (t.Value.GetService<ApprovalRequiredAIFunction>() is { } araf && araf.Name == functionCall.Name)
15601560
{
15611561
anyApprovalRequired = true;
15621562
break;

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Functions/DelegatingAIFunctionTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public void Constructor_NullInnerFunction_ThrowsArgumentNullException()
2020
[Fact]
2121
public void DefaultOverrides_DelegateToInnerFunction()
2222
{
23-
AIFunction expected = AIFunctionFactory.Create(() => 42);
23+
AIFunction expected = new ApprovalRequiredAIFunction(AIFunctionFactory.Create(() => 42));
2424
DerivedFunction actual = new(expected);
2525

2626
Assert.Same(expected, actual.InnerFunction);
@@ -32,6 +32,7 @@ public void DefaultOverrides_DelegateToInnerFunction()
3232
Assert.Same(expected.UnderlyingMethod, actual.UnderlyingMethod);
3333
Assert.Same(expected.AdditionalProperties, actual.AdditionalProperties);
3434
Assert.Equal(expected.ToString(), actual.ToString());
35+
Assert.Same(expected, actual.GetService<ApprovalRequiredAIFunction>());
3536
}
3637

3738
private sealed class DerivedFunction(AIFunction innerFunction) : DelegatingAIFunction(innerFunction)

0 commit comments

Comments
 (0)