From eeacc084729b272b7605af2bb52a3fc0fd092d16 Mon Sep 17 00:00:00 2001 From: Rene Evertz Date: Fri, 24 Jul 2026 12:21:11 +0200 Subject: [PATCH] =?UTF-8?q?Zus=C3=A4tzliche=20Checkups=20eingebunden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AiCoreUnitTest/Ollama2FacadeTest.cs | 16 ++---- AiCoreUnitTest/OllamaFacadeTest.cs | 16 +----- Server/Components/AICore/AICore.csproj | 1 + .../AICore/Core/ContextSizeAnalyzer.cs | 22 ++++--- .../AICore/Facade/LLM/BaseLLMApiClient.cs | 2 +- .../AICore/Facade/LLM/LLMResponse.cs | 28 +++++++++ .../AICore/Facade/LLM/OllamaApiClient.cs | 24 +++----- .../AICore/Facade/LLM/OpenWebApiClient.cs | 14 ++--- Service/Plugins/AiService2.cs | 56 ++++++++++-------- Service/Status/AiStatusService.cs | 57 +++++++++++++++++++ .../BeWoInvalidOperationException.cs | 4 ++ Shared/Shared.csproj | 1 + Shared/Tests/Objects/AiTestObjects.cs | 29 ++++++++++ 13 files changed, 191 insertions(+), 79 deletions(-) create mode 100644 Server/Components/AICore/Facade/LLM/LLMResponse.cs create mode 100644 Shared/Tests/Objects/AiTestObjects.cs diff --git a/AiCoreUnitTest/Ollama2FacadeTest.cs b/AiCoreUnitTest/Ollama2FacadeTest.cs index a5fa2d403..7a59ec042 100644 --- a/AiCoreUnitTest/Ollama2FacadeTest.cs +++ b/AiCoreUnitTest/Ollama2FacadeTest.cs @@ -1,5 +1,7 @@ using AICore.Facade.LLM; +using BeWo.Data.Security; using BeWo.Service.Core; +using BS.Shared.Tests.Objects; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace AiCoreUnitTest @@ -32,19 +34,9 @@ namespace AiCoreUnitTest var logger = ServiceLogger.GetRequestLogger(out string id); // Build Payload - var payload = new - { - model = "gemma3:12b", - messages = new[] { - new { - role = "user", - message = "why is the sky blue?" - } - }, - requestid = id - }; + var payload = AiTestObjects.GetExampleSendRequest(UserRightHelper.GetTenant(), id); - var response = Client.SendAiMessageRequest(payload, out string message, out string model, out int? duration, logger); + var response = Client.SendAiMessageRequest(payload, out LLMResponse llm_response, logger); Assert.IsTrue(response?.Success); } diff --git a/AiCoreUnitTest/OllamaFacadeTest.cs b/AiCoreUnitTest/OllamaFacadeTest.cs index 2eeaf6d36..ce718abab 100644 --- a/AiCoreUnitTest/OllamaFacadeTest.cs +++ b/AiCoreUnitTest/OllamaFacadeTest.cs @@ -1,6 +1,7 @@ using AICore.Facade.LLM; using BeWo.Data.Security; using BeWo.Service.Core; +using BS.Shared.Tests.Objects; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; using System; @@ -42,20 +43,9 @@ namespace AiCoreUnitTest var logger = ServiceLogger.GetRequestLogger(out string id); // Build Payload - var payload = new - { - model = "llama3:latest", - messages = new[] { - new { - role = "user", - message = "Say 'Hi'!" - } - }, - customerid = UserRightHelper.GetTenant(), - requestid = id - }; + var payload = AiTestObjects.GetExampleSendRequest(UserRightHelper.GetTenant(), id); - var response = client.SendAiMessageRequest(payload, out string message, out string model, out int? duration, logger); + var response = client.SendAiMessageRequest(payload, out LLMResponse llm_response, logger); Assert.IsTrue(response?.Success); } diff --git a/Server/Components/AICore/AICore.csproj b/Server/Components/AICore/AICore.csproj index e4e2f1e57..1dcd070fb 100644 --- a/Server/Components/AICore/AICore.csproj +++ b/Server/Components/AICore/AICore.csproj @@ -112,6 +112,7 @@ + diff --git a/Server/Components/AICore/Core/ContextSizeAnalyzer.cs b/Server/Components/AICore/Core/ContextSizeAnalyzer.cs index 65603b766..c4825f73a 100644 --- a/Server/Components/AICore/Core/ContextSizeAnalyzer.cs +++ b/Server/Components/AICore/Core/ContextSizeAnalyzer.cs @@ -32,20 +32,20 @@ namespace AICore.Core switch (mode) { case ContextSizeAnalyzeMode.InitContextSize: - valid = CalculateContextSize(json, (int)(max_context_size * MAX_TOKENS_SYSTEM_PERCENTAGE), out token_count); + valid = CalculateContextSize(json, max_context_size, MAX_TOKENS_SYSTEM_PERCENTAGE, out token_count); break; case ContextSizeAnalyzeMode.FirstMessage: - valid = CalculateContextSize(json, (int)(max_context_size * MAX_TOKENS_FIRST_MESSAGE_PERCENTAGE), out token_count); + valid = CalculateContextSize(json, max_context_size, MAX_TOKENS_FIRST_MESSAGE_PERCENTAGE, out token_count); break; case ContextSizeAnalyzeMode.ConversationMessage: - valid = CalculateContextSize(json, (int)(max_context_size * MAX_TOKENS_CONVERSATION_PERCENTAGE), out token_count); + valid = CalculateContextSize(json, max_context_size, MAX_TOKENS_CONVERSATION_PERCENTAGE, out token_count); break; } return valid; } - private static bool CalculateContextSize(string json, int context_size, out int token_size) + private static bool CalculateContextSize(string json, int context_size, double proportion, out int token_size) { token_size = 0; @@ -54,7 +54,12 @@ namespace AICore.Core token_size = (int)(json.Length / 3.5); - if (token_size < context_size) + if (context_size <= 0) + return true; + + var relative_context_size = (int)(context_size * proportion); + + if (token_size < relative_context_size) return true; // try1 (grobe Zeichen-Heuristik) liegt bereits über dem Limit - @@ -62,7 +67,7 @@ namespace AICore.Core var cl100k = _cl100kEncoding.Value.Encode(json).Count; token_size = cl100k - (cl100k / 10); - if (token_size < context_size) + if (token_size < relative_context_size) return true; return false; @@ -73,8 +78,11 @@ namespace AICore.Core return estimated_tokens >= 2 * prompt_eval_count; } - public static bool CheckForAssistent(int eval_count, int prompt_eval_count, int context_size) + public static bool CheckForValidAssistent(int eval_count, int prompt_eval_count, int context_size) { + if (context_size <= 0) + return true; + return eval_count + prompt_eval_count <= context_size; } } diff --git a/Server/Components/AICore/Facade/LLM/BaseLLMApiClient.cs b/Server/Components/AICore/Facade/LLM/BaseLLMApiClient.cs index 6b81f50c2..ec645c468 100644 --- a/Server/Components/AICore/Facade/LLM/BaseLLMApiClient.cs +++ b/Server/Components/AICore/Facade/LLM/BaseLLMApiClient.cs @@ -31,6 +31,6 @@ namespace AICore.Facade.LLM } public abstract ApiResponse> GetAiModelle(); - public abstract ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration, FileLogger logger = null); + public abstract ApiResponse SendAiMessageRequest(dynamic payload, out LLMResponse llm_response, FileLogger logger = null); } } diff --git a/Server/Components/AICore/Facade/LLM/LLMResponse.cs b/Server/Components/AICore/Facade/LLM/LLMResponse.cs new file mode 100644 index 000000000..dbe28d0e1 --- /dev/null +++ b/Server/Components/AICore/Facade/LLM/LLMResponse.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AICore.Facade.LLM +{ + public class LLMResponse + { + public string Message { get; set; } + public string Model { get; set; } + public int? Duration { get; set; } + public int? PromptEvalCount { get; set; } + public int? EvalCount { get; set; } + + public LLMResponse() { } + + public LLMResponse(string message, string model, int? duration, int? promptEvalCount, int? evalCount) + { + Message = message; + Model = model; + Duration = duration; + PromptEvalCount = promptEvalCount; + EvalCount = evalCount; + } + } +} diff --git a/Server/Components/AICore/Facade/LLM/OllamaApiClient.cs b/Server/Components/AICore/Facade/LLM/OllamaApiClient.cs index 8325f843d..6a5fca01a 100644 --- a/Server/Components/AICore/Facade/LLM/OllamaApiClient.cs +++ b/Server/Components/AICore/Facade/LLM/OllamaApiClient.cs @@ -71,18 +71,9 @@ namespace AICore.Facade.LLM return ApiResponse>.SuccessResponse(data); } - public override ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration, FileLogger logger = null) + public override ApiResponse SendAiMessageRequest(dynamic payload, out LLMResponse llm_response, FileLogger logger = null) { - return SendAiMessageRequest((object)payload, out message, out model, out duration, out long? _, out long? _, logger); - } - - public ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration, out long? prompt_eval_count, out long? eval_count, FileLogger logger = null) - { - message = null; - duration = null; - model = null; - prompt_eval_count = null; - eval_count = null; + llm_response = null; _PostClientFacade.JsonObject = payload; @@ -130,20 +121,21 @@ namespace AICore.Facade.LLM var json = response.GetResponseData(); - prompt_eval_count = json.prompt_eval_count; - eval_count = json.eval_count; + llm_response = new LLMResponse(); + llm_response.PromptEvalCount = (int)json.prompt_eval_count; + llm_response.EvalCount = (int)json.eval_count; 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; + llm_response.Duration = (int)total_ms; + llm_response.Model = json.model; var assi_msg = BS.Shared.Core.Utils.ConvertToISO88591(json.message.content); - message = assi_msg; + llm_response.Message = assi_msg; return ApiResponse.SuccessResponse(assi_msg); } diff --git a/Server/Components/AICore/Facade/LLM/OpenWebApiClient.cs b/Server/Components/AICore/Facade/LLM/OpenWebApiClient.cs index 81fe5872f..8a93af667 100644 --- a/Server/Components/AICore/Facade/LLM/OpenWebApiClient.cs +++ b/Server/Components/AICore/Facade/LLM/OpenWebApiClient.cs @@ -53,11 +53,9 @@ namespace AICore.Facade.LLM return ApiResponse>.SuccessResponse(data); } - public override ApiResponse SendAiMessageRequest(dynamic payload, out string message, out string model, out int? duration, FileLogger logger = null) + public override ApiResponse SendAiMessageRequest(dynamic payload, out LLMResponse llm_response, FileLogger logger = null) { - message = null; - duration = null; - model = null; + llm_response = null; _PostClientFacade.JsonObject = payload; @@ -110,12 +108,14 @@ namespace AICore.Facade.LLM var total_ns = json.usage.total_duration; var total_ms = total_ns / 1000000; - duration = (int)total_ms; - model = json.model; + llm_response = new LLMResponse(); + + llm_response.Duration = (int)total_ms; + llm_response.Model = json.model; var assi_msg = BS.Shared.Core.Utils.ConvertToISO88591(choice.message.content); - message = assi_msg; + llm_response.Message = assi_msg; return ApiResponse.SuccessResponse(assi_msg); } diff --git a/Service/Plugins/AiService2.cs b/Service/Plugins/AiService2.cs index 5dc4bc2a9..ff6c672ae 100644 --- a/Service/Plugins/AiService2.cs +++ b/Service/Plugins/AiService2.cs @@ -375,7 +375,7 @@ namespace BeWo.Service.Plugins var modell_oid = config.SelectedModel.Oid.Value; var current_user = UserRightHelper.GetLoggedInUserWithOid(); - + var conv = new AiConversation(); conv.Created = DateTime.Now; @@ -401,7 +401,7 @@ namespace BeWo.Service.Plugins var context_Type = config.Context_Type; var sampleCount = prompt?.SamplesCount; - if(prompt is object && !prompt.IsOwnsoftPrompt) + if (prompt is object && !prompt.IsOwnsoftPrompt) { sampleCount = null; } @@ -426,14 +426,14 @@ namespace BeWo.Service.Plugins { CheckPermissions(conv.ActionType); - IAiModel configModel = conv.Modell; - if(configModel == null) + IAiModel configModel = conv.Modell; + if (configModel == null) { configModel = GetAiConfig().SelectedModel; } - var configModelValue = configModel.Value; var configModelName = configModel.ModelName; + var configMaxContextWindow = configModel.Value; var logger = ServiceLogger.GetRequestLogger(out string id); // Updated @@ -481,21 +481,17 @@ namespace BeWo.Service.Plugins requestid = id }; - // Check Token Size - if (-1 < configModel.Value) + int estimated_tokens = 0; + + var mode = conv.Messages.Count <= 2 ? ContextSizeAnalyzeMode.FirstMessage : ContextSizeAnalyzeMode.ConversationMessage; + + var success = ContextSizeAnalyzer.CheckContextSize(messages2, configMaxContextWindow, mode, out estimated_tokens); + + if (!success) { - int token_count = 0; + var error = $"send error: windowsize exceeded ({estimated_tokens}/{configMaxContextWindow} (mode:{mode}))"; - var mode = conv.Messages.Count <= 2 ? ContextSizeAnalyzeMode.FirstMessage : ContextSizeAnalyzeMode.ConversationMessage; - - var success = ContextSizeAnalyzer.CheckContextSize(messages2, configModel.Value, mode, out token_count); - - if (!success) - { - var error = $"Windowsize exceeded: {token_count}/{configModel.Value} (mode:{mode})"; - - throw new BeWoNotImplementedException(error); - } + throw new BeWoInvalidOperationException(error); } // Update DB Entity @@ -509,24 +505,38 @@ namespace BeWo.Service.Plugins var client = ServiceFacade.GetLLMApiClientByConfig(configModel.ModelSource); - var response = client.SendAiMessageRequest(payload, out var message2, out var model, out var duration, logger); + var response = client.SendAiMessageRequest(payload, out var llm_response, logger); var result = response.GetResponseData(); + if (llm_response.PromptEvalCount is int prompt_eval_count && + llm_response.EvalCount is int eval_count) + { + if (ContextSizeAnalyzer.CheckForTruncation(estimated_tokens, prompt_eval_count)) + { + throw new BeWoInvalidOperationException($"send error: truncation ({estimated_tokens}/{llm_response.PromptEvalCount.Value})"); + } + + if (!ContextSizeAnalyzer.CheckForValidAssistent(eval_count, prompt_eval_count, configMaxContextWindow)) + { + throw new BeWoInvalidOperationException($"send error: assist check failed: ({eval_count}/{prompt_eval_count}/{configMaxContextWindow})"); + } + } + // Antwort var assi_msg = new AiConversationMessage(); - assi_msg.Message = message2; + assi_msg.Message = llm_response.Message; assi_msg.AiConversation = conv; assi_msg.Role = BS.Shared.AiConversationMessageRole.Assistent; assi_msg.Created = dt_before_request; - assi_msg.Duration = duration ?? 0; - assi_msg.ModelName = model; + assi_msg.Duration = llm_response.Duration ?? 0; + assi_msg.ModelName = llm_response.Model; assi_msg.ShowMessage = true; conv.Messages.Add(assi_msg); // Update conv. - if(save) + if (save) DAOFactory.GenericDAO.Update(conv); var assi_msg_dc = MapperFactory.AiConversationMessage.MapToNewDC(assi_msg); diff --git a/Service/Status/AiStatusService.cs b/Service/Status/AiStatusService.cs index dbf33d326..1433bf32f 100644 --- a/Service/Status/AiStatusService.cs +++ b/Service/Status/AiStatusService.cs @@ -1,3 +1,11 @@ +using AICore.Facade.LLM; +using BeWo.Data.Access; +using BeWo.Data.Entities; +using BeWo.Data.Security; +using BeWo.Service.Core; +using BeWo.Service.ServiceProxy; +using BS.Shared.Tests.Objects; + namespace BeWo.Service.Status { public class AiStatusService : BaseStatusService @@ -16,6 +24,55 @@ namespace BeWo.Service.Status "Changes_2026-07-08 AiConversation Promptreferenz", "Changes_2026-07-10 AiConversationMessage ShowMessage", "Changes_2026-07-22 AiConversationMessage UsedPromptbausteinTitle"); + + RegisterCheckup(CheckEndpointOneGetModels, + CheckEndpointOneSendMessage, + CheckEndpointTwoGetModels, + CheckEndpointTwoSendMessage); + } + + private string CheckEndpointOneGetModels() + { + var response = ServiceFacade.OllamaApiClient.GetAiModelle(); + + if (!response.Success) + return "endpoint 1m: " + response.ToErrorString(); + + return null; + } + + private string CheckEndpointOneSendMessage() + { + var logger = ServiceLogger.GetRequestLogger(out string id); + var payload = AiTestObjects.GetExampleSendRequest(UserRightHelper.GetTenant(), id); + var response = ServiceFacade.OllamaApiClient.SendAiMessageRequest(payload, out LLMResponse llm_response, logger); + + if (!response.Success) + return "endpoint 1s: " + response.ToErrorString(); + + return null; + } + + private string CheckEndpointTwoGetModels() + { + var response = ServiceFacade.Ollama2ApiClient.GetAiModelle(); + + if (!response.Success) + return "endpoint 2m: " + response.ToErrorString(); + + return null; + } + + private string CheckEndpointTwoSendMessage() + { + var logger = ServiceLogger.GetRequestLogger(out string id); + var payload = AiTestObjects.GetExampleSendRequest(UserRightHelper.GetTenant(), id); + var response = ServiceFacade.Ollama2ApiClient.SendAiMessageRequest(payload, out LLMResponse llm_response, logger); + + if (!response.Success) + return "endpoint 2s: " + response.ToErrorString(); + + return null; } } } diff --git a/Shared/Exceptions/BeWoInvalidOperationException.cs b/Shared/Exceptions/BeWoInvalidOperationException.cs index 05a798132..24574d7a5 100644 --- a/Shared/Exceptions/BeWoInvalidOperationException.cs +++ b/Shared/Exceptions/BeWoInvalidOperationException.cs @@ -14,6 +14,10 @@ namespace BS.Shared.Exceptions } + public BeWoInvalidOperationException(string message) : base(message) + { + } + public BeWoInvalidOperationException(AppError appError) : base(appError.Displayname) { diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 5e9b71b4b..9cf6c64f4 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -601,6 +601,7 @@ + diff --git a/Shared/Tests/Objects/AiTestObjects.cs b/Shared/Tests/Objects/AiTestObjects.cs new file mode 100644 index 000000000..9a178d920 --- /dev/null +++ b/Shared/Tests/Objects/AiTestObjects.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BS.Shared.Tests.Objects +{ + public static class AiTestObjects + { + public static dynamic GetExampleSendRequest(string customerid, string request_id) + { + var payload = new + { + model = "gemma3:12b", + messages = new[] { + new { + role = "user", + message = "Say 'Hi'!" + } + }, + customerid = customerid, + requestid = request_id + }; + + return payload; + } + } +}