diff --git a/README.md b/README.md index 5c93c11..1d8f32a 100644 Binary files a/README.md and b/README.md differ diff --git a/Server/Fracture.Server.csproj b/Server/Fracture.Server.csproj index b40c582..63a99b9 100644 --- a/Server/Fracture.Server.csproj +++ b/Server/Fracture.Server.csproj @@ -20,6 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Server/Modules/AI/Models/AIBackendConfiguration.cs b/Server/Modules/AI/Models/AIBackendConfiguration.cs new file mode 100644 index 0000000..a91ac56 --- /dev/null +++ b/Server/Modules/AI/Models/AIBackendConfiguration.cs @@ -0,0 +1,23 @@ +namespace Fracture.Server.Modules.AI.Models; + +/// +/// The config for the AI provider's endpoint. +/// +public class AIBackendConfiguration +{ + /// + /// The OpenAI-compatible endpoint's URL. If null, the default one (OpenAI) + /// will be used. + /// + public string? EndpointUrl { get; set; } + + /// + /// The API key to be used during the communication with the AI backend. + /// + public string? ApiKey { get; set; } + + /// + /// Name of the model to be used (e.g. "chatgpt-3.5-turbo" or "mistral") + /// + public required string Model { get; set; } +} diff --git a/Server/Modules/AI/Models/AIGenerationContext.cs b/Server/Modules/AI/Models/AIGenerationContext.cs new file mode 100644 index 0000000..8a41967 --- /dev/null +++ b/Server/Modules/AI/Models/AIGenerationContext.cs @@ -0,0 +1,51 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Fracture.Server.Modules.AI.Models; + +/// +/// The neccessary thing for generating AI responses. +/// +public class AIGenerationContext +{ + /// + /// The prompt for generation. + /// + public required string Prompt { get; init; } + + /// + /// The name of the used model, if null the default one will be used + /// + public string? Model { get; init; } + + /// + /// The stop tokens. + /// Tokens, that will stop the further generation of the response, + /// typically used to limit the response for example to not include the + /// predicted next parts of the conversation. + /// + /// + public string[]? StopTokens { get; init; } + + /// + /// The maximum count of the tokens predicted. + /// The number of the tokens predicted plus the number of the + /// tokens in prompt must not exceed the model's context length. + /// + public int? MaxTokens { get; set; } = 128; + + /// + /// Temperature, controls the randomness of the model + /// It should be between 0.0 and 2.0, higher values like 0.8 will + /// make the output more random, while the lower values (e.g. 0.2) will + /// make it more deterministic. + /// + public double Temperature { get; set; } = 0.7; + + /// + /// Nucleus sampling, controls probability of the tokens + /// 0.1 will mean that only tokens from the top of 10% of + /// probability are considered. Generally it is recommended to alter + /// this or temperature, but not both. + /// + public double? TopP { get; set; } +} diff --git a/Server/Modules/AI/Services/IAIInstructionProvider.cs b/Server/Modules/AI/Services/IAIInstructionProvider.cs new file mode 100644 index 0000000..d1023c8 --- /dev/null +++ b/Server/Modules/AI/Services/IAIInstructionProvider.cs @@ -0,0 +1,24 @@ +using Fracture.Server.Modules.AI.Models; + +namespace Fracture.Server.Modules.AI.Services; + +/// +/// Provides the ability to generate the response to a prompt or instruction +/// +public interface IAIInstructionProvider +{ + /// + /// Generates the response to an instruction (for instruction-following models) + /// + /// The actual instruction for the model to follow. + /// The resulting generated response to said instruction. + Task GenerateInstructionResponse(string instruction); + + /// + /// Generates the response for a generation context, which is including prompt + /// and more parameters. + /// + /// Generation context with prompt and generation parameters + /// The resulting generated response to the given context. + Task GenerateResponse(AIGenerationContext context); +} diff --git a/Server/Modules/AI/Services/OpenAICompatibleInstructionProvider.cs b/Server/Modules/AI/Services/OpenAICompatibleInstructionProvider.cs new file mode 100644 index 0000000..22dc23d --- /dev/null +++ b/Server/Modules/AI/Services/OpenAICompatibleInstructionProvider.cs @@ -0,0 +1,59 @@ +using Fracture.Server.Modules.AI.Models; +using Microsoft.Extensions.Options; +using OpenAI; +using OpenAI.Chat; + +namespace Fracture.Server.Modules.AI.Services; + +public class OpenAICompatibleInstructionProvider : IAIInstructionProvider +{ + private readonly OpenAIClient _api; + private readonly AIBackendConfiguration _configuration; + + public OpenAICompatibleInstructionProvider(IOptions configuration) + { + _configuration = configuration.Value; + + if (_configuration.EndpointUrl is null) + { + ArgumentException.ThrowIfNullOrEmpty(nameof(_configuration.ApiKey)); + + _api = new OpenAIClient(new OpenAIAuthentication(_configuration.ApiKey)); + } + else + { + var settings = new OpenAIClientSettings(domain: _configuration.EndpointUrl); + + if (_configuration.ApiKey is null) + { + _api = new OpenAIClient(clientSettings: settings); + } + else + { + _api = new OpenAIClient(new OpenAIAuthentication(_configuration.ApiKey), settings); + } + } + } + + public async Task GenerateInstructionResponse(string instruction) + { + return await GenerateResponse(new() { Prompt = instruction }); + } + + public async Task GenerateResponse(AIGenerationContext context) + { + var messages = new List { new Message(Role.User, context.Prompt) }; + + var chatRequest = new ChatRequest( + messages, + model: context.Model ?? _configuration.Model, + temperature: context.Temperature, + stops: context.StopTokens, + maxTokens: context.MaxTokens, + topP: context.TopP + ); + var response = await _api.ChatEndpoint.GetCompletionAsync(chatRequest); + + return response.FirstChoice; + } +} diff --git a/Server/Program.cs b/Server/Program.cs index 817fae2..0c59c7e 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -1,4 +1,6 @@ using Fracture.Server.Components; +using Fracture.Server.Modules.AI.Models; +using Fracture.Server.Modules.AI.Services; using Fracture.Server.Modules.Database; using Fracture.Server.Modules.Items.Models; using Fracture.Server.Modules.Items.Services; @@ -12,11 +14,14 @@ // Add services to the container. builder.Services.Configure(builder.Configuration.GetSection("NameGenerator")); +builder.Services.Configure(builder.Configuration.GetSection("AiBackend")); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/docs/ai.md b/docs/ai.md index c3e7015..c6771d7 100644 --- a/docs/ai.md +++ b/docs/ai.md @@ -1,84 +1,47 @@ # Backend AI Projekt komunikuje się z modelem językowym na backendzie, w tym momencie -dostosowany jest do serwera REST uruchamianego przez -[llama.cpp](https://github.com/ggerganov/llama.cpp) (aplikacja `server`), oraz -testowana była na modelu Mistral-7B i jego pochodnych, w szczególności -[Mistral-RP-0.1-7B-GGUF](https://huggingface.co/Undi95/Mistral-RP-0.1-7B-GGUF?not-for-all-audiences=true). +dostosowany jest do serwerów, które oferują API podobne do tego, które oferuje +OpenAI, więc można wykorzystać zarówno oficjalny system OpenAI, jak i produkty w +rodzaju vLLM, serwera llama.cpp lub [ollama](https://ollama.com/). -Aby uruchomić serwer modelu językowego, należy pobrać (lub skompilować) -llama.cpp, pobrać plik modelu z serwera huggingface.co i uruchomić serwer -wydając polecenie, na przykład: +## ollama -```bash -./server -m -ngl 35 --host 127.0.0.1 -``` - -(tutaj następuje przeniesienie 35 warstw modelu na urządzenie CUDA, wymagana -jest odmiana llama.cpp z obsługą CUDA, w przeciwnym wypadku parametr `-ngl` nie -jest dostępny) - -Pełna dokumentacja serwera: - - -## docker - -Można też uruchomić Backend AI llama.cpp w oparciu o plik -`docker-compose-;lamacpp.yml`. W tym celu należy w pliku `.env`, w którym -znajduje się konfiguracja ustawień lokalnej bazy danych dodać kolejne dwie -zmienne definiujące wariant serwera oraz ścieżkę lokalną do pliku modelu, np.: - -```env -LLAMA_VARIANT=full -MODEL_PATH=U:\ml\krakowiak-7b.gguf.q4_k_m.bin -``` - -Dostępne warianty serwera to: `full` (CPU), `full-cuda` (NVIDIA GPU) i -`full-rocm` (AMD ROCm GPU). - -Teraz, zamiast wydawać polecenie `docker compose up` tak jak zwykle, możesz -wydać komendę: - -```sh -docker compose -f docker-compose-llamacpp.yml up -``` - -Co uruchomi zarówno serwer modelu językowego, jak i bazy danych niezbędne -aplikacji głównej. +[Ollama](https://ollama.com/) to najłatwiejszy sposób na uruchomienie modelu na +własnym komputerze - wystarczy zainstalować oprogramowanie, wydać komendę +`ollama pull mistral` i `ollama serve` aby uruchomić kompatybilny z OpenAI +serwer na własnym komputerze, który będzie mogł uruchamiać popularny i mało +wymagający model Mistral-7B. Ollama dostępna jest zarówno dla Windows, Linuksa, +jak i macOS i dostosowana jest zarówno do wykorzystania CUDA na kartach NVIDII, +jak i ROCm na kartach graficznych AMD (lub pozwala również skorzystać tylko z +procesora). ## Konfiguracja backendu AI w aplikacji -Konfiguracja backendu AI jest oparta o plik sekretów (`secrets.json`). +Konfiguracja backendu AI jest oparta o plik sekretów (`secrets.json`), aby klucz +API (potencjalnie bardzo wrażliwa informacja) nie "wyciekł", a także, aby każdy +programista mógł korzystać z własnej konfiguracji AI. + [Przeczytaj dokumentację.](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-8.0&tabs=linux). -Sekrety aplikacji definiują URL serwera llama.cpp, na przykład: +Sekrety aplikacji definiują URL serwera i domyślny model, na przykład: ```json -"AiEndpoint": { - "EndpointUrl": "http://127.0.0.1:8080/completion" +"AiBackend": { + "EndpointUrl": "http://127.0.0.1:11434", + "Model": "mistral" } ``` Opcjonalnie można również dostarczyć klucz API wykorzystywany do komunikacji: ```json -{ - "AiEndpoint": { - "EndpointUrl": "http://127.0.0.1:8080/completion", - "ApiKey": "this-is-secret" - } +"AiBackend": { + "EndpointUrl": "http://127.0.0.1:11434", + "Model": "mistral", + "ApiKey": "this is a secret" } ``` URL i klucz API serwera uczelnianego są na Discordzie. - -Niezbędne jest również wybranie modułów odpowiedzialnych za komunikację z -backendem AI i przygotowaniem promptów, które to należy wybrać jako pełne nazwy -typów, włącznie z ich _assembly_, dla Mistral-7B należy wybrać -`AlpacaPromptProvider`: - -```json -"AiBackendProvider": "Fracture.Shared.External.Providers.Ai.LlamaCpp.LlamaCppBackendProvider, Fracture.Shared.External, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", -"AiPromptTemplateProvider": "Fracture.Shared.External.Providers.Ai.AlpacaPromptProvider, Fracture.Shared.External, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", -```