Skip to content

Commit 0868900

Browse files
Copilotstephentoub
andauthored
Rename FunctionCallContent.InvocationRequired to InformationalOnly with inverted polarity (#7262)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
1 parent b26e2d2 commit 0868900

File tree

7 files changed

+61
-61
lines changed

7 files changed

+61
-61
lines changed

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

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

55
- Unsealed `FunctionCallContent` and `FunctionResultContent`.
6-
- Added `InvocationRequired` property to `FunctionCallContent` to indicate whether function invocation is required.
6+
- Added `InformationalOnly` property to `FunctionCallContent` to indicate whether the content is informing the consumer about a call that's being made elsewhere or that is a request for the call to be performed.
77
- Added `LoadFromAsync` and `SaveToAsync` helper methods to `DataContent` for file I/O operations.
88
- Fixed JSON schema generation for nullable reference type annotations on parameters in AIFunctions.
99

src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/FunctionCallContent.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ public FunctionCallContent(string callId, string name, IDictionary<string, objec
5757
public Exception? Exception { get; set; }
5858

5959
/// <summary>
60-
/// Gets or sets a value indicating whether this function call requires invocation.
60+
/// Gets or sets a value indicating whether this function call is purely informational.
6161
/// </summary>
6262
/// <remarks>
63-
/// This property defaults to <see langword="true"/>, indicating that the function call should be processed.
64-
/// When set to <see langword="false"/>, it indicates that the function has already been processed or is otherwise
63+
/// This property defaults to <see langword="false"/>, indicating that the function call should be processed.
64+
/// When set to <see langword="true"/>, it indicates that the function has already been processed or is otherwise
6565
/// purely informational and should be ignored by components that process function calls.
6666
/// </remarks>
67-
public bool InvocationRequired { get; set; } = true;
67+
public bool InformationalOnly { get; set; }
6868

6969
/// <summary>
7070
/// Creates a new instance of <see cref="FunctionCallContent"/> parsing arguments using a specified encoding and parser.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1818,7 +1818,7 @@
18181818
"Stage": "Stable"
18191819
},
18201820
{
1821-
"Member": "bool Microsoft.Extensions.AI.FunctionCallContent.InvocationRequired { get; set; }",
1821+
"Member": "bool Microsoft.Extensions.AI.FunctionCallContent.InformationalOnly { get; set; }",
18221822
"Stage": "Stable"
18231823
},
18241824
{

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ private static bool CopyFunctionCalls(
828828
int count = content.Count;
829829
for (int i = 0; i < count; i++)
830830
{
831-
if (content[i] is FunctionCallContent functionCall && functionCall.InvocationRequired)
831+
if (content[i] is FunctionCallContent functionCall && !functionCall.InformationalOnly)
832832
{
833833
(functionCalls ??= []).Add(functionCall);
834834
any = true;
@@ -1125,8 +1125,8 @@ private async Task<FunctionInvocationResult> ProcessFunctionCallAsync(
11251125
{
11261126
var callContent = callContents[functionCallIndex];
11271127

1128-
// Mark the function call as no longer requiring invocation since we're handling it
1129-
callContent.InvocationRequired = false;
1128+
// Mark the function call as purely informational since we're handling it
1129+
callContent.InformationalOnly = true;
11301130

11311131
// Look up the AIFunction for the function call. If the requested function isn't available, send back an error.
11321132
AIFunctionDeclaration? tool = FindTool(callContent.Name, options?.Tools, AdditionalTools);
@@ -1568,8 +1568,8 @@ private static bool CurrentActivityIsInvokeAgent
15681568
result = $"{result} {m.Response.Reason}";
15691569
}
15701570

1571-
// Mark the function call as no longer requiring invocation since we're handling it (by rejecting it)
1572-
m.Response.FunctionCall.InvocationRequired = false;
1571+
// Mark the function call as purely informational since we're handling it (by rejecting it)
1572+
m.Response.FunctionCall.InformationalOnly = true;
15731573
return (AIContent)new FunctionResultContent(m.Response.FunctionCall.CallId, result);
15741574
}) :
15751575
null;
@@ -1708,7 +1708,7 @@ private static bool TryReplaceFunctionCallsWithApprovalRequests(IList<AIContent>
17081708
{
17091709
for (int i = 0; i < content.Count; i++)
17101710
{
1711-
if (content[i] is FunctionCallContent fcc && fcc.InvocationRequired)
1711+
if (content[i] is FunctionCallContent fcc && !fcc.InformationalOnly)
17121712
{
17131713
updatedContent ??= [.. content]; // Clone the list if we haven't already
17141714
updatedContent[i] = new FunctionApprovalRequestContent(fcc.CallId, fcc);
@@ -1739,7 +1739,7 @@ private IList<ChatMessage> ReplaceFunctionCallsWithApprovalRequests(
17391739
var content = messages[i].Contents;
17401740
for (int j = 0; j < content.Count; j++)
17411741
{
1742-
if (content[j] is FunctionCallContent functionCall && functionCall.InvocationRequired)
1742+
if (content[j] is FunctionCallContent functionCall && !functionCall.InformationalOnly)
17431743
{
17441744
(allFunctionCallContentIndices ??= []).Add((i, j));
17451745

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void Constructor_PropsDefault()
2828

2929
Assert.Null(c.Arguments);
3030
Assert.Null(c.Exception);
31-
Assert.True(c.InvocationRequired);
31+
Assert.False(c.InformationalOnly);
3232
}
3333

3434
[Fact]
@@ -73,87 +73,87 @@ public void Constructor_PropsRoundtrip()
7373
c.Exception = e;
7474
Assert.Same(e, c.Exception);
7575

76-
Assert.True(c.InvocationRequired);
77-
c.InvocationRequired = false;
78-
Assert.False(c.InvocationRequired);
76+
Assert.False(c.InformationalOnly);
77+
c.InformationalOnly = true;
78+
Assert.True(c.InformationalOnly);
7979
}
8080

8181
[Theory]
8282
[InlineData(true)]
8383
[InlineData(false)]
84-
public void InvocationRequired_Serialization(bool invocationRequired)
84+
public void InformationalOnly_Serialization(bool informationalOnly)
8585
{
8686
// Arrange
8787
var sut = new FunctionCallContent("callId1", "functionName", new Dictionary<string, object?> { ["key"] = "value" })
8888
{
89-
InvocationRequired = invocationRequired
89+
InformationalOnly = informationalOnly
9090
};
9191

9292
// Act
9393
var json = JsonSerializer.SerializeToNode(sut, TestJsonSerializerContext.Default.Options);
9494

95-
// Assert - InvocationRequired should always be in the JSON (for roundtrip)
95+
// Assert - InformationalOnly should always be in the JSON (for roundtrip)
9696
Assert.NotNull(json);
9797
var jsonObj = json!.AsObject();
98-
Assert.True(jsonObj.ContainsKey("invocationRequired") || jsonObj.ContainsKey("InvocationRequired"));
98+
Assert.True(jsonObj.ContainsKey("informationalOnly") || jsonObj.ContainsKey("InformationalOnly"));
9999

100-
JsonNode? invocationRequiredValue = null;
101-
if (jsonObj.TryGetPropertyValue("invocationRequired", out var value1))
100+
JsonNode? informationalOnlyValue = null;
101+
if (jsonObj.TryGetPropertyValue("informationalOnly", out var value1))
102102
{
103-
invocationRequiredValue = value1;
103+
informationalOnlyValue = value1;
104104
}
105-
else if (jsonObj.TryGetPropertyValue("InvocationRequired", out var value2))
105+
else if (jsonObj.TryGetPropertyValue("InformationalOnly", out var value2))
106106
{
107-
invocationRequiredValue = value2;
107+
informationalOnlyValue = value2;
108108
}
109109

110-
Assert.NotNull(invocationRequiredValue);
111-
Assert.Equal(invocationRequired, invocationRequiredValue!.GetValue<bool>());
110+
Assert.NotNull(informationalOnlyValue);
111+
Assert.Equal(informationalOnly, informationalOnlyValue!.GetValue<bool>());
112112
}
113113

114114
[Theory]
115115
[InlineData(true)]
116116
[InlineData(false)]
117-
public void InvocationRequired_Deserialization(bool invocationRequired)
117+
public void InformationalOnly_Deserialization(bool informationalOnly)
118118
{
119119
// Test deserialization
120-
var json = $$"""{"callId":"callId1","name":"functionName","invocationRequired":{{(invocationRequired ? "true" : "false")}}}""";
120+
var json = $$"""{"callId":"callId1","name":"functionName","informationalOnly":{{(informationalOnly ? "true" : "false")}}}""";
121121
var deserialized = JsonSerializer.Deserialize<FunctionCallContent>(json, TestJsonSerializerContext.Default.Options);
122122

123123
Assert.NotNull(deserialized);
124124
Assert.Equal("callId1", deserialized.CallId);
125125
Assert.Equal("functionName", deserialized.Name);
126-
Assert.Equal(invocationRequired, deserialized.InvocationRequired);
126+
Assert.Equal(informationalOnly, deserialized.InformationalOnly);
127127
}
128128

129129
[Fact]
130-
public void InvocationRequired_DeserializedToTrueWhenMissing()
130+
public void InformationalOnly_DeserializedToFalseWhenMissing()
131131
{
132-
// Test deserialization when InvocationRequired is not in JSON (should default to true from field initializer)
132+
// Test deserialization when InformationalOnly is not in JSON (should default to false from field initializer)
133133
var json = """{"callId":"callId1","name":"functionName"}""";
134134
var deserialized = JsonSerializer.Deserialize<FunctionCallContent>(json, TestJsonSerializerContext.Default.Options);
135135

136136
Assert.NotNull(deserialized);
137137
Assert.Equal("callId1", deserialized.CallId);
138138
Assert.Equal("functionName", deserialized.Name);
139-
Assert.True(deserialized.InvocationRequired);
139+
Assert.False(deserialized.InformationalOnly);
140140
}
141141

142142
[Theory]
143143
[InlineData(true)]
144144
[InlineData(false)]
145-
public void InvocationRequired_Roundtrip(bool invocationRequired)
145+
public void InformationalOnly_Roundtrip(bool informationalOnly)
146146
{
147-
// Test that InvocationRequired roundtrips correctly through JSON serialization
148-
var original = new FunctionCallContent("callId1", "functionName") { InvocationRequired = invocationRequired };
147+
// Test that InformationalOnly roundtrips correctly through JSON serialization
148+
var original = new FunctionCallContent("callId1", "functionName") { InformationalOnly = informationalOnly };
149149
var json = JsonSerializer.SerializeToNode(original, TestJsonSerializerContext.Default.Options);
150150
var deserialized = JsonSerializer.Deserialize<FunctionCallContent>(json, TestJsonSerializerContext.Default.Options);
151151

152152
Assert.NotNull(deserialized);
153153
Assert.Equal(original.CallId, deserialized.CallId);
154154
Assert.Equal(original.Name, deserialized.Name);
155-
Assert.Equal(original.InvocationRequired, deserialized.InvocationRequired);
156-
Assert.Equal(invocationRequired, deserialized.InvocationRequired);
155+
Assert.Equal(original.InformationalOnly, deserialized.InformationalOnly);
156+
Assert.Equal(informationalOnly, deserialized.InformationalOnly);
157157
}
158158

159159
[Fact]

test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientApprovalsTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,7 @@ async IAsyncEnumerable<ChatResponseUpdate> YieldInnerClientUpdates(
11161116
[Theory]
11171117
[InlineData(false)]
11181118
[InlineData(true)]
1119-
public async Task FunctionCallsWithInvocationRequiredFalseAreNotReplacedWithApprovalsAsync(bool streaming)
1119+
public async Task FunctionCallsWithInformationalOnlyTrueAreNotReplacedWithApprovalsAsync(bool streaming)
11201120
{
11211121
var functionInvokedCount = 0;
11221122
var options = new ChatOptions
@@ -1130,8 +1130,8 @@ public async Task FunctionCallsWithInvocationRequiredFalseAreNotReplacedWithAppr
11301130

11311131
List<ChatMessage> input = [new ChatMessage(ChatRole.User, "hello")];
11321132

1133-
// FunctionCallContent with InvocationRequired = false should pass through unchanged
1134-
var alreadyProcessedFunctionCall = new FunctionCallContent("callId1", "Func1") { InvocationRequired = false };
1133+
// FunctionCallContent with InformationalOnly = true should pass through unchanged
1134+
var alreadyProcessedFunctionCall = new FunctionCallContent("callId1", "Func1") { InformationalOnly = true };
11351135
List<ChatMessage> downstreamClientOutput =
11361136
[
11371137
new ChatMessage(ChatRole.Assistant, [alreadyProcessedFunctionCall]),
@@ -1152,7 +1152,7 @@ public async Task FunctionCallsWithInvocationRequiredFalseAreNotReplacedWithAppr
11521152
await InvokeAndAssertAsync(options, input, downstreamClientOutput, expectedOutput);
11531153
}
11541154

1155-
// The function should NOT have been invoked since InvocationRequired was false
1155+
// The function should NOT have been invoked since InformationalOnly was true
11561156
Assert.Equal(0, functionInvokedCount);
11571157
}
11581158

test/Libraries/Microsoft.Extensions.AI.Tests/ChatCompletion/FunctionInvokingChatClientTests.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1925,7 +1925,7 @@ public async Task CreatesOrchestrateToolsSpanWhenNoInvokeAgentParent(bool stream
19251925
}
19261926

19271927
[Fact]
1928-
public async Task InvocationRequired_SetToFalseAfterProcessing()
1928+
public async Task InformationalOnly_SetToTrueAfterProcessing()
19291929
{
19301930
var options = new ChatOptions
19311931
{
@@ -1946,14 +1946,14 @@ public async Task InvocationRequired_SetToFalseAfterProcessing()
19461946
var functionCallMessage = chat.First(m => m.Contents.Any(c => c is FunctionCallContent));
19471947
var functionCallContent = functionCallMessage.Contents.OfType<FunctionCallContent>().First();
19481948

1949-
// Verify InvocationRequired was set to false after processing
1950-
Assert.False(functionCallContent.InvocationRequired);
1949+
// Verify InformationalOnly was set to true after processing
1950+
Assert.True(functionCallContent.InformationalOnly);
19511951
}
19521952

19531953
[Theory]
19541954
[InlineData(false)]
19551955
[InlineData(true)]
1956-
public async Task InvocationRequired_IgnoresFunctionCallsWithInvocationRequiredFalse(bool streaming)
1956+
public async Task InformationalOnly_IgnoresFunctionCallsWithInformationalOnlyTrue(bool streaming)
19571957
{
19581958
var functionInvokedCount = 0;
19591959
var options = new ChatOptions
@@ -1962,21 +1962,21 @@ public async Task InvocationRequired_IgnoresFunctionCallsWithInvocationRequiredF
19621962
};
19631963

19641964
// Create a function call that has already been processed
1965-
var alreadyProcessedFunctionCall = new FunctionCallContent("callId1", "Func1") { InvocationRequired = false };
1965+
var alreadyProcessedFunctionCall = new FunctionCallContent("callId1", "Func1") { InformationalOnly = true };
19661966

19671967
using var innerClient = new TestChatClient
19681968
{
19691969
GetResponseAsyncCallback = async (contents, actualOptions, actualCancellationToken) =>
19701970
{
19711971
await Task.Yield();
19721972

1973-
// Return a response with a FunctionCallContent that has InvocationRequired = false
1973+
// Return a response with a FunctionCallContent that has InformationalOnly = true
19741974
var message = new ChatMessage(ChatRole.Assistant, [alreadyProcessedFunctionCall]);
19751975
return new ChatResponse(message);
19761976
},
19771977
GetStreamingResponseAsyncCallback = (contents, actualOptions, actualCancellationToken) =>
19781978
{
1979-
// Return a response with a FunctionCallContent that has InvocationRequired = false
1979+
// Return a response with a FunctionCallContent that has InformationalOnly = true
19801980
var message = new ChatMessage(ChatRole.Assistant, [alreadyProcessedFunctionCall]);
19811981
return YieldAsync(new ChatResponse(message).ToChatResponseUpdates());
19821982
}
@@ -1988,18 +1988,18 @@ public async Task InvocationRequired_IgnoresFunctionCallsWithInvocationRequiredF
19881988
? await client.GetStreamingResponseAsync([new ChatMessage(ChatRole.User, "hello")], options).ToChatResponseAsync()
19891989
: await client.GetResponseAsync([new ChatMessage(ChatRole.User, "hello")], options);
19901990

1991-
// The function should not have been invoked since InvocationRequired was false
1991+
// The function should not have been invoked since InformationalOnly was true
19921992
Assert.Equal(0, functionInvokedCount);
19931993

19941994
// The response should contain the FunctionCallContent but no FunctionResultContent
1995-
Assert.Contains(response.Messages, m => m.Contents.Any(c => c is FunctionCallContent fcc && !fcc.InvocationRequired));
1995+
Assert.Contains(response.Messages, m => m.Contents.Any(c => c is FunctionCallContent fcc && fcc.InformationalOnly));
19961996
Assert.DoesNotContain(response.Messages, m => m.Contents.Any(c => c is FunctionResultContent));
19971997
}
19981998

19991999
[Theory]
20002000
[InlineData(false)]
20012001
[InlineData(true)]
2002-
public async Task InvocationRequired_ProcessesMixedFunctionCalls(bool streaming)
2002+
public async Task InformationalOnly_ProcessesMixedFunctionCalls(bool streaming)
20032003
{
20042004
var func1InvokedCount = 0;
20052005
var func2InvokedCount = 0;
@@ -2014,8 +2014,8 @@ public async Task InvocationRequired_ProcessesMixedFunctionCalls(bool streaming)
20142014
};
20152015

20162016
// Create one function call that needs processing and one that doesn't
2017-
var needsProcessing = new FunctionCallContent("callId1", "Func1") { InvocationRequired = true };
2018-
var alreadyProcessed = new FunctionCallContent("callId2", "Func2") { InvocationRequired = false };
2017+
var needsProcessing = new FunctionCallContent("callId1", "Func1") { InformationalOnly = false };
2018+
var alreadyProcessed = new FunctionCallContent("callId2", "Func2") { InformationalOnly = true };
20192019

20202020
using var innerClient = new TestChatClient
20212021
{
@@ -2059,7 +2059,7 @@ public async Task InvocationRequired_ProcessesMixedFunctionCalls(bool streaming)
20592059
? await client.GetStreamingResponseAsync([new ChatMessage(ChatRole.User, "hello")], options).ToChatResponseAsync()
20602060
: await client.GetResponseAsync([new ChatMessage(ChatRole.User, "hello")], options);
20612061

2062-
// Only Func1 should have been invoked (the one with InvocationRequired = true)
2062+
// Only Func1 should have been invoked (the one with InformationalOnly = false)
20632063
Assert.Equal(1, func1InvokedCount);
20642064
Assert.Equal(0, func2InvokedCount);
20652065

@@ -2069,7 +2069,7 @@ public async Task InvocationRequired_ProcessesMixedFunctionCalls(bool streaming)
20692069
}
20702070

20712071
[Fact]
2072-
public async Task InvocationRequired_MultipleFunctionInvokingChatClientsOnlyProcessOnce()
2072+
public async Task InformationalOnly_MultipleFunctionInvokingChatClientsOnlyProcessOnce()
20732073
{
20742074
// Test that when multiple FunctionInvokingChatClients are in a pipeline,
20752075
// each FunctionCallContent is only processed once (by the first one that sees it)
@@ -2108,8 +2108,8 @@ public async Task InvocationRequired_MultipleFunctionInvokingChatClientsOnlyProc
21082108
// The function should have been invoked EXACTLY ONCE, not twice (once per FICC)
21092109
Assert.Equal(1, functionInvokedCount);
21102110

2111-
// The response should contain the FunctionCallContent with InvocationRequired = false
2112-
Assert.Contains(response.Messages, m => m.Contents.Any(c => c is FunctionCallContent fcc && fcc.CallId == "callId1" && !fcc.InvocationRequired));
2111+
// The response should contain the FunctionCallContent with InformationalOnly = true
2112+
Assert.Contains(response.Messages, m => m.Contents.Any(c => c is FunctionCallContent fcc && fcc.CallId == "callId1" && fcc.InformationalOnly));
21132113

21142114
// There should be a FunctionResultContent since the function was processed
21152115
Assert.Contains(response.Messages, m => m.Contents.Any(c => c is FunctionResultContent frc && frc.CallId == "callId1"));
@@ -2255,12 +2255,12 @@ private static List<AIContent> CloneContents(IList<AIContent> contents)
22552255
var cloned = new List<AIContent>(contents.Count);
22562256
foreach (var content in contents)
22572257
{
2258-
// Clone FunctionCallContent to avoid sharing InvocationRequired state
2258+
// Clone FunctionCallContent to avoid sharing InformationalOnly state
22592259
if (content is FunctionCallContent fcc)
22602260
{
22612261
cloned.Add(new FunctionCallContent(fcc.CallId, fcc.Name, fcc.Arguments)
22622262
{
2263-
InvocationRequired = fcc.InvocationRequired,
2263+
InformationalOnly = fcc.InformationalOnly,
22642264
Exception = fcc.Exception,
22652265
AdditionalProperties = fcc.AdditionalProperties,
22662266
Annotations = fcc.Annotations,

0 commit comments

Comments
 (0)