Alpha Commit

master
Harald Wolff 2024-04-26 14:46:45 +02:00
commit 824bdd034b
26 changed files with 1434 additions and 0 deletions

5
.gitignore vendored 100644
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

View File

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/projectSettingsUpdater.xml
/.idea.ln.ai.assistant.iml
/contentModel.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,81 @@

using ln.ai.assistant.auth;
namespace ln.ai.assistant.cli;
public class AiAssistantCli
{
public static void Main(string[] arguments)
{
if (arguments.Length < 2) {
Console.Error.WriteLine("usage: AiAssistantCli <username> <password>");
return;
}
IdentityService identityService = new IdentityService(arguments[0]);
if (!identityService.TryGetLoginIdentity(arguments[1], out LoginIdentity loginIdentity)) {
if (!identityService.CreateLoginIdentity(arguments[1], out loginIdentity)) {
Console.Error.WriteLine("could not create identity {0}", arguments[1]);
return;
}
}
if (arguments.Length > 2) {
loginIdentity.SetPassword(arguments[2]);
}
else
{
string newPassword = ReadPassword("Enter new password for user {0} or <ENTER> to quit: ", arguments[1]);
if (!string.Empty.Equals(newPassword))
{
string repeatPassword = ReadPassword("repeat new password for user {0} : ", arguments[1]);
if (newPassword.Equals(repeatPassword))
{
loginIdentity.SetPassword(newPassword);
identityService.UpdateLoginIdentity(loginIdentity);
}
else
{
Console.WriteLine("Passwords do not match!");
}
}
}
}
public static string ReadPassword(string prompt, params object[] arguments)
{
Console.Write(prompt, arguments);
string password = "";
while (true)
{
ConsoleKeyInfo key = Console.ReadKey(true);
if (key.Key == ConsoleKey.Enter)
{
break;
}
else if (key.Key == ConsoleKey.Backspace)
{
if (password.Length > 0)
{
password = password.Substring(0, password.Length - 1);
Console.Write("\b \b");
}
}
else
{
password += key.KeyChar;
Console.Write("*");
}
}
Console.WriteLine("");
return password;
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ln.ai.assistant\ln.ai.assistant.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.ai.assistant", "ln.ai.assistant\ln.ai.assistant.csproj", "{B232F124-4775-408D-A684-06D5A1E1E6C0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.http", "..\dotnet\ln.http\ln.http\ln.http.csproj", "{25931030-FE87-422A-9210-175432F26D3B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ln.ai.assistant.cli", "ln.ai.assistant.cli\ln.ai.assistant.cli.csproj", "{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B232F124-4775-408D-A684-06D5A1E1E6C0}.Release|Any CPU.Build.0 = Release|Any CPU
{25931030-FE87-422A-9210-175432F26D3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{25931030-FE87-422A-9210-175432F26D3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25931030-FE87-422A-9210-175432F26D3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25931030-FE87-422A-9210-175432F26D3B}.Release|Any CPU.Build.0 = Release|Any CPU
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE1BABE3-0BDC-481C-86F7-BEB11FCBCBE6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/Environment/AssemblyExplorer/XmlDocument/@EntryValue">&lt;AssemblyExplorer&gt;
&lt;Assembly Path="/home/haraldwolff/.nuget/packages/ln.http/0.9.8/lib/net7.0/ln.http.dll" /&gt;
&lt;/AssemblyExplorer&gt;</s:String></wpf:ResourceDictionary>

View File

@ -0,0 +1,41 @@

using System.Net;
using ln.ai.assistant.auth;
using ln.http;
using ln.mime;
using HttpMethod = ln.http.HttpMethod;
namespace ln.ai.assistant
{
public class AiAssistantServer : HttpRouter
{
private FileSystemRouter FileSystemRouter { get; }
public string RootPath { get; }
private IdentityService _identityService;
public AiAssistantServer(string path, IdentityService identityService)
{
_identityService = identityService;
RootPath = path;
FileSystemRouter = new FileSystemRouter(this, "/", new MimeTypes(), path);
}
public override bool RouteRequest(HttpRequestContext httpRequestContext, string routePath)
{
bool success = base.RouteRequest(httpRequestContext, routePath);
if (success)
Console.Error.WriteLine("{0} {1} {2} {3} {4}",
httpRequestContext.Request.Method.ToString(),
httpRequestContext.Request.RequestUri.ToString(),
httpRequestContext.Response.HttpStatusCode.ToString(),
httpRequestContext.Response.HttpContent?.Length.ToString() ?? "-",
httpRequestContext.Response.Headers.Get("content-type",
httpRequestContext.Response.HttpContent?.ContentType ?? "-")
);
return success;
}
}
}

View File

@ -0,0 +1,135 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ln.type;
namespace ln.ai.assistant;
public class CertificateAuthority
{
public string StoragePath { get; }
public string Name { get; }
private X509Certificate2 _privateCertificate;
public CertificateAuthority(string storagePath, string caName)
{
StoragePath = storagePath;
Name = caName;
Initialize();
}
public void Initialize()
{
string caCertFileName = Path.Combine(StoragePath, ".ca.pem");
string caKeyFileName = Path.Combine(StoragePath, ".ca.key");
if (File.Exists(caCertFileName) && File.Exists(caKeyFileName))
{
_privateCertificate = X509Certificate2.CreateFromPemFile(caCertFileName, caKeyFileName);
}
else
{
using (RSA rsa = RSA.Create(4096))
{
var request = new CertificateRequest($"CN={Name}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.CrlSign | X509KeyUsageFlags.KeyCertSign, false));
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, false));
_privateCertificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now + TimeSpan.FromDays(3650));
WritePemFile(caCertFileName, _privateCertificate);
WritePemFile(caKeyFileName, rsa);
}
}
}
public bool GetOrCreateCertificate(string distinguishedName, out X509Certificate2? certificate)
{
return TryLoadCertificate(distinguishedName, out certificate) ||
CreateSignedCertificate(distinguishedName, out certificate);
}
private bool CreateSignedCertificate(string distinguishedName, out X509Certificate2? certificate)
{
string certFileName = Path.Combine(StoragePath, $"{distinguishedName}.cert.pem");
string keyFileName = Path.Combine(StoragePath, $"{distinguishedName}.key.pem");
using (RSA rsa = RSA.Create(4096))
{
var request = new CertificateRequest($"CN={distinguishedName}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DataEncipherment | X509KeyUsageFlags.KeyEncipherment |
X509KeyUsageFlags.DigitalSignature, false));
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
var san = new SubjectAlternativeNameBuilder();
san.AddDnsName(distinguishedName);
request.CertificateExtensions.Add(san.Build());
certificate = request.Create(_privateCertificate, DateTimeOffset.Now,
DateTimeOffset.Now + TimeSpan.FromDays(365), BitConverter.GetBytes(DateTime.Now.ToUnixTimeSeconds()));
WritePemFile(certFileName, certificate);
WritePemFile(keyFileName, rsa);
return true;
}
certificate = null;
return false;
}
private bool TryLoadCertificate(string distinguishedName, out X509Certificate2? certificate)
{
string certFileName = Path.Combine(StoragePath, $"{distinguishedName}.cert.pem");
string keyFileName = Path.Combine(StoragePath, $"{distinguishedName}.key.pem");
if (File.Exists(certFileName) && File.Exists(keyFileName))
{
// certificate = new X509Certificate2(X509Certificate2.CreateFromPemFile(certFileName, keyFileName).Export(X509ContentType.Pfx, "alpha"), "alpha");
certificate = X509Certificate2.CreateFromPemFile(certFileName, keyFileName);
return true;
}
certificate = null;
return false;
}
private void WritePemFile(string pemFileName, X509Certificate2 certificate2, RSA rsa)
{
using (var pemFileStream =
new StreamWriter(new FileStream(pemFileName, FileMode.CreateNew)))
{
WritePemFile("CERTIFICATE", certificate2.RawData, pemFileStream);
WritePemFile("RSA PRIVATE KEY", rsa.ExportRSAPrivateKey(), pemFileStream);
}
}
private void WritePemFile(string pemFileName, X509Certificate2 certificate2)
{
using (var pemFileStream =
new StreamWriter(new FileStream(pemFileName, FileMode.CreateNew)))
{
WritePemFile("CERTIFICATE", certificate2.RawData, pemFileStream);
}
}
private void WritePemFile(string pemFileName, RSA rsa)
{
using (var pemFileStream =
new StreamWriter(new FileStream(pemFileName, FileMode.CreateNew)))
{
WritePemFile("RSA PRIVATE KEY", rsa.ExportRSAPrivateKey(), pemFileStream);
}
}
private static void WritePemFile(string type, byte[] data, TextWriter outputFile)
{
outputFile.WriteLine($"-----BEGIN {type}-----");
outputFile.WriteLine(Convert.ToBase64String(data, Base64FormattingOptions.InsertLineBreaks));
outputFile.WriteLine($"-----END {type}-----");
}
}

View File

@ -0,0 +1,45 @@
using ln.http;
using ln.http.client;
using HttpClient = ln.http.client.HttpClient;
using HttpMethod = ln.http.HttpMethod;
namespace ln.ai.assistant;
public class ChatCompletionProxy : HttpEndpointController
{
public Uri BackendUri { get; }
private HttpClient _httpClient = new HttpClient();
private string apiKey;
public ChatCompletionProxy(HttpRouter parentRouter, Uri backendUri):this(parentRouter, backendUri, null){}
public ChatCompletionProxy(HttpRouter parentRouter, Uri backendUri, string apiKey)
:base(parentRouter)
{
BackendUri = backendUri;
this.apiKey = apiKey;
}
[Map(HttpMethod.POST, "/v1/chat/completion")]
[Requires]
public HttpResponse ChatCompletion(HttpRequest request)
{
Uri uri = new Uri(BackendUri, request.RequestUri.PathAndQuery);
HeaderContainer backendHeaders = new HeaderContainer();
if (apiKey != null)
backendHeaders.Add("Authorization", $"Bearer { apiKey }");
var o = _httpClient.Request(request.Method, uri, backendHeaders, request.ContentStream);
if (o && o.Value is HttpClientResponse response)
{
HttpResponse httpResponse = new HttpResponse(response.StatusCode);
httpResponse.Content(response.ContentStream);
httpResponse.SetHeader("Content-Type", response.Headers.Get("Content-Type"));
return httpResponse;
}
return HttpResponse.BadGateway();
}
}

View File

@ -0,0 +1,77 @@
using LiteDB;
namespace ln.ai.assistant;
public class ChatRole
{
private static List<ChatRole> _roles = new List<ChatRole>();
private string _role;
private ChatRole(string role)
{
_role = role;
}
public readonly static ChatRole System = new ChatRole("system");
public readonly static ChatRole User = new ChatRole("user");
public readonly static ChatRole Assistant = new ChatRole("assistant");
public readonly static ChatRole Tool = new ChatRole("tool");
public override string ToString() => _role;
public static ChatRole from(string role)
{
foreach (var chatRole in _roles)
{
if (chatRole._role.Equals(role))
return chatRole;
}
return null;
}
static ChatRole()
{
BsonMapper.Global.RegisterType<ChatRole>
(
serialize: (chatRole) => chatRole.ToString(),
deserialize: (bson) => ChatRole.from(bson.AsString)
);
}
}
public class ChatMessage
{
public ChatRole role = ChatRole.User;
public string content = "";
}
public class Chat
{
public Guid id;
public string owner;
public List<ChatMessage> messages = new List<ChatMessage>();
}
public class ChatStorage
{
private LiteDatabase _liteDatabase;
private ILiteCollection<Chat> _chats;
public ChatStorage(string storagePath)
{
_liteDatabase = new LiteDatabase(storagePath);
_chats = _liteDatabase.GetCollection<Chat>();
_chats.EnsureIndex(c => c.owner);
}
public IEnumerable<Chat> GetUserChats(string username) => _chats.Find(c => c.owner.Equals(username));
public Chat GetUserChat(string username, Guid id) =>
_chats.FindOne(c => c.owner.Equals(username) && c.id.Equals(id));
public void AddChat(Chat chat) => _chats.Insert(chat);
public bool UpdateChat(Chat chat) => _chats.Update(chat);
public bool RemoveChat(Chat chat) => _chats.Delete(chat.id);
}

View File

@ -0,0 +1,61 @@
using ln.http;
using ln.json;
using ln.json.mapping;
using HttpMethod = ln.http.HttpMethod;
namespace ln.ai.assistant;
public class ChatStorageApi : HttpEndpointController
{
public ChatStorage _chatStorage;
public ChatStorageApi(ChatStorage chatStorage)
{
_chatStorage = chatStorage;
}
[Map(HttpMethod.GET,"/v1/chats")]
[Requires]
public HttpResponse GetUserChats(HttpPrincipal principal)
{
JSONValue jsonChats = JSONMapper.DefaultMapper.ToJson(
_chatStorage.GetUserChats(principal.UniqueId)
);
return HttpResponse.OK().Content(jsonChats);
}
[Map(HttpMethod.POST, "/v1/chats")]
[Requires]
public HttpResponse PostUserChat(HttpPrincipal principal, HttpRequest request,
[HttpArgumentSource(HttpArgumentSource.CONTENT)] JSONObject jsonChat)
{
Chat newChat = JSONMapper.DefaultMapper.FromJson<Chat>(jsonChat);
newChat.id = Guid.NewGuid();
newChat.owner = principal.UniqueId;
_chatStorage.AddChat(newChat);
return HttpResponse.SeeOther($"{ request.BaseUri }/v1/chats/{ newChat.id.ToString() }");
}
[Map(HttpMethod.GET, "/v1/chats/:id")]
[Requires]
public HttpResponse GetUserChat(HttpPrincipal principal, Guid id)
{
Chat chat = _chatStorage.GetUserChat(principal.UniqueId, id);
JSONValue jsonChat = JSONMapper.DefaultMapper.ToJson(chat);
return HttpResponse.OK().Content(jsonChat);
}
[Map(HttpMethod.POST, "/v1/chats/:id")]
[Requires]
public HttpResponse AddNewUserMessage(HttpPrincipal principal, Guid id,
[HttpArgumentSource(HttpArgumentSource.CONTENT)] ChatMessage chatMessage)
{
Chat chat = _chatStorage.GetUserChat(principal.UniqueId, id);
chat.messages.Add(chatMessage);
_chatStorage.UpdateChat(chat);
JSONValue jsonChat = JSONMapper.DefaultMapper.ToJson(chat);
return HttpResponse.OK().Content(jsonChat);
}
}

View File

@ -0,0 +1,114 @@
using LiteDB;
using ln.ai.assistant.api.openai;
using ln.ai.assistant.persist;
using ln.http;
using ln.json.mapping;
using HttpMethod = ln.http.HttpMethod;
namespace ln.ai.assistant;
public class CompletionApi : HttpEndpointController
{
private CompletionProvider _completionProvider;
public CompletionApi(HttpRouter parentRouter, CompletionProvider completionProvider) :base(parentRouter)
{
_completionProvider = completionProvider;
}
[Requires]
[Map(HttpMethod.POST, "/v1/chat/completions")]
public HttpResponse PostChatCompletion(HttpPrincipal principal,
[JsonBody] api.openai.ChatCompletionRequest chatCompletionRequest)
{
var completionTask = new CompletionTask<ChatCompletionResponse>(principal.UniqueId, () =>
{
var result = _completionProvider.ChatCompletion(chatCompletionRequest);
if (result)
return result.Value;
return result.Exception;
});
_cache.Add(completionTask);
return HttpResponse.SeeOther($"/v1/chat/completions/{completionTask.Id}");
}
[Requires]
[Map(HttpMethod.GET, "/v1/chat/completions/(?<completionTaskId>\\w+)$")]
public HttpResponse GetChatCompletion(
HttpPrincipal principal,
[Route] string completionTaskId,
[Query] bool wait = false)
{
if (_cache.TryGet(completionTaskId, out CompletionTask _completionTask) &&
_completionTask.Owner.Equals(principal.UniqueId))
{
if (!(_completionTask is CompletionTask<ChatCompletionResponse> completionTask))
return HttpResponse.InternalServerError().Content("_completionTask is not CompletionTask<ChatCompletionResponse>");
if ((completionTask.Result is null) && wait)
completionTask.Wait(TimeSpan.FromMinutes(1));
if (completionTask.Result is not null)
{
_cache.Remove(completionTaskId);
if (completionTask.Result)
return HttpResponse.OK().Json(completionTask.Result.Value);
else
return HttpResponse.InternalServerError().Content(completionTask.Result.Exception);
}
return HttpResponse.NoContent();
}
return HttpResponse.NotFound();
}
[Requires]
[Map(HttpMethod.POST, "/v1/completions")]
public HttpResponse PostCompletion(HttpPrincipal principal,
[JsonBody] api.openai.CompletionRequest completionRequest)
{
CompletionTask completionTask = new CompletionTask<CompletionResponse>(principal.UniqueId,
() => _completionProvider.Completion(completionRequest));
_cache.Add(completionTask);
return HttpResponse.SeeOther($"/v1/completions/{completionTask.Id}");
}
[Requires]
[Map(HttpMethod.GET, "/v1/completions/(?<completionTaskId>\\w+)$")]
public HttpResponse GetCompletion(
HttpPrincipal principal,
[Route] string completionTaskId,
[Query] bool wait = false)
{
if (_cache.TryGet(completionTaskId, out CompletionTask _completionTask) &&
_completionTask.Owner.Equals(principal.UniqueId))
{
if (!(_completionTask is CompletionTask<CompletionResponse> completionTask))
return HttpResponse.InternalServerError().Content("_completionTask is not CompletionTask<CompletionResponse>");
if ((completionTask.Result is null) && wait)
completionTask.Wait(TimeSpan.FromMinutes(1));
if (completionTask.Result is not null)
{
if (completionTask.Result)
return HttpResponse.OK().Json(completionTask.Result.Value);
else
return HttpResponse.InternalServerError().Content(completionTask.Result.Exception);
}
return HttpResponse.NoContent();
}
return HttpResponse.NotFound();
}
private TaskCache _cache = new TaskCache();
class TaskCache : Cache<string, CompletionTask>
{
public void Add(CompletionTask task) => Add(task.Id, task);
}
}

View File

@ -0,0 +1,12 @@
using System.Reflection;
using ln.ai.assistant.api.openai;
using ln.patterns;
namespace ln.ai.assistant;
public abstract class CompletionProvider
{
public CompletionProvider(){}
public abstract Optional<ChatCompletionResponse> ChatCompletion(ChatCompletionRequest chatCompletionRequest);
public abstract Optional<CompletionResponse> Completion(CompletionRequest chatCompletionRequest);
}

View File

@ -0,0 +1,60 @@
using ln.json;
using ln.patterns;
namespace ln.ai.assistant;
public class CompletionTask
{
public string Id { get; protected set; }
public string Owner { get; protected set; }
}
public class CompletionTask<T> : CompletionTask
{
public Optional<T>? _result;
public Optional<T>? Result => _result;
public CompletionTask(string owner, Func<Optional<T>> taskDelegate)
{
Id = GetRandomId();
Owner = owner;
ThreadPool.QueueUserWorkItem((o) =>
{
try
{
_result = taskDelegate();
if (_result is null)
_result = new NullReferenceException("taskDelegate returned null");
}
catch (Exception e)
{
_result = e;
}
finally
{
lock (this)
Monitor.PulseAll(this);
}
});
}
public bool Wait(TimeSpan timeout)
{
lock (this)
{
if (_result is null)
return Monitor.Wait(this, timeout);
return true;
}
}
static string GetRandomId()
{
byte[] b = new byte[32];
Random.Shared.NextBytes(b);
return Convert.ToHexString(b);
}
}

View File

@ -0,0 +1,96 @@
using ln.ai.assistant.auth;
using ln.ai.assistant.persist;
using ln.http;
using ln.json;
using HttpMethod = ln.http.HttpMethod;
namespace ln.ai.assistant;
public class IdentityApi : HttpEndpointController
{
private IdentityService _identityService;
private Cache<string, byte[]> _challenges = new Cache<string, byte[]>();
public IdentityApi(HttpRouter httpRouter, IdentityService identityService)
:base(httpRouter)
{
_identityService = identityService;
httpRouter.HttpFilters += httpFilter;
}
private void httpFilter(HttpRequestContext httprequestcontext)
{
var request = httprequestcontext.Request;
if (request.Headers.TryGetValue("Authorization", out string authorizationValue))
{
string[] authSplit = authorizationValue.Split(new char[]{ }, StringSplitOptions.RemoveEmptyEntries);
if ((authSplit.Length == 2) && authSplit[0].Equals("Token"))
{
byte[] token = Convert.FromBase64String(authSplit[1]);
if (_identityService.TryGetAuthenticationTokenIdentity(token, out LoginIdentity loginIdentity))
{
httprequestcontext.AuthenticatedPrincipal = new HttpPrincipal(
loginIdentity.Username,
loginIdentity.Username,
loginIdentity.Roles
);
}
}
}
}
[Map(HttpMethod.GET, "/v1/login")]
public HttpResponse Login([Query] string username = null)
{
if (username is null)
return HttpResponse.BadRequest();
byte[] salt = LoginIdentity.GenerateRandomBytes(128);
byte[] challenge = LoginIdentity.GenerateRandomBytes(128);
if (_identityService.TryGetLoginIdentity(username, out LoginIdentity loginIdentity))
salt = loginIdentity.Salt;
_challenges.Set(username, challenge);
JSONObject jsonResponse = new JSONObject()
{
["salt"] = Convert.ToBase64String(salt),
["challenge"] = Convert.ToBase64String(challenge),
["username"] = username
};
return HttpResponse.OK().Content(jsonResponse);
}
[Map(HttpMethod.GET, "/v1/authenticate")]
public HttpResponse Authenticate(
[Query] string username,
[Query] byte[] response)
{
if (_challenges.TryGet(username, out byte[] challenge))
{
if (_identityService.TryGetLoginIdentity(username, out LoginIdentity loginIdentity))
{
if (loginIdentity.ValidateResponse(loginIdentity.Salt, challenge, response))
{
var authenticationToken = _identityService.CreateAuthenticationToken(loginIdentity);
JSONObject jsonResult = new JSONObject()
{
["token"] = Convert.ToBase64String(authenticationToken.TokenValue),
["username"] = authenticationToken.LoginIdentity.Username
};
return HttpResponse.OK().Content(jsonResult);
}
}
return HttpResponse.Unauthorized();
}
else
return HttpResponse.NoContent();
}
[Requires]
[Map(HttpMethod.GET, "/v1/checkToken")]
public HttpResponse CheckToken() => HttpResponse.OK().Content("Token Valid");
}

View File

@ -0,0 +1,66 @@
using System.Reflection;
using System.Text;
using ln.ai.assistant.api.openai;
using ln.http;
using ln.patterns;
using Newtonsoft.Json;
using HttpClient = ln.http.client.HttpClient;
using HttpMethod = ln.http.HttpMethod;
namespace ln.ai.assistant;
public class OpenAiApiCompletionProvider : CompletionProvider
{
private Uri _baseUri;
private string? _apiKey;
private string? _model;
private HttpClient _httpClient = new HttpClient();
public OpenAiApiCompletionProvider(Uri baseUri, string? apiKey = null, string? model = null)
{
_baseUri = baseUri;
_apiKey = apiKey;
_model = model;
}
public override Optional<ChatCompletionResponse> ChatCompletion(ChatCompletionRequest chatCompletionRequest)
{
if (_model is string)
chatCompletionRequest.Model = _model;
Uri uri = new Uri(_baseUri, "/v1/chat/completions");
var r = _httpClient.Post<ChatCompletionRequest>(
uri,
headers: (_apiKey != null)
? new Header[] { new Header("Authorization", $"Bearer {_apiKey}") }
: Array.Empty<Header>(),
body: chatCompletionRequest
);
if (r)
{
return r.Value.Json<ChatCompletionResponse>();
}
return new IOException();
}
public override Optional<CompletionResponse> Completion(CompletionRequest completionRequest)
{
if (_model is string)
completionRequest.Model = _model;
Uri uri = new Uri(_baseUri, "/v1/completions");
var r = _httpClient.Post<CompletionRequest>(
uri,
headers: (_apiKey != null)
? new Header[] { new Header("Authorization", $"Bearer {_apiKey}") }
: Array.Empty<Header>(),
body: completionRequest
);
if (r)
{
return r.Value.Json<CompletionResponse>();
}
return new IOException();
}
}

View File

@ -0,0 +1,40 @@
using System.Resources;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ln.http;
namespace ln.ai.assistant;
public class SelfSigningCertificateStore : CertificateStore
{
public DirectoryInfo StorageDirectory { get; }
private CertificateAuthority _certificateAuthority;
public SelfSigningCertificateStore(string path)
{
_certificateAuthority = new CertificateAuthority(path, "ca.local");
StorageDirectory = new DirectoryInfo(path);
if (!StorageDirectory.Exists)
StorageDirectory.Create();
}
public override bool TryGetCertificate(string hostname, out X509Certificate certificate)
{
lock (this)
{
if (hostname.Equals(String.Empty))
hostname = "localhost";
if (!base.TryGetCertificate(hostname, out certificate))
{
if (!_certificateAuthority.GetOrCreateCertificate(hostname, out X509Certificate2? cert))
return false;
certificate = cert;
AddCertificate(cert);
}
return true;
}
}
}

View File

@ -0,0 +1,136 @@
using Newtonsoft.Json;
namespace ln.ai.assistant.api.openai
{
public struct ChatCompletionRequest
{
[JsonProperty("model",NullValueHandling = NullValueHandling.Ignore)]
public string Model;
[JsonProperty("messages")]
public List<Message> Messages;
[JsonProperty("max_tokens",NullValueHandling = NullValueHandling.Ignore)]
public int? MaxTokens;
[JsonProperty("temperature",NullValueHandling = NullValueHandling.Ignore)]
public float? Temperature;
[JsonProperty("top_p",NullValueHandling = NullValueHandling.Ignore)]
public float? TopP;
[JsonProperty("n",NullValueHandling = NullValueHandling.Ignore)]
public int? N;
[JsonProperty("stream",NullValueHandling = NullValueHandling.Ignore)]
public bool? Stream;
[JsonProperty("stop",NullValueHandling = NullValueHandling.Ignore)]
public List<string> Stop;
}
public struct ChatCompletionResponse
{
[JsonProperty("id")] public string Id;
[JsonProperty("object")] public string Object;
[JsonProperty("created")] public int Created;
[JsonProperty("model")] public string Model;
[JsonProperty("choices")] public List<ChatCompletionChoice> Choices;
[JsonProperty("usage")] public Usage Usage;
}
public struct ChatCompletionChoice
{
[JsonProperty("message")] public Message Message;
[JsonProperty("finish_reason")] public string FinishReason;
[JsonProperty("index")] public int Index;
}
public struct Message
{
[JsonProperty("role")] public string Role;
[JsonProperty("content")] public string Content;
}
public struct Usage
{
[JsonProperty("prompt_tokens")] public int PromptTokens;
[JsonProperty("completion_tokens")] public int CompletionTokens;
[JsonProperty("total_tokens")] public int TotalTokens;
}
public struct CompletionRequest
{
[JsonProperty("model")]
public string Model;
[JsonProperty("prompt")]
public string Prompt;
[JsonProperty("max_tokens")]
public int MaxTokens;
[JsonProperty("temperature")]
public float Temperature;
[JsonProperty("top_p")]
public float TopP;
[JsonProperty("n")]
public int N;
[JsonProperty("stream")]
public bool Stream;
[JsonProperty("logprobs")]
public int? Logprobs;
[JsonProperty("stop")]
public List<string> Stop;
}
public struct CompletionResponse
{
[JsonProperty("id")]
public string Id;
[JsonProperty("object")]
public string Object;
[JsonProperty("created")]
public int Created;
[JsonProperty("model")]
public string Model;
[JsonProperty("choices")]
public List<CompletionChoice> Choices;
[JsonProperty("usage")]
public Usage Usage;
}
public struct CompletionChoice
{
[JsonProperty("text")]
public string Text;
[JsonProperty("index")]
public int Index;
[JsonProperty("logprobs")]
public Dictionary<string, float> Logprobs;
[JsonProperty("finish_reason")]
public string FinishReason;
}
public struct TranscriptionRequest
{
[JsonProperty("model")]
private string Model;
}
}

View File

@ -0,0 +1,71 @@
using LiteDB;
using ln.ai.assistant.persist;
namespace ln.ai.assistant.auth;
public class AuthenticationToken
{
public LoginIdentity LoginIdentity { get; }
public byte[] TokenValue { get; }
public DateTime ValidUntil { get; }
public AuthenticationToken(LoginIdentity loginIdentity)
{
LoginIdentity = loginIdentity;
TokenValue = LoginIdentity.GenerateRandomBytes(32);
ValidUntil = DateTime.Now + TimeSpan.FromMinutes(10);
}
}
public class IdentityService
{
private LiteDatabase _liteDatabase;
private ILiteCollection<LoginIdentity> _identitiesCollection;
public IdentityService(string storagePath)
{
_liteDatabase = new LiteDatabase(storagePath);
_identitiesCollection = _liteDatabase.GetCollection<LoginIdentity>();
_identitiesCollection.EnsureIndex(identity => identity.Username);
}
public bool TryGetLoginIdentity(string username, out LoginIdentity loginIdentity)
{
loginIdentity = _identitiesCollection.FindById(username);
return loginIdentity is LoginIdentity;
}
public bool CreateLoginIdentity(string username) =>
CreateLoginIdentity(username, out LoginIdentity loginIdentity);
public bool CreateLoginIdentity(string username, out LoginIdentity loginIdentity)
{
loginIdentity = new LoginIdentity(username);
return _identitiesCollection.Insert(loginIdentity) is BsonValue;
}
public bool RemoveLoginIdentity(string username) => _identitiesCollection.Delete(username);
public bool UpdateLoginIdentity(LoginIdentity loginIdentity) => _identitiesCollection.Update(loginIdentity);
public IEnumerable<LoginIdentity> GetAllLoginIdentities() => _identitiesCollection.FindAll();
// AuthTokens
private Cache<string, AuthenticationToken> _authenticationTokens = new Cache<string, AuthenticationToken>(TimeSpan.FromMinutes(10));
public AuthenticationToken CreateAuthenticationToken(LoginIdentity loginIdentity)
{
AuthenticationToken authenticationToken = new AuthenticationToken(loginIdentity);
_authenticationTokens.Add(Convert.ToBase64String(authenticationToken.TokenValue), authenticationToken);
return authenticationToken;
}
public void RemoveAuthenticationToken(byte[] token) => _authenticationTokens.Remove(Convert.ToBase64String(token));
public bool TryGetAuthenticationTokenIdentity(byte[] token, out LoginIdentity loginIdentity)
{
if (_authenticationTokens.TryGet(Convert.ToBase64String(token), out AuthenticationToken authenticationToken))
{
loginIdentity = authenticationToken.LoginIdentity;
return true;
}
loginIdentity = null;
return false;
}
}

View File

@ -0,0 +1,98 @@
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using LiteDB;
namespace ln.ai.assistant.auth;
public class LoginIdentity
{
[BsonId]
public string Username { get; set; }
public byte[] HashedPassword { get; set; }
public byte[] Salt { get; set; }
public string[] Roles { get; set; } = Array.Empty<string>();
private LoginIdentity(){}
public LoginIdentity(string username, string password)
{
Username = username;
CreatePasswordHash(password);
}
public LoginIdentity(string username)
{
Username = username;
}
public LoginIdentity(string username, byte[] salt, byte[] hashedPassword)
{
Username = username;
Salt = salt;
HashedPassword = hashedPassword;
}
public void SetPassword(string password) => CreatePasswordHash(password);
private void CreatePasswordHash(string password)
{
using (var hmac = new HMACSHA512())
{
Salt = hmac.Key;
HashedPassword = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
}
}
public bool VerifyPassword(string password)
{
using (var hmac = new HMACSHA512(Salt))
{
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != HashedPassword[i])
return false;
}
return true;
}
}
public bool ValidateResponse(byte[] salt, byte[] challenge, byte[] response)
{
ArgumentNullException.ThrowIfNull(salt, nameof(salt));
ArgumentNullException.ThrowIfNull(challenge, nameof(challenge));
ArgumentNullException.ThrowIfNull(response, nameof(response));
var combined = new byte[challenge.Length * 2 + HashedPassword.Length];
Buffer.BlockCopy(challenge, 0, combined, 0, challenge.Length);
Buffer.BlockCopy(HashedPassword, 0, combined,challenge.Length, HashedPassword.Length);
Buffer.BlockCopy(challenge, 0, combined, challenge.Length + HashedPassword.Length, challenge.Length);
using (var hmac = new HMACSHA512(salt))
{
var computedHash = hmac.ComputeHash(combined);
if (response.Length != computedHash.Length)
return false;
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != response[i])
return false;
}
return true;
}
}
public static byte[] GenerateChallenge() => Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());
private static Random _random = new Random();
public static byte[] GenerateRandomBytes(int length)
{
byte[] randomBytes = new byte[length];
_random.NextBytes(randomBytes);
return randomBytes;
}
}

View File

@ -0,0 +1,44 @@
{
"ln.ai.assistant.auth.IdentityService": {
"set": {
"storagePath": "identities.db"
}
},
"ln.ai.assistant.AiAssistantServer": {
"set": {
"path": "/home/haraldwolff/src/www/ai.chat"
},
"select": {
"parentRouter": "ln.ai.assistant.AiAssistantServer"
}
},
"ln.ai.assistant.OpenAiApiCompletionProvider": {
"set": {
"baseUri": "http://10.96.2.20:8080"
},
"select": {
"parentRouter": "ln.ai.assistant.AiAssistantServer"
}
},
"ln.ai.assistant.IdentityApi":{
"select": {
"parentRouter": "ln.ai.assistant.AiAssistantServer"
}
},
"ln.ai.assistant.CompletionApi":{
"select": {
"parentRouter": "ln.ai.assistant.AiAssistantServer"
}
},
"ln.ai.assistant.SelfSigningCertificateStore": {
"set": {
"path": "certs"
}
},
"ln.http.TlsListener": {
"set": {
"port": 8080,
"bind": "::1"
}
}
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.19" />
<PackageReference Include="ln.bootstrap" Version="1.5.0-preview1" />
<PackageReference Include="ln.http" Version="0.9.10-preview0" />
<PackageReference Include="ln.json" Version="1.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<None Update="bootstrap.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\dotnet\ln.http\ln.http\ln.http.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
using ln.bootstrap;
using ln.http;
namespace ln.ai.assistant
{
public static class Program
{
static void Main(string[] arguments)
{
Bootstrap.Debug = true;
Bootstrap
.DefaultInstance
.Start(arguments);
}
}
}

View File

@ -0,0 +1,134 @@
namespace ln.ai.assistant.persist;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
public class Cache<TKey, TValue>
{
private readonly int _maxSize;
private readonly Dictionary<TKey, CachedItem> _cache;
private Timer _timer;
private TimeSpan _expirationTime;
public Cache() :this(TimeSpan.Zero, 0) { }
public Cache(int maxSize) :this(TimeSpan.Zero, maxSize) { }
public Cache(TimeSpan expirationTime) :this(expirationTime, 0) { }
public Cache(TimeSpan expirationTime, int maxSize)
{
_maxSize = maxSize;
_cache = new Dictionary<TKey, CachedItem>();
ExpirationTime = expirationTime;
}
public void Add(TKey key, TValue value)
{
lock (_cache)
{
if ((_maxSize > 0) && (_cache.Count >= _maxSize))
RemoveOldestItem();
_cache.Add(key, new CachedItem() { Value = value });
}
}
public void Set(TKey key, TValue value)
{
lock (_cache)
{
if ((_maxSize > 0) && (_cache.Count >= _maxSize))
RemoveOldestItem();
_cache[key] = new CachedItem() { Value = value };
}
}
public void Remove(TKey key)
{
lock (_cache)
_cache.Remove(key);
}
public TValue Get(TKey key)
{
lock (_cache)
{
if (_cache.TryGetValue(key, out var item))
{
item.LastAccessed = DateTime.Now;
return item.Value;
}
return default(TValue);
}
}
public bool TryGet(TKey key, out TValue value)
{
lock (_cache)
{
if (_cache.TryGetValue(key, out var item))
{
item.LastAccessed = DateTime.Now;
value = item.Value;
return true;
}
}
value = default(TValue);
return false;
}
private void CleanUp(object state)
{
lock (_cache)
{
var expiredItems = _cache.Where(item => DateTime.Now - item.Value.LastAccessed > _expirationTime).ToList();
foreach (var item in expiredItems)
{
_cache.Remove(item.Key);
}
}
}
private void RemoveOldestItem()
{
var oldestItem = _cache.OrderBy(item => item.Value.LastAccessed).First();
_cache.Remove(oldestItem.Key);
}
private class CachedItem
{
public TValue Value { get; set; }
public DateTime LastAccessed { get; set; } = DateTime.Now;
}
public TimeSpan ExpirationTime
{
get => _expirationTime;
set
{
if (_timer is Timer)
{
if (value == TimeSpan.Zero)
{
_timer.Dispose();
_timer = null;
}
else
{
_timer.Change(value, value);
}
} else if (value != TimeSpan.Zero)
{
_timer = new Timer(CleanUp, null, value, value);
}
_expirationTime = value;
}
}
}