@@ -25,14 +25,36 @@ import type {
25
25
Message ,
26
26
PendingChat ,
27
27
} from '@shared/src/models/IPlaygroundMessage' ;
28
- import type { Disposable , Webview } from '@podman-desktop/api' ;
28
+ import {
29
+ type Disposable ,
30
+ type Webview ,
31
+ type ContainerCreateOptions ,
32
+ containerEngine ,
33
+ type ContainerProviderConnection ,
34
+ type ImageInfo ,
35
+ type PullEvent ,
36
+ } from '@podman-desktop/api' ;
29
37
import { Messages } from '@shared/Messages' ;
38
+ import type { ConfigurationRegistry } from './ConfigurationRegistry' ;
39
+ import path from 'node:path' ;
40
+ import fs from 'node:fs' ;
41
+ import type { InferenceServer } from '@shared/src/models/IInference' ;
42
+ import { getFreeRandomPort } from '../utils/ports' ;
43
+ import { DISABLE_SELINUX_LABEL_SECURITY_OPTION } from '../utils/utils' ;
44
+ import { getImageInfo } from '../utils/inferenceUtils' ;
45
+ import type { TaskRegistry } from './TaskRegistry' ;
46
+ import type { PodmanConnection } from '../managers/podmanConnection' ;
30
47
31
48
export class ConversationRegistry extends Publisher < Conversation [ ] > implements Disposable {
32
49
#conversations: Map < string , Conversation > ;
33
50
#counter: number ;
34
51
35
- constructor ( webview : Webview ) {
52
+ constructor (
53
+ webview : Webview ,
54
+ private configurationRegistry : ConfigurationRegistry ,
55
+ private taskRegistry : TaskRegistry ,
56
+ private podmanConnection : PodmanConnection ,
57
+ ) {
36
58
super ( webview , Messages . MSG_CONVERSATIONS_UPDATE , ( ) => this . getAll ( ) ) ;
37
59
this . #conversations = new Map < string , Conversation > ( ) ;
38
60
this . #counter = 0 ;
@@ -76,13 +98,32 @@ export class ConversationRegistry extends Publisher<Conversation[]> implements D
76
98
this . notify ( ) ;
77
99
}
78
100
79
- deleteConversation ( id : string ) : void {
101
+ async deleteConversation ( id : string ) : Promise < void > {
102
+ const conversation = this . get ( id ) ;
103
+ if ( conversation . container ) {
104
+ await containerEngine . stopContainer ( conversation . container ?. engineId , conversation . container ?. containerId ) ;
105
+ }
106
+ await fs . promises . rm ( path . join ( this . configurationRegistry . getConversationsPath ( ) , id ) , {
107
+ recursive : true ,
108
+ force : true ,
109
+ } ) ;
80
110
this . #conversations. delete ( id ) ;
81
111
this . notify ( ) ;
82
112
}
83
113
84
- createConversation ( name : string , modelId : string ) : string {
114
+ async createConversation ( name : string , modelId : string ) : Promise < string > {
85
115
const conversationId = this . getUniqueId ( ) ;
116
+ const conversationFolder = path . join ( this . configurationRegistry . getConversationsPath ( ) , conversationId ) ;
117
+ await fs . promises . mkdir ( conversationFolder , {
118
+ recursive : true ,
119
+ } ) ;
120
+ //WARNING: this will not work in production mode but didn't find how to embed binary assets
121
+ //this code get an initialized database so that default user is not admin thus did not get the initial
122
+ //welcome modal dialog
123
+ await fs . promises . copyFile (
124
+ path . join ( __dirname , '..' , 'src' , 'assets' , 'webui.db' ) ,
125
+ path . join ( conversationFolder , 'webui.db' ) ,
126
+ ) ;
86
127
this . #conversations. set ( conversationId , {
87
128
name : name ,
88
129
modelId : modelId ,
@@ -93,6 +134,77 @@ export class ConversationRegistry extends Publisher<Conversation[]> implements D
93
134
return conversationId ;
94
135
}
95
136
137
+ async startConversationContainer ( server : InferenceServer , trackingId : string , conversationId : string ) : Promise < void > {
138
+ const conversation = this . get ( conversationId ) ;
139
+ const port = await getFreeRandomPort ( '127.0.0.1' ) ;
140
+ const connection = await this . podmanConnection . getConnectionByEngineId ( server . container . engineId ) ;
141
+ await this . pullImage ( connection , 'ghcr.io/open-webui/open-webui:main' , {
142
+ trackingId : trackingId ,
143
+ } ) ;
144
+ const inferenceServerContainer = await containerEngine . inspectContainer (
145
+ server . container . engineId ,
146
+ server . container . containerId ,
147
+ ) ;
148
+ const options : ContainerCreateOptions = {
149
+ Env : [
150
+ 'DEFAULT_LOCALE=en-US' ,
151
+ 'WEBUI_AUTH=false' ,
152
+ 'ENABLE_OLLAMA_API=false' ,
153
+ `OPENAI_API_BASE_URL=http://${ inferenceServerContainer . NetworkSettings . IPAddress } :8000/v1` ,
154
+ 'OPENAI_API_KEY=sk_dummy' ,
155
+ `WEBUI_URL=http://localhost:${ port } ` ,
156
+ `DEFAULT_MODELS=/models/${ server . models [ 0 ] . file ?. file } ` ,
157
+ ] ,
158
+ Image : 'ghcr.io/open-webui/open-webui:main' ,
159
+ HostConfig : {
160
+ AutoRemove : true ,
161
+ Mounts : [
162
+ {
163
+ Source : path . join ( this . configurationRegistry . getConversationsPath ( ) , conversationId ) ,
164
+ Target : '/app/backend/data' ,
165
+ Type : 'bind' ,
166
+ } ,
167
+ ] ,
168
+ PortBindings : {
169
+ '8080/tcp' : [
170
+ {
171
+ HostPort : `${ port } ` ,
172
+ } ,
173
+ ] ,
174
+ } ,
175
+ SecurityOpt : [ DISABLE_SELINUX_LABEL_SECURITY_OPTION ] ,
176
+ } ,
177
+ } ;
178
+ const c = await containerEngine . createContainer ( server . container . engineId , options ) ;
179
+ conversation . container = { engineId : c . engineId , containerId : c . id , port } ;
180
+ }
181
+
182
+ protected pullImage (
183
+ connection : ContainerProviderConnection ,
184
+ image : string ,
185
+ labels : { [ id : string ] : string } ,
186
+ ) : Promise < ImageInfo > {
187
+ // Creating a task to follow pulling progress
188
+ const pullingTask = this . taskRegistry . createTask ( `Pulling ${ image } .` , 'loading' , labels ) ;
189
+
190
+ // get the default image info for this provider
191
+ return getImageInfo ( connection , image , ( _event : PullEvent ) => { } )
192
+ . catch ( ( err : unknown ) => {
193
+ pullingTask . state = 'error' ;
194
+ pullingTask . progress = undefined ;
195
+ pullingTask . error = `Something went wrong while pulling ${ image } : ${ String ( err ) } ` ;
196
+ throw err ;
197
+ } )
198
+ . then ( imageInfo => {
199
+ pullingTask . state = 'success' ;
200
+ pullingTask . progress = undefined ;
201
+ return imageInfo ;
202
+ } )
203
+ . finally ( ( ) => {
204
+ this . taskRegistry . updateTask ( pullingTask ) ;
205
+ } ) ;
206
+ }
207
+
96
208
/**
97
209
* This method will be responsible for finalizing the message by concatenating all the choices
98
210
* @param conversationId
0 commit comments