Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sync Models Catalog and API Enhancements #11

Merged
merged 4 commits into from
Jul 28, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
For="@(() => Action!.Model)"
HelperText="The model that will be used when executing actions.">

@foreach (var model in ModelCatalog)
@foreach (var model in ModelCatalog.Models)
{
<MudSelectItem Value="@model.Name" />
}
Expand Down Expand Up @@ -112,7 +112,7 @@
[Parameter] public Guid ActiontId { get; set; }
[Parameter] public EventCallback OnActionSubmit { get; set; }
private NativeActionResponse Action { get; set; } = new();
private List<ModelCatalogResponse> ModelCatalog { get; set; } = new();
private ModelCatalogResponse ModelCatalog { get; set; } = new();

public List<string> Categories { get; set; } = new()
{
Expand Down Expand Up @@ -207,7 +207,7 @@ Some actions require just the latest message, while others need the full convers

if (modelCatalog is not null)
{
ModelCatalog = modelCatalog.ToList();
ModelCatalog = modelCatalog;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
[Parameter] public List<NativeActionResponse> Actions { get; set; } = new();

private MudAutocomplete<NativeActionResponse>? _autocomplete;
private List<ModelCatalogResponse> ModelCatalog { get; set; } = new();
private ModelCatalogResponse ModelCatalog { get; set; } = new();

private bool coerceText;
private bool coerceValue;
Expand Down Expand Up @@ -89,7 +89,7 @@

if (modelCatalog is not null)
{
ModelCatalog = modelCatalog.ToList();
ModelCatalog = modelCatalog;
}
}

Expand Down Expand Up @@ -153,7 +153,7 @@
{
if (action is not null)
{
var serviceId = ModelCatalog.SingleOrDefault(x => x.Name == action.Model)?.Provider ?? string.Empty;
var serviceId = ModelCatalog.Models.SingleOrDefault(x => x.Name == action.Model)?.Provider ?? string.Empty;
_chatState.SetNativeActionServiceId(serviceId);

ActionChanged.InvokeAsync(action);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<MudText>Model</MudText>
<MudSelect @bind-Value="Assistant.DefaultModel" Margin="Margin.Dense" T="string" Variant="Variant.Outlined" For="@(() => Assistant!.DefaultModel)">
@foreach (var model in AvailableModels)
@foreach (var model in ModelCatalog.Models)
{
<MudSelectItem Value="@model.Name" />
}
Expand Down Expand Up @@ -52,7 +52,7 @@
[Parameter] public Guid AssistantId { get; set; }
[Parameter] public EventCallback OnAssistantSubmit { get; set; }
private AssistantResponse Assistant { get; set; } = new();
private List<ModelCatalogResponse> AvailableModels { get; set; } = new();
private ModelCatalogResponse ModelCatalog { get; set; } = new();

private const string assistantBehaviorGuidelines = @"
Enter the Metaprompt here. This set of instructions will define your assistant's behavior
Expand Down Expand Up @@ -108,16 +108,16 @@ reflective of the assistant's intended personality and capabilities.<br /><br />
private async Task LoadModels()
{
var response = await _modelCatalogService.BrowseModelsCatalog();
var models = response.Value;
var modelCatalog = response.Value;

if (!response.Succeeded)
{
ShowSnackbar("Failed to load available models. Please try again.", Severity.Error);
ShowSnackbar("Failed to load model catalog. Please try again.", Severity.Error);
}

if (models is not null)
if (modelCatalog is not null)
{
AvailableModels = models.ToList();
ModelCatalog = modelCatalog;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

@code
{
public List<ModelCatalogResponse> ModelCatalog { get; set; } = new();
public ModelCatalogResponse ModelCatalog { get; set; } = new();
private string CurrentValue { get; set; } = string.Empty;
private string ServiceId { get; set; } = string.Empty;

Expand All @@ -35,6 +35,8 @@
_chatState.SelectedModelChanged += StateHasChanged;
_chatState.ServiceIdChanged += StateHasChanged;
_chatState.SelectedAssistantChanged += SetDefaultModel;
_chatState.LastFetchedModelsCatalogChanged += StateHasChanged;
_chatState.ModelsCatalogRefreshed += async () => await LoadModelCatalog();

await LoadModelCatalog();
SetDefaultModel();
Expand All @@ -52,7 +54,8 @@

if(modelCatalog is not null)
{
ModelCatalog = modelCatalog.ToList();
ModelCatalog = modelCatalog;
_chatState.SetLastFetchedModelsCatalog(modelCatalog.LastFetched);
}
}

Expand All @@ -65,7 +68,7 @@
CurrentValue = selectedModel!;
_chatState.SetSelectedModel(selectedModel!);

var serviceId = ModelCatalog.SingleOrDefault(x => x.Name == CurrentValue)?.Provider ?? string.Empty;
var serviceId = ModelCatalog.Models.SingleOrDefault(x => x.Name == CurrentValue)?.Provider ?? string.Empty;
_chatState.SetServiceId(serviceId);
}
}
Expand All @@ -74,10 +77,10 @@
{
if (string.IsNullOrEmpty(value))
{
return ModelCatalog.Select(model => model.Name);
return ModelCatalog.Models.Select(model => model.Name);
}

var result = ModelCatalog
var result = ModelCatalog.Models
.Where(x => x.Name.Contains(value, StringComparison.InvariantCultureIgnoreCase))
.Select(model => model.Name);

Expand All @@ -87,7 +90,7 @@
private void OnValueChanged(string value)
{
CurrentValue = value.IsEmpty() ? string.Empty : value;
var serviceId = ModelCatalog.SingleOrDefault(x => x.Name == value)?.Provider ?? string.Empty;
var serviceId = ModelCatalog.Models.SingleOrDefault(x => x.Name == value)?.Provider ?? string.Empty;

_chatState.SetServiceId(serviceId);
_chatState.SetSelectedModel(CurrentValue);
Expand All @@ -100,7 +103,7 @@
=> SetDefaultModel();

private bool IsValidModel(string value)
=> value.IsNotEmpty() && ModelCatalog.Any(model => model.Name == value);
=> value.IsNotEmpty() && ModelCatalog.Models.Any(model => model.Name == value);

private void ShowSnackbar(string message, Severity severity)
{
Expand All @@ -117,5 +120,7 @@
_chatState.SelectedModelChanged -= StateHasChanged;
_chatState.ServiceIdChanged -= StateHasChanged;
_chatState.SelectedAssistantChanged -= SetDefaultModel;
_chatState.LastFetchedModelsCatalogChanged -= StateHasChanged;
_chatState.ModelsCatalogRefreshed -= async () => await LoadModelCatalog();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
ο»Ώ@inject ISettingsService _settingsService
@inject IModelCatalogService _modelCatalogService
@inject ChatState _chatState
@inject ISnackbar Snackbar

<EditForm Model="@AppSettings" OnValidSubmit="OnValidSubmit">
Expand Down Expand Up @@ -59,8 +61,26 @@
</MudExpansionPanel>
</MudExpansionPanels>
</div>
<div class="d-flex flex-row flex-grow-1 justify-end gap-4 pa-4">
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Save</MudButton>
<div class="d-flex flex-row flex-grow-1 gap-4 pa-4">
<MudTooltip Text="@($"Last updated: {_chatState.LastFetchedModelsCatalog:yyyy-MM-dd HH:mm:ss}")" Arrow="true" Placement="Placement.Top">
<MudButton OnClick="ForceRefreshModelCatalogAsync" Variant="Variant.Text" Color="isFetching ? Color.Default : Color.Primary" Class="justify-start">
@if (isFetching)
{
<div class="d-flex gap-2 my-auto">
<MudProgressCircular Color="Color.Default" Size="Size.Small" Indeterminate="true" />
<MudText Typo="Typo.button">Fetching models catalog</MudText>
</div>
}
else
{
<div class="d-flex gap-2 my-auto">
<MudIcon Icon="@Icons.Material.Filled.Refresh" />
<MudText Typo="Typo.button">Sync models catalog</MudText>
</div>
}
</MudButton>
</MudTooltip>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="justify-end ml-auto">Save</MudButton>
</div>
</EditForm>

Expand Down Expand Up @@ -96,6 +116,7 @@
private const string OpenAiProvider = "OpenAI";
private bool showOpenAiApiKey = false;
private bool showOllamaEndpoint = false;
private bool isFetching = false;

public string OllamaEndpoint { get; set; } = string.Empty;
public bool IsEnabledOllama { get; set; } = false;
Expand All @@ -109,6 +130,8 @@

protected async override Task OnInitializedAsync()
{
_chatState.LastFetchedModelsCatalogChanged += StateHasChanged;

SetupInitialSettings();
await PopulateForm();
}
Expand Down Expand Up @@ -186,6 +209,21 @@
}
}

private async Task ForceRefreshModelCatalogAsync()
{
isFetching = true;
var response = await _modelCatalogService.BrowseModelsCatalog(forceRefresh: true);

if (!response.Succeeded)
{
ShowSnackbar("Failed to load model catalog. Please try again.", Severity.Error);
}

_chatState.SetLastFetchedModelsCatalog(response.Value.LastFetched);
_chatState.NotifyModelsCatalogRefreshed();
isFetching = false;
}

private void ShowSnackbar(string message, Severity severity)
{
Snackbar.Configuration.PositionClass = Defaults.Classes.Position.TopRight;
Expand All @@ -195,4 +233,9 @@
options.HideTransitionDuration = 100;
});
}

public void Dispose()
{
_chatState.LastFetchedModelsCatalogChanged -= StateHasChanged;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ namespace StellarChat.Client.Web.Services.Models;

public interface IModelCatalogService
{
ValueTask<ApiResponse<IEnumerable<ModelCatalogResponse>>> BrowseModelsCatalog();
ValueTask<ApiResponse<ModelCatalogResponse>> BrowseModelsCatalog(string provider = "openai", string filter = "completions", bool forceRefresh = false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ public class ModelCatalogService(IRestHttpClient httpClient) : IModelCatalogServ
{
private readonly IRestHttpClient _httpClient = httpClient;

public async ValueTask<ApiResponse<IEnumerable<ModelCatalogResponse>>> BrowseModelsCatalog()
=> await _httpClient.GetAsync<IEnumerable<ModelCatalogResponse>>($"/models?Provider=openai&Filter=completions");
public async ValueTask<ApiResponse<ModelCatalogResponse>> BrowseModelsCatalog(string provider = "openai", string filter = "completions", bool forceRefresh = false)
=> await _httpClient.GetAsync<ModelCatalogResponse>($"/models?Provider={provider}&Filter={filter}&forceRefresh={forceRefresh}");
}
16 changes: 16 additions & 0 deletions src/Client/StellarChat.Client.Web/State/ChatState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ public void SetSelectedModel(string selectedModel)
SelectedModelChanged?.Invoke();
}

public DateTimeOffset LastFetchedModelsCatalog { get; set; }
public event Action? LastFetchedModelsCatalogChanged;

public void SetLastFetchedModelsCatalog(DateTimeOffset lastFetchedModelsCatalog)
{
LastFetchedModelsCatalog = lastFetchedModelsCatalog;
LastFetchedModelsCatalogChanged?.Invoke();
}

public string ServiceId { get; set; } = string.Empty;
public event Action? ServiceIdChanged;

Expand Down Expand Up @@ -116,4 +125,11 @@ public void NotifyActionsRefreshed()
{
ActionsRefreshed?.Invoke();
}

public event Action? ModelsCatalogRefreshed;

public void NotifyModelsCatalogRefreshed()
{
ModelsCatalogRefreshed?.Invoke();
}
}
1 change: 1 addition & 0 deletions src/Client/StellarChat.Client.Web/_Imports.razor
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@using Microsoft.JSInterop
@using MudBlazor
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@using System.Net.Http
@using System.Net.Http.Json
@using System.Text
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
ο»Ώusing Microsoft.SemanticKernel;
using StellarChat.Server.Api.Features.Actions.CreateNativeAction;
ο»Ώusing StellarChat.Server.Api.Features.Actions.CreateNativeAction;
using StellarChat.Server.Api.Features.Actions.Webhooks.Exceptions;
using StellarChat.Server.Api.Features.Actions.Webhooks.Services;
using StellarChat.Server.Api.Features.Chat.CarryConversation;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
ο»Ώnamespace StellarChat.Server.Api.Features.Models.BrowseModelsCatalog;

internal sealed record BrowseModelsCatalog : IQuery<IEnumerable<ModelCatalogResponse>>
internal sealed record BrowseModelsCatalog : IQuery<ModelCatalogResponse>
{
public string? Provider { get; set; }
public string? Filter { get; set; }
public bool ForceRefresh { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,50 @@

namespace StellarChat.Server.Api.Features.Models.BrowseModelsCatalog;

internal sealed class BrowseModelsCatalogHandler : IQueryHandler<BrowseModelsCatalog, IEnumerable<ModelCatalogResponse>?>
internal sealed class BrowseModelsCatalogHandler : IQueryHandler<BrowseModelsCatalog, ModelCatalogResponse?>
{
private readonly IEnumerable<IModelCatalog> _modelCatalogs;
private readonly IMemoryCache _memoryCache;
private readonly TimeProvider _clock;

private readonly ConcurrentBag<string> _activeCacheKeys = new();

public BrowseModelsCatalogHandler(IEnumerable<IModelCatalog> modelCatalogs, IMemoryCache memoryCache)
private readonly ConcurrentBag<string> _activeCacheKeys = [];

public BrowseModelsCatalogHandler(IEnumerable<IModelCatalog> modelCatalogs, IMemoryCache memoryCache, TimeProvider clock)
{
_modelCatalogs = modelCatalogs;
_memoryCache = memoryCache;
_clock = clock;
}

public async ValueTask<IEnumerable<ModelCatalogResponse>?> Handle(BrowseModelsCatalog query, CancellationToken cancellationToken)
public async ValueTask<ModelCatalogResponse?> Handle(BrowseModelsCatalog query, CancellationToken cancellationToken)
{
var cacheKey = GenerateCacheKey(query);
_activeCacheKeys.Add(cacheKey);

if (query.ForceRefresh)
{
_memoryCache.Remove(cacheKey);
}

return await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(6);

return await FetchModelsFromProvider(query, cancellationToken) ?? [];
var models = await FetchModelsFromProvider(query, cancellationToken) ?? [];
var now = _clock.GetLocalNow();

return new ModelCatalogResponse
{
Models = models,
LastFetched = now
};
});
}

private async Task<IEnumerable<ModelCatalogResponse>> FetchModelsFromProvider(BrowseModelsCatalog query, CancellationToken cancellationToken)
private async Task<IEnumerable<ModelCatalog>> FetchModelsFromProvider(BrowseModelsCatalog query, CancellationToken cancellationToken)
{
var availableModels = new List<ModelCatalogResponse>();
var availableModels = new List<ModelCatalog>();

foreach (var provider in _modelCatalogs)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
internal interface IModelCatalog
{
string ProviderName { get; }
ValueTask<IEnumerable<ModelCatalogResponse>> FetchModelsAsync(BrowseModelsCatalog query, CancellationToken cancellationToken = default);
IEnumerable<ModelCatalogResponse> FilterModels(string filter, IEnumerable<ModelCatalogResponse> models);
ValueTask<IEnumerable<ModelCatalog>> FetchModelsAsync(BrowseModelsCatalog query, CancellationToken cancellationToken = default);
IEnumerable<ModelCatalog> FilterModels(string filter, IEnumerable<ModelCatalog> models);
}
Loading
Loading