|
15 | 15 | using Xunit; |
16 | 16 |
|
17 | 17 | #pragma warning disable SA1118 // Parameter should not span multiple lines |
| 18 | +#pragma warning disable SA1204 // Static elements should appear before instance elements |
18 | 19 |
|
19 | 20 | namespace Microsoft.Extensions.AI; |
20 | 21 |
|
@@ -1232,6 +1233,220 @@ public async Task ClonesChatOptionsAndResetContinuationTokenForBackgroundRespons |
1232 | 1233 | Assert.Null(actualChatOptions!.ContinuationToken); |
1233 | 1234 | } |
1234 | 1235 |
|
| 1236 | + [Fact] |
| 1237 | + public async Task DoesNotCreateOrchestrateToolsSpanWhenInvokeAgentIsParent() |
| 1238 | + { |
| 1239 | + string agentSourceName = Guid.NewGuid().ToString(); |
| 1240 | + string clientSourceName = Guid.NewGuid().ToString(); |
| 1241 | + |
| 1242 | + List<ChatMessage> plan = |
| 1243 | + [ |
| 1244 | + new ChatMessage(ChatRole.User, "hello"), |
| 1245 | + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), |
| 1246 | + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), |
| 1247 | + new ChatMessage(ChatRole.Assistant, "world"), |
| 1248 | + ]; |
| 1249 | + |
| 1250 | + ChatOptions options = new() |
| 1251 | + { |
| 1252 | + Tools = [AIFunctionFactory.Create(() => "Result 1", "Func1")] |
| 1253 | + }; |
| 1254 | + |
| 1255 | + Func<ChatClientBuilder, ChatClientBuilder> configure = b => b.Use(c => |
| 1256 | + new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: clientSourceName))); |
| 1257 | + |
| 1258 | + var activities = new List<Activity>(); |
| 1259 | + |
| 1260 | + using TracerProvider tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() |
| 1261 | + .AddSource(agentSourceName) |
| 1262 | + .AddSource(clientSourceName) |
| 1263 | + .AddInMemoryExporter(activities) |
| 1264 | + .Build(); |
| 1265 | + |
| 1266 | + using (var agentSource = new ActivitySource(agentSourceName)) |
| 1267 | + using (var invokeAgentActivity = agentSource.StartActivity("invoke_agent")) |
| 1268 | + { |
| 1269 | + Assert.NotNull(invokeAgentActivity); |
| 1270 | + await InvokeAndAssertAsync(options, plan, configurePipeline: configure); |
| 1271 | + } |
| 1272 | + |
| 1273 | + Assert.DoesNotContain(activities, a => a.DisplayName == "orchestrate_tools"); |
| 1274 | + Assert.Contains(activities, a => a.DisplayName == "chat"); |
| 1275 | + Assert.Contains(activities, a => a.DisplayName == "execute_tool Func1"); |
| 1276 | + |
| 1277 | + var invokeAgent = Assert.Single(activities, a => a.DisplayName == "invoke_agent"); |
| 1278 | + var childActivities = activities.Where(a => a != invokeAgent).ToList(); |
| 1279 | + Assert.All(childActivities, activity => Assert.Same(invokeAgent, activity.Parent)); |
| 1280 | + } |
| 1281 | + |
| 1282 | + [Fact] |
| 1283 | + public async Task UsesAgentActivitySourceWhenInvokeAgentIsParent() |
| 1284 | + { |
| 1285 | + string agentSourceName = Guid.NewGuid().ToString(); |
| 1286 | + string clientSourceName = Guid.NewGuid().ToString(); |
| 1287 | + |
| 1288 | + List<ChatMessage> plan = |
| 1289 | + [ |
| 1290 | + new ChatMessage(ChatRole.User, "hello"), |
| 1291 | + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), |
| 1292 | + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), |
| 1293 | + new ChatMessage(ChatRole.Assistant, "world"), |
| 1294 | + ]; |
| 1295 | + |
| 1296 | + ChatOptions options = new() |
| 1297 | + { |
| 1298 | + Tools = [AIFunctionFactory.Create(() => "Result 1", "Func1")] |
| 1299 | + }; |
| 1300 | + |
| 1301 | + Func<ChatClientBuilder, ChatClientBuilder> configure = b => b.Use(c => |
| 1302 | + new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: clientSourceName))); |
| 1303 | + |
| 1304 | + var activities = new List<Activity>(); |
| 1305 | + |
| 1306 | + using TracerProvider tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() |
| 1307 | + .AddSource(agentSourceName) |
| 1308 | + .AddSource(clientSourceName) |
| 1309 | + .AddInMemoryExporter(activities) |
| 1310 | + .Build(); |
| 1311 | + |
| 1312 | + using (var agentSource = new ActivitySource(agentSourceName)) |
| 1313 | + using (var invokeAgentActivity = agentSource.StartActivity("invoke_agent")) |
| 1314 | + { |
| 1315 | + Assert.NotNull(invokeAgentActivity); |
| 1316 | + await InvokeAndAssertAsync(options, plan, configurePipeline: configure); |
| 1317 | + } |
| 1318 | + |
| 1319 | + var executeToolActivities = activities.Where(a => a.DisplayName == "execute_tool Func1").ToList(); |
| 1320 | + Assert.NotEmpty(executeToolActivities); |
| 1321 | + Assert.All(executeToolActivities, executeTool => Assert.Equal(agentSourceName, executeTool.Source.Name)); |
| 1322 | + } |
| 1323 | + |
| 1324 | + public static IEnumerable<object[]> SensitiveDataPropagatesFromAgentActivityWhenInvokeAgentIsParent_MemberData() => |
| 1325 | + from invokeAgentSensitiveData in new bool?[] { null, false, true } |
| 1326 | + from innerOpenTelemetryChatClient in new bool?[] { null, false, true } |
| 1327 | + select new object?[] { invokeAgentSensitiveData, innerOpenTelemetryChatClient }; |
| 1328 | + |
| 1329 | + [Theory] |
| 1330 | + [MemberData(nameof(SensitiveDataPropagatesFromAgentActivityWhenInvokeAgentIsParent_MemberData))] |
| 1331 | + public async Task SensitiveDataPropagatesFromAgentActivityWhenInvokeAgentIsParent( |
| 1332 | + bool? invokeAgentSensitiveData, bool? innerOpenTelemetryChatClient) |
| 1333 | + { |
| 1334 | + string agentSourceName = Guid.NewGuid().ToString(); |
| 1335 | + string clientSourceName = Guid.NewGuid().ToString(); |
| 1336 | + |
| 1337 | + List<ChatMessage> plan = |
| 1338 | + [ |
| 1339 | + new ChatMessage(ChatRole.User, "hello"), |
| 1340 | + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1", new Dictionary<string, object?> { ["arg1"] = "secret" })]), |
| 1341 | + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), |
| 1342 | + new ChatMessage(ChatRole.Assistant, "world"), |
| 1343 | + ]; |
| 1344 | + |
| 1345 | + ChatOptions options = new() |
| 1346 | + { |
| 1347 | + Tools = [AIFunctionFactory.Create(() => "Result 1", "Func1")] |
| 1348 | + }; |
| 1349 | + |
| 1350 | + var activities = new List<Activity>(); |
| 1351 | + |
| 1352 | + using TracerProvider tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() |
| 1353 | + .AddSource(agentSourceName) |
| 1354 | + .AddSource(clientSourceName) |
| 1355 | + .AddInMemoryExporter(activities) |
| 1356 | + .Build(); |
| 1357 | + |
| 1358 | + using (var agentSource = new ActivitySource(agentSourceName)) |
| 1359 | + using (var invokeAgentActivity = agentSource.StartActivity("invoke_agent")) |
| 1360 | + { |
| 1361 | + if (invokeAgentSensitiveData is not null) |
| 1362 | + { |
| 1363 | + invokeAgentActivity?.SetCustomProperty("__EnableSensitiveData__", invokeAgentSensitiveData is true ? "true" : "false"); |
| 1364 | + } |
| 1365 | + |
| 1366 | + await InvokeAndAssertAsync(options, plan, configurePipeline: b => |
| 1367 | + { |
| 1368 | + b.UseFunctionInvocation(); |
| 1369 | + |
| 1370 | + if (innerOpenTelemetryChatClient is not null) |
| 1371 | + { |
| 1372 | + b.UseOpenTelemetry(sourceName: clientSourceName, configure: c => |
| 1373 | + { |
| 1374 | + c.EnableSensitiveData = innerOpenTelemetryChatClient.Value; |
| 1375 | + }); |
| 1376 | + } |
| 1377 | + |
| 1378 | + return b; |
| 1379 | + }); |
| 1380 | + } |
| 1381 | + |
| 1382 | + var executeToolActivity = Assert.Single(activities, a => a.DisplayName == "execute_tool Func1"); |
| 1383 | + |
| 1384 | + var hasArguments = executeToolActivity.Tags.Any(t => t.Key == "gen_ai.tool.call.arguments"); |
| 1385 | + var hasResult = executeToolActivity.Tags.Any(t => t.Key == "gen_ai.tool.call.result"); |
| 1386 | + |
| 1387 | + if (invokeAgentSensitiveData is true) |
| 1388 | + { |
| 1389 | + Assert.True(hasArguments, "Expected arguments to be logged when agent EnableSensitiveData is true"); |
| 1390 | + Assert.True(hasResult, "Expected result to be logged when agent EnableSensitiveData is true"); |
| 1391 | + |
| 1392 | + var argsTag = Assert.Single(executeToolActivity.Tags, t => t.Key == "gen_ai.tool.call.arguments"); |
| 1393 | + Assert.Contains("arg1", argsTag.Value); |
| 1394 | + } |
| 1395 | + else |
| 1396 | + { |
| 1397 | + Assert.False(hasArguments, "Expected arguments NOT to be logged when agent EnableSensitiveData is false"); |
| 1398 | + Assert.False(hasResult, "Expected result NOT to be logged when agent EnableSensitiveData is false"); |
| 1399 | + } |
| 1400 | + } |
| 1401 | + |
| 1402 | + [Theory] |
| 1403 | + [InlineData(false)] |
| 1404 | + [InlineData(true)] |
| 1405 | + public async Task CreatesOrchestrateToolsSpanWhenNoInvokeAgentParent(bool streaming) |
| 1406 | + { |
| 1407 | + string clientSourceName = Guid.NewGuid().ToString(); |
| 1408 | + |
| 1409 | + List<ChatMessage> plan = |
| 1410 | + [ |
| 1411 | + new ChatMessage(ChatRole.User, "hello"), |
| 1412 | + new ChatMessage(ChatRole.Assistant, [new FunctionCallContent("callId1", "Func1")]), |
| 1413 | + new ChatMessage(ChatRole.Tool, [new FunctionResultContent("callId1", result: "Result 1")]), |
| 1414 | + new ChatMessage(ChatRole.Assistant, "world"), |
| 1415 | + ]; |
| 1416 | + |
| 1417 | + ChatOptions options = new() |
| 1418 | + { |
| 1419 | + Tools = [AIFunctionFactory.Create(() => "Result 1", "Func1")] |
| 1420 | + }; |
| 1421 | + |
| 1422 | + Func<ChatClientBuilder, ChatClientBuilder> configure = b => b.Use(c => |
| 1423 | + new FunctionInvokingChatClient(new OpenTelemetryChatClient(c, sourceName: clientSourceName))); |
| 1424 | + |
| 1425 | + var activities = new List<Activity>(); |
| 1426 | + using TracerProvider tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder() |
| 1427 | + .AddSource(clientSourceName) |
| 1428 | + .AddInMemoryExporter(activities) |
| 1429 | + .Build(); |
| 1430 | + |
| 1431 | + if (streaming) |
| 1432 | + { |
| 1433 | + await InvokeAndAssertStreamingAsync(options, plan, configurePipeline: configure); |
| 1434 | + } |
| 1435 | + else |
| 1436 | + { |
| 1437 | + await InvokeAndAssertAsync(options, plan, configurePipeline: configure); |
| 1438 | + } |
| 1439 | + |
| 1440 | + var orchestrateTools = Assert.Single(activities, a => a.DisplayName == "orchestrate_tools"); |
| 1441 | + |
| 1442 | + var executeTools = activities.Where(a => a.DisplayName.StartsWith("execute_tool")).ToList(); |
| 1443 | + Assert.NotEmpty(executeTools); |
| 1444 | + foreach (var executeTool in executeTools) |
| 1445 | + { |
| 1446 | + Assert.Same(orchestrateTools, executeTool.Parent); |
| 1447 | + } |
| 1448 | + } |
| 1449 | + |
1235 | 1450 | private sealed class CustomSynchronizationContext : SynchronizationContext |
1236 | 1451 | { |
1237 | 1452 | public override void Post(SendOrPostCallback d, object? state) |
|
0 commit comments