@@ -25,14 +25,36 @@ import type {
2525 Message ,
2626 PendingChat ,
2727} 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' ;
2937import { 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' ;
3047
3148export class ConversationRegistry extends Publisher < Conversation [ ] > implements Disposable {
3249 #conversations: Map < string , Conversation > ;
3350 #counter: number ;
3451
35- constructor ( webview : Webview ) {
52+ constructor (
53+ webview : Webview ,
54+ private configurationRegistry : ConfigurationRegistry ,
55+ private taskRegistry : TaskRegistry ,
56+ private podmanConnection : PodmanConnection ,
57+ ) {
3658 super ( webview , Messages . MSG_CONVERSATIONS_UPDATE , ( ) => this . getAll ( ) ) ;
3759 this . #conversations = new Map < string , Conversation > ( ) ;
3860 this . #counter = 0 ;
@@ -76,13 +98,32 @@ export class ConversationRegistry extends Publisher<Conversation[]> implements D
7698 this . notify ( ) ;
7799 }
78100
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+ } ) ;
80110 this . #conversations. delete ( id ) ;
81111 this . notify ( ) ;
82112 }
83113
84- createConversation ( name : string , modelId : string ) : string {
114+ async createConversation ( name : string , modelId : string ) : Promise < string > {
85115 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+ ) ;
86127 this . #conversations. set ( conversationId , {
87128 name : name ,
88129 modelId : modelId ,
@@ -93,6 +134,77 @@ export class ConversationRegistry extends Publisher<Conversation[]> implements D
93134 return conversationId ;
94135 }
95136
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+
96208 /**
97209 * This method will be responsible for finalizing the message by concatenating all the choices
98210 * @param conversationId
0 commit comments