Skip to content

Commit f933c03

Browse files
TaoChenOSUcrickman
andauthored
.Net: Input & output attributes in invoke_agent span (#12928)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> The OTel workgroup has been thinking about enhancing tracing for agents. This is a proposal from our tracing team: open-telemetry/semantic-conventions#2528. This PR implements part of the proposal so that when the proposal gets merged, we can quickly follow. This is a follow-up PR after the Python PR #12834 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Two new attributes are added to the invoke_agent span, namely the `gen_ai.agent.invocation_input` and `gen_ai.agent.invocation_output`. <img width="870" height="384" alt="image" src="https://github.com/user-attachments/assets/c2cf069a-af00-4ae2-8fed-745c166d9cbc" /> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄 --------- Co-authored-by: Chris <[email protected]>
1 parent 96bad5c commit f933c03

File tree

9 files changed

+205
-64
lines changed

9 files changed

+205
-64
lines changed

dotnet/samples/Demos/TelemetryWithAppInsights/Program.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using Microsoft.Extensions.DependencyInjection;
1313
using Microsoft.Extensions.Logging;
1414
using Microsoft.SemanticKernel;
15+
using Microsoft.SemanticKernel.Agents;
16+
using Microsoft.SemanticKernel.ChatCompletion;
1517
using Microsoft.SemanticKernel.Connectors.AzureAIInference;
1618
using Microsoft.SemanticKernel.Connectors.Google;
1719
using Microsoft.SemanticKernel.Connectors.HuggingFace;
@@ -102,6 +104,13 @@ public static async Task Main()
102104
await RunAzureOpenAIToolCallsAsync(kernel);
103105
Console.WriteLine();
104106
}
107+
108+
Console.WriteLine("Run ChatCompletion Agent.");
109+
using (var _ = s_activitySource.StartActivity("Agent"))
110+
{
111+
await RunChatCompletionAgentAsync(kernel);
112+
Console.WriteLine();
113+
}
105114
}
106115

107116
#region Private
@@ -307,6 +316,40 @@ private static async Task RunAutoToolCallAsync(Kernel kernel)
307316
}
308317
#endregion
309318

319+
#region Agent
320+
321+
private static async Task RunChatCompletionAgentAsync(Kernel kernel)
322+
{
323+
Console.WriteLine("============= ChatCompletion Agent =============");
324+
325+
if (TestConfiguration.AzureOpenAI is null)
326+
{
327+
Console.WriteLine("Azure OpenAI is not configured. Skipping.");
328+
return;
329+
}
330+
331+
SetTargetService(kernel, AzureOpenAIServiceKey);
332+
333+
// Define the agent
334+
ChatCompletionAgent agent =
335+
new()
336+
{
337+
Name = "TestAgent",
338+
Instructions = "You are a helpful assistant.",
339+
Kernel = kernel
340+
};
341+
342+
ChatMessageContent message = new(AuthorRole.User, "Write a poem about John Doe.");
343+
Console.WriteLine($"User: {message.Content}");
344+
345+
await foreach (AgentResponseItem<ChatMessageContent> response in agent.InvokeAsync(message))
346+
{
347+
Console.WriteLine($"Agent: {response.Message.Content}");
348+
}
349+
}
350+
351+
#endregion
352+
310353
private static Kernel GetKernel(ILoggerFactory loggerFactory)
311354
{
312355
var folder = RepoFiles.SamplePluginsPath();

dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<ProjectReference Include="..\..\..\src\Connectors\Connectors.Google\Connectors.Google.csproj" />
2626
<ProjectReference Include="..\..\..\src\Connectors\Connectors.HuggingFace\Connectors.HuggingFace.csproj" />
2727
<ProjectReference Include="..\..\..\src\SemanticKernel.Core\SemanticKernel.Core.csproj" />
28+
<ProjectReference Include="..\..\..\src\Agents\Core\Agents.Core.csproj" />
2829
<ProjectReference Include="..\..\..\src\Plugins\Plugins.Core\Plugins.Core.csproj" />
2930
<ProjectReference Include="..\..\..\src\Plugins\Plugins.Web\Plugins.Web.csproj" />
3031
</ItemGroup>

dotnet/src/Agents/AzureAI/AzureAIAgent.cs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,16 @@ public async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync
161161
new AzureAIAgentInvokeOptions() { AdditionalInstructions = mergedAdditionalInstructions } :
162162
new AzureAIAgentInvokeOptions(options) { AdditionalInstructions = mergedAdditionalInstructions };
163163

164-
var invokeResults = ActivityExtensions.RunWithActivityAsync(
165-
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description),
166-
() => InternalInvokeAsync(),
167-
cancellationToken);
164+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description, messages);
165+
List<ChatMessageContent>? chatMessageContents = activity is not null ? [] : null;
166+
167+
await foreach (var result in InternalInvokeAsync().ConfigureAwait(false))
168+
{
169+
yield return new(result, azureAIAgentThread);
170+
chatMessageContents?.Add(result);
171+
}
172+
173+
activity?.SetAgentResponse(chatMessageContents);
168174

169175
async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync()
170176
{
@@ -191,12 +197,6 @@ async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync()
191197
}
192198
}
193199
}
194-
195-
// Notify the thread of new messages and return them to the caller.
196-
await foreach (var result in invokeResults.ConfigureAwait(false))
197-
{
198-
yield return new(result, azureAIAgentThread);
199-
}
200200
}
201201

202202
/// <inheritdoc/>
@@ -264,21 +264,21 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
264264
new AzureAIAgentInvokeOptions() { AdditionalInstructions = mergedAdditionalInstructions } :
265265
new AzureAIAgentInvokeOptions(options) { AdditionalInstructions = mergedAdditionalInstructions };
266266

267+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description, messages);
268+
List<StreamingChatMessageContent>? streamedContents = activity is not null ? [] : null;
269+
267270
// Invoke the Agent with the thread that we already added our message to, and with
268271
// a chat history to receive complete messages.
269272
ChatHistory newMessagesReceiver = [];
270-
var invokeResults = ActivityExtensions.RunWithActivityAsync(
271-
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description),
272-
() => AgentThreadActions.InvokeStreamingAsync(
273-
this,
274-
this.Client,
275-
azureAIAgentThread.Id!,
276-
newMessagesReceiver,
277-
extensionsContextOptions.ToAzureAIInvocationOptions(),
278-
this.Logger,
279-
kernel,
280-
options?.KernelArguments,
281-
cancellationToken),
273+
var invokeResults = AgentThreadActions.InvokeStreamingAsync(
274+
this,
275+
this.Client,
276+
azureAIAgentThread.Id!,
277+
newMessagesReceiver,
278+
extensionsContextOptions.ToAzureAIInvocationOptions(),
279+
this.Logger,
280+
kernel,
281+
options?.KernelArguments,
282282
cancellationToken);
283283

284284
// Return the chunks to the caller.
@@ -289,11 +289,14 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
289289
await NotifyMessagesAsync().ConfigureAwait(false);
290290

291291
yield return new(result, azureAIAgentThread);
292+
streamedContents?.Add(result);
292293
}
293294

294295
// Notify the thread of any remaining messages that were assembled from the streaming response after all iterations are complete.
295296
await NotifyMessagesAsync().ConfigureAwait(false);
296297

298+
activity?.EndAgentStreamingResponse(streamedContents);
299+
297300
async Task NotifyMessagesAsync()
298301
{
299302
for (; messageIndex < newMessagesReceiver.Count; messageIndex++)

dotnet/src/Agents/AzureAI/AzureAIChannel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ protected override async Task ReceiveAsync(IEnumerable<ChatMessageContent> histo
4545
CancellationToken cancellationToken)
4646
{
4747
return ActivityExtensions.RunWithActivityAsync(
48-
() => ModelDiagnostics.StartAgentInvocationActivity(agent.Id, agent.GetDisplayName(), agent.Description),
48+
() => ModelDiagnostics.StartAgentInvocationActivity(agent.Id, agent.GetDisplayName(), agent.Description, []),
4949
() => AgentThreadActions.InvokeAsync(agent, client, threadId, invocationOptions: null, this.Logger, agent.Kernel, agent.Arguments, cancellationToken),
5050
cancellationToken);
5151
}
@@ -54,7 +54,7 @@ protected override async Task ReceiveAsync(IEnumerable<ChatMessageContent> histo
5454
protected override IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamingAsync(AzureAIAgent agent, IList<ChatMessageContent> messages, CancellationToken cancellationToken = default)
5555
{
5656
return ActivityExtensions.RunWithActivityAsync(
57-
() => ModelDiagnostics.StartAgentInvocationActivity(agent.Id, agent.GetDisplayName(), agent.Description),
57+
() => ModelDiagnostics.StartAgentInvocationActivity(agent.Id, agent.GetDisplayName(), agent.Description, messages),
5858
() => AgentThreadActions.InvokeStreamingAsync(agent, client, threadId, messages, invocationOptions: null, this.Logger, agent.Kernel, agent.Arguments, cancellationToken),
5959
cancellationToken);
6060
}

dotnet/src/Agents/Bedrock/BedrockAgent.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
107107
AgentInvokeOptions? options = null,
108108
[EnumeratorCancellation] CancellationToken cancellationToken = default)
109109
{
110-
Verify.NotNull(messages, nameof(messages));
111110
if (messages.Count == 0)
112111
{
113112
throw new InvalidOperationException("The Bedrock agent requires a message to be invoked.");
@@ -145,15 +144,21 @@ public override async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> In
145144
};
146145
});
147146

147+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description, messages);
148+
148149
// Invoke the agent
149150
var invokeResults = this.InvokeInternalAsync(invokeAgentRequest, options?.KernelArguments, cancellationToken);
151+
List<ChatMessageContent>? chatMessageContents = activity is not null ? [] : null;
150152

151153
// Return the results to the caller in AgentResponseItems.
152154
await foreach (var result in invokeResults.ConfigureAwait(false))
153155
{
154156
await this.NotifyThreadOfNewMessage(bedrockThread, result, cancellationToken).ConfigureAwait(false);
155157
yield return new(result, bedrockThread);
158+
chatMessageContents?.Add(result);
156159
}
160+
161+
activity?.SetAgentResponse(chatMessageContents);
157162
}
158163

159164
/// <summary>
@@ -206,6 +211,9 @@ public async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync
206211
invokeAgentRequest.SessionId = bedrockThread.Id;
207212
invokeAgentRequest = this.ConfigureAgentRequest(options, () => invokeAgentRequest);
208213

214+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description, []);
215+
List<ChatMessageContent>? chatMessageContents = activity is not null ? [] : null;
216+
209217
// Invoke the agent
210218
var invokeResults = this.InvokeInternalAsync(invokeAgentRequest, options?.KernelArguments, cancellationToken);
211219

@@ -214,7 +222,10 @@ public async IAsyncEnumerable<AgentResponseItem<ChatMessageContent>> InvokeAsync
214222
{
215223
await this.NotifyThreadOfNewMessage(bedrockThread, result, cancellationToken).ConfigureAwait(false);
216224
yield return new(result, bedrockThread);
225+
chatMessageContents?.Add(result);
217226
}
227+
228+
activity?.SetAgentResponse(chatMessageContents);
218229
}
219230

220231
#endregion
@@ -286,14 +297,20 @@ public override async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageCon
286297
};
287298
});
288299

300+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description, []);
301+
List<StreamingChatMessageContent>? streamedContents = activity is not null ? [] : null;
302+
289303
// Invoke the agent
290304
var invokeResults = this.InvokeStreamingInternalAsync(invokeAgentRequest, bedrockThread, options?.KernelArguments, cancellationToken);
291305

292306
// Return the results to the caller in AgentResponseItems.
293307
await foreach (var result in invokeResults.ConfigureAwait(false))
294308
{
295309
yield return new(result, bedrockThread);
310+
streamedContents?.Add(result);
296311
}
312+
313+
activity?.EndAgentStreamingResponse(streamedContents);
297314
}
298315

299316
/// <summary>
@@ -348,6 +365,9 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
348365
invokeAgentRequest.SessionId = bedrockThread.Id;
349366
invokeAgentRequest = this.ConfigureAgentRequest(options, () => invokeAgentRequest);
350367

368+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description, []);
369+
List<StreamingChatMessageContent>? streamedContents = activity is not null ? [] : null;
370+
351371
var invokeResults = this.InvokeStreamingInternalAsync(invokeAgentRequest, bedrockThread, options?.KernelArguments, cancellationToken);
352372

353373
// The Bedrock agent service has the same API for both streaming and non-streaming responses.
@@ -364,7 +384,10 @@ public async IAsyncEnumerable<AgentResponseItem<StreamingChatMessageContent>> In
364384
Metadata = chatMessageContent.Metadata,
365385
},
366386
thread: bedrockThread);
387+
streamedContents?.Add(chatMessageContent);
367388
}
389+
390+
activity?.EndAgentStreamingResponse(streamedContents);
368391
}
369392

370393
#endregion
@@ -409,10 +432,7 @@ private IAsyncEnumerable<ChatMessageContent> InvokeInternalAsync(
409432
{
410433
return invokeAgentRequest.StreamingConfigurations != null && (invokeAgentRequest.StreamingConfigurations.StreamFinalResponse ?? false)
411434
? throw new ArgumentException("The streaming configuration must be null for non-streaming responses.")
412-
: ActivityExtensions.RunWithActivityAsync(
413-
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description),
414-
InvokeInternal,
415-
cancellationToken);
435+
: InvokeInternal();
416436

417437
// Collect all responses from the agent and return them as a single chat message content since this
418438
// is a non-streaming API.
@@ -471,10 +491,7 @@ private IAsyncEnumerable<StreamingChatMessageContent> InvokeStreamingInternalAsy
471491
throw new ArgumentException("The streaming configuration must have StreamFinalResponse set to true.");
472492
}
473493

474-
return ActivityExtensions.RunWithActivityAsync(
475-
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, this.GetDisplayName(), this.Description),
476-
InvokeInternal,
477-
cancellationToken);
494+
return InvokeInternal();
478495

479496
async IAsyncEnumerable<StreamingChatMessageContent> InvokeInternal()
480497
{

dotnet/src/Agents/Core/ChatCompletionAgent.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,7 @@ protected internal override IAsyncEnumerable<ChatMessageContent> InvokeAsync(
158158
{
159159
string agentName = this.GetDisplayName();
160160

161-
return ActivityExtensions.RunWithActivityAsync(
162-
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, agentName, this.Description),
163-
() => this.InternalInvokeAsync(agentName, history, (m) => Task.CompletedTask, arguments, kernel, null, cancellationToken),
164-
cancellationToken);
161+
return this.InternalInvokeAsync(agentName, history, (m) => Task.CompletedTask, arguments, kernel, null, cancellationToken);
165162
}
166163

167164
/// <inheritdoc/>
@@ -241,16 +238,13 @@ protected internal override IAsyncEnumerable<StreamingChatMessageContent> Invoke
241238
{
242239
string agentName = this.GetDisplayName();
243240

244-
return ActivityExtensions.RunWithActivityAsync(
245-
() => ModelDiagnostics.StartAgentInvocationActivity(this.Id, agentName, this.Description),
246-
() => this.InternalInvokeStreamingAsync(
247-
agentName,
248-
history,
249-
(newMessage) => Task.CompletedTask,
250-
arguments,
251-
kernel,
252-
null,
253-
cancellationToken),
241+
return this.InternalInvokeStreamingAsync(
242+
agentName,
243+
history,
244+
(newMessage) => Task.CompletedTask,
245+
arguments,
246+
kernel,
247+
null,
254248
cancellationToken);
255249
}
256250

@@ -355,6 +349,8 @@ private async IAsyncEnumerable<ChatMessageContent> InternalInvokeAsync(
355349

356350
this.Logger.LogAgentChatServiceInvokingAgent(nameof(InvokeAsync), this.Id, agentName, serviceType);
357351

352+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, agentName, this.Description, chat);
353+
358354
IReadOnlyList<ChatMessageContent> messages =
359355
await chatCompletionService.GetChatMessageContentsAsync(
360356
chat,
@@ -381,6 +377,8 @@ await chatCompletionService.GetChatMessageContentsAsync(
381377

382378
yield return message;
383379
}
380+
381+
activity?.SetAgentResponse(messages);
384382
}
385383

386384
private async IAsyncEnumerable<StreamingChatMessageContent> InternalInvokeStreamingAsync(
@@ -404,6 +402,8 @@ private async IAsyncEnumerable<StreamingChatMessageContent> InternalInvokeStream
404402

405403
this.Logger.LogAgentChatServiceInvokingAgent(nameof(InvokeAsync), this.Id, agentName, serviceType);
406404

405+
using var activity = ModelDiagnostics.StartAgentInvocationActivity(this.Id, agentName, this.Description, chat);
406+
407407
IAsyncEnumerable<StreamingChatMessageContent> messages =
408408
chatCompletionService.GetStreamingChatMessageContentsAsync(
409409
chat,
@@ -416,6 +416,7 @@ private async IAsyncEnumerable<StreamingChatMessageContent> InternalInvokeStream
416416
int messageIndex = messageCount;
417417
AuthorRole? role = null;
418418
StringBuilder builder = new();
419+
List<StreamingChatMessageContent>? streamedContents = activity is not null ? [] : null;
419420
await foreach (StreamingChatMessageContent message in messages.ConfigureAwait(false))
420421
{
421422
role = message.Role;
@@ -435,6 +436,7 @@ private async IAsyncEnumerable<StreamingChatMessageContent> InternalInvokeStream
435436
history.Add(chatMessage);
436437
}
437438

439+
streamedContents?.Add(message);
438440
yield return message;
439441
}
440442

@@ -444,6 +446,8 @@ private async IAsyncEnumerable<StreamingChatMessageContent> InternalInvokeStream
444446
await onNewMessage(new(role ?? AuthorRole.Assistant, builder.ToString()) { AuthorName = this.Name }).ConfigureAwait(false);
445447
history.Add(new(role ?? AuthorRole.Assistant, builder.ToString()) { AuthorName = this.Name });
446448
}
449+
450+
activity?.EndAgentStreamingResponse(streamedContents);
447451
}
448452

449453
#endregion

0 commit comments

Comments
 (0)