diff --git a/dotnet/.editorconfig b/dotnet/.editorconfig index 4da1adc5de68..5a604ce00961 100644 --- a/dotnet/.editorconfig +++ b/dotnet/.editorconfig @@ -141,7 +141,7 @@ csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true # Code block -csharp_prefer_braces = false:none +csharp_prefer_braces = true:warning # Using statements csharp_using_directive_placement = outside_namespace:error @@ -173,6 +173,11 @@ dotnet_diagnostic.CS1573.severity = none # disable CS1570: XML comment has badly formed XML dotnet_diagnostic.CS1570.severity = none +dotnet_diagnostic.IDE0035.severity = warning # Remove unreachable code +dotnet_diagnostic.IDE0161.severity = warning # Use file-scoped namespace + +csharp_style_var_elsewhere = true:suggestion # Prefer 'var' everywhere + # disable check for generated code [*.generated.cs] generated_code = true \ No newline at end of file diff --git a/dotnet/AutoGen.sln b/dotnet/AutoGen.sln index b29e5e21e950..3d0848fb3110 100644 --- a/dotnet/AutoGen.sln +++ b/dotnet/AutoGen.sln @@ -35,12 +35,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Mistral.Tests", "te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.SemanticKernel.Tests", "test\AutoGen.SemanticKernel.Tests\AutoGen.SemanticKernel.Tests.csproj", "{1DFABC4A-8458-4875-8DCB-59F3802DAC65}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.OpenAI.Tests", "test\AutoGen.OpenAI.Tests\AutoGen.OpenAI.Tests.csproj", "{D36A85F9-C172-487D-8192-6BFE5D05B4A7}" -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.DotnetInteractive.Tests", "test\AutoGen.DotnetInteractive.Tests\AutoGen.DotnetInteractive.Tests.csproj", "{B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.OpenAI.Tests", "test\AutoGen.OpenAI.Tests\AutoGen.OpenAI.Tests.csproj", "{D36A85F9-C172-487D-8192-6BFE5D05B4A7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autogen.Ollama", "src\Autogen.Ollama\Autogen.Ollama.csproj", "{A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.DotnetInteractive.Tests", "test\AutoGen.DotnetInteractive.Tests\AutoGen.DotnetInteractive.Tests.csproj", "{B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autogen.Ollama.Tests", "test\Autogen.Ollama.Tests\Autogen.Ollama.Tests.csproj", "{C24FDE63-952D-4F8E-A807-AF31D43AD675}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Ollama", "src\AutoGen.Ollama\AutoGen.Ollama.csproj", "{9F9E6DED-3D92-4970-909A-70FC11F1A665}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.Ollama.Tests", "test\AutoGen.Ollama.Tests\AutoGen.Ollama.Tests.csproj", "{03E31CAA-3728-48D3-B936-9F11CF6C18FE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -96,14 +97,6 @@ Global {15441693-3659-4868-B6C1-B106F52FF3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU {15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.Build.0 = Release|Any CPU - {A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Release|Any CPU.Build.0 = Release|Any CPU - {C24FDE63-952D-4F8E-A807-AF31D43AD675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C24FDE63-952D-4F8E-A807-AF31D43AD675}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C24FDE63-952D-4F8E-A807-AF31D43AD675}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C24FDE63-952D-4F8E-A807-AF31D43AD675}.Release|Any CPU.Build.0 = Release|Any CPU {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.Build.0 = Debug|Any CPU {1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -116,6 +109,14 @@ Global {B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}.Debug|Any CPU.Build.0 = Debug|Any CPU {B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}.Release|Any CPU.ActiveCfg = Release|Any CPU {B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}.Release|Any CPU.Build.0 = Release|Any CPU + {9F9E6DED-3D92-4970-909A-70FC11F1A665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F9E6DED-3D92-4970-909A-70FC11F1A665}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F9E6DED-3D92-4970-909A-70FC11F1A665}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F9E6DED-3D92-4970-909A-70FC11F1A665}.Release|Any CPU.Build.0 = Release|Any CPU + {03E31CAA-3728-48D3-B936-9F11CF6C18FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03E31CAA-3728-48D3-B936-9F11CF6C18FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03E31CAA-3728-48D3-B936-9F11CF6C18FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03E31CAA-3728-48D3-B936-9F11CF6C18FE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -133,11 +134,11 @@ Global {63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {6585D1A4-3D97-4D76-A688-1933B61AEB19} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} {15441693-3659-4868-B6C1-B106F52FF3BA} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} - {A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} - {C24FDE63-952D-4F8E-A807-AF31D43AD675} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {1DFABC4A-8458-4875-8DCB-59F3802DAC65} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {D36A85F9-C172-487D-8192-6BFE5D05B4A7} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} {B61388CA-DC73-4B7F-A7B2-7B9A86C9229E} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} + {9F9E6DED-3D92-4970-909A-70FC11F1A665} = {18BF8DD7-0585-48BF-8F97-AD333080CE06} + {03E31CAA-3728-48D3-B936-9F11CF6C18FE} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {93384647-528D-46C8-922C-8DB36A382F0B} diff --git a/dotnet/src/AutoGen.Core/Message/ImageMessage.cs b/dotnet/src/AutoGen.Core/Message/ImageMessage.cs index 1239785c411a..d2e2d0803003 100644 --- a/dotnet/src/AutoGen.Core/Message/ImageMessage.cs +++ b/dotnet/src/AutoGen.Core/Message/ImageMessage.cs @@ -49,7 +49,9 @@ public ImageMessage(Role role, BinaryData data, string? from = null) public string BuildDataUri() { if (this.Data is null) + { throw new NullReferenceException($"{nameof(Data)}"); + } return $"data:{this.Data.MediaType};base64,{Convert.ToBase64String(this.Data.ToArray())}"; } diff --git a/dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs b/dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs index b0a05d85730b..b0fa1757c12e 100644 --- a/dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs +++ b/dotnet/src/AutoGen.Mistral/DTOs/ChatMessage.cs @@ -13,7 +13,7 @@ public class ChatMessage /// /// role. /// content. - public ChatMessage(RoleEnum? role = default(RoleEnum?), string? content = null) + public ChatMessage(RoleEnum? role = default, string? content = null) { this.Role = role; this.Content = content; diff --git a/dotnet/src/Autogen.Ollama/Agent/OllamaAgent.cs b/dotnet/src/AutoGen.Ollama/Agent/OllamaAgent.cs similarity index 67% rename from dotnet/src/Autogen.Ollama/Agent/OllamaAgent.cs rename to dotnet/src/AutoGen.Ollama/Agent/OllamaAgent.cs index 6f87e20e2332..9ef68388d605 100644 --- a/dotnet/src/Autogen.Ollama/Agent/OllamaAgent.cs +++ b/dotnet/src/AutoGen.Ollama/Agent/OllamaAgent.cs @@ -13,7 +13,7 @@ using System.Threading.Tasks; using AutoGen.Core; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; /// /// An agent that can interact with ollama models. @@ -21,7 +21,6 @@ namespace Autogen.Ollama; public class OllamaAgent : IStreamingAgent { private readonly HttpClient _httpClient; - public string Name { get; } private readonly string _modelName; private readonly string _systemMessage; private readonly OllamaReplyOptions? _replyOptions; @@ -36,13 +35,14 @@ public OllamaAgent(HttpClient httpClient, string name, string modelName, _systemMessage = systemMessage; _replyOptions = replyOptions; } + public async Task GenerateReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, CancellationToken cancellation = default) { ChatRequest request = await BuildChatRequest(messages, options); request.Stream = false; - using (HttpResponseMessage? response = await _httpClient - .SendAsync(BuildRequestMessage(request), HttpCompletionOption.ResponseContentRead, cancellation)) + var httpRequest = BuildRequest(request); + using (HttpResponseMessage? response = await _httpClient.SendAsync(httpRequest, HttpCompletionOption.ResponseContentRead, cancellation)) { response.EnsureSuccessStatusCode(); Stream? streamResponse = await response.Content.ReadAsStreamAsync(); @@ -52,6 +52,7 @@ public async Task GenerateReplyAsync( return output; } } + public async IAsyncEnumerable GenerateStreamingReplyAsync( IEnumerable messages, GenerateReplyOptions? options = null, @@ -59,7 +60,7 @@ public async IAsyncEnumerable GenerateStreamingReplyAsync( { ChatRequest request = await BuildChatRequest(messages, options); request.Stream = true; - HttpRequestMessage message = BuildRequestMessage(request); + HttpRequestMessage message = BuildRequest(request); using (HttpResponseMessage? response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) { response.EnsureSuccessStatusCode(); @@ -69,22 +70,28 @@ public async IAsyncEnumerable GenerateStreamingReplyAsync( while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested) { string? line = await reader.ReadLineAsync(); - if (string.IsNullOrWhiteSpace(line)) continue; + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } ChatResponseUpdate? update = JsonSerializer.Deserialize(line); - if (update != null) + if (update is { Done: false }) { yield return new MessageEnvelope(update, from: Name); } + else + { + var finalUpdate = JsonSerializer.Deserialize(line) ?? throw new Exception("Failed to deserialize response"); - if (update is { Done: false }) continue; - - ChatResponse? chatMessage = JsonSerializer.Deserialize(line); - if (chatMessage == null) continue; - yield return new MessageEnvelope(chatMessage, from: Name); + yield return new MessageEnvelope(finalUpdate, from: Name); + } } } } + + public string Name { get; } + private async Task BuildChatRequest(IEnumerable messages, GenerateReplyOptions? options) { var request = new ChatRequest @@ -152,49 +159,22 @@ private void BuildChatRequestOptions(OllamaReplyOptions replyOptions, ChatReques } private async Task> BuildChatHistory(IEnumerable messages) { - if (!messages.Any(m => m.IsSystemMessage())) + var history = messages.Select(m => m switch { - var systemMessage = new TextMessage(Role.System, _systemMessage, from: Name); - messages = new[] { systemMessage }.Concat(messages); - } + IMessage chatMessage => chatMessage.Content, + _ => throw new ArgumentException("Invalid message type") + }); - var collection = new List(); - foreach (IMessage? message in messages) + // if there's no system message in the history, add one to the beginning + if (!history.Any(m => m.Role == "system")) { - Message item; - switch (message) - { - case TextMessage tm: - item = new Message { Role = tm.Role.ToString(), Value = tm.Content }; - break; - case ImageMessage im: - string base64Image = await ImageUrlToBase64(im.Url!); - item = new Message { Role = im.Role.ToString(), Images = [base64Image] }; - break; - case MultiModalMessage mm: - var textsGroupedByRole = mm.Content.OfType().GroupBy(tm => tm.Role) - .ToDictionary(g => g.Key, g => string.Join(Environment.NewLine, g.Select(tm => tm.Content))); - - string content = string.Join($"{Environment.NewLine}", textsGroupedByRole - .Select(g => $"{g.Key}{Environment.NewLine}:{g.Value}")); - - IEnumerable> imagesConversionTasks = mm.Content - .OfType() - .Select(async im => await ImageUrlToBase64(im.Url!)); - - string[]? imagesBase64 = await Task.WhenAll(imagesConversionTasks); - item = new Message { Role = mm.Role.ToString(), Value = content, Images = imagesBase64 }; - break; - default: - throw new NotSupportedException(); - } - - collection.Add(item); + history = new[] { new Message() { Role = "system", Value = _systemMessage } }.Concat(history); } - return collection; + return history.ToList(); } - private static HttpRequestMessage BuildRequestMessage(ChatRequest request) + + private static HttpRequestMessage BuildRequest(ChatRequest request) { string serialized = JsonSerializer.Serialize(request); return new HttpRequestMessage(HttpMethod.Post, OllamaConsts.ChatCompletionEndpoint) @@ -202,15 +182,4 @@ private static HttpRequestMessage BuildRequestMessage(ChatRequest request) Content = new StringContent(serialized, Encoding.UTF8, OllamaConsts.JsonMediaType) }; } - private async Task ImageUrlToBase64(string imageUrl) - { - if (string.IsNullOrWhiteSpace(imageUrl)) - { - throw new ArgumentException("required parameter", nameof(imageUrl)); - } - byte[] imageBytes = await _httpClient.GetByteArrayAsync(imageUrl); - return imageBytes != null - ? Convert.ToBase64String(imageBytes) - : throw new InvalidOperationException("no image byte array"); - } } diff --git a/dotnet/src/Autogen.Ollama/Autogen.Ollama.csproj b/dotnet/src/AutoGen.Ollama/AutoGen.Ollama.csproj similarity index 86% rename from dotnet/src/Autogen.Ollama/Autogen.Ollama.csproj rename to dotnet/src/AutoGen.Ollama/AutoGen.Ollama.csproj index 9a01f95ca8ea..20924a476b76 100644 --- a/dotnet/src/Autogen.Ollama/Autogen.Ollama.csproj +++ b/dotnet/src/AutoGen.Ollama/AutoGen.Ollama.csproj @@ -2,6 +2,7 @@ netstandard2.0 + AutoGen.Ollama True diff --git a/dotnet/src/Autogen.Ollama/DTOs/ChatRequest.cs b/dotnet/src/AutoGen.Ollama/DTOs/ChatRequest.cs similarity index 94% rename from dotnet/src/Autogen.Ollama/DTOs/ChatRequest.cs rename to dotnet/src/AutoGen.Ollama/DTOs/ChatRequest.cs index a48fb42cfbfb..3b0cf04a1a0d 100644 --- a/dotnet/src/Autogen.Ollama/DTOs/ChatRequest.cs +++ b/dotnet/src/AutoGen.Ollama/DTOs/ChatRequest.cs @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // ChatRequest.cs -using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public class ChatRequest { @@ -19,7 +18,7 @@ public class ChatRequest /// the messages of the chat, this can be used to keep a chat memory /// [JsonPropertyName("messages")] - public IList Messages { get; set; } = Array.Empty(); + public IList Messages { get; set; } = []; /// /// the format to return a response in. Currently, the only accepted value is json diff --git a/dotnet/src/Autogen.Ollama/DTOs/ChatResponse.cs b/dotnet/src/AutoGen.Ollama/DTOs/ChatResponse.cs similarity index 97% rename from dotnet/src/Autogen.Ollama/DTOs/ChatResponse.cs rename to dotnet/src/AutoGen.Ollama/DTOs/ChatResponse.cs index 2de150f7235a..7d8142de785c 100644 --- a/dotnet/src/Autogen.Ollama/DTOs/ChatResponse.cs +++ b/dotnet/src/AutoGen.Ollama/DTOs/ChatResponse.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public class ChatResponse : ChatResponseUpdate { diff --git a/dotnet/src/AutoGen.Ollama/DTOs/ChatResponseUpdate.cs b/dotnet/src/AutoGen.Ollama/DTOs/ChatResponseUpdate.cs new file mode 100644 index 000000000000..8b4dac194f46 --- /dev/null +++ b/dotnet/src/AutoGen.Ollama/DTOs/ChatResponseUpdate.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// ChatResponseUpdate.cs + +using System.Text.Json.Serialization; + +namespace AutoGen.Ollama; + +public class ChatResponseUpdate +{ + [JsonPropertyName("model")] + public string Model { get; set; } = string.Empty; + + [JsonPropertyName("created_at")] + public string CreatedAt { get; set; } = string.Empty; + + [JsonPropertyName("message")] + public Message? Message { get; set; } + + [JsonPropertyName("done")] + public bool Done { get; set; } +} diff --git a/dotnet/src/Autogen.Ollama/DTOs/ChatResponseUpdate.cs b/dotnet/src/AutoGen.Ollama/DTOs/Message.cs similarity index 66% rename from dotnet/src/Autogen.Ollama/DTOs/ChatResponseUpdate.cs rename to dotnet/src/AutoGen.Ollama/DTOs/Message.cs index 181dacfc34b5..2e0d891cc61e 100644 --- a/dotnet/src/Autogen.Ollama/DTOs/ChatResponseUpdate.cs +++ b/dotnet/src/AutoGen.Ollama/DTOs/Message.cs @@ -4,25 +4,20 @@ using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; -public class ChatResponseUpdate +public class Message { - [JsonPropertyName("model")] - public string Model { get; set; } = string.Empty; - - [JsonPropertyName("created_at")] - public string CreatedAt { get; set; } = string.Empty; - - [JsonPropertyName("message")] - public Message? Message { get; set; } + public Message() + { + } - [JsonPropertyName("done")] - public bool Done { get; set; } -} + public Message(string role, string value) + { + Role = role; + Value = value; + } -public class Message -{ /// /// the role of the message, either system, user or assistant /// diff --git a/dotnet/src/Autogen.Ollama/DTOs/ModelReplyOptions.cs b/dotnet/src/AutoGen.Ollama/DTOs/ModelReplyOptions.cs similarity index 99% rename from dotnet/src/Autogen.Ollama/DTOs/ModelReplyOptions.cs rename to dotnet/src/AutoGen.Ollama/DTOs/ModelReplyOptions.cs index d7854b77b20e..9d54a1bb83b4 100644 --- a/dotnet/src/Autogen.Ollama/DTOs/ModelReplyOptions.cs +++ b/dotnet/src/AutoGen.Ollama/DTOs/ModelReplyOptions.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; //https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values public class ModelReplyOptions diff --git a/dotnet/src/Autogen.Ollama/DTOs/OllamaReplyOptions.cs b/dotnet/src/AutoGen.Ollama/DTOs/OllamaReplyOptions.cs similarity index 99% rename from dotnet/src/Autogen.Ollama/DTOs/OllamaReplyOptions.cs rename to dotnet/src/AutoGen.Ollama/DTOs/OllamaReplyOptions.cs index 97bf57cb10cb..c7c77d1db256 100644 --- a/dotnet/src/Autogen.Ollama/DTOs/OllamaReplyOptions.cs +++ b/dotnet/src/AutoGen.Ollama/DTOs/OllamaReplyOptions.cs @@ -3,12 +3,12 @@ using AutoGen.Core; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public enum FormatType { None, - Json + Json, } public class OllamaReplyOptions : GenerateReplyOptions diff --git a/dotnet/src/Autogen.Ollama/Embeddings/ITextEmbeddingService.cs b/dotnet/src/AutoGen.Ollama/Embeddings/ITextEmbeddingService.cs similarity index 92% rename from dotnet/src/Autogen.Ollama/Embeddings/ITextEmbeddingService.cs rename to dotnet/src/AutoGen.Ollama/Embeddings/ITextEmbeddingService.cs index f1ea1b8406c6..5ce0dc8cc40a 100644 --- a/dotnet/src/Autogen.Ollama/Embeddings/ITextEmbeddingService.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/ITextEmbeddingService.cs @@ -4,7 +4,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public interface ITextEmbeddingService { diff --git a/dotnet/src/Autogen.Ollama/Embeddings/OllamaTextEmbeddingService.cs b/dotnet/src/AutoGen.Ollama/Embeddings/OllamaTextEmbeddingService.cs similarity index 98% rename from dotnet/src/Autogen.Ollama/Embeddings/OllamaTextEmbeddingService.cs rename to dotnet/src/AutoGen.Ollama/Embeddings/OllamaTextEmbeddingService.cs index db913377a5f5..2e431e7bcb81 100644 --- a/dotnet/src/Autogen.Ollama/Embeddings/OllamaTextEmbeddingService.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/OllamaTextEmbeddingService.cs @@ -9,7 +9,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public class OllamaTextEmbeddingService : ITextEmbeddingService { diff --git a/dotnet/src/Autogen.Ollama/Embeddings/TextEmbeddingsRequest.cs b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsRequest.cs similarity index 97% rename from dotnet/src/Autogen.Ollama/Embeddings/TextEmbeddingsRequest.cs rename to dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsRequest.cs index 1577dc536433..7f2531c522ad 100644 --- a/dotnet/src/Autogen.Ollama/Embeddings/TextEmbeddingsRequest.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsRequest.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public class TextEmbeddingsRequest { diff --git a/dotnet/src/Autogen.Ollama/Embeddings/TextEmbeddingsResponse.cs b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsResponse.cs similarity index 90% rename from dotnet/src/Autogen.Ollama/Embeddings/TextEmbeddingsResponse.cs rename to dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsResponse.cs index eb46359fdb2d..580059c033b5 100644 --- a/dotnet/src/Autogen.Ollama/Embeddings/TextEmbeddingsResponse.cs +++ b/dotnet/src/AutoGen.Ollama/Embeddings/TextEmbeddingsResponse.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public class TextEmbeddingsResponse { diff --git a/dotnet/src/AutoGen.Ollama/Extension/OllamaAgentExtension.cs b/dotnet/src/AutoGen.Ollama/Extension/OllamaAgentExtension.cs new file mode 100644 index 000000000000..4c0df513ef84 --- /dev/null +++ b/dotnet/src/AutoGen.Ollama/Extension/OllamaAgentExtension.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OllamaAgentExtension.cs + +using AutoGen.Core; + +namespace AutoGen.Ollama.Extension; + +public static class OllamaAgentExtension +{ + /// + /// Register an to the + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this OllamaAgent agent, OllamaMessageConnector? connector = null) + { + if (connector == null) + { + connector = new OllamaMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } + + /// + /// Register an to the where T is + /// + /// the connector to use. If null, a new instance of will be created. + public static MiddlewareStreamingAgent RegisterMessageConnector( + this MiddlewareStreamingAgent agent, OllamaMessageConnector? connector = null) + { + if (connector == null) + { + connector = new OllamaMessageConnector(); + } + + return agent.RegisterStreamingMiddleware(connector); + } +} diff --git a/dotnet/src/AutoGen.Ollama/Middlewares/OllamaMessageConnector.cs b/dotnet/src/AutoGen.Ollama/Middlewares/OllamaMessageConnector.cs new file mode 100644 index 000000000000..e4e1c4ba47bb --- /dev/null +++ b/dotnet/src/AutoGen.Ollama/Middlewares/OllamaMessageConnector.cs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OllamaMessageConnector.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.Core; + +namespace AutoGen.Ollama; + +public class OllamaMessageConnector : IStreamingMiddleware +{ + public string Name => nameof(OllamaMessageConnector); + + public async Task InvokeAsync(MiddlewareContext context, IAgent agent, + CancellationToken cancellationToken = default) + { + var messages = ProcessMessage(context.Messages, agent); + IMessage reply = await agent.GenerateReplyAsync(messages, context.Options, cancellationToken); + + return reply switch + { + IMessage messageEnvelope when messageEnvelope.Content.Message?.Value is string content => new TextMessage(Role.Assistant, content, messageEnvelope.From), + IMessage messageEnvelope when messageEnvelope.Content.Message?.Value is null => throw new InvalidOperationException("Message content is null"), + _ => reply + }; + } + + public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var messages = ProcessMessage(context.Messages, agent); + var chunks = new List(); + await foreach (var update in agent.GenerateStreamingReplyAsync(messages, context.Options, cancellationToken)) + { + if (update is IStreamingMessage chatResponseUpdate) + { + var response = chatResponseUpdate.Content switch + { + _ when chatResponseUpdate.Content.Message?.Value is string content => new TextMessageUpdate(Role.Assistant, content, chatResponseUpdate.From), + _ => null, + }; + + if (response != null) + { + chunks.Add(chatResponseUpdate.Content); + yield return response; + } + } + else + { + yield return update; + } + } + + if (chunks.Count == 0) + { + yield break; + } + + // if the chunks are not empty, aggregate them into a single message + var messageContent = string.Join(string.Empty, chunks.Select(c => c.Message?.Value)); + var message = new Message + { + Role = "assistant", + Value = messageContent, + }; + + yield return MessageEnvelope.Create(message, agent.Name); + } + + private IEnumerable ProcessMessage(IEnumerable messages, IAgent agent) + { + return messages.SelectMany(m => + { + if (m is IMessage messageEnvelope) + { + return [m]; + } + else + { + return m switch + { + TextMessage textMessage => ProcessTextMessage(textMessage, agent), + ImageMessage imageMessage => ProcessImageMessage(imageMessage, agent), + MultiModalMessage multiModalMessage => ProcessMultiModalMessage(multiModalMessage, agent), + _ => [m], + }; + } + }); + } + + private IEnumerable ProcessMultiModalMessage(MultiModalMessage multiModalMessage, IAgent agent) + { + var messages = new List(); + foreach (var message in multiModalMessage.Content) + { + messages.AddRange(message switch + { + TextMessage textMessage => ProcessTextMessage(textMessage, agent), + ImageMessage imageMessage => ProcessImageMessage(imageMessage, agent), + _ => throw new InvalidOperationException("Invalid message type"), + }); + } + + return messages; + } + + private IEnumerable ProcessImageMessage(ImageMessage imageMessage, IAgent agent) + { + byte[]? data = imageMessage.Data?.ToArray(); + if (data is null) + { + if (imageMessage.Url is null) + { + throw new InvalidOperationException("Invalid ImageMessage, the data or url must be provided"); + } + + var uri = new Uri(imageMessage.Url); + // download the image from the URL + using var client = new HttpClient(); + var response = client.GetAsync(uri).Result; + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"Failed to download the image from {uri}"); + } + + data = response.Content.ReadAsByteArrayAsync().Result; + } + + var base64Image = Convert.ToBase64String(data); + var message = imageMessage.From switch + { + null when imageMessage.Role == Role.User => new Message { Role = "user", Images = [base64Image] }, + null => throw new InvalidOperationException("Invalid Role, the role must be user"), + _ when imageMessage.From != agent.Name => new Message { Role = "user", Images = [base64Image] }, + _ => throw new InvalidOperationException("The from field must be null or the agent name"), + }; + + return [MessageEnvelope.Create(message, agent.Name)]; + } + + private IEnumerable ProcessTextMessage(TextMessage textMessage, IAgent agent) + { + if (textMessage.Role == Role.System) + { + var message = new Message + { + Role = "system", + Value = textMessage.Content + }; + + return [MessageEnvelope.Create(message, agent.Name)]; + } + else if (textMessage.From == agent.Name) + { + var message = new Message + { + Role = "assistant", + Value = textMessage.Content + }; + + return [MessageEnvelope.Create(message, agent.Name)]; + } + else + { + var message = textMessage.From switch + { + null when textMessage.Role == Role.User => new Message { Role = "user", Value = textMessage.Content }, + null when textMessage.Role == Role.Assistant => new Message { Role = "assistant", Value = textMessage.Content }, + null => throw new InvalidOperationException("Invalid Role"), + _ when textMessage.From != agent.Name => new Message { Role = "user", Value = textMessage.Content }, + _ => throw new InvalidOperationException("The from field must be null or the agent name"), + }; + + return [MessageEnvelope.Create(message, agent.Name)]; + } + } +} diff --git a/dotnet/src/Autogen.Ollama/OllamaConsts.cs b/dotnet/src/AutoGen.Ollama/OllamaConsts.cs similarity index 93% rename from dotnet/src/Autogen.Ollama/OllamaConsts.cs rename to dotnet/src/AutoGen.Ollama/OllamaConsts.cs index 49e91ebc318a..f305446a9aa4 100644 --- a/dotnet/src/Autogen.Ollama/OllamaConsts.cs +++ b/dotnet/src/AutoGen.Ollama/OllamaConsts.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // OllamaConsts.cs -namespace Autogen.Ollama; +namespace AutoGen.Ollama; public class OllamaConsts { diff --git a/dotnet/src/Autogen.Ollama/Middlewares/OllamaMessageConnector.cs b/dotnet/src/Autogen.Ollama/Middlewares/OllamaMessageConnector.cs deleted file mode 100644 index 6defedbe02a4..000000000000 --- a/dotnet/src/Autogen.Ollama/Middlewares/OllamaMessageConnector.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// OllamaMessageConnector.cs - -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using AutoGen.Core; - -namespace Autogen.Ollama; - -public class OllamaMessageConnector : IMiddleware, IStreamingMiddleware -{ - public string Name => nameof(OllamaMessageConnector); - - public async Task InvokeAsync(MiddlewareContext context, IAgent agent, - CancellationToken cancellationToken = default) - { - IEnumerable messages = context.Messages; - IMessage reply = await agent.GenerateReplyAsync(messages, context.Options, cancellationToken); - switch (reply) - { - case IMessage messageEnvelope: - Message? message = messageEnvelope.Content.Message; - return new TextMessage(Role.Assistant, message != null ? message.Value : "EMPTY_CONTENT", messageEnvelope.From); - default: - throw new NotSupportedException(); - } - } - - public async IAsyncEnumerable InvokeAsync(MiddlewareContext context, IStreamingAgent agent, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (IStreamingMessage? update in agent.GenerateStreamingReplyAsync(context.Messages, context.Options, cancellationToken)) - { - switch (update) - { - case IMessage complete: - { - string? textContent = complete.Content.Message?.Value; - yield return new TextMessage(Role.Assistant, textContent!, complete.From); - break; - } - case IMessage updatedMessage: - { - string? textContent = updatedMessage.Content.Message?.Value; - yield return new TextMessageUpdate(Role.Assistant, textContent, updatedMessage.From); - break; - } - default: - throw new InvalidOperationException("Message type not supported."); - } - } - } -} diff --git a/dotnet/test/AutoGen.Ollama.Tests/AutoGen.Ollama.Tests.csproj b/dotnet/test/AutoGen.Ollama.Tests/AutoGen.Ollama.Tests.csproj new file mode 100644 index 000000000000..27f80716f1c0 --- /dev/null +++ b/dotnet/test/AutoGen.Ollama.Tests/AutoGen.Ollama.Tests.csproj @@ -0,0 +1,33 @@ + + + + $(TestTargetFramework) + enable + false + True + + + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/dotnet/test/AutoGen.Ollama.Tests/OllamaAgentTests.cs b/dotnet/test/AutoGen.Ollama.Tests/OllamaAgentTests.cs new file mode 100644 index 000000000000..c1fb466f0b09 --- /dev/null +++ b/dotnet/test/AutoGen.Ollama.Tests/OllamaAgentTests.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OllamaAgentTests.cs + +using System.Text.Json; +using AutoGen.Core; +using AutoGen.Ollama.Extension; +using AutoGen.Tests; +using FluentAssertions; + +namespace AutoGen.Ollama.Tests; + +public class OllamaAgentTests +{ + [ApiKeyFact("OLLAMA_HOST", "OLLAMA_MODEL_NAME")] + public async Task GenerateReplyAsync_ReturnsValidMessage_WhenCalled() + { + string host = Environment.GetEnvironmentVariable("OLLAMA_HOST") + ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); + string modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") + ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set."); + OllamaAgent ollamaAgent = BuildOllamaAgent(host, modelName); + + var message = new Message("user", "hey how are you"); + var messages = new IMessage[] { MessageEnvelope.Create(message, from: modelName) }; + IMessage result = await ollamaAgent.GenerateReplyAsync(messages); + + result.Should().NotBeNull(); + result.Should().BeOfType>(); + result.From.Should().Be(ollamaAgent.Name); + } + + [ApiKeyFact("OLLAMA_HOST", "OLLAMA_MODEL_NAME")] + public async Task GenerateReplyAsync_ReturnsValidJsonMessageContent_WhenCalled() + { + string host = Environment.GetEnvironmentVariable("OLLAMA_HOST") + ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); + string modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") + ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set."); + OllamaAgent ollamaAgent = BuildOllamaAgent(host, modelName); + + var message = new Message("user", "What color is the sky at different times of the day? Respond using JSON"); + var messages = new IMessage[] { MessageEnvelope.Create(message, from: modelName) }; + IMessage result = await ollamaAgent.GenerateReplyAsync(messages, new OllamaReplyOptions + { + Format = FormatType.Json + }); + + result.Should().NotBeNull(); + result.Should().BeOfType>(); + result.From.Should().Be(ollamaAgent.Name); + + string jsonContent = ((MessageEnvelope)result).Content.Message!.Value; + bool isValidJson = IsValidJsonMessage(jsonContent); + isValidJson.Should().BeTrue(); + } + + [ApiKeyFact("OLLAMA_HOST", "OLLAMA_MODEL_NAME")] + public async Task GenerateStreamingReplyAsync_ReturnsValidMessages_WhenCalled() + { + string host = Environment.GetEnvironmentVariable("OLLAMA_HOST") + ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); + string modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") + ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set."); + OllamaAgent ollamaAgent = BuildOllamaAgent(host, modelName); + + var msg = new Message("user", "hey how are you"); + var messages = new IMessage[] { MessageEnvelope.Create(msg, from: modelName) }; + IStreamingMessage? finalReply = default; + await foreach (IStreamingMessage message in ollamaAgent.GenerateStreamingReplyAsync(messages)) + { + message.Should().NotBeNull(); + message.From.Should().Be(ollamaAgent.Name); + var streamingMessage = (IMessage)message; + if (streamingMessage.Content.Done) + { + finalReply = message; + break; + } + else + { + streamingMessage.Content.Message.Should().NotBeNull(); + streamingMessage.Content.Done.Should().BeFalse(); + } + } + + finalReply.Should().BeOfType>(); + var update = ((MessageEnvelope)finalReply!).Content; + update.Done.Should().BeTrue(); + update.TotalDuration.Should().BeGreaterThan(0); + } + + [ApiKeyFact("OLLAMA_HOST")] + public async Task ItReturnValidMessageUsingLLavaAsync() + { + var host = Environment.GetEnvironmentVariable("OLLAMA_HOST") + ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); + var modelName = "llava:latest"; + var ollamaAgent = BuildOllamaAgent(host, modelName); + var imagePath = Path.Combine("images", "image.png"); + var base64Image = Convert.ToBase64String(File.ReadAllBytes(imagePath)); + var message = new Message() + { + Role = "user", + Value = "What's the color of the background in this image", + Images = [base64Image], + }; + + var messages = new IMessage[] { MessageEnvelope.Create(message, from: modelName) }; + var reply = await ollamaAgent.GenerateReplyAsync(messages); + + reply.Should().BeOfType>(); + var chatResponse = ((MessageEnvelope)reply).Content; + chatResponse.Message.Should().NotBeNull(); + } + + [ApiKeyFact("OLLAMA_HOST")] + public async Task ItCanProcessMultiModalMessageUsingLLavaAsync() + { + var host = Environment.GetEnvironmentVariable("OLLAMA_HOST") + ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); + var modelName = "llava:latest"; + var ollamaAgent = BuildOllamaAgent(host, modelName) + .RegisterMessageConnector(); + var image = Path.Combine("images", "image.png"); + var binaryData = BinaryData.FromBytes(File.ReadAllBytes(image), "image/png"); + var imageMessage = new ImageMessage(Role.User, binaryData); + var textMessage = new TextMessage(Role.User, "What's in this image?"); + var multiModalMessage = new MultiModalMessage(Role.User, [textMessage, imageMessage]); + + var reply = await ollamaAgent.SendAsync(multiModalMessage); + reply.Should().BeOfType(); + reply.GetRole().Should().Be(Role.Assistant); + reply.GetContent().Should().NotBeNullOrEmpty(); + reply.From.Should().Be(ollamaAgent.Name); + } + + [ApiKeyFact("OLLAMA_HOST")] + public async Task ItCanProcessImageMessageUsingLLavaAsync() + { + var host = Environment.GetEnvironmentVariable("OLLAMA_HOST") + ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); + var modelName = "llava:latest"; + var ollamaAgent = BuildOllamaAgent(host, modelName) + .RegisterMessageConnector(); + var image = Path.Combine("images", "image.png"); + var binaryData = BinaryData.FromBytes(File.ReadAllBytes(image), "image/png"); + var imageMessage = new ImageMessage(Role.User, binaryData); + + var reply = await ollamaAgent.SendAsync(imageMessage); + reply.Should().BeOfType(); + reply.GetRole().Should().Be(Role.Assistant); + reply.GetContent().Should().NotBeNullOrEmpty(); + reply.From.Should().Be(ollamaAgent.Name); + } + + [ApiKeyFact("OLLAMA_HOST")] + public async Task ItReturnValidStreamingMessageUsingLLavaAsync() + { + var host = Environment.GetEnvironmentVariable("OLLAMA_HOST") + ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); + var modelName = "llava:latest"; + var ollamaAgent = BuildOllamaAgent(host, modelName); + var squareImagePath = Path.Combine("images", "square.png"); + var base64Image = Convert.ToBase64String(File.ReadAllBytes(squareImagePath)); + var imageMessage = new Message() + { + Role = "user", + Value = "What's in this image?", + Images = [base64Image], + }; + + var messages = new IMessage[] { MessageEnvelope.Create(imageMessage, from: modelName) }; + + IStreamingMessage? finalReply = default; + await foreach (IStreamingMessage message in ollamaAgent.GenerateStreamingReplyAsync(messages)) + { + message.Should().NotBeNull(); + message.From.Should().Be(ollamaAgent.Name); + var streamingMessage = (IMessage)message; + if (streamingMessage.Content.Done) + { + finalReply = message; + break; + } + else + { + streamingMessage.Content.Message.Should().NotBeNull(); + streamingMessage.Content.Done.Should().BeFalse(); + } + } + + finalReply.Should().BeOfType>(); + var update = ((MessageEnvelope)finalReply!).Content; + update.Done.Should().BeTrue(); + update.TotalDuration.Should().BeGreaterThan(0); + } + + private static bool IsValidJsonMessage(string input) + { + try + { + JsonDocument.Parse(input); + return true; + } + catch (JsonException) + { + return false; + } + catch (Exception ex) + { + Console.WriteLine("An unexpected exception occurred: " + ex.Message); + return false; + } + } + + private static OllamaAgent BuildOllamaAgent(string host, string modelName) + { + var httpClient = new HttpClient + { + BaseAddress = new Uri(host) + }; + return new OllamaAgent(httpClient, "TestAgent", modelName); + } +} diff --git a/dotnet/test/AutoGen.Ollama.Tests/OllamaMessageTests.cs b/dotnet/test/AutoGen.Ollama.Tests/OllamaMessageTests.cs new file mode 100644 index 000000000000..3f37db70275f --- /dev/null +++ b/dotnet/test/AutoGen.Ollama.Tests/OllamaMessageTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// OllamaMessageTests.cs + +using AutoGen.Core; +using AutoGen.Ollama; +using AutoGen.Tests; +using FluentAssertions; +using Xunit; +using Message = AutoGen.Ollama.Message; + +namespace Autogen.Ollama.Tests; + +public class OllamaMessageTests +{ + [Fact] + public async Task ItProcessUserTextMessageAsync() + { + var messageConnector = new OllamaMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, ct) => + { + msgs.Count().Should().Be(1); + var innerMessage = msgs.First(); + innerMessage.Should().BeOfType>(); + var message = (IMessage)innerMessage; + message.Content.Value.Should().Be("Hello"); + message.Content.Images.Should().BeNullOrEmpty(); + message.Content.Role.Should().Be("user"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(messageConnector); + + // when from is null and role is user + await agent.SendAsync("Hello"); + + // when from is user and role is user + var userMessage = new TextMessage(Role.User, "Hello", from: "user"); + await agent.SendAsync(userMessage); + + // when from is user but role is assistant + userMessage = new TextMessage(Role.Assistant, "Hello", from: "user"); + await agent.SendAsync(userMessage); + } + + [Fact] + public async Task ItProcessAssistantTextMessageAsync() + { + var messageConnector = new OllamaMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, ct) => + { + msgs.Count().Should().Be(1); + var innerMessage = msgs.First(); + innerMessage.Should().BeOfType>(); + var message = (IMessage)innerMessage; + message.Content.Value.Should().Be("Hello"); + message.Content.Images.Should().BeNullOrEmpty(); + message.Content.Role.Should().Be("assistant"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(messageConnector); + + // when from is null and role is assistant + var assistantMessage = new TextMessage(Role.Assistant, "Hello"); + await agent.SendAsync(assistantMessage); + + // when from is assistant and role is assistant + assistantMessage = new TextMessage(Role.Assistant, "Hello", from: "assistant"); + await agent.SendAsync(assistantMessage); + + // when from is assistant but role is user + assistantMessage = new TextMessage(Role.User, "Hello", from: "assistant"); + await agent.SendAsync(assistantMessage); + } + + [Fact] + public async Task ItProcessSystemTextMessageAsync() + { + var messageConnector = new OllamaMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, ct) => + { + msgs.Count().Should().Be(1); + var innerMessage = msgs.First(); + innerMessage.Should().BeOfType>(); + var message = (IMessage)innerMessage; + message.Content.Value.Should().Be("Hello"); + message.Content.Images.Should().BeNullOrEmpty(); + message.Content.Role.Should().Be("system"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(messageConnector); + + // when role is system + var systemMessage = new TextMessage(Role.System, "Hello"); + await agent.SendAsync(systemMessage); + } + + [Fact] + public async Task ItProcessImageMessageAsync() + { + var messageConnector = new OllamaMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, ct) => + { + msgs.Count().Should().Be(1); + var innerMessage = msgs.First(); + innerMessage.Should().BeOfType>(); + var message = (IMessage)innerMessage; + message.Content.Images!.Count.Should().Be(1); + message.Content.Role.Should().Be("user"); + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(messageConnector); + + var square = Path.Combine("images", "square.png"); + BinaryData imageBinaryData = BinaryData.FromBytes(File.ReadAllBytes(square), "image/png"); + var imageMessage = new ImageMessage(Role.User, imageBinaryData); + await agent.SendAsync(imageMessage); + } + + [Fact] + public async Task ItProcessMultiModalMessageAsync() + { + var messageConnector = new OllamaMessageConnector(); + var agent = new EchoAgent("assistant") + .RegisterMiddleware(async (msgs, _, innerAgent, ct) => + { + msgs.Count().Should().Be(2); + var textMessage = msgs.First(); + textMessage.Should().BeOfType>(); + var message = (IMessage)textMessage; + message.Content.Role.Should().Be("user"); + + var imageMessage = msgs.Last(); + imageMessage.Should().BeOfType>(); + message = (IMessage)imageMessage; + message.Content.Role.Should().Be("user"); + message.Content.Images!.Count.Should().Be(1); + + return await innerAgent.GenerateReplyAsync(msgs); + }) + .RegisterMiddleware(messageConnector); + + var square = Path.Combine("images", "square.png"); + BinaryData imageBinaryData = BinaryData.FromBytes(File.ReadAllBytes(square), "image/png"); + var imageMessage = new ImageMessage(Role.User, imageBinaryData); + var textMessage = new TextMessage(Role.User, "Hello"); + var multiModalMessage = new MultiModalMessage(Role.User, [textMessage, imageMessage]); + + await agent.SendAsync(multiModalMessage); + } +} diff --git a/dotnet/test/Autogen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs b/dotnet/test/AutoGen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs similarity index 97% rename from dotnet/test/Autogen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs rename to dotnet/test/AutoGen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs index 7f2d94e147db..06522bdd8238 100644 --- a/dotnet/test/Autogen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs +++ b/dotnet/test/AutoGen.Ollama.Tests/OllamaTextEmbeddingServiceTests.cs @@ -4,7 +4,7 @@ using AutoGen.Tests; using FluentAssertions; -namespace Autogen.Ollama.Tests; +namespace AutoGen.Ollama.Tests; public class OllamaTextEmbeddingServiceTests { diff --git a/dotnet/test/AutoGen.Ollama.Tests/images/image.png b/dotnet/test/AutoGen.Ollama.Tests/images/image.png new file mode 100644 index 000000000000..ca276f81f5b0 --- /dev/null +++ b/dotnet/test/AutoGen.Ollama.Tests/images/image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:300b7c9d6ba0c23a3e52fbd2e268141ddcca0434a9fb9dcf7e58e7e903d36dcf +size 2126185 diff --git a/dotnet/test/AutoGen.Ollama.Tests/images/square.png b/dotnet/test/AutoGen.Ollama.Tests/images/square.png new file mode 100644 index 000000000000..afb4f4cd4df8 --- /dev/null +++ b/dotnet/test/AutoGen.Ollama.Tests/images/square.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8323d0b8eceb752e14c29543b2e28bb2fc648ed9719095c31b7708867a4dc918 +size 491 diff --git a/dotnet/test/Autogen.Ollama.Tests/Autogen.Ollama.Tests.csproj b/dotnet/test/Autogen.Ollama.Tests/Autogen.Ollama.Tests.csproj deleted file mode 100644 index a10ce496ae7e..000000000000 --- a/dotnet/test/Autogen.Ollama.Tests/Autogen.Ollama.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - True - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/dotnet/test/Autogen.Ollama.Tests/OllamaAgentTests.cs b/dotnet/test/Autogen.Ollama.Tests/OllamaAgentTests.cs deleted file mode 100644 index b22432fdad69..000000000000 --- a/dotnet/test/Autogen.Ollama.Tests/OllamaAgentTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// OllamaAgentTests.cs - -using System.Text.Json; -using AutoGen.Core; -using AutoGen.Tests; -using FluentAssertions; - -namespace Autogen.Ollama.Tests; - -public class OllamaAgentTests -{ - - [ApiKeyFact("OLLAMA_HOST", "OLLAMA_MODEL_NAME")] - public async Task GenerateReplyAsync_ReturnsValidMessage_WhenCalled() - { - string host = Environment.GetEnvironmentVariable("OLLAMA_HOST") - ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); - string modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") - ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set."); - OllamaAgent ollamaAgent = BuildOllamaAgent(host, modelName); - - var messages = new IMessage[] { new TextMessage(Role.User, "Hello, how are you") }; - IMessage result = await ollamaAgent.GenerateReplyAsync(messages); - - result.Should().NotBeNull(); - result.Should().BeOfType>(); - result.From.Should().Be(ollamaAgent.Name); - } - - [ApiKeyFact("OLLAMA_HOST", "OLLAMA_MODEL_NAME")] - public async Task GenerateReplyAsync_ReturnsValidJsonMessageContent_WhenCalled() - { - string host = Environment.GetEnvironmentVariable("OLLAMA_HOST") - ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); - string modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") - ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set."); - OllamaAgent ollamaAgent = BuildOllamaAgent(host, modelName); - - var messages = new IMessage[] { new TextMessage(Role.User, "Hello, how are you") }; - IMessage result = await ollamaAgent.GenerateReplyAsync(messages, new OllamaReplyOptions - { - Format = FormatType.Json - }); - - result.Should().NotBeNull(); - result.Should().BeOfType>(); - result.From.Should().Be(ollamaAgent.Name); - - string jsonContent = ((MessageEnvelope)result).Content.Message!.Value; - bool isValidJson = IsValidJsonMessage(jsonContent); - isValidJson.Should().BeTrue(); - } - - [ApiKeyFact("OLLAMA_HOST", "OLLAMA_MODEL_NAME")] - public async Task GenerateStreamingReplyAsync_ReturnsValidMessages_WhenCalled() - { - string host = Environment.GetEnvironmentVariable("OLLAMA_HOST") - ?? throw new InvalidOperationException("OLLAMA_HOST is not set."); - string modelName = Environment.GetEnvironmentVariable("OLLAMA_MODEL_NAME") - ?? throw new InvalidOperationException("OLLAMA_MODEL_NAME is not set."); - OllamaAgent ollamaAgent = BuildOllamaAgent(host, modelName); - - var messages = new IMessage[] { new TextMessage(Role.User, "Hello how are you") }; - IStreamingMessage? finalReply = default; - await foreach (IStreamingMessage message in ollamaAgent.GenerateStreamingReplyAsync(messages)) - { - message.Should().NotBeNull(); - message.From.Should().Be(ollamaAgent.Name); - finalReply = message; - } - - finalReply.Should().BeOfType>(); - } - - private static bool IsValidJsonMessage(string input) - { - try - { - JsonDocument.Parse(input); - return true; - } - catch (JsonException) - { - return false; - } - catch (Exception ex) - { - Console.WriteLine("An unexpected exception occurred: " + ex.Message); - return false; - } - } - - private static OllamaAgent BuildOllamaAgent(string host, string modelName) - { - var httpClient = new HttpClient - { - BaseAddress = new Uri(host) - }; - return new OllamaAgent(httpClient, "TestAgent", modelName); - } -}