Skip to content

Commit 1c3ae92

Browse files
[.Net] feature: Ollama integration (#2693)
* [.Net] feature: Ollama integration with * [.Net] ollama agent improvements and reorganization * added ollama fact logic * [.Net] added ollama embeddings service * [.Net] Ollama embeddings integration * cleaned the agent and connector code * [.Net] cleaned ollama agent tests * [.Net] standardize api key fact ollama host variable * [.Net] fixed solution issue --------- Co-authored-by: Xiaoyun Zhang <[email protected]>
1 parent 8457757 commit 1c3ae92

17 files changed

+953
-0
lines changed

dotnet/AutoGen.sln

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoGen.SemanticKernel.Test
3737
EndProject
3838
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoGen.DotnetInteractive.Tests", "test\AutoGen.DotnetInteractive.Tests\AutoGen.DotnetInteractive.Tests.csproj", "{B61388CA-DC73-4B7F-A7B2-7B9A86C9229E}"
3939
EndProject
40+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autogen.Ollama", "src\Autogen.Ollama\Autogen.Ollama.csproj", "{A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}"
41+
EndProject
42+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Autogen.Ollama.Tests", "test\Autogen.Ollama.Tests\Autogen.Ollama.Tests.csproj", "{C24FDE63-952D-4F8E-A807-AF31D43AD675}"
43+
EndProject
4044
Global
4145
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4246
Debug|Any CPU = Debug|Any CPU
@@ -91,6 +95,14 @@ Global
9195
{15441693-3659-4868-B6C1-B106F52FF3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
9296
{15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
9397
{15441693-3659-4868-B6C1-B106F52FF3BA}.Release|Any CPU.Build.0 = Release|Any CPU
98+
{A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
99+
{A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
100+
{A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
101+
{A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1}.Release|Any CPU.Build.0 = Release|Any CPU
102+
{C24FDE63-952D-4F8E-A807-AF31D43AD675}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
103+
{C24FDE63-952D-4F8E-A807-AF31D43AD675}.Debug|Any CPU.Build.0 = Debug|Any CPU
104+
{C24FDE63-952D-4F8E-A807-AF31D43AD675}.Release|Any CPU.ActiveCfg = Release|Any CPU
105+
{C24FDE63-952D-4F8E-A807-AF31D43AD675}.Release|Any CPU.Build.0 = Release|Any CPU
94106
{1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
95107
{1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Debug|Any CPU.Build.0 = Debug|Any CPU
96108
{1DFABC4A-8458-4875-8DCB-59F3802DAC65}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -116,6 +128,8 @@ Global
116128
{63445BB7-DBB9-4AEF-9D6F-98BBE75EE1EC} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
117129
{6585D1A4-3D97-4D76-A688-1933B61AEB19} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
118130
{15441693-3659-4868-B6C1-B106F52FF3BA} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
131+
{A4EFA175-44CC-44A9-B93E-1C7B6FAC38F1} = {18BF8DD7-0585-48BF-8F97-AD333080CE06}
132+
{C24FDE63-952D-4F8E-A807-AF31D43AD675} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
119133
{1DFABC4A-8458-4875-8DCB-59F3802DAC65} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
120134
{B61388CA-DC73-4B7F-A7B2-7B9A86C9229E} = {F823671B-3ECA-4AE6-86DA-25E920D3FE64}
121135
EndGlobalSection
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// OllamaAgent.cs
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Net.Http;
9+
using System.Runtime.CompilerServices;
10+
using System.Text;
11+
using System.Text.Json;
12+
using System.Threading;
13+
using System.Threading.Tasks;
14+
using AutoGen.Core;
15+
16+
namespace Autogen.Ollama;
17+
18+
/// <summary>
19+
/// An agent that can interact with ollama models.
20+
/// </summary>
21+
public class OllamaAgent : IStreamingAgent
22+
{
23+
private readonly HttpClient _httpClient;
24+
public string Name { get; }
25+
private readonly string _modelName;
26+
private readonly string _systemMessage;
27+
private readonly OllamaReplyOptions? _replyOptions;
28+
29+
public OllamaAgent(HttpClient httpClient, string name, string modelName,
30+
string systemMessage = "You are a helpful AI assistant",
31+
OllamaReplyOptions? replyOptions = null)
32+
{
33+
Name = name;
34+
_httpClient = httpClient;
35+
_modelName = modelName;
36+
_systemMessage = systemMessage;
37+
_replyOptions = replyOptions;
38+
}
39+
public async Task<IMessage> GenerateReplyAsync(
40+
IEnumerable<IMessage> messages, GenerateReplyOptions? options = null, CancellationToken cancellation = default)
41+
{
42+
ChatRequest request = await BuildChatRequest(messages, options);
43+
request.Stream = false;
44+
using (HttpResponseMessage? response = await _httpClient
45+
.SendAsync(BuildRequestMessage(request), HttpCompletionOption.ResponseContentRead, cancellation))
46+
{
47+
response.EnsureSuccessStatusCode();
48+
Stream? streamResponse = await response.Content.ReadAsStreamAsync();
49+
ChatResponse chatResponse = await JsonSerializer.DeserializeAsync<ChatResponse>(streamResponse, cancellationToken: cancellation)
50+
?? throw new Exception("Failed to deserialize response");
51+
var output = new MessageEnvelope<ChatResponse>(chatResponse, from: Name);
52+
return output;
53+
}
54+
}
55+
public async IAsyncEnumerable<IStreamingMessage> GenerateStreamingReplyAsync(
56+
IEnumerable<IMessage> messages,
57+
GenerateReplyOptions? options = null,
58+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
59+
{
60+
ChatRequest request = await BuildChatRequest(messages, options);
61+
request.Stream = true;
62+
HttpRequestMessage message = BuildRequestMessage(request);
63+
using (HttpResponseMessage? response = await _httpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
64+
{
65+
response.EnsureSuccessStatusCode();
66+
using Stream? stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
67+
using var reader = new StreamReader(stream);
68+
69+
while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
70+
{
71+
string? line = await reader.ReadLineAsync();
72+
if (string.IsNullOrWhiteSpace(line)) continue;
73+
74+
ChatResponseUpdate? update = JsonSerializer.Deserialize<ChatResponseUpdate>(line);
75+
if (update != null)
76+
{
77+
yield return new MessageEnvelope<ChatResponseUpdate>(update, from: Name);
78+
}
79+
80+
if (update is { Done: false }) continue;
81+
82+
ChatResponse? chatMessage = JsonSerializer.Deserialize<ChatResponse>(line);
83+
if (chatMessage == null) continue;
84+
yield return new MessageEnvelope<ChatResponse>(chatMessage, from: Name);
85+
}
86+
}
87+
}
88+
private async Task<ChatRequest> BuildChatRequest(IEnumerable<IMessage> messages, GenerateReplyOptions? options)
89+
{
90+
var request = new ChatRequest
91+
{
92+
Model = _modelName,
93+
Messages = await BuildChatHistory(messages)
94+
};
95+
96+
if (options is OllamaReplyOptions replyOptions)
97+
{
98+
BuildChatRequestOptions(replyOptions, request);
99+
return request;
100+
}
101+
102+
if (_replyOptions != null)
103+
{
104+
BuildChatRequestOptions(_replyOptions, request);
105+
return request;
106+
}
107+
return request;
108+
}
109+
private void BuildChatRequestOptions(OllamaReplyOptions replyOptions, ChatRequest request)
110+
{
111+
request.Format = replyOptions.Format == FormatType.Json ? OllamaConsts.JsonFormatType : null;
112+
request.Template = replyOptions.Template;
113+
request.KeepAlive = replyOptions.KeepAlive;
114+
115+
if (replyOptions.Temperature != null
116+
|| replyOptions.MaxToken != null
117+
|| replyOptions.StopSequence != null
118+
|| replyOptions.Seed != null
119+
|| replyOptions.MiroStat != null
120+
|| replyOptions.MiroStatEta != null
121+
|| replyOptions.MiroStatTau != null
122+
|| replyOptions.NumCtx != null
123+
|| replyOptions.NumGqa != null
124+
|| replyOptions.NumGpu != null
125+
|| replyOptions.NumThread != null
126+
|| replyOptions.RepeatLastN != null
127+
|| replyOptions.RepeatPenalty != null
128+
|| replyOptions.TopK != null
129+
|| replyOptions.TopP != null
130+
|| replyOptions.TfsZ != null)
131+
{
132+
request.Options = new ModelReplyOptions
133+
{
134+
Temperature = replyOptions.Temperature,
135+
NumPredict = replyOptions.MaxToken,
136+
Stop = replyOptions.StopSequence?[0],
137+
Seed = replyOptions.Seed,
138+
MiroStat = replyOptions.MiroStat,
139+
MiroStatEta = replyOptions.MiroStatEta,
140+
MiroStatTau = replyOptions.MiroStatTau,
141+
NumCtx = replyOptions.NumCtx,
142+
NumGqa = replyOptions.NumGqa,
143+
NumGpu = replyOptions.NumGpu,
144+
NumThread = replyOptions.NumThread,
145+
RepeatLastN = replyOptions.RepeatLastN,
146+
RepeatPenalty = replyOptions.RepeatPenalty,
147+
TopK = replyOptions.TopK,
148+
TopP = replyOptions.TopP,
149+
TfsZ = replyOptions.TfsZ
150+
};
151+
}
152+
}
153+
private async Task<List<Message>> BuildChatHistory(IEnumerable<IMessage> messages)
154+
{
155+
if (!messages.Any(m => m.IsSystemMessage()))
156+
{
157+
var systemMessage = new TextMessage(Role.System, _systemMessage, from: Name);
158+
messages = new[] { systemMessage }.Concat(messages);
159+
}
160+
161+
var collection = new List<Message>();
162+
foreach (IMessage? message in messages)
163+
{
164+
Message item;
165+
switch (message)
166+
{
167+
case TextMessage tm:
168+
item = new Message { Role = tm.Role.ToString(), Value = tm.Content };
169+
break;
170+
case ImageMessage im:
171+
string base64Image = await ImageUrlToBase64(im.Url!);
172+
item = new Message { Role = im.Role.ToString(), Images = [base64Image] };
173+
break;
174+
case MultiModalMessage mm:
175+
var textsGroupedByRole = mm.Content.OfType<TextMessage>().GroupBy(tm => tm.Role)
176+
.ToDictionary(g => g.Key, g => string.Join(Environment.NewLine, g.Select(tm => tm.Content)));
177+
178+
string content = string.Join($"{Environment.NewLine}", textsGroupedByRole
179+
.Select(g => $"{g.Key}{Environment.NewLine}:{g.Value}"));
180+
181+
IEnumerable<Task<string>> imagesConversionTasks = mm.Content
182+
.OfType<ImageMessage>()
183+
.Select(async im => await ImageUrlToBase64(im.Url!));
184+
185+
string[]? imagesBase64 = await Task.WhenAll(imagesConversionTasks);
186+
item = new Message { Role = mm.Role.ToString(), Value = content, Images = imagesBase64 };
187+
break;
188+
default:
189+
throw new NotSupportedException();
190+
}
191+
192+
collection.Add(item);
193+
}
194+
195+
return collection;
196+
}
197+
private static HttpRequestMessage BuildRequestMessage(ChatRequest request)
198+
{
199+
string serialized = JsonSerializer.Serialize(request);
200+
return new HttpRequestMessage(HttpMethod.Post, OllamaConsts.ChatCompletionEndpoint)
201+
{
202+
Content = new StringContent(serialized, Encoding.UTF8, OllamaConsts.JsonMediaType)
203+
};
204+
}
205+
private async Task<string> ImageUrlToBase64(string imageUrl)
206+
{
207+
if (string.IsNullOrWhiteSpace(imageUrl))
208+
{
209+
throw new ArgumentException("required parameter", nameof(imageUrl));
210+
}
211+
byte[] imageBytes = await _httpClient.GetByteArrayAsync(imageUrl);
212+
return imageBytes != null
213+
? Convert.ToBase64String(imageBytes)
214+
: throw new InvalidOperationException("no image byte array");
215+
}
216+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<GenerateDocumentationFile>True</GenerateDocumentationFile>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<ProjectReference Include="..\AutoGen.Core\AutoGen.Core.csproj" />
10+
</ItemGroup>
11+
12+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// ChatRequest.cs
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text.Json.Serialization;
7+
8+
namespace Autogen.Ollama;
9+
10+
public class ChatRequest
11+
{
12+
/// <summary>
13+
/// (required) the model name
14+
/// </summary>
15+
[JsonPropertyName("model")]
16+
public string Model { get; set; } = string.Empty;
17+
18+
/// <summary>
19+
/// the messages of the chat, this can be used to keep a chat memory
20+
/// </summary>
21+
[JsonPropertyName("messages")]
22+
public IList<Message> Messages { get; set; } = Array.Empty<Message>();
23+
24+
/// <summary>
25+
/// the format to return a response in. Currently, the only accepted value is json
26+
/// </summary>
27+
[JsonPropertyName("format")]
28+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
29+
public string? Format { get; set; }
30+
31+
/// <summary>
32+
/// additional model parameters listed in the documentation for the Modelfile such as temperature
33+
/// </summary>
34+
[JsonPropertyName("options")]
35+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
36+
public ModelReplyOptions? Options { get; set; }
37+
/// <summary>
38+
/// the prompt template to use (overrides what is defined in the Modelfile)
39+
/// </summary>
40+
[JsonPropertyName("template")]
41+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
42+
public string? Template { get; set; }
43+
/// <summary>
44+
/// if false the response will be returned as a single response object, rather than a stream of objects
45+
/// </summary>
46+
[JsonPropertyName("stream")]
47+
public bool Stream { get; set; }
48+
/// <summary>
49+
/// controls how long the model will stay loaded into memory following the request (default: 5m)
50+
/// </summary>
51+
[JsonPropertyName("keep_alive")]
52+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
53+
public string? KeepAlive { get; set; }
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// ChatResponse.cs
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Autogen.Ollama;
7+
8+
public class ChatResponse : ChatResponseUpdate
9+
{
10+
/// <summary>
11+
/// time spent generating the response
12+
/// </summary>
13+
[JsonPropertyName("total_duration")]
14+
public long TotalDuration { get; set; }
15+
16+
/// <summary>
17+
/// time spent in nanoseconds loading the model
18+
/// </summary>
19+
[JsonPropertyName("load_duration")]
20+
public long LoadDuration { get; set; }
21+
22+
/// <summary>
23+
/// number of tokens in the prompt
24+
/// </summary>
25+
[JsonPropertyName("prompt_eval_count")]
26+
public int PromptEvalCount { get; set; }
27+
28+
/// <summary>
29+
/// time spent in nanoseconds evaluating the prompt
30+
/// </summary>
31+
[JsonPropertyName("prompt_eval_duration")]
32+
public long PromptEvalDuration { get; set; }
33+
34+
/// <summary>
35+
/// number of tokens the response
36+
/// </summary>
37+
[JsonPropertyName("eval_count")]
38+
public int EvalCount { get; set; }
39+
40+
/// <summary>
41+
/// time in nanoseconds spent generating the response
42+
/// </summary>
43+
[JsonPropertyName("eval_duration")]
44+
public long EvalDuration { get; set; }
45+
}

0 commit comments

Comments
 (0)