Skip to content

Commit

Permalink
WIP: Fix some issues in WASM self-hosted coding assistant config.
Browse files Browse the repository at this point in the history
  • Loading branch information
highbyte committed Oct 2, 2024
1 parent 8077d13 commit d0c1cf0
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -241,31 +241,33 @@ public static async Task<ICodeSuggestion> GetCodeSuggestionImplementation(C64Hos
public static async Task<ApiConfig> GetOpenAIConfig(ILocalStorageService localStorageService)
{
var apiKey = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:ApiKey");

var deploymentName = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:DeploymentName");
if (string.IsNullOrEmpty(deploymentName))
deploymentName = "gpt-4o"; // Default to a model that works well

var endpoint = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint");
Uri.TryCreate(endpoint, UriKind.Absolute, out var endPointUri);
deploymentName = "gpt-4o"; // Default to a OpenAI model that works well

var selfHosted = await localStorageService.GetItemAsync<bool>($"{ApiConfig.CONFIG_SECTION}:SelfHosted");
// For future use: Endpoint can be set if OpenAI is accessed via Azure endpoint.
//var endpoint = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint");
//Uri.TryCreate(endpoint, UriKind.Absolute, out var endPointUri);

var apiConfig = new ApiConfig()
{
ApiKey = apiKey, // Api key for OpenAI (required), Azure OpenAI (required), or SelfHosted (optional).
DeploymentName = deploymentName, // AI model name
Endpoint = endPointUri, // Used if using Azure OpenAI, or SelfHosted
SelfHosted = selfHosted, // Set to true to use self-hosted OpenAI API compatible endpoint.
//Endpoint = endPointUri, // Used if using Azure OpenAI
SelfHosted = false,
};
return apiConfig;
}

public static async Task<ApiConfig> GetSelfHostedOpenAICompatibleConfig(ILocalStorageService localStorageService)
{
var apiKey = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:ApiKey");
if (apiKey == string.Empty)
apiKey = null;
var deploymentName = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:DeploymentName");
if (string.IsNullOrEmpty(deploymentName))
deploymentName = "stable-code:3b-code-q4_0"; // Default to a Ollama model that is optimized for code completion
deploymentName = "codellama:13b"; // Default to a Ollama model that (sometimes) works... TODO: Improve parsing of response (which does not seem as exact as from OpenAI models), or improve prompt with examples?
var endpoint = await localStorageService.GetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:Endpoint");
if (string.IsNullOrEmpty(endpoint))
endpoint = "http://localhost:11434/api"; // Default to local Ollama
Expand All @@ -281,21 +283,6 @@ public static async Task<ApiConfig> GetSelfHostedOpenAICompatibleConfig(ILocalSt
return apiConfig;
}


public static async Task SaveOpenAICodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, ApiConfig apiConfig)
{
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:ApiKey", apiConfig.ApiKey);
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:DeploymentName", apiConfig.DeploymentName);
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint", apiConfig.Endpoint != null ? apiConfig.Endpoint.OriginalString : "");
}

public static async Task SaveSelfHostedOpenAICompatibleCodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, ApiConfig apiConfig)
{
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:ApiKey", !string.IsNullOrEmpty(apiConfig.ApiKey) ? apiConfig.ApiKey : string.Empty);
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:DeploymentName", !string.IsNullOrEmpty(apiConfig.DeploymentName) ? apiConfig.DeploymentName : string.Empty);
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:Endpoint", apiConfig.Endpoint != null ? apiConfig.Endpoint.OriginalString : "");
}

public static async Task<CustomAIEndpointConfig> GetCustomAIEndpointConfig(ILocalStorageService localStorageService)
{
var apiKey = await localStorageService.GetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:ApiKey");
Expand All @@ -315,9 +302,22 @@ public static async Task<CustomAIEndpointConfig> GetCustomAIEndpointConfig(ILoca
return apiConfig;
}

public static async Task SaveOpenAICodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, ApiConfig apiConfig)
{
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:ApiKey", apiConfig.ApiKey ?? string.Empty);
//await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION}:Endpoint", apiConfig.Endpoint != null ? apiConfig.Endpoint.OriginalString : string.Empty);
}

public static async Task SaveSelfHostedOpenAICompatibleCodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, ApiConfig apiConfig)
{
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:ApiKey", apiConfig.ApiKey ?? string.Empty);
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:DeploymentName", apiConfig.DeploymentName ?? string.Empty);
await localStorageService.SetItemAsStringAsync($"{ApiConfig.CONFIG_SECTION_SELF_HOSTED}:Endpoint", apiConfig.Endpoint != null ? apiConfig.Endpoint.OriginalString : string.Empty);
}

public static async Task SaveCustomCodingAssistantConfigToLocalStorage(ILocalStorageService localStorageService, CustomAIEndpointConfig customAIEndpointConfig)
{
await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:ApiKey", customAIEndpointConfig.ApiKey);
await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:Endpoint", customAIEndpointConfig.Endpoint != null ? customAIEndpointConfig.Endpoint.OriginalString : "");
await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:ApiKey", customAIEndpointConfig.ApiKey ?? string.Empty);
await localStorageService.SetItemAsStringAsync($"{CustomAIEndpointConfig.CONFIG_SECTION}:Endpoint", customAIEndpointConfig.Endpoint != null ? customAIEndpointConfig.Endpoint.OriginalString : string.Empty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,17 @@
@if (@C64HostConfig.CodeSuggestionBackendType == CodeSuggestionBackendTypeEnum.SelfHostedOpenAICompatible)
{
<div class="table-row">
<div class="table-cell table-cell-fixed-width-medium twocol">Self-hosted OpenAI compatible endpoint (ex: Ollama on http://localhost:11434/api)</div>
<div class="table-cell table-cell-fixed-width-medium twocol">Self-hosted OpenAI compatible endpoint</div>
<div class="table-cell table-cell-fixed-width-large twocol">
@if (_selfHostedOpenAICompatibleAIApiConfig != null)
{
@* <InputText TValue="string" @bind-Value:get="this._selfHostedOpenAICompatibleAIApiConfig.Endpoint.ToString()" @bind-Value:set="((string e) => this._selfHostedOpenAICompatibleAIApiConfig.Endpoint = new Uri(e))" style="width: inherit" /> *@
}
<InputText @ref="_selfHostedOpenAICompatibleEndpointInputText" @bind-Value="_selfHostedOpenAICompatibleAIApiConfig.EndpointString" style="width: inherit" />
}
</div>
</div>

<div class="table-row">
<div class="table-cell table-cell-fixed-width-medium twocol">Model name (ex: stable-code:3b-code-q4_0)</div>
<div class="table-cell table-cell-fixed-width-medium twocol">Model name</div>
<div class="table-cell table-cell-fixed-width-large twocol">
@if (_selfHostedOpenAICompatibleAIApiConfig != null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Predict what text the user in would insert at the cursor position indicated by ^
Do not make up new information. If you're not sure, just reply with NO_PREDICTION.
RULES:
1. Reply with OK:,then in square brackets (with not preceeding space) the predicted text, then END_INSERTION, and no other output.
1. Reply with OK:,then in square brackets [] (without preceeding space) the predicted text, then END_INSERTION, and no other output.
2. If there isn't enough information to predict any words that the user would type next, just reply with the word NO_PREDICTION.
3. NEVER invent new information. If you can't be sure what the user is about to type, ALWAYS stop the prediction with END_INSERTION.");

Expand Down Expand Up @@ -51,8 +51,15 @@ public virtual async Task<string> GetInsertionSuggestionAsync(IInferenceBackend
{
var chatOptions = BuildPrompt(config, textBefore, textAfter);
var response = await inference.GetChatResponseAsync(chatOptions);
if (response.Length > 5 && response.StartsWith("OK:[", StringComparison.Ordinal))

if (response.Length > 5 &&
(response.StartsWith("OK:[", StringComparison.Ordinal)
|| response.StartsWith("OK: [", StringComparison.Ordinal)))
{
// Some tested Ollama models respons starts with "OK: [" , some with "OK:[" (even though the prompt doesn't have a space)
if (response.StartsWith("OK: [", StringComparison.Ordinal))
response = response.Replace("OK: [", "OK:[");

// Avoid returning multiple sentences as it's unlikely to avoid inventing some new train of thought.
var trimAfter = response.IndexOfAny(['.', '?', '!']);
if (trimAfter > 0 && response.Length > trimAfter + 1 && response[trimAfter + 1] == ' ')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ public class ApiConfig
{
public string? ApiKey { get; set; }
public string? DeploymentName { get; set; }
public string EndpointString
{
get
{
return Endpoint?.ToString() ?? string.Empty;
}
set
{
Endpoint = string.IsNullOrWhiteSpace(value) ? null : new Uri(value);
}
}
public Uri? Endpoint { get; set; }
public bool SelfHosted { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Diagnostics;

namespace Highbyte.DotNet6502.AI.CodingAssistant.Inference.OpenAI;
public class DisableActivityHandler : DelegatingHandler
{
/// <summary>
/// Distributed tracing headers that is set automatically by .NET that local Ollama API CORS rules doesn't allow.
/// </summary>
static readonly List<string> s_HeadersToRemove = new List<string>
{
"x-ms-client-request-id",
"x-ms-return-client-request-id"
};

public DisableActivityHandler(HttpMessageHandler innerHandler) : base(innerHandler)
{

}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Note: A workaround by setting Activity.Current = null doesn't seem to work. Instead remove headers manually below.
//Activity.Current = null;

// Remove s_HeadersToRemove list of header from request if they exist.
foreach (var headerName in s_HeadersToRemove)
{
if (request.Headers.Contains(headerName))
{
request.Headers.Remove(headerName);
Activity.Current = null;
}
}

return await base.SendAsync(request, cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Based on https://github.com/dotnet/smartcomponents

using System.Net;
using System.Runtime.InteropServices;
using Azure;
using Azure.AI.OpenAI;
Expand Down Expand Up @@ -72,7 +73,13 @@ private static OpenAIClient CreateClient(ApiConfig apiConfig)
{
if (apiConfig.SelfHosted)
{
var transport = new SelfHostedLlmTransport(apiConfig.Endpoint!);
//var transport = new SelfHostedLlmTransport(apiConfig.Endpoint!);

var httpClientHandler = new HttpClientHandler();
var disableActivityHandler = new DisableActivityHandler(httpClientHandler);
var httpClient = new HttpClient(disableActivityHandler);
var transport = new SelfHostedLlmTransport(apiConfig.Endpoint!, httpClient);

return new OpenAIClient(apiConfig.ApiKey, new() { Transport = transport });
}
else if (apiConfig.Endpoint is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,24 @@ namespace Highbyte.DotNet6502.AI.CodingAssistant.Inference.OpenAI;
/// Used to resolve queries using Ollama or anything else that exposes an OpenAI-compatible
/// endpoint with a scheme/host/port set of your choice.
/// </summary>
internal class SelfHostedLlmTransport(Uri endpoint) : HttpClientTransport
internal class SelfHostedLlmTransport : HttpClientTransport
{
private readonly Uri _endpoint;

internal SelfHostedLlmTransport(Uri endpoint) : base()
{
_endpoint = endpoint;
}
internal SelfHostedLlmTransport(Uri endpoint, HttpClient httpClient) : base(httpClient)
{
_endpoint = endpoint;
}

public override ValueTask ProcessAsync(HttpMessage message)
{
message.Request.Uri.Scheme = endpoint.Scheme;
message.Request.Uri.Host = endpoint.Host;
message.Request.Uri.Port = endpoint.Port;
message.Request.Uri.Scheme = _endpoint.Scheme;
message.Request.Uri.Host = _endpoint.Host;
message.Request.Uri.Port = _endpoint.Port;
return base.ProcessAsync(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.17" />
<PackageReference Include="Azure.AI.OpenAI" Version="1.0.0-beta.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />

</ItemGroup>
Expand Down

0 comments on commit d0c1cf0

Please sign in to comment.