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
103 changes: 98 additions & 5 deletions src/AzureOpenAIProxy.Management/AoaiProxyContext.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using AzureOpenAIProxy.Management.Database;
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using AzureOpenAIProxy.Management.Database;

namespace AzureOpenAIProxy.Management;

Expand All @@ -14,10 +16,18 @@ public AoaiProxyContext(DbContextOptions<AoaiProxyContext> options)
{
}

public virtual DbSet<ActiveAttendeeGrowthView> ActiveAttendeeGrowthViews { get; set; }

public virtual DbSet<Event> Events { get; set; }

public virtual DbSet<EventAttendee> EventAttendees { get; set; }

public virtual DbSet<EventAttendeeRequest> EventAttendeeRequests { get; set; }

public virtual DbSet<Metric> Metrics { get; set; }

public virtual DbSet<MetricView> MetricViews { get; set; }

public virtual DbSet<Owner> Owners { get; set; }

public virtual DbSet<OwnerCatalog> OwnerCatalogs { get; set; }
Expand All @@ -30,6 +40,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasPostgresEnum("aoai", "model_type", new[] { "openai-chat", "openai-embedding", "openai-dalle3", "openai-whisper", "openai-completion", "openai-instruct", "azure-ai-search" })
.HasPostgresExtension("aoai", "pgcrypto");

modelBuilder.Entity<ActiveAttendeeGrowthView>(entity =>
{
entity
.HasNoKey()
.ToView("active_attendee_growth_view", "aoai");

entity.Property(e => e.Attendees).HasColumnName("attendees");
entity.Property(e => e.DateStamp).HasColumnName("date_stamp");
entity.Property(e => e.EventId)
.HasMaxLength(50)
.HasColumnName("event_id");
});

modelBuilder.Entity<Event>(entity =>
{
entity.HasKey(e => e.EventId).HasName("event_pkey");
Expand All @@ -48,17 +71,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.EventCode)
.HasMaxLength(64)
.HasColumnName("event_code");
entity.Property(e => e.EventSharedCode)
.HasMaxLength(64)
.IsRequired(false)
.HasColumnName("event_shared_code");
entity.Property(e => e.EventImageUrl)
.HasMaxLength(256)
.IsRequired(false)
.HasColumnName("event_image_url");
entity.Property(e => e.EventMarkdown)
.HasMaxLength(8192)
.HasColumnName("event_markdown");
entity.Property(e => e.EventSharedCode)
.HasMaxLength(64)
.IsRequired(false)
.HasColumnName("event_shared_code");
entity.Property(e => e.MaxTokenCap).HasColumnName("max_token_cap");
entity.Property(e => e.OrganizerEmail)
.HasMaxLength(128)
Expand Down Expand Up @@ -121,6 +144,76 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.HasConstraintName("fk_eventattendee_event");
});

modelBuilder.Entity<EventAttendeeRequest>(entity =>
{
entity.HasKey(e => new { e.ApiKey, e.DateStamp }).HasName("eventattendeerequest_pkey");

entity.ToTable("event_attendee_request", "aoai");

entity.Property(e => e.ApiKey)
.HasColumnType("character varying")
.HasColumnName("api_key");
entity.Property(e => e.DateStamp).HasColumnName("date_stamp");
entity.Property(e => e.RequestCount).HasColumnName("request_count");
entity.Property(e => e.TokenCount).HasColumnName("token_count");

entity.HasOne(d => d.ApiKeyNavigation).WithMany(p => p.EventAttendeeRequests)
.HasPrincipalKey(p => p.ApiKey)
.HasForeignKey(d => d.ApiKey)
.HasConstraintName("fk_eventattendeerequest_eventattendee");
});

modelBuilder.Entity<Metric>(entity =>
{
entity
.HasNoKey()
.ToTable("metric", "aoai");

entity.HasIndex(e => e.EventId, "event_id_index");

entity.Property(e => e.ApiKey)
.HasColumnType("character varying")
.HasColumnName("api_key");
entity.Property(e => e.DateStamp)
.HasDefaultValueSql("CURRENT_DATE")
.HasColumnName("date_stamp");
entity.Property(e => e.EventId)
.HasMaxLength(50)
.HasColumnName("event_id");
entity.Property(e => e.Resource)
.HasMaxLength(64)
.HasColumnName("resource");
entity.Property(e => e.TimeStamp)
.HasDefaultValueSql("CURRENT_TIME")
.HasColumnName("time_stamp");
entity.Property(e => e.Usage)
.HasColumnType("jsonb")
.HasColumnName("usage");

entity.HasOne(d => d.Event).WithMany()
.HasForeignKey(d => d.EventId)
.HasConstraintName("fk_metric");
});

modelBuilder.Entity<MetricView>(entity =>
{
entity
.HasNoKey()
.ToView("metric_view", "aoai");

entity.Property(e => e.CompletionTokens).HasColumnName("completion_tokens");
entity.Property(e => e.DateStamp).HasColumnName("date_stamp");
entity.Property(e => e.EventId)
.HasMaxLength(50)
.HasColumnName("event_id");
entity.Property(e => e.PromptTokens).HasColumnName("prompt_tokens");
entity.Property(e => e.Resource)
.HasMaxLength(64)
.HasColumnName("resource");
entity.Property(e => e.TimeStamp).HasColumnName("time_stamp");
entity.Property(e => e.TotalTokens).HasColumnName("total_tokens");
});

modelBuilder.Entity<Owner>(entity =>
{
entity.HasKey(e => e.OwnerId).HasName("owner_pkey");
Expand Down
59 changes: 36 additions & 23 deletions src/AzureOpenAIProxy.Management/Components/Pages/EventList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,57 @@
<MudStack>
<Help Title="Events" />

<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/events/new">New Event</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/events/new" Style="max-width: 200px"
StartIcon="@Icons.Material.Filled.Add">New Event</MudButton>

<MudTable Items="@Events" Loading="@(Events is null)" LoadingProgressColor="Color.Info" Hover="true"
<MudTable Items="@Events" Loading="@(Events is null)" LoadingProgressColor="Color.Info" Hover="true" Dense="true" Striped="true"
Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>Event</MudTh>
<MudTh>Organiser</MudTh>
<MudTh>Duration</MudTh>
<MudTh>Active</MudTh>
<MudTh></MudTh>
<MudTh></MudTh>
<MudTh>When</MudTh>
<MudTh>Regd</MudTh>
<MudTh style="padding: 0;">Active</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Event">@context.EventCode</MudTd>
<MudTd DataLabel="OrganiserName">@context.OrganizerName<br />@context.OrganizerEmail</MudTd>
<MudTd DataLabel="Duration">@context.StartTimestamp.ToString("dd/MM/yyyy HH:mm") -
@context.EndTimestamp.ToString("dd/MM/yyyy HH:mm")<br />
@{
var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(@context.TimeZoneLabel);
var displayName = timeZoneInfo.DisplayName;
}
@displayName
<MudTd style="max-width: 450px;">
<MudStack Spacing="1">
<MudText Typo="Typo.body1" Color="Color.Primary">@context.EventCode</MudText>
<MudText Typo="Typo.caption">
@string.Join(", ", context.Catalogs.Select(catalog => catalog.FriendlyName).OrderBy(catalog => catalog))
</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="Active">@(context.Active ? "Yes" : "No")</MudTd>
<MudTd>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="@($"/event/{context.EventId}")">Edit
</MudButton>
<MudStack Spacing="1">
<MudText Typo="Typo.body1">@context.OrganizerName</MudText>
<MudText Typo="Typo.caption">@context.OrganizerEmail</MudText>
</MudStack>
</MudTd>
<MudTd>
<MudButton Variant="Variant.Filled" Color="Color.Secondary" Target="_blank"
Href="@($"{Configuration["PlaygroundUrl"]}/event/{context.EventId}")">Attendee</MudButton>
<MudStack Spacing="1">
<MudText Typo="Typo.body1" Style="min-width: 140px">
@context.StartTimestamp.ToString("yyyy-MMM-dd HH:mm")
</MudText>
<MudText Typo="Typo.body1">
@context.EndTimestamp.ToString("yyyy-MMM-dd HH:mm")
</MudText>
<MudText Typo="Typo.caption">@context.TimeZoneLabel</MudText>
</MudStack>
</MudTd>
<MudTd>
<MudButton Variant="Variant.Filled" Color="Color.Secondary"
Href="@($"/event/{context.EventId}/metrics")">Metrics</MudButton>
<MudText Typo="Typo.body1">@context.EventAttendees.Count</MudText>
</MudTd>
<MudTd DataLabel="Active" style="padding: 0;">@(context.Active ? "Yes" : "No")</MudTd>
<MudTd>
<MudStack Row="true" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Primary" Href="@($"/event/{context.EventId}")" />
<MudIconButton Icon="@Icons.Material.Filled.People" Color="Color.Primary" Href="@($"{Configuration["PlaygroundUrl"]}/event/{context.EventId}")" Target="_blank" />
<MudIconButton Icon="@Icons.Material.Filled.InsertChartOutlined" Color="Color.Primary" Href="@($"/event/{context.EventId}/metrics")" />
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Primary" @onclick="() => OpenDialog(context)" Disabled="context.EventAttendees.Count > 0 ? true : false" />
</MudStack>
</MudTd>
</RowTemplate>
</MudTable>

</MudStack>
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using AzureOpenAIProxy.Management.Database;
using AzureOpenAIProxy.Management.Services;
using Microsoft.AspNetCore.Components;

namespace AzureOpenAIProxy.Management.Components.Pages;
namespace AzureOpenAIProxy.Management.Components.Pages;

public partial class EventList : ComponentBase
{
Expand All @@ -12,7 +8,29 @@ public partial class EventList : ComponentBase
[Inject]
public required IConfiguration Configuration { get; set; }

[Inject]
public required IDialogService DialogService { get; set; }

public IEnumerable<Event>? Events { get; set; }

protected override async Task OnInitializedAsync() => Events = await EventService.GetOwnerEventsAsync();

private async Task OpenDialog(Event @event)
{
DialogParameters<DeleteConfirmation> parameters = new()
{
{ x => x.ContentText, $"Do you really want to delete the event '{@event.EventCode}'?" },
{ x => x.ButtonText, "Delete" },
{ x => x.Color, Color.Error }
};
var options = new DialogOptions { CloseOnEscapeKey = true };
var dialog = await DialogService.ShowAsync<DeleteConfirmation>("Delete Record", parameters, options);
var result = await dialog.Result;

if (!result.Canceled)
{
await EventService.DeleteEventAsync(@event.EventId);
Events = await EventService.GetOwnerEventsAsync();
}
}
}
100 changes: 51 additions & 49 deletions src/AzureOpenAIProxy.Management/Components/Pages/EventMetrics.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,65 @@
@namespace AzureOpenAIProxy.Management.Components.Pages


@if (Event is not null && EventMetric is not null && EventMetric?.ModelData?.ModelCounts is not null)
@if (Event is not null && ModelCounts is not null)
{
<PageTitle>AI Proxy Metrics | @Event.EventCode</PageTitle>

<MudStack>
<MudLink Target="_blank" Href="@($"{Configuration["PlaygroundUrl"]}/event/{EventId}")">
<MudText><strong>@Event.EventCode</strong></MudText>
</MudLink>

<MudLink Target="_blank" Href="@($"{Configuration["PlaygroundUrl"]}/event/{EventId}")">
<MudText><strong>@Event.EventCode</strong></MudText>
</MudLink>
<MudStack Spacing="1">
<MudText Typo="Typo.body1">Organizer: <strong>@Event.OrganizerName (@Event.OrganizerEmail)</strong></MudText>
<MudText Typo="Typo.body1">When: <strong>@Event.StartTimestamp.ToString("yyyy-MMM-dd HH:mm") - @Event.EndTimestamp.ToString("yyyy-MMM-dd HH:mm") (@Event.TimeZoneLabel)</strong></MudText>
<MudText Typo="Typo.body1">Max Token Cap: <strong>@Event.MaxTokenCap</strong> | Daily Request Cap: <strong>@Event.DailyRequestCap</strong></MudText>
<MudText Typo="Typo.body1">Registrations: <strong>@AttendeeCount</strong> | Active Registrations: <strong>@ActiveRegistrations</strong></MudText>
<MudText Typo="Typo.body1">Total Requests: <strong>@RequestCount</strong></MudText>
</MudStack>

<MudStack Spacing="1">
<MudText Typo="Typo.body1">Registrations: <strong>@EventMetric.AttendeeCount</strong></MudText>
<MudText Typo="Typo.body1">Active Registrations: <strong>@ActiveRegistrations</strong></MudText>
<MudText Typo="Typo.body1">Total Request count: <strong>@EventMetric.RequestCount</strong></MudText>
</MudStack>

<MudIconButton Icon="@Icons.Material.Filled.Refresh" Variant="Variant.Filled" Color="Color.Primary"
Size="Size.Small" Style="width: 80px;" OnClick="RefreshData" />
<MudIconButton Icon="@Icons.Material.Filled.Refresh" Variant="Variant.Filled" Color="Color.Primary"
Size="Size.Small" Style="width: 80px;" OnClick="RefreshData" />

<MudChart ChartType="ChartType.Line" ChartSeries="@ActiveUsersChartSeries" XAxisLabels="@ActiveUsersChartLabels"
Width="100%" Height="450px" />
<MudChart ChartType="ChartType.Line" ChartSeries="@ActiveUsersChartSeries" XAxisLabels="@ActiveUsersChartLabels"
Width="100%" Height="450px" />

<MudChart ChartType="ChartType.Line" ChartSeries="@RequestChartSeries" XAxisLabels="@RequestChartLabels"
Width="100%" Height="450px" />
<MudChart ChartType="ChartType.Line" ChartSeries="@RequestChartSeries" XAxisLabels="@RequestChartLabels"
Width="100%" Height="450px" />

<MudTable Items="@EventMetric.ModelData.ModelCounts" Dense="true" Striped="true" SortLabel="Sort By" Hover="true" Style="margin-bottom: 60px;">
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.Resource)">
Resource Name (Deployment or Index Name)</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.PromptTokens)">
Prompt tokens</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.CompletionTokens)">
Completion tokens</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.TotalTokens)">
Total tokens</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel InitialDirection="SortDirection.Descending"
SortBy="new Func<ModelCounts, object>(x => x.Count)">
Requests</MudTableSortLabel>
</MudTh>
</HeaderContent>
<MudTable Items="@ModelCounts" Dense="true" Striped="true" SortLabel="Sort By" Hover="true"
Style="margin-bottom: 60px;">
<HeaderContent>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.Resource)">
Resource Name (Deployment or Index Name)</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.PromptTokens)">
Prompt tokens</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.CompletionTokens)">
Completion tokens</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel SortBy="new Func<ModelCounts, object>(x => x.TotalTokens)">
Total tokens</MudTableSortLabel>
</MudTh>
<MudTh>
<MudTableSortLabel InitialDirection="SortDirection.Descending"
SortBy="new Func<ModelCounts, object>(x => x.Count)">
Requests</MudTableSortLabel>
</MudTh>
</HeaderContent>

<RowTemplate>
<MudTd DataLabel="Resource Name (Deployment or Index Name)">@context.Resource</MudTd>
<MudTd DataLabel="Prompt tokens">@context.PromptTokens</MudTd>
<MudTd DataLabel="Completion tokens">@context.CompletionTokens</MudTd>
<MudTd DataLabel="Total tokens">@context.TotalTokens</MudTd>
<MudTd DataLabel="Request Count">@context.Count</MudTd>
</RowTemplate>
</MudTable>
</MudStack>
<RowTemplate>
<MudTd DataLabel="Resource Name (Deployment or Index Name)">@context.Resource</MudTd>
<MudTd DataLabel="Prompt tokens">@context.PromptTokens</MudTd>
<MudTd DataLabel="Completion tokens">@context.CompletionTokens</MudTd>
<MudTd DataLabel="Total tokens">@context.TotalTokens</MudTd>
<MudTd DataLabel="Request Count">@context.Count</MudTd>
</RowTemplate>
</MudTable>
</MudStack>
}
Loading