1
- using Aspire . Hosting . ApplicationModel ;
1
+ using Aspire . Hosting ;
2
+ using Aspire . Hosting . ApplicationModel ;
2
3
using Aspire . Hosting . Lifecycle ;
4
+ using Microsoft . Extensions . Logging ;
3
5
using OllamaSharp ;
4
6
using OllamaSharp . Models ;
7
+ using System . Globalization ;
5
8
6
9
namespace CommunityToolkit . Aspire . Hosting . Ollama ;
7
- internal class OllamaResourceLifecycleHook ( ResourceNotificationService notificationService ) : IDistributedApplicationLifecycleHook , IAsyncDisposable
10
+ internal class OllamaResourceLifecycleHook (
11
+ ResourceLoggerService loggerService ,
12
+ ResourceNotificationService notificationService ,
13
+ DistributedApplicationExecutionContext context ) : IDistributedApplicationLifecycleHook , IAsyncDisposable
8
14
{
9
15
private readonly ResourceNotificationService _notificationService = notificationService ;
10
16
11
17
private readonly CancellationTokenSource _tokenSource = new ( ) ;
12
18
13
19
public Task AfterResourcesCreatedAsync ( DistributedApplicationModel appModel , CancellationToken cancellationToken = default )
14
20
{
21
+ if ( context . IsPublishMode )
22
+ {
23
+ return Task . CompletedTask ;
24
+ }
25
+
15
26
foreach ( var resource in appModel . Resources . OfType < OllamaResource > ( ) )
16
27
{
17
28
DownloadModel ( resource , _tokenSource . Token ) ;
@@ -27,11 +38,13 @@ private void DownloadModel(OllamaResource resource, CancellationToken cancellati
27
38
return ;
28
39
}
29
40
41
+ var logger = loggerService . GetLogger ( resource ) ;
42
+
30
43
_ = Task . Run ( async ( ) =>
31
44
{
32
45
try
33
46
{
34
- var connectionString = await resource . ConnectionStringExpression . GetValueAsync ( cancellationToken ) ;
47
+ var connectionString = await resource . ConnectionStringExpression . GetValueAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
35
48
36
49
if ( string . IsNullOrWhiteSpace ( connectionString ) )
37
50
{
@@ -47,7 +60,18 @@ private void DownloadModel(OllamaResource resource, CancellationToken cancellati
47
60
48
61
if ( ! hasModel )
49
62
{
50
- await PullModel ( resource , ollamaClient , model , cancellationToken ) ;
63
+ logger . LogInformation ( "{TimeStamp}: [{Model}] needs to be downloaded for {ResourceName}" ,
64
+ DateTime . UtcNow . ToString ( "yyyy-MM-ddTHH:mm:ss.fffZ" , CultureInfo . InvariantCulture ) ,
65
+ resource . ModelName ,
66
+ resource . Name ) ;
67
+ await PullModel ( resource , ollamaClient , model , logger , cancellationToken ) ;
68
+ }
69
+ else
70
+ {
71
+ logger . LogInformation ( "{TimeStamp}: [{Model}] already exists for {ResourceName}" ,
72
+ DateTime . UtcNow . ToString ( "yyyy-MM-ddTHH:mm:ss.fffZ" , CultureInfo . InvariantCulture ) ,
73
+ resource . ModelName ,
74
+ resource . Name ) ;
51
75
}
52
76
53
77
await _notificationService . PublishUpdateAsync ( resource , state => state with { State = new ResourceStateSnapshot ( "Running" , KnownResourceStateStyles . Success ) } ) ;
@@ -57,24 +81,42 @@ private void DownloadModel(OllamaResource resource, CancellationToken cancellati
57
81
await _notificationService . PublishUpdateAsync ( resource , state => state with { State = new ResourceStateSnapshot ( ex . Message , KnownResourceStateStyles . Error ) } ) ;
58
82
}
59
83
60
- } , cancellationToken ) ;
84
+ } , cancellationToken ) . ConfigureAwait ( false ) ;
61
85
}
62
86
63
87
private static async Task < bool > HasModelAsync ( OllamaApiClient ollamaClient , string model , CancellationToken cancellationToken )
64
88
{
65
- var localModels = await ollamaClient . ListLocalModels ( cancellationToken ) ;
66
- return localModels . Any ( m => m . Name . StartsWith ( model ) ) ;
89
+ int retryCount = 0 ;
90
+ while ( retryCount < 5 )
91
+ {
92
+ try
93
+ {
94
+ var localModels = await ollamaClient . ListLocalModels ( cancellationToken ) ;
95
+ return localModels . Any ( m => m . Name . StartsWith ( model ) ) ;
96
+ }
97
+ catch ( TaskCanceledException )
98
+ {
99
+ // wait 30 seconds before retrying
100
+ await Task . Delay ( TimeSpan . FromSeconds ( 30 ) , cancellationToken ) ;
101
+ retryCount ++ ;
102
+ }
103
+ }
104
+
105
+ throw new TimeoutException ( "Failed to list local models after 5 retries. Likely that the container image was not pulled in time, or the container is not running." ) ;
67
106
}
68
107
69
- private async Task PullModel ( OllamaResource resource , OllamaApiClient ollamaClient , string model , CancellationToken cancellationToken )
108
+ private async Task PullModel ( OllamaResource resource , OllamaApiClient ollamaClient , string model , ILogger logger , CancellationToken cancellationToken )
70
109
{
110
+ logger . LogInformation ( "{TimeStamp}: Pulling ollama model {Model}..." ,
111
+ DateTime . UtcNow . ToString ( "yyyy-MM-ddTHH:mm:ss.fffZ" , CultureInfo . InvariantCulture ) ,
112
+ model ) ;
71
113
await _notificationService . PublishUpdateAsync ( resource , state => state with { State = new ResourceStateSnapshot ( "Downloading model" , KnownResourceStateStyles . Info ) } ) ;
72
114
73
115
long percentage = 0 ;
74
116
75
117
await foreach ( PullModelResponse ? status in ollamaClient . PullModel ( model , cancellationToken ) )
76
118
{
77
- if ( status == null )
119
+ if ( status is null )
78
120
{
79
121
continue ;
80
122
}
@@ -95,6 +137,10 @@ await _notificationService.PublishUpdateAsync(resource,
95
137
}
96
138
}
97
139
}
140
+
141
+ logger . LogInformation ( "{TimeStamp}: Finished pulling ollama model {Model}" ,
142
+ DateTime . UtcNow . ToString ( "yyyy-MM-ddTHH:mm:ss.fffZ" , CultureInfo . InvariantCulture ) ,
143
+ model ) ;
98
144
}
99
145
100
146
public ValueTask DisposeAsync ( )
0 commit comments