Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions playground/Stress/Stress.ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,18 +398,59 @@ async IAsyncEnumerable<string> WriteOutput()
"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=="
}
]
},
{
"role": "assistant",
"parts": [
{
"type": "text",
"content": "Assistant content"
}
]
},
{
"role": "user",
"parts": [
{
"type": "text",
"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."
},
{
"type": "text",
"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."
}
]
}
]
""");
}

// Avoid zero seconds span.
await Task.Delay(100);

activity?.Stop();

return "Created GenAI trace";
});

app.MapGet("/genai-trace-display-error", async () =>
{
var source = new ActivitySource("Services.Api", "1.0.0");

var activity = source.StartActivity("chat gpt", ActivityKind.Client);
if (activity != null)
{
activity.SetTag("gen_ai.system", "gpt");
activity.SetTag("gen_ai.input.messages", "invalid");
}

// Avoid zero seconds span.
await Task.Delay(100);

activity?.Stop();

return "Created GenAI trace";
});
app.Run();

public record WeatherForecast(DateOnly Date, int TemperatureC, string Summary);
28 changes: 12 additions & 16 deletions src/Aspire.Dashboard/Model/GenAI/GenAIVisualizerDialogViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ namespace Aspire.Dashboard.Model.GenAI;
[DebuggerDisplay("Span = {Span.SpanId}, Title = {Title}, Items = {Items.Count}")]
public sealed class GenAIVisualizerDialogViewModel
{
// The exact name doesn't matter. A value is required when resolving color for peer.
private const string UnknownPeerName = "unknown-peer";

public required OtlpSpan Span { get; init; }
public required string Title { get; init; }
public required SpanDetailsViewModel SpanDetailsViewModel { get; init; }
public required long? SelectedLogEntryId { get; init; }
public required Func<List<OtlpSpan>> GetContextGenAISpans { get; init; }

public string? PeerName { get; set; }
public string? SourceName { get; set; }
public required string PeerName { get; init; }
public required string SourceName { get; init; }

public FluentTreeItem? SelectedTreeItem { get; set; }
public List<GenAIItemViewModel> Items { get; } = new List<GenAIItemViewModel>();
Expand All @@ -42,27 +44,21 @@ public static GenAIVisualizerDialogViewModel Create(
TelemetryRepository telemetryRepository,
Func<List<OtlpSpan>> getContextGenAISpans)
{
var resources = telemetryRepository.GetResources();

var viewModel = new GenAIVisualizerDialogViewModel
{
Span = spanDetailsViewModel.Span,
Title = SpanWaterfallViewModel.GetTitle(spanDetailsViewModel.Span, spanDetailsViewModel.Resources),
SpanDetailsViewModel = spanDetailsViewModel,
SelectedLogEntryId = selectedLogEntryId,
GetContextGenAISpans = getContextGenAISpans
GetContextGenAISpans = getContextGenAISpans,
SourceName = OtlpResource.GetResourceName(spanDetailsViewModel.Span.Source, resources),
PeerName = telemetryRepository.GetPeerResource(spanDetailsViewModel.Span) is { } peerResource
? OtlpResource.GetResourceName(peerResource, resources)
: OtlpHelpers.GetPeerAddress(spanDetailsViewModel.Span.Attributes) ?? UnknownPeerName
};

var resources = telemetryRepository.GetResources();
viewModel.SourceName = OtlpResource.GetResourceName(viewModel.Span.Source, resources);

if (telemetryRepository.GetPeerResource(viewModel.Span) is { } peerResource)
{
viewModel.PeerName = OtlpResource.GetResourceName(peerResource, resources);
}
else
{
viewModel.PeerName = OtlpHelpers.GetPeerAddress(viewModel.Span.Attributes)!;
}

viewModel.ModelName = viewModel.Span.Attributes.GetValue(GenAIHelpers.GenAIResponseModel);
viewModel.InputTokens = viewModel.Span.Attributes.GetValueAsInteger(GenAIHelpers.GenAIUsageInputTokens);
viewModel.OutputTokens = viewModel.Span.Attributes.GetValueAsInteger(GenAIHelpers.GenAIUsageOutputTokens);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Dashboard.Components.Dialogs;
using Aspire.Dashboard.Components.Resize;
using Aspire.Dashboard.Components.Tests.Shared;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.GenAI;
using Aspire.Dashboard.Otlp.Model;
using Aspire.Dashboard.Otlp.Storage;
using Bunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.FluentUI.AspNetCore.Components;
using Xunit;
using static Aspire.Tests.Shared.Telemetry.TelemetryTestHelpers;

namespace Aspire.Dashboard.Components.Tests.Controls;

public class GenAIVisualizerDialogTests : DashboardTestContext
{
private static readonly DateTime s_testTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

[Fact]
public async Task Render_NoGenAIAttributes_Success()
{
var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
var resource = new OtlpResource("app", "instance", uninstrumentedPeer: false, context);

var trace = new OtlpTrace(new byte[] { 1, 2, 3 }, DateTime.MinValue);
var scope = CreateOtlpScope(context);

var cut = SetUpDialog(out var dialogService);
await GenAIVisualizerDialog.OpenDialogAsync(
viewportInformation: new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false),
dialogService: dialogService,
dialogsLoc: Services.GetRequiredService<IStringLocalizer<Aspire.Dashboard.Resources.Dialogs>>(),
span: CreateOtlpSpan(resource, trace, scope, spanId: "abc", parentSpanId: null, startDate: s_testTime),
selectedLogEntryId: null,
telemetryRepository: Services.GetRequiredService<TelemetryRepository>(),
resources: [],
getContextGenAISpans: () => []
);

var instance = cut.FindComponent<GenAIVisualizerDialog>().Instance;

Assert.Empty(instance.Content.Items);
Assert.Equal("app", instance.Content.SourceName);
Assert.Equal("unknown-peer", instance.Content.PeerName);
}

[Fact]
public async Task Render_HasGenAIMessages_Success()
{
var context = new OtlpContext { Logger = NullLogger.Instance, Options = new() };
var resource = new OtlpResource("app", "instance", uninstrumentedPeer: false, context);

var systemInstruction = JsonSerializer.Serialize(new List<MessagePart>
{
new TextPart { Content = "System!" }
}, GenAIMessagesContext.Default.ListMessagePart);

var inputMessages = JsonSerializer.Serialize(new List<ChatMessage>
{
new ChatMessage
{
Role = "user",
Parts = [new TextPart { Content = "User!" }]
},
new ChatMessage
{
Role = "assistant",
Parts = [new ToolCallRequestPart { Name = "generate_names", Arguments = JsonNode.Parse(@"{""count"":2}") }]
},
new ChatMessage
{
Role = "user",
Parts = [new ToolCallResponsePart { Response = JsonNode.Parse(@"[""Jack"",""Jane""]") }]
}
}, GenAIMessagesContext.Default.ListChatMessage);

var outputMessages = JsonSerializer.Serialize(new List<ChatMessage>
{
new ChatMessage
{
Role = "assistant",
Parts = [new TextPart { Content = "Output!" }]
}
}, GenAIMessagesContext.Default.ListChatMessage);

var trace = new OtlpTrace(new byte[] { 1, 2, 3 }, DateTime.MinValue);
var scope = CreateOtlpScope(context);
var span = CreateOtlpSpan(resource, trace, scope, spanId: "abc", parentSpanId: null, startDate: s_testTime, attributes: [
KeyValuePair.Create(GenAIHelpers.GenAISystemInstructions, systemInstruction),
KeyValuePair.Create(GenAIHelpers.GenAIInputMessages, inputMessages),
KeyValuePair.Create(GenAIHelpers.GenAIOutputInstructions, outputMessages)
]);

var cut = SetUpDialog(out var dialogService);
await GenAIVisualizerDialog.OpenDialogAsync(
viewportInformation: new ViewportInformation(IsDesktop: true, IsUltraLowHeight: false, IsUltraLowWidth: false),
dialogService: dialogService,
dialogsLoc: Services.GetRequiredService<IStringLocalizer<Aspire.Dashboard.Resources.Dialogs>>(),
span: span,
selectedLogEntryId: null,
telemetryRepository: Services.GetRequiredService<TelemetryRepository>(),
resources: [],
getContextGenAISpans: () => []
);

var instance = cut.FindComponent<GenAIVisualizerDialog>().Instance;

Assert.Equal(5, instance.Content.Items.Count);
}

private IRenderedFragment SetUpDialog(out IDialogService dialogService)
{
var version = typeof(FluentMain).Assembly.GetName().Version!;

Services.AddFluentUIComponents();
Services.AddSingleton<LibraryConfiguration>();
Services.AddSingleton<TelemetryRepository>();
Services.AddSingleton<PauseManager>();
Services.AddSingleton(new ThemeManager(new TestThemeResolver()));

Services.AddLocalization();
Services.AddSingleton<BrowserTimeProvider, TestTimeProvider>();

var cut = Render(builder =>
{
builder.OpenComponent<FluentDialogProvider>(0);
builder.CloseComponent();
});

// Setting a provider ID on menu service is required to simulate <FluentMenuProvider> on the page.
// This makes FluentMenu render without error.
var menuService = Services.GetRequiredService<IMenuService>();
menuService.ProviderId = "Test";

dialogService = Services.GetRequiredService<IDialogService>();
return cut;
}
}