11using DotNet . Testcontainers . Builders ;
2+ using DotNet . Testcontainers . Containers ;
23
34namespace Ollama . IntegrationTests ;
45
56public partial class Tests
67{
7- private static async Task < Environment > PrepareEnvironmentAsync ( EnvironmentType environmentType , string model = "" )
8+ private static async Task < TestEnvironment > PrepareEnvironmentAsync ( TestEnvironmentType environmentType , string model = "" )
89 {
910 switch ( environmentType )
1011 {
11- case EnvironmentType . Local :
12+ case TestEnvironmentType . Local :
13+ return await CreateEnvironmentAsync (
14+ new OllamaApiClient ( baseUri : CreateLocalBaseUri ( ) ) ,
15+ model ) . ConfigureAwait ( false ) ;
16+ case TestEnvironmentType . Container :
1217 {
13- // set OLLAMA_HOST=172.16.50.107:11434
14- // ollama serve
15- var apiClient = new OllamaApiClient ( baseUri : new Uri ( "http://172.16.50.107:11434/api" ) ) ;
16-
17- if ( ! string . IsNullOrEmpty ( model ) )
18- {
19- await apiClient . PullModelAndEnsureSuccessAsync ( model ) ;
20- }
18+ const int ollamaPort = 11434 ;
2119
22- return new Environment
23- {
24- ApiClient = apiClient ,
25- } ;
26- }
27- case EnvironmentType . Container :
28- {
2920 var container = new ContainerBuilder ( )
3021 . WithImage ( "ollama/ollama" )
31- . WithPortBinding ( hostPort : 11434 , containerPort : 11434 )
32- . WithWaitStrategy ( Wait . ForUnixContainer ( ) . UntilPortIsAvailable ( 11434 ) )
22+ . WithPortBinding ( ollamaPort , true )
23+ . WithWaitStrategy ( Wait . ForUnixContainer ( ) . UntilPortIsAvailable ( ollamaPort ) )
3324 . Build ( ) ;
34-
35- await container . StartAsync ( ) ;
36-
37- var apiClient = new OllamaApiClient ( ) ;
38- if ( ! string . IsNullOrEmpty ( model ) )
39- {
40- await apiClient . PullModelAndEnsureSuccessAsync ( model ) ;
41- }
42-
43- return new Environment
44- {
45- Container = container ,
46- ApiClient = apiClient ,
47- } ;
25+
26+ await container . StartAsync ( ) . ConfigureAwait ( false ) ;
27+
28+ return await CreateEnvironmentAsync (
29+ new OllamaApiClient (
30+ baseUri : new Uri ( $ "http://{ container . Hostname } :{ container . GetMappedPublicPort ( ollamaPort ) } /api") ) ,
31+ model ,
32+ container ) . ConfigureAwait ( false ) ;
4833 }
4934 default :
5035 throw new ArgumentOutOfRangeException ( nameof ( environmentType ) , environmentType , null ) ;
5136 }
5237 }
53-
38+
39+ private static Uri CreateLocalBaseUri ( )
40+ {
41+ var ollamaHost = System . Environment . GetEnvironmentVariable ( "OLLAMA_HOST" ) ;
42+ if ( string . IsNullOrWhiteSpace ( ollamaHost ) )
43+ {
44+ return new Uri ( "http://localhost:11434/api" ) ;
45+ }
46+
47+ if ( ! ollamaHost . Contains ( "://" , StringComparison . Ordinal ) )
48+ {
49+ ollamaHost = $ "http://{ ollamaHost } ";
50+ }
51+
52+ if ( ! ollamaHost . EndsWith ( "/api" , StringComparison . OrdinalIgnoreCase ) )
53+ {
54+ ollamaHost = $ "{ ollamaHost . TrimEnd ( '/' ) } /api";
55+ }
56+
57+ return new Uri ( ollamaHost , UriKind . Absolute ) ;
58+ }
59+
60+ private static async Task < TestEnvironment > CreateEnvironmentAsync (
61+ OllamaApiClient apiClient ,
62+ string model ,
63+ IContainer ? container = null )
64+ {
65+ if ( ! string . IsNullOrEmpty ( model ) )
66+ {
67+ await PullModelAndEnsureSuccessAsync ( apiClient , model ) . ConfigureAwait ( false ) ;
68+ }
69+
70+ return new TestEnvironment ( apiClient , container ) ;
71+ }
72+
73+ private static async Task PullModelAndEnsureSuccessAsync ( OllamaApiClient apiClient , string model )
74+ {
75+ var response = await ReadLastAsync (
76+ apiClient . PullModelAsync ( model , stream : false ) ) . ConfigureAwait ( false ) ;
77+
78+ response . Status . Should ( ) . Be ( PullModelResponseStatus . Success ) ;
79+ }
80+
81+ private static async Task < T > ReadLastAsync < T > ( IAsyncEnumerable < T > source )
82+ {
83+ var hasValue = false ;
84+ var last = default ( T ) ;
85+
86+ await foreach ( var item in source . ConfigureAwait ( false ) )
87+ {
88+ last = item ;
89+ hasValue = true ;
90+ }
91+
92+ if ( ! hasValue )
93+ {
94+ throw new InvalidOperationException ( "The operation did not return any results." ) ;
95+ }
96+
97+ return last ! ;
98+ }
99+
54100 [ TestMethod ]
55101 public async Task ListModels ( )
56102 {
57- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Container ) ;
58-
103+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Container ) ;
104+
59105 var models = await container . ApiClient . ListModelsAsync ( ) ;
60106 models . Models . Should ( ) . BeNullOrEmpty ( ) ;
61-
62- await container . ApiClient . PullModelAndEnsureSuccessAsync ( "all-minilm" ) ;
63-
107+
108+ await PullModelAndEnsureSuccessAsync ( container . ApiClient , "all-minilm" ) ;
109+
64110 models = await container . ApiClient . ListModelsAsync ( ) ;
65111 models . Models . Should ( ) . NotBeNull ( ) ;
66112 models . Models . Should ( ) . HaveCount ( 1 ) ;
67113 models . Models ! [ 0 ] . Model . Should ( ) . Be ( "all-minilm:latest" ) ;
68114 }
69-
115+
70116 [ TestMethod ]
71117 public async Task PullModel ( )
72118 {
73- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Container ) ;
74-
119+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Container ) ;
120+
75121 await foreach ( var response in container . ApiClient . PullModelAsync ( "all-minilm" , stream : true ) )
76122 {
77123 Console . WriteLine ( $ "{ response . Status } . Progress: { response . Completed } /{ response . Total } ") ;
78124 }
79-
80- var response2 = await container . ApiClient . PullModelAsync ( "all-minilm" ) . WaitAsync ( ) ;
81- response2 . EnsureSuccess ( ) ;
82-
83- await container . ApiClient . PullModelAndEnsureSuccessAsync ( "all-minilm" ) ;
125+
126+ var response2 = await ReadLastAsync ( container . ApiClient . PullModelAsync ( "all-minilm" , stream : false ) ) ;
127+ response2 . Status . Should ( ) . Be ( PullModelResponseStatus . Success ) ;
128+
129+ await PullModelAndEnsureSuccessAsync ( container . ApiClient , "all-minilm" ) ;
84130 }
85-
131+
86132 [ TestMethod ]
87133 public async Task Embeddings ( )
88134 {
89- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Container ) ;
90-
91- await container . ApiClient . PullModelAndEnsureSuccessAsync ( "all-minilm" ) ;
92-
135+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Container ) ;
136+
137+ await PullModelAndEnsureSuccessAsync ( container . ApiClient , "all-minilm" ) ;
138+
93139 var embeddingResponse = await container . ApiClient . GenerateEmbeddingAsync (
94- model : "all-minilm" ,
140+ model : "all-minilm" ,
95141 prompt : "hello" ) ;
96-
142+
97143 embeddingResponse . Embedding . Should ( ) . NotBeNullOrEmpty ( ) ;
98144 }
99-
145+
100146 [ TestMethod ]
101147 public async Task GetCompletionWithOptions ( )
102148 {
103149#if DEBUG
104- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Local , "llama3" ) ;
150+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Local , "llama3" ) ;
105151#else
106- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Container , "llama3" ) ;
152+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Container , "llama3" ) ;
107153#endif
108154
109- var response = await container . ApiClient . GenerateCompletionAsync ( new GenerateCompletionRequest
155+ var response = await ReadLastAsync ( container . ApiClient . GenerateCompletionAsync ( new GenerateCompletionRequest
110156 {
111157 Model = "llama3" ,
112158 Prompt = "answer me just \" 123\" " ,
113- Stream = true ,
159+ Stream = false ,
114160 Options = new RequestOptions
115161 {
116162 Temperature = 0 ,
117163 } ,
118- } ) . WaitAsync ( ) ;
164+ } ) ) ;
119165 Console . WriteLine ( response . Response ) ;
120166 }
121-
167+
122168 [ TestMethod ]
123169 public async Task GetCompletion ( )
124170 {
125171#if DEBUG
126- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Local , "llama3" ) ;
172+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Local , "llama3" ) ;
127173#else
128- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Container , "llama3" ) ;
174+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Container , "llama3" ) ;
129175#endif
130176
131177 IList < long > ? context = null ;
132178 var enumerable = container . ApiClient . GenerateCompletionAsync ( "llama3" , "answer 5 random words" , stream : true ) ;
133179 await foreach ( var response in enumerable )
134180 {
135181 Console . WriteLine ( $ "> { response . Response } ") ;
136-
182+
137183 context = response . Context ;
138184 }
139-
140- var lastResponse = await container . ApiClient . GenerateCompletionAsync ( "llama3" , "answer 123" , stream : false , context : context ) . WaitAsync ( ) ;
185+
186+ var lastResponse = await ReadLastAsync (
187+ container . ApiClient . GenerateCompletionAsync ( "llama3" , "answer 123" , stream : false , context : context ) ) ;
141188 Console . WriteLine ( lastResponse . Response ) ;
142189 }
143-
190+
144191 [ TestMethod ]
145192 public async Task GetChat ( )
146193 {
147194#if DEBUG
148- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Local , "llama3" ) ;
195+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Local , "llama3" ) ;
149196#else
150- await using var container = await PrepareEnvironmentAsync ( EnvironmentType . Container , "llama3" ) ;
197+ await using var container = await PrepareEnvironmentAsync ( TestEnvironmentType . Container , "llama3" ) ;
151198#endif
152-
153- var chat = container . ApiClient . Chat ( "llama3" ) ;
154- var message = await chat . SendAsync ( "answer in 5 words" ) ;
155-
156- Console . WriteLine ( message . Content ) ;
199+
200+ var response = await ReadLastAsync ( container . ApiClient . GenerateChatCompletionAsync (
201+ model : "llama3" ,
202+ messages :
203+ [
204+ new Message
205+ {
206+ Role = MessageRole . User ,
207+ Content = "answer in 5 words" ,
208+ } ,
209+ ] ,
210+ stream : false ) ) ;
211+
212+ Console . WriteLine ( response . Message ? . Content ) ;
213+ }
214+ }
215+
216+ internal enum TestEnvironmentType
217+ {
218+ Local ,
219+ Container ,
220+ }
221+
222+ internal sealed class TestEnvironment : IAsyncDisposable
223+ {
224+ internal TestEnvironment ( OllamaApiClient apiClient , IContainer ? container = null )
225+ {
226+ ApiClient = apiClient ;
227+ Container = container ;
228+ }
229+
230+ internal OllamaApiClient ApiClient { get ; }
231+
232+ internal IContainer ? Container { get ; }
233+
234+ public async ValueTask DisposeAsync ( )
235+ {
236+ ApiClient . Dispose ( ) ;
237+
238+ if ( Container is not null )
239+ {
240+ await Container . DisposeAsync ( ) . ConfigureAwait ( false ) ;
241+ }
157242 }
158- }
243+ }
0 commit comments