Skip to content

Commit 12df453

Browse files
committed
Refactor chat helper and AnhropicService
Centralize message, image and tool handling into ChatHelper and update services to use it. Added ServiceConstants properties for ToolCalls/ToolCallId/ToolName and replaced hardcoded property keys in LLMService/OpenAiCompatibleService with ServiceConstants.Properties. Moved image extraction, message merging and message-array building logic out of Anthropic/OpenAi services into ChatHelper, made BuildAnthropicRequestBody asynchronous and now use ChatHelper.BuildMessagesArray. Removed duplicated helper methods and consolidated image MIME detection and message content construction.
1 parent 276b21a commit 12df453

File tree

5 files changed

+247
-398
lines changed

5 files changed

+247
-398
lines changed

src/MaIN.Services/Constants/ServiceConstants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ public static class Properties
6363
public const string DisableCacheProperty = "DisableCache";
6464
public const string AgentIdProperty = "AgentId";
6565
public const string MmProjNameProperty = "MmProjName";
66+
public const string ToolCallsProperty = "ToolCalls";
67+
public const string ToolCallIdProperty = "ToolCallId";
68+
public const string ToolNameProperty = "ToolName";
6669
}
6770

6871
public static class Defaults

src/MaIN.Services/Services/LLMService/AnthropicService.cs

Lines changed: 10 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public sealed class AnthropicService(
2727
{
2828
private readonly MaINSettings _settings = settings ?? throw new ArgumentNullException(nameof(settings));
2929

30-
private static readonly HashSet<string> AnthropicImageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
3130
private static readonly ConcurrentDictionary<string, List<ChatMessage>> SessionCache = new();
3231

3332
private const string CompletionsUrl = ServiceConstants.ApiUrls.AnthropicChatMessages;
@@ -70,16 +69,14 @@ private void ValidateApiKey()
7069
if (!chat.Messages.Any())
7170
return null;
7271

73-
var apiKey = GetApiKey();
74-
7572
var lastMessage = chat.Messages.Last();
76-
await ExtractImageFromFiles(lastMessage);
73+
await ChatHelper.ExtractImageFromFiles(lastMessage);
7774

7875
var conversation = GetOrCreateConversation(chat, options.CreateSession);
7976
var resultBuilder = new StringBuilder();
8077
var tokens = new List<LLMTokenValue>();
8178

82-
if (HasFiles(lastMessage))
79+
if (ChatHelper.HasFiles(lastMessage))
8380
{
8481
var result = ChatHelper.ExtractMemoryOptions(lastMessage);
8582
var memoryResult = await AskMemory(chat, result, options, cancellationToken);
@@ -103,7 +100,6 @@ await options.TokenCallback(new LLMTokenValue()
103100
return await ProcessWithToolsAsync(
104101
chat,
105102
conversation,
106-
apiKey,
107103
tokens,
108104
options,
109105
cancellationToken);
@@ -114,7 +110,6 @@ await options.TokenCallback(new LLMTokenValue()
114110
await ProcessStreamingChatAsync(
115111
chat,
116112
conversation,
117-
apiKey,
118113
tokens,
119114
resultBuilder,
120115
options.TokenCallback,
@@ -126,7 +121,6 @@ await ProcessStreamingChatAsync(
126121
await ProcessNonStreamingChatAsync(
127122
chat,
128123
conversation,
129-
apiKey,
130124
resultBuilder,
131125
cancellationToken);
132126
}
@@ -150,7 +144,6 @@ await notificationService.DispatchNotification(
150144
private async Task<ChatResult> ProcessWithToolsAsync(
151145
Chat chat,
152146
List<ChatMessage> conversation,
153-
string apiKey,
154147
List<LLMTokenValue> tokens,
155148
ChatRequestOptions options,
156149
CancellationToken cancellationToken)
@@ -183,7 +176,6 @@ await notificationService.DispatchNotification(
183176
currentToolUses = await ProcessStreamingChatWithToolsAsync(
184177
chat,
185178
conversation,
186-
apiKey,
187179
tokens,
188180
resultBuilder,
189181
options,
@@ -194,7 +186,6 @@ await notificationService.DispatchNotification(
194186
currentToolUses = await ProcessNonStreamingChatWithToolsAsync(
195187
chat,
196188
conversation,
197-
apiKey,
198189
resultBuilder,
199190
cancellationToken);
200191
}
@@ -319,15 +310,14 @@ await notificationService.DispatchNotification(
319310
private async Task<List<AnthropicToolUse>?> ProcessStreamingChatWithToolsAsync(
320311
Chat chat,
321312
List<ChatMessage> conversation,
322-
string apiKey,
323313
List<LLMTokenValue> tokens,
324314
StringBuilder resultBuilder,
325315
ChatRequestOptions options,
326316
CancellationToken cancellationToken)
327317
{
328318
var httpClient = CreateAnthropicHttpClient();
329319

330-
var requestBody = BuildAnthropicRequestBody(chat, conversation, true);
320+
var requestBody = await BuildAnthropicRequestBody(chat, conversation, true);
331321
var requestJson = JsonSerializer.Serialize(requestBody);
332322
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
333323

@@ -464,18 +454,17 @@ private async Task HandleApiError(HttpResponseMessage response, CancellationToke
464454
private async Task<List<AnthropicToolUse>?> ProcessNonStreamingChatWithToolsAsync(
465455
Chat chat,
466456
List<ChatMessage> conversation,
467-
string apiKey,
468457
StringBuilder resultBuilder,
469458
CancellationToken cancellationToken)
470459
{
471460
var httpClient = CreateAnthropicHttpClient();
472461

473-
var requestBody = BuildAnthropicRequestBody(chat, conversation, false);
462+
var requestBody = await BuildAnthropicRequestBody(chat, conversation, false);
474463
var requestJson = JsonSerializer.Serialize(requestBody);
475464
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
476465

477466
using var response = await httpClient.PostAsync(CompletionsUrl, content, cancellationToken);
478-
467+
479468
if (!response.IsSuccessStatusCode)
480469
{
481470
await HandleApiError(response, cancellationToken);
@@ -510,7 +499,7 @@ private async Task HandleApiError(HttpResponseMessage response, CancellationToke
510499
return toolUses.Any() ? toolUses : null;
511500
}
512501

513-
private object BuildAnthropicRequestBody(Chat chat, List<ChatMessage> conversation, bool stream)
502+
private async Task<Dictionary<string, object>> BuildAnthropicRequestBody(Chat chat, List<ChatMessage> conversation, bool stream)
514503
{
515504
var anthParams = chat.BackendParams as AnthropicInferenceParams;
516505

@@ -519,7 +508,7 @@ private object BuildAnthropicRequestBody(Chat chat, List<ChatMessage> conversati
519508
["model"] = chat.ModelId,
520509
["max_tokens"] = anthParams?.MaxTokens ?? 4096,
521510
["stream"] = stream,
522-
["messages"] = BuildAnthropicMessages(conversation)
511+
["messages"] = await ChatHelper.BuildMessagesArray(conversation, chat, ImageType.AsBase64)
523512
};
524513

525514
if (anthParams != null)
@@ -562,40 +551,6 @@ private object BuildAnthropicRequestBody(Chat chat, List<ChatMessage> conversati
562551
return requestBody;
563552
}
564553

565-
private List<object> BuildAnthropicMessages(List<ChatMessage> conversation)
566-
{
567-
var messages = new List<object>();
568-
569-
foreach (var msg in conversation)
570-
{
571-
if (msg.Role.Equals("system", StringComparison.OrdinalIgnoreCase))
572-
continue;
573-
574-
object content;
575-
576-
if (msg.Content is string textContent)
577-
{
578-
content = textContent;
579-
}
580-
else if (msg.Content is List<object> contentBlocks)
581-
{
582-
content = contentBlocks;
583-
}
584-
else
585-
{
586-
content = msg.Content;
587-
}
588-
589-
messages.Add(new
590-
{
591-
role = msg.Role,
592-
content = content
593-
});
594-
}
595-
596-
return messages;
597-
}
598-
599554
public async Task<ChatResult?> AskMemory(Chat chat, ChatMemoryOptions memoryOptions, ChatRequestOptions requestOptions,
600555
CancellationToken cancellationToken = default)
601556
{
@@ -639,7 +594,7 @@ private List<ChatMessage> GetOrCreateConversation(Chat chat, bool createSession)
639594
conversation = new List<ChatMessage>();
640595
}
641596

642-
OpenAiCompatibleService.MergeMessages(conversation, chat.Messages);
597+
ChatHelper.MergeMessages(conversation, chat.Messages);
643598
return conversation;
644599
}
645600

@@ -651,51 +606,9 @@ private void UpdateSessionCache(string chatId, string assistantResponse, bool cr
651606
}
652607
}
653608

654-
private static bool HasFiles(Message message)
655-
{
656-
return message.Files != null && message.Files.Count > 0;
657-
}
658-
659-
private static async Task ExtractImageFromFiles(Message message)
660-
{
661-
if (message.Files == null || message.Files.Count == 0)
662-
return;
663-
664-
var imageFiles = message.Files
665-
.Where(f => AnthropicImageExtensions.Contains(f.Extension.ToLowerInvariant()))
666-
.ToList();
667-
668-
if (imageFiles.Count == 0)
669-
return;
670-
671-
var imageBytesList = new List<byte[]>();
672-
foreach (var imageFile in imageFiles)
673-
{
674-
if (imageFile.StreamContent != null)
675-
{
676-
using var ms = new MemoryStream();
677-
imageFile.StreamContent.Position = 0;
678-
await imageFile.StreamContent.CopyToAsync(ms);
679-
imageBytesList.Add(ms.ToArray());
680-
}
681-
else if (imageFile.Path != null)
682-
{
683-
imageBytesList.Add(await File.ReadAllBytesAsync(imageFile.Path));
684-
}
685-
686-
message.Files.Remove(imageFile);
687-
}
688-
689-
message.Images = imageBytesList;
690-
691-
if (message.Files.Count == 0)
692-
message.Files = null;
693-
}
694-
695609
private async Task ProcessStreamingChatAsync(
696610
Chat chat,
697611
List<ChatMessage> conversation,
698-
string apiKey,
699612
List<LLMTokenValue> tokens,
700613
StringBuilder resultBuilder,
701614
Func<LLMTokenValue, Task>? tokenCallback,
@@ -704,29 +617,7 @@ private async Task ProcessStreamingChatAsync(
704617
{
705618
var httpClient = CreateAnthropicHttpClient();
706619

707-
var anthParams2 = chat.BackendParams as AnthropicInferenceParams;
708-
var requestBody = new Dictionary<string, object>
709-
{
710-
["model"] = chat.ModelId,
711-
["max_tokens"] = anthParams2?.MaxTokens ?? 4096,
712-
["stream"] = true,
713-
["system"] = chat.InferenceGrammar is not null
714-
? $"Respond only using the following grammar format: \n{chat.InferenceGrammar.Value}\n. Do not add explanations, code tags, or any extra content."
715-
: "",
716-
["messages"] = await OpenAiCompatibleService.BuildMessagesArray(conversation, chat, ImageType.AsBase64)
717-
};
718-
if (anthParams2 != null)
719-
{
720-
if (anthParams2.Temperature.HasValue) requestBody["temperature"] = anthParams2.Temperature.Value;
721-
if (anthParams2.TopP.HasValue) requestBody["top_p"] = anthParams2.TopP.Value;
722-
if (anthParams2.TopK.HasValue) requestBody["top_k"] = anthParams2.TopK.Value;
723-
}
724-
if (chat.BackendParams?.AdditionalParams != null)
725-
{
726-
foreach (var (key, value) in chat.BackendParams.AdditionalParams)
727-
requestBody[key] = value;
728-
}
729-
620+
var requestBody = await BuildAnthropicRequestBody(chat, conversation, true);
730621
var requestJson = JsonSerializer.Serialize(requestBody);
731622
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
732623

@@ -798,35 +689,12 @@ await notificationService.DispatchNotification(
798689
private async Task ProcessNonStreamingChatAsync(
799690
Chat chat,
800691
List<ChatMessage> conversation,
801-
string apiKey,
802692
StringBuilder resultBuilder,
803693
CancellationToken cancellationToken)
804694
{
805695
var httpClient = CreateAnthropicHttpClient();
806696

807-
var anthParams3 = chat.BackendParams as AnthropicInferenceParams;
808-
var requestBody = new Dictionary<string, object>
809-
{
810-
["model"] = chat.ModelId,
811-
["max_tokens"] = anthParams3?.MaxTokens ?? 4096,
812-
["stream"] = false,
813-
["system"] = chat.InferenceGrammar is not null
814-
? $"Respond only using the following grammar format: \n{chat.InferenceGrammar.Value}\n. Do not add explanations, code tags, or any extra content."
815-
: "",
816-
["messages"] = await OpenAiCompatibleService.BuildMessagesArray(conversation, chat, ImageType.AsBase64)
817-
};
818-
if (anthParams3 != null)
819-
{
820-
if (anthParams3.Temperature.HasValue) requestBody["temperature"] = anthParams3.Temperature.Value;
821-
if (anthParams3.TopP.HasValue) requestBody["top_p"] = anthParams3.TopP.Value;
822-
if (anthParams3.TopK.HasValue) requestBody["top_k"] = anthParams3.TopK.Value;
823-
}
824-
if (chat.BackendParams?.AdditionalParams != null)
825-
{
826-
foreach (var (key, value) in chat.BackendParams.AdditionalParams)
827-
requestBody[key] = value;
828-
}
829-
697+
var requestBody = await BuildAnthropicRequestBody(chat, conversation, false);
830698
var requestJson = JsonSerializer.Serialize(requestBody);
831699
var content = new StringContent(requestJson, Encoding.UTF8, "application/json");
832700

src/MaIN.Services/Services/LLMService/LLMService.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ private static void ProcessTextMessage(Conversation conversation,
391391
bool isNewConversation)
392392
{
393393
var template = new LLamaTemplate(llmModel);
394-
var finalPrompt = ChatHelper.GetFinalPrompt(lastMsg, model, isNewConversation);
394+
var finalPrompt = GetFinalPrompt(lastMsg, model, isNewConversation);
395395

396396
var hasTools = chat.ToolsConfiguration?.Tools != null && chat.ToolsConfiguration.Tools.Count != 0;
397397

@@ -470,7 +470,19 @@ private static string FormatToolsForPrompt(ToolsConfiguration toolsConfig)
470470
using var sampler = LLMService.CreateSampler(chat.LocalParams!);
471471
var decoder = new StreamingTokenDecoder(executor.Context);
472472

473-
var inferenceParams = ChatHelper.CreateInferenceParams(chat, llmModel);
473+
var inferenceParams = new InferenceParams
474+
{
475+
SamplingPipeline = new DefaultSamplingPipeline
476+
{
477+
Temperature = chat.LocalParams!.Temperature,
478+
TopK = chat.LocalParams!.TopK,
479+
TopP = chat.LocalParams!.TopP
480+
},
481+
AntiPrompts = [llmModel.Vocab.EOT?.ToString() ?? "User:"],
482+
TokensKeep = chat.LocalParams!.TokensKeep,
483+
MaxTokens = chat.LocalParams!.MaxTokens
484+
};
485+
474486
var maxTokens = inferenceParams.MaxTokens == -1 ? int.MaxValue : inferenceParams.MaxTokens;
475487
var reasoningModel = model as IReasoningModel;
476488

@@ -704,7 +716,7 @@ private async Task<ChatResult> ProcessWithToolsAsync(
704716
}
705717

706718
var toolCalls = parseResult.ToolCalls!;
707-
responseMessage.Properties[ToolCallsProperty] = JsonSerializer.Serialize(toolCalls);
719+
responseMessage.Properties[ServiceConstants.Properties.ToolCallsProperty] = JsonSerializer.Serialize(toolCalls);
708720

709721
foreach (var toolCall in toolCalls)
710722
{
@@ -758,8 +770,8 @@ await requestOptions.ToolCallback.Invoke(new ToolInvocation
758770
Type = MessageType.LocalLLM,
759771
Tool = true
760772
};
761-
toolMessage.Properties[ToolCallIdProperty] = toolCall.Id;
762-
toolMessage.Properties[ToolNameProperty] = toolCall.Function.Name;
773+
toolMessage.Properties[ServiceConstants.Properties.ToolCallIdProperty] = toolCall.Id;
774+
toolMessage.Properties[ServiceConstants.Properties.ToolNameProperty] = toolCall.Function.Name;
763775
chat.Messages.Add(toolMessage.MarkProcessed());
764776
}
765777
catch (Exception ex)
@@ -772,8 +784,8 @@ await requestOptions.ToolCallback.Invoke(new ToolInvocation
772784
Type = MessageType.LocalLLM,
773785
Tool = true
774786
};
775-
toolMessage.Properties[ToolCallIdProperty] = toolCall.Id;
776-
toolMessage.Properties[ToolNameProperty] = toolCall.Function.Name;
787+
toolMessage.Properties[ServiceConstants.Properties.ToolCallIdProperty] = toolCall.Id;
788+
toolMessage.Properties[ServiceConstants.Properties.ToolNameProperty] = toolCall.Function.Name;
777789
chat.Messages.Add(toolMessage.MarkProcessed());
778790
}
779791
}
@@ -808,7 +820,12 @@ await requestOptions.ToolCallback.Invoke(new ToolInvocation
808820
};
809821
}
810822

811-
private const string ToolCallsProperty = "ToolCalls";
812-
private const string ToolCallIdProperty = "ToolCallId";
813-
private const string ToolNameProperty = "ToolName";
823+
private static string GetFinalPrompt(Message message, AIModel model, bool startSession)
824+
{
825+
var additionalPrompt = (model as IReasoningModel)?.AdditionalPrompt;
826+
return startSession && additionalPrompt != null
827+
? $"{message.Content}{additionalPrompt}"
828+
: message.Content;
829+
}
830+
814831
}

0 commit comments

Comments
 (0)