Skip to content

Commit 2d6351d

Browse files
JamesNKCopilot
andauthored
[release/9.5] Fix GenAI visualizer when span is missing peer attribute (#11765)
* Fix GenAI visualizer when span is missing peer attribute * Update src/Aspire.Dashboard/Model/GenAI/GenAIVisualizerDialogViewModel.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix build --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c04989d commit 2d6351d

File tree

3 files changed

+199
-16
lines changed

3 files changed

+199
-16
lines changed

playground/Stress/Stress.ApiService/Program.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,18 +398,59 @@ async IAsyncEnumerable<string> WriteOutput()
398398
"content": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFwAAABcCAMAAADUMSJqAAAAilBMVEX///+XgOV0Vd1RK9S5qu7c1fZHGdKto+hOJ9Oag+ZNJNOWfuVyU91LINPi3Pi4qO6WiuOEbt9EEtJtTdtaNtZnRtn39f1WMdX6+f6ReeTs6fr08fwhAM68sO1iQtjIvvA8ANHl4fhePNeEaeHQx/Kjj+iwoOyeiednUtnCuO+jlOfWzvR5Xd5+Yt+aCjLkAAAD3ElEQVRoge2ZW3eqMBCFC0gM4VIJKlIPQkXbqvj//94hk1a5TChI+uZefes6X2dl78wMOS8vTz31VFPb7R+BN+nXv0pf6UY/295xalWifGfrZmcSDfg808sO7mxR/LtWeMSACj+WxSKd7BMX6DgyzSgWeH7Sx17vBTE2QbH4O/u1NviZCGAk4ZH4Q+Ssi73NxUEvzW8tBT3XdZ0K4Sb1fuBQOgv1sC/g5t68CRzgFy1wGUPvDvf0xTGFwldmTSsoPZ3OXse1GP4I4kinx/FYj2HDU3Kcyn5vu1mL4+QWU/gtN2ue+sU09gEK37XZprmD0g+T4BBpq8s2TeiS+ylTadaNYTOOs8fZa9JoKi1Pxe/I4y1mgcWwEUffeZQdEBlDz+ukRQj8IMGDcA+6YZkkYYnRZYspH2Nnoo0z0zEMw7lidIjjY6vABv7t0gC56LHHD8dxTuASOgB3Qqx0iCP5GM9+kxGX7EqlMo78bTQ8EU2FJT9sAy1dxjEZy7ZzmJu3wg0HTQzEMR+7PJoQQ9e4K1HH0RzHhhWLXZ0a3CmUno5bwGC20b3RUE8c4zFxhBXLD50GHI/j6AWsHUOtcQxFDGnSZuOeyjgOXsBgtsmm0lRfixk48TYQw9jtsHvj6A2Dw2xjRbfw3jjmgybeRm4qGLuSMo4WGRLHT2gqIQ7vi+Pid/Y7zLaV6yYuJqP0upILmP/7xIMVyz8f7MvpFdHsAzUa4Nff2IccZnpg23aQvc4QHbHTKgfFUYSWLm2pFKO/YqUb0GJ2/ew5hz0qkPALWvoZoTtwmnzex94yKu7DN9u2TygdvQFibFDat4AZ0A0z+yaMjXsaQhwNNTvgMBKDO/yAlv6Jle7B2FDHsRRNhdh1pYNLd1nvAgYrFjkHdTju6RHzFEpTLWAbmG07uyk07HgcxT1VTbwP4SZJgxYd9RSPoyidowvYmy9iWLbZCk8djA43kGETD2YbObTZYzxNmGLiBdBU3E7hlafowajjmHfjGIEbly5b5SlmqQHfeJ2JBysWPyOFC08xOhrHK0OeHDbw8hbhbJWn2MFAHK1mHBcyhji7amCD4xiyzgK2piKGhaLwSsNbzIq2X0tgDcJi2OupeuI1HgXE5fRDdeGKe4qWLmLX+E4SLzZEERUp9Cbh45S1Dl2szHIqK+HYuaCVm7QFFym/jWVMaBjR3uha7U8NWMj9wg4Uwk4lnX06Xbny27TRvKBr+XGyQGWEiApsa4wsZFt/g8d3ynxcDBPFBM2l/bKWiYPRI8o7o+6Q+3rYfpddnUzCOZksThL822ubzScr+6v/pnrqqad06z+o/mHi4pLvAgAAAABJRU5ErkJggg=="
399399
}
400400
]
401+
},
402+
{
403+
"role": "assistant",
404+
"parts": [
405+
{
406+
"type": "text",
407+
"content": "Assistant content"
408+
}
409+
]
410+
},
411+
{
412+
"role": "user",
413+
"parts": [
414+
{
415+
"type": "text",
416+
"content": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBB Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n\nLorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n\nLorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.\n\nLorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor."
417+
},
418+
{
419+
"type": "text",
420+
"content": "# 📝 Markdown Feature Showcase\n\nWelcome to a **comprehensive example** of markdown in action. \nThis document demonstrates *all* the main features.\n\n---\n\n## 1. Headings\n\n# H1 Heading \n## H2 Heading \n### H3 Heading \n#### H4 Heading \n##### H5 Heading \n###### H6 Heading \n\n---\n\n## 2. Emphasis\n\n- *Italic text* \n- **Bold text** \n- ***Bold and italic*** \n- ~~Strikethrough~~ \n- <u>Underlined (via HTML)</u> \n\n---\n\n## 3. Lists\n\n### Unordered list:\n- Item A\n - Sub-item A1\n - Sub-item A2\n- Item B \n- Item C \n\n### Ordered list:\n1. First\n2. Second\n 1. Sub-second\n 2. Sub-second again\n3. Third \n\n### Task list:\n- [x] Done item \n- [ ] Pending item \n- [ ] Another pending item \n\n---\n\n## 4. Links\n\n- Inline link: [OpenAI](https://openai.com) \n- Reference link: [Search Engine][google] \n- Autolink: <https://example.com> \n\n[google]: https://google.com \"Google Search\"\n\n---\n\n## 5. Images\n\nInline image: \n![Example](/img/TokenExample.png) \n\nLinked image: \n[![Example](/img/TokenExample.png)](https://openai.com)\n\n---\n\n## 6. Blockquotes\n\n> This is a blockquote. \n> \n> > Nested blockquote inside. \n\n---\n\n## 7. Horizontal Rules\n\n--- \n*** \n___ \n\n---\n\n## 8. Tables\n\n| Feature | Supported | Notes |\n|----------------|-----------|--------------------------------|\n| **Bold** | ✅ | Works inside tables too |\n| *Italics* | ✅ | Styling works fine |\n| Links | ✅ | [Example](https://openai.com) |\n| Images | ✅ | ![Img](/img/TokenExample.png) |\n| Task List | ❌ | Not supported in table cells |\n\n---\n\n## 9. Inline Formatting\n\nSuperscript: X² \nSubscript: H₂O \nEmoji: 🎉 🚀 🌍 \nHTML inside markdown: <mark>highlighted text</mark> \n\n---\n\n## 10. Footnotes\n\nHere’s a statement with a footnote.[^1] \n\n[^1]: This is the footnote explanation. \n\n---\n\n## 11. Definition Lists\n\nTerm 1 \n: Definition of term 1 \n\nTerm 2 \n: Definition of term 2 with *emphasis* \n\n---\n\n## 12. Escaping Characters\n\n\\*Not italic\\* but literal asterisks \nUse a backslash for: \\# \\* \\[ \\] \\( \\) \n\n---\n\n## 13. Code Blocks\n\n```csharp\n\nConsole.WriteLine(\"test\");\n\n```\n\n---\n\nThat’s the **full tour** of markdown features."
421+
}
422+
]
401423
}
402424
]
403425
""");
404426
}
405427

428+
// Avoid zero seconds span.
406429
await Task.Delay(100);
407430

408431
activity?.Stop();
409432

410433
return "Created GenAI trace";
411434
});
412435

436+
app.MapGet("/genai-trace-display-error", async () =>
437+
{
438+
var source = new ActivitySource("Services.Api", "1.0.0");
439+
440+
var activity = source.StartActivity("chat gpt", ActivityKind.Client);
441+
if (activity != null)
442+
{
443+
activity.SetTag("gen_ai.system", "gpt");
444+
activity.SetTag("gen_ai.input.messages", "invalid");
445+
}
446+
447+
// Avoid zero seconds span.
448+
await Task.Delay(100);
449+
450+
activity?.Stop();
451+
452+
return "Created GenAI trace";
453+
});
413454
app.Run();
414455

415456
public record WeatherForecast(DateOnly Date, int TemperatureC, string Summary);

src/Aspire.Dashboard/Model/GenAI/GenAIVisualizerDialogViewModel.cs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ namespace Aspire.Dashboard.Model.GenAI;
1616
[DebuggerDisplay("Span = {Span.SpanId}, Title = {Title}, Items = {Items.Count}")]
1717
public sealed class GenAIVisualizerDialogViewModel
1818
{
19+
// The exact name doesn't matter. A value is required when resolving color for peer.
20+
private const string UnknownPeerName = "unknown-peer";
21+
1922
public required OtlpSpan Span { get; init; }
2023
public required string Title { get; init; }
2124
public required SpanDetailsViewModel SpanDetailsViewModel { get; init; }
2225
public required long? SelectedLogEntryId { get; init; }
2326
public required Func<List<OtlpSpan>> GetContextGenAISpans { get; init; }
24-
25-
public string? PeerName { get; set; }
26-
public string? SourceName { get; set; }
27+
public required string PeerName { get; init; }
28+
public required string SourceName { get; init; }
2729

2830
public FluentTreeItem? SelectedTreeItem { get; set; }
2931
public List<GenAIItemViewModel> Items { get; } = new List<GenAIItemViewModel>();
@@ -42,27 +44,21 @@ public static GenAIVisualizerDialogViewModel Create(
4244
TelemetryRepository telemetryRepository,
4345
Func<List<OtlpSpan>> getContextGenAISpans)
4446
{
47+
var resources = telemetryRepository.GetResources();
48+
4549
var viewModel = new GenAIVisualizerDialogViewModel
4650
{
4751
Span = spanDetailsViewModel.Span,
4852
Title = SpanWaterfallViewModel.GetTitle(spanDetailsViewModel.Span, spanDetailsViewModel.Resources),
4953
SpanDetailsViewModel = spanDetailsViewModel,
5054
SelectedLogEntryId = selectedLogEntryId,
51-
GetContextGenAISpans = getContextGenAISpans
55+
GetContextGenAISpans = getContextGenAISpans,
56+
SourceName = OtlpResource.GetResourceName(spanDetailsViewModel.Span.Source, resources),
57+
PeerName = telemetryRepository.GetPeerResource(spanDetailsViewModel.Span) is { } peerResource
58+
? OtlpResource.GetResourceName(peerResource, resources)
59+
: OtlpHelpers.GetPeerAddress(spanDetailsViewModel.Span.Attributes) ?? UnknownPeerName
5260
};
5361

54-
var resources = telemetryRepository.GetResources();
55-
viewModel.SourceName = OtlpResource.GetResourceName(viewModel.Span.Source, resources);
56-
57-
if (telemetryRepository.GetPeerResource(viewModel.Span) is { } peerResource)
58-
{
59-
viewModel.PeerName = OtlpResource.GetResourceName(peerResource, resources);
60-
}
61-
else
62-
{
63-
viewModel.PeerName = OtlpHelpers.GetPeerAddress(viewModel.Span.Attributes)!;
64-
}
65-
6662
viewModel.ModelName = viewModel.Span.Attributes.GetValue(GenAIHelpers.GenAIResponseModel);
6763
viewModel.InputTokens = viewModel.Span.Attributes.GetValueAsInteger(GenAIHelpers.GenAIUsageInputTokens);
6864
viewModel.OutputTokens = viewModel.Span.Attributes.GetValueAsInteger(GenAIHelpers.GenAIUsageOutputTokens);
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Nodes;
6+
using Aspire.Dashboard.Components.Dialogs;
7+
using Aspire.Dashboard.Components.Resize;
8+
using Aspire.Dashboard.Components.Tests.Shared;
9+
using Aspire.Dashboard.Model;
10+
using Aspire.Dashboard.Model.GenAI;
11+
using Aspire.Dashboard.Otlp.Model;
12+
using Aspire.Dashboard.Otlp.Storage;
13+
using Bunit;
14+
using Microsoft.Extensions.DependencyInjection;
15+
using Microsoft.Extensions.Localization;
16+
using Microsoft.Extensions.Logging.Abstractions;
17+
using Microsoft.FluentUI.AspNetCore.Components;
18+
using Xunit;
19+
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;
20+
21+
namespace Aspire.Dashboard.Components.Tests.Controls;
22+
23+
public class GenAIVisualizerDialogTests : DashboardTestContext
24+
{
25+
private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
26+
27+
[Fact]
28+
public async Task Render_NoGenAIAttributes_Success()
29+
{
30+
var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
31+
var resource = new OtlpResource("app", "instance", uninstrumentedPeer: false, context);
32+
33+
var trace = new OtlpTrace(new byte[] { 1, 2, 3 }, DateTime.MinValue);
34+
var scope = CreateOtlpScope(context);
35+
36+
var cut = SetUpDialog(out var dialogService);
37+
await GenAIVisualizerDialog.OpenDialogAsync(
38+
viewportInformation: new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false),
39+
dialogService: dialogService,
40+
dialogsLoc: Services.GetRequiredService<IStringLocalizer<Aspire.Dashboard.Resources.Dialogs>>(),
41+
span: CreateOtlpSpan(resource, trace, scope, spanId: "abc", parentSpanId: null, startDate: s_testTime),
42+
selectedLogEntryId: null,
43+
telemetryRepository: Services.GetRequiredService<TelemetryRepository>(),
44+
resources: [],
45+
getContextGenAISpans: () => []
46+
);
47+
48+
var instance = cut.FindComponent<GenAIVisualizerDialog>().Instance;
49+
50+
Assert.Empty(instance.Content.Items);
51+
Assert.Equal("app", instance.Content.SourceName);
52+
Assert.Equal("unknown-peer", instance.Content.PeerName);
53+
}
54+
55+
[Fact]
56+
public async Task Render_HasGenAIMessages_Success()
57+
{
58+
var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
59+
var resource = new OtlpResource("app", "instance", uninstrumentedPeer: false, context);
60+
61+
var systemInstruction = JsonSerializer.Serialize(new List<MessagePart>
62+
{
63+
new TextPart { Content = "System!" }
64+
}, GenAIMessagesContext.Default.ListMessagePart);
65+
66+
var inputMessages = JsonSerializer.Serialize(new List<ChatMessage>
67+
{
68+
new ChatMessage
69+
{
70+
Role = "user",
71+
Parts = [new TextPart { Content = "User!" }]
72+
},
73+
new ChatMessage
74+
{
75+
Role = "assistant",
76+
Parts = [new ToolCallRequestPart { Name = "generate_names", Arguments = JsonNode.Parse(@"{""count"":2}") }]
77+
},
78+
new ChatMessage
79+
{
80+
Role = "user",
81+
Parts = [new ToolCallResponsePart { Response = JsonNode.Parse(@"[""Jack"",""Jane""]") }]
82+
}
83+
}, GenAIMessagesContext.Default.ListChatMessage);
84+
85+
var outputMessages = JsonSerializer.Serialize(new List<ChatMessage>
86+
{
87+
new ChatMessage
88+
{
89+
Role = "assistant",
90+
Parts = [new TextPart { Content = "Output!" }]
91+
}
92+
}, GenAIMessagesContext.Default.ListChatMessage);
93+
94+
var trace = new OtlpTrace(new byte[] { 1, 2, 3 }, DateTime.MinValue);
95+
var scope = CreateOtlpScope(context);
96+
var span = CreateOtlpSpan(resource, trace, scope, spanId: "abc", parentSpanId: null, startDate: s_testTime, attributes: [
97+
KeyValuePair.Create(GenAIHelpers.GenAISystemInstructions, systemInstruction),
98+
KeyValuePair.Create(GenAIHelpers.GenAIInputMessages, inputMessages),
99+
KeyValuePair.Create(GenAIHelpers.GenAIOutputInstructions, outputMessages)
100+
]);
101+
102+
var cut = SetUpDialog(out var dialogService);
103+
await GenAIVisualizerDialog.OpenDialogAsync(
104+
viewportInformation: new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false),
105+
dialogService: dialogService,
106+
dialogsLoc: Services.GetRequiredService<IStringLocalizer<Aspire.Dashboard.Resources.Dialogs>>(),
107+
span: span,
108+
selectedLogEntryId: null,
109+
telemetryRepository: Services.GetRequiredService<TelemetryRepository>(),
110+
resources: [],
111+
getContextGenAISpans: () => []
112+
);
113+
114+
var instance = cut.FindComponent<GenAIVisualizerDialog>().Instance;
115+
116+
Assert.Equal(5, instance.Content.Items.Count);
117+
}
118+
119+
private IRenderedFragment SetUpDialog(out IDialogService dialogService)
120+
{
121+
var version = typeof(FluentMain).Assembly.GetName().Version!;
122+
123+
Services.AddFluentUIComponents();
124+
Services.AddSingleton<LibraryConfiguration>();
125+
Services.AddSingleton<TelemetryRepository>();
126+
Services.AddSingleton<PauseManager>();
127+
Services.AddSingleton(new ThemeManager(new TestThemeResolver()));
128+
129+
Services.AddLocalization();
130+
Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();
131+
132+
var cut = Render(builder =>
133+
{
134+
builder.OpenComponent<FluentDialogProvider>(0);
135+
builder.CloseComponent();
136+
});
137+
138+
// Setting a provider ID on menu service is required to simulate <FluentMenuProvider> on the page.
139+
// This makes FluentMenu render without error.
140+
var menuService = Services.GetRequiredService<IMenuService>();
141+
menuService.ProviderId = "Test";
142+
143+
dialogService = Services.GetRequiredService<IDialogService>();
144+
return cut;
145+
}
146+
}

0 commit comments

Comments
 (0)