diff --git a/AICore/AICore.csproj b/AICore/AICore.csproj index 69e0c021d..07a0c5f6a 100644 --- a/AICore/AICore.csproj +++ b/AICore/AICore.csproj @@ -39,6 +39,7 @@ ..\packages\System.IO.Pipelines.9.0.2\lib\net462\System.IO.Pipelines.dll + ..\packages\System.Text.Encodings.Web.9.0.2\lib\net462\System.Text.Encodings.Web.dll @@ -92,6 +93,10 @@ + + + + @@ -111,6 +116,10 @@ {B0D73E3D-4AE7-4024-93A6-DB1F46D7CCEE} Data + + {4B2F3959-3CDA-4514-83A2-FA3C24E57BB7} + ApiFacade + {EC2349FB-7FE0-4AD1-B28B-A7A27AF80A57} ServiceUtils diff --git a/AICore/Context/SummaryParser/CsvContextSummaryParser.cs b/AICore/Context/SummaryParser/CsvContextSummaryParser.cs index 874332c57..9a7a900e3 100644 --- a/AICore/Context/SummaryParser/CsvContextSummaryParser.cs +++ b/AICore/Context/SummaryParser/CsvContextSummaryParser.cs @@ -15,11 +15,12 @@ namespace AICore.Context.SummaryParser public class CsvContextSummaryParser : BaseContextSummaryParser { private string _Delimiter; + private bool _StrictEscapeCsvField; - public CsvContextSummaryParser() : this(",") { } - public CsvContextSummaryParser(string delimiter) + public CsvContextSummaryParser(string delimiter = ",", bool strictEscapeCsvField = false) { _Delimiter = delimiter; + _StrictEscapeCsvField = strictEscapeCsvField; } public override string Parse(AiContextType uicontext, BaseContextSummary context) @@ -111,14 +112,21 @@ namespace AICore.Context.SummaryParser if (string.IsNullOrEmpty(field)) return string.Empty; - // Wenn das Feld Kommas, Anführungszeichen oder Zeilumbrüche enthält, - // muss es in Anführungszeichen gesetzt werden - if (field.Contains(_Delimiter) || field.Contains("\"") || field.Contains("\n") || field.Contains("\r")) + if (_StrictEscapeCsvField) { - // Anführungszeichen im Feld verdoppeln - field = field.Replace("\"", "\"\""); return $"\"{field}\""; } + else + { + // Wenn das Feld Kommas, Anführungszeichen oder Zeilumbrüche enthält, + // muss es in Anführungszeichen gesetzt werden + if (field.Contains(_Delimiter) || field.Contains("\"") || field.Contains("\n") || field.Contains("\r")) + { + // Anführungszeichen im Feld verdoppeln + field = field.Replace("\"", "\"\""); + return $"\"{field}\""; + } + } return field; } diff --git a/Service/Api/ErrorExtractor/DefaultErrorExtractor.cs b/AICore/Facade/DefaultErrorExtractor.cs similarity index 92% rename from Service/Api/ErrorExtractor/DefaultErrorExtractor.cs rename to AICore/Facade/DefaultErrorExtractor.cs index 1a2e8ab4e..812dc5b39 100644 --- a/Service/Api/ErrorExtractor/DefaultErrorExtractor.cs +++ b/AICore/Facade/DefaultErrorExtractor.cs @@ -6,7 +6,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; -namespace BeWo.Service.Api.ErrorExtractor +namespace AICore.Facade { public class DefaultErrorExtractor : IApiErrorExtractor { diff --git a/AICore/Facade/LLM/BaseLLMApiClient.cs b/AICore/Facade/LLM/BaseLLMApiClient.cs new file mode 100644 index 000000000..970c4240c --- /dev/null +++ b/AICore/Facade/LLM/BaseLLMApiClient.cs @@ -0,0 +1,30 @@ +using BeWo.Data.Entities; +using BeWo.ServiceUtils.Core; +using BS.Shared.DataContracts.Feature.AI; +using BS.Shared.DataContracts; +using BS.Shared.Interface; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Text.Json; + +namespace AICore.Facade.LLM +{ + public abstract class BaseLLMApiClient + { + public string BaseUrl { get; set; } + public string ApiKey { get; set; } + + protected BaseLLMApiClient(string base_url, string api_key) + { + base_url = BaseUrl; + ApiKey = api_key; + } + + public abstract ApiResponse> GetAiModelle(); + public abstract ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration); + } +} diff --git a/AICore/Facade/LLM/OllamaApiClient.cs b/AICore/Facade/LLM/OllamaApiClient.cs new file mode 100644 index 000000000..5fc33c845 --- /dev/null +++ b/AICore/Facade/LLM/OllamaApiClient.cs @@ -0,0 +1,124 @@ +using BeWo.Data.Entities; +using BS.Shared.DataContracts; +using BS.Shared.DataContracts.Feature.AI; +using BS.Shared.Extensions; +using BS.Shared.Interface; +using System.Collections.Generic; +using System; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using BeWo.Server.ApiFacade.Core; + +namespace AICore.Facade.LLM +{ + public class OllamaApiClient : BaseLLMApiClient + { + private protected readonly HttpClientFacade _GetClientFacade; + private protected readonly JsonHttpClientFacade _PostClientFacade; + private protected readonly IApiErrorExtractor _ErrorExtractor; + + public OllamaApiClient(string base_url, string api_key) : base(base_url, api_key) + { + var authorization = $"Bearer {api_key}"; + _GetClientFacade = new HttpClientFacade(base_url); + _GetClientFacade.SetToken(authorization); + _PostClientFacade = new JsonHttpClientFacade(base_url); + _PostClientFacade.SetToken(authorization); + _PostClientFacade.Timeout = TimeSpan.FromSeconds(300); + _ErrorExtractor = new DefaultErrorExtractor(); + + try + { + var addresses = Dns.GetHostAddresses("w2.ownchat.de"); + foreach (var addr in addresses) + Console.WriteLine(addr); + } + catch (SocketException ex) + { + Console.WriteLine($"DNS resolution failed: {ex.Message}"); + } + } + + public override ApiResponse> GetAiModelle() + { + var models = new List(); + + var response_format = new + { + models = new[] + { + new + { + name = "", + model = "" + } + + } + }; + + var response = _GetClientFacade.GetAnonymousTypeAsync("api/ollama/models", response_format, _ErrorExtractor).GetAwaiter().GetResult(); + + if (!response.Success) + { + throw new NotImplementedException(); + } + + var data = response.Data.models.Select(x => new AiModel(x.name, 0)); + + return ApiResponse>.SuccessResponse(data); + } + + public override ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration) + { + message = null; + duration = null; + model = null; + + _PostClientFacade.JsonObject = payload; + + var response_format = new + { + id = string.Empty, + model = string.Empty, + message = new + { + content = string.Empty, + role = string.Empty + }, + done_reason = "stop", + done = true, + total_duration = 1316754573L, + load_duration = 31739136L, + prompt_eval_count = 22L, + prompt_eval_duration = 2132887L, + eval_count = 66L, + eval_duration = 1282438235L + }; + + var response = _PostClientFacade.GetAnonymousTypeAsync("api/ollama/chat/completions", response_format, _ErrorExtractor).GetAwaiter().GetResult(); + + if (!response.Success) + return response.Copy(); + + var json = response.GetResponseData(); + + if (json.message is object && !string.IsNullOrEmpty(json.message.content)) + { + var total_ns = json.total_duration; + var total_ms = total_ns / 1000000; + + duration = (int)total_ms; + model = json.model; + + var assi_msg = BS.Shared.Core.Utils.ConvertToISO88591(json.message.content); + + message = assi_msg; + + return ApiResponse.SuccessResponse(assi_msg); + } + + return null; + } + } +} diff --git a/Service/ServiceProxy/OpenWebApiClient.cs b/AICore/Facade/LLM/OpenWebApiClient.cs similarity index 55% rename from Service/ServiceProxy/OpenWebApiClient.cs rename to AICore/Facade/LLM/OpenWebApiClient.cs index 9fad4453d..2778472e3 100644 --- a/Service/ServiceProxy/OpenWebApiClient.cs +++ b/AICore/Facade/LLM/OpenWebApiClient.cs @@ -1,36 +1,23 @@ using BeWo.Data.Entities; -using BeWo.Service.Api.ErrorExtractor; -using BeWo.Service.Core; -using BeWo.Service.DCEntityMapper; -using BS.Shared.Core; -using BS.Shared.Core.Facade; +using BeWo.Server.ApiFacade.Core; using BS.Shared.DataContracts; using BS.Shared.DataContracts.Feature.AI; -using BS.Shared.Exceptions; using BS.Shared.Extensions; using BS.Shared.Interface; -using DevExpress.DataProcessing.InMemoryDataProcessor; -using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Configuration; using System.IO; using System.Linq; -using System.Net; -using System.Text; -using System.Text.Json.Nodes; -using System.Threading.Tasks; -using static DevExpress.Xpo.Helpers.AssociatedCollectionCriteriaHelper; +using System.Text.Json; -namespace BeWo.Service.ServiceProxy +namespace AICore.Facade.LLM { - public class OpenWebApiClient + public class OpenWebApiClient : BaseLLMApiClient { - private readonly HttpClientFacade _GetClientFacade; - private readonly JsonHttpClientFacade _PostClientFacade; - private readonly IApiErrorExtractor _ErrorExtractor; + private protected readonly HttpClientFacade _GetClientFacade; + private protected readonly JsonHttpClientFacade _PostClientFacade; + private protected readonly IApiErrorExtractor _ErrorExtractor; - public OpenWebApiClient() : this(MergedConfig.GetSetting("OpenWebUIUrl"), MergedConfig.GetSetting("OpenWebUIKey")) { } public OpenWebApiClient(string base_url, string api_key) { var authorization = $"Bearer {api_key}"; @@ -42,13 +29,21 @@ namespace BeWo.Service.ServiceProxy _ErrorExtractor = new DefaultErrorExtractor(); } - public ApiResponse> GetAiModelle() + public override ApiResponse> GetAiModelle() { var models = new List(); var response_format = new { - data = models + data = new[] + { + new + { + id = "", + name = "" + } + + } }; var response = _GetClientFacade.GetAnonymousTypeAsync("api/models", response_format, _ErrorExtractor).GetAwaiter().GetResult(); @@ -58,18 +53,18 @@ namespace BeWo.Service.ServiceProxy throw new NotImplementedException(); } - var data = MapperFactory.AiModel.MapToNewEntities(response.Data.data); + var data = response.Data.data.Select(x => new AiModel(x.name, 0)); return ApiResponse>.SuccessResponse(data); } - public ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration) + public override ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration) { message = null; duration = null; model = null; - var sub_url = "api/chat/completions"; + _PostClientFacade.JsonObject = payload; var response_format = new { @@ -94,20 +89,7 @@ namespace BeWo.Service.ServiceProxy } }; - _PostClientFacade.JsonObject = payload; - - if (MergedConfig.GetSetting("AiServiceLastRequestEnabled") is string str && bool.TryParse(str, out bool b) && b) - { - var path = MergedConfig.GetSetting("AiServiceLastRequestFilePath"); - if (Directory.GetParent(path) == null) - throw new ArgumentException("AiServiceLastRequestFilePath ungültig"); - - var payload_ser = JsonConvert.SerializeObject(_PostClientFacade.JsonObject, Formatting.Indented); - - File.WriteAllText(path, payload_ser); - } - - var response = _PostClientFacade.GetAnonymousTypeAsync(sub_url, response_format, _ErrorExtractor).GetAwaiter().GetResult(); + var response = _PostClientFacade.GetAnonymousTypeAsync("api/chat/completions", response_format, _ErrorExtractor).GetAwaiter().GetResult(); if (!response.Success) return response.Copy(); @@ -120,7 +102,7 @@ namespace BeWo.Service.ServiceProxy { var total_ns = json.usage.total_duration; var total_ms = total_ns / 1000000; - + duration = (int)total_ms; model = json.model; diff --git a/AICore/Prompt/Factories/AiPromptFactory.cs b/AICore/Prompt/Factories/AiPromptFactory.cs index 13eef7e58..cc79e3ab7 100644 --- a/AICore/Prompt/Factories/AiPromptFactory.cs +++ b/AICore/Prompt/Factories/AiPromptFactory.cs @@ -60,6 +60,10 @@ namespace AICore.Prompt.Factories return new CsvContextSummaryParser(); case AiDataContextType.tsv: return new CsvContextSummaryParser("\t"); + case AiDataContextType.csv2: + return new CsvContextSummaryParser(";", true); + case AiDataContextType.tsv2: + return new CsvContextSummaryParser("\t", true); default: break; } diff --git a/AiCoreUnitTest/AiCoreUnitTest.csproj b/AiCoreUnitTest/AiCoreUnitTest.csproj new file mode 100644 index 000000000..0723a5f99 --- /dev/null +++ b/AiCoreUnitTest/AiCoreUnitTest.csproj @@ -0,0 +1,83 @@ + + + + + + Debug + AnyCPU + {68C1F8EA-7828-4E46-8F04-91105C55860E} + Library + Properties + AiCoreUnitTest + AiCoreUnitTest + v4.7.2 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + + + + + + + + + + + + + {69D70CA9-0DF9-419E-BFAF-F4539F129BD9} + AICore + + + {B0D73E3D-4AE7-4024-93A6-DB1F46D7CCEE} + Data + + + {2F50B83D-A3F0-4EC4-979A-3F9B7E3D8ED4} + Shared + + + + + + + Dieses Projekt verweist auf mindestens ein NuGet-Paket, das auf diesem Computer fehlt. Verwenden Sie die Wiederherstellung von NuGet-Paketen, um die fehlenden Dateien herunterzuladen. Weitere Informationen finden Sie unter "http://go.microsoft.com/fwlink/?LinkID=322105". Die fehlende Datei ist "{0}". + + + + + + \ No newline at end of file diff --git a/AiCoreUnitTest/OllamaFacadeTest.cs b/AiCoreUnitTest/OllamaFacadeTest.cs new file mode 100644 index 000000000..761d8758e --- /dev/null +++ b/AiCoreUnitTest/OllamaFacadeTest.cs @@ -0,0 +1,34 @@ +using AICore.Facade.LLM; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Net; + +namespace AiCoreUnitTest +{ + [TestClass] + public class OllamaFacadeTest + { + public string OllamaUrl { get; set; } + public string OllamaKey { get; set; } + + [TestInitialize] + public void TestInitialize() + { + OllamaUrl = "https://w2.owchat.de"; + OllamaKey = "mu9ZPuUXJ4aVGLU1WL2RfPCeSMRmJREwyjVJBP7bHfSz8quZRqN7e7UwnwCDi8GZ"; + } + + public OllamaApiClient GetOllamaApiClient() + => new OllamaApiClient(OllamaUrl, OllamaKey); + + [TestMethod] + public void GetModels() + { + var client = GetOllamaApiClient(); + + var models = client.GetAiModelle(); + + Assert.IsNotNull(models); + } + } +} diff --git a/AiCoreUnitTest/OpenWebUIFacadeTest.cs b/AiCoreUnitTest/OpenWebUIFacadeTest.cs new file mode 100644 index 000000000..9b3c67f70 --- /dev/null +++ b/AiCoreUnitTest/OpenWebUIFacadeTest.cs @@ -0,0 +1,34 @@ +using AICore.Facade.LLM; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Net; + +namespace AiCoreUnitTest +{ + [TestClass] + public class OpenWebUIFacadeTest + { + public string OpenWebUIUrl { get; set; } + public string OpenWebUIKey { get; set; } + + [TestInitialize] + public void TestInitialize() + { + OpenWebUIUrl = "https://owui1.ownsoft.de/"; + OpenWebUIKey = "sk-7f5d8c418630448dbe31ab1c7e8b100a"; + } + + public OpenWebApiClient GetOpenWebApiClient() + => new OpenWebApiClient(OpenWebUIUrl, OpenWebUIKey); + + [TestMethod] + public void GetModels() + { + var client = GetOpenWebApiClient(); + + var models = client.GetAiModelle(); + + Assert.IsNotNull(models); + } + } +} diff --git a/AiCoreUnitTest/Properties/AssemblyInfo.cs b/AiCoreUnitTest/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e6db92c1a --- /dev/null +++ b/AiCoreUnitTest/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("AiCoreUnitTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AiCoreUnitTest")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("68c1f8ea-7828-4e46-8f04-91105c55860e")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/AiCoreUnitTest/packages.config b/AiCoreUnitTest/packages.config new file mode 100644 index 000000000..36cbc3e9e --- /dev/null +++ b/AiCoreUnitTest/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/BeWo/BeWoApp.xaml.cs b/BeWo/BeWoApp.xaml.cs index 74bf1e253..dca813ca8 100644 --- a/BeWo/BeWoApp.xaml.cs +++ b/BeWo/BeWoApp.xaml.cs @@ -59,7 +59,7 @@ namespace BeWo public static string ShortVersion = "3.26"; - public static string Version = "Version 3.26 - 1.2 Test"; + public static string Version = "Version 3.26 - 1.2.1 Test"; public static int RenderTier = 0; public static bool IsInDesignMode = DesignerProperties.GetIsInDesignMode(new DependencyObject()); diff --git a/BeWo/View/Detail/AI/AiConfigView.xaml b/BeWo/View/Detail/AI/AiConfigView.xaml index 03b7f3b1a..bb8c9ab97 100644 --- a/BeWo/View/Detail/AI/AiConfigView.xaml +++ b/BeWo/View/Detail/AI/AiConfigView.xaml @@ -6,11 +6,11 @@ xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors" xmlns:listviewmodel="clr-namespace:BeWo.ViewModel.ListViewModel" xmlns:local="clr-namespace:BeWo.View.Detail.AI" - xmlns:shared="clr-namespace:BS.Shared;assembly=BS.Shared" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:shared="clr-namespace:BS.Shared;assembly=BS.Shared" xmlns:vmv="clr-namespace:BeWo.ViewModel.View" d:DataContext="{d:DesignInstance Type=vmv:AiConversationChatViewModel, - IsDesignTimeCreatable=True}" + IsDesignTimeCreatable=True}" mc:Ignorable="d"> @@ -25,7 +25,7 @@ -