Skip to content

Commit bf5a5f8

Browse files
committed
feat: open-webui playground prototype
Signed-off-by: Jeff MAURY <[email protected]>
1 parent 7435841 commit bf5a5f8

File tree

7 files changed

+153
-233
lines changed

7 files changed

+153
-233
lines changed

packages/backend/src/assets/webui.db

140 KB
Binary file not shown.

packages/backend/src/managers/playgroundV2Manager.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import { getRandomString } from '../utils/randomUtils';
3636
import type { TaskRegistry } from '../registries/TaskRegistry';
3737
import type { CancellationTokenRegistry } from '../registries/CancellationTokenRegistry';
3838
import { getHash } from '../utils/sha';
39+
import type { ConfigurationRegistry } from '../registries/ConfigurationRegistry';
40+
import type { PodmanConnection } from './podmanConnection';
3941

4042
export class PlaygroundV2Manager implements Disposable {
4143
#conversationRegistry: ConversationRegistry;
@@ -46,17 +48,24 @@ export class PlaygroundV2Manager implements Disposable {
4648
private taskRegistry: TaskRegistry,
4749
private telemetry: TelemetryLogger,
4850
private cancellationTokenRegistry: CancellationTokenRegistry,
51+
configurationRegistry: ConfigurationRegistry,
52+
podmanConnection: PodmanConnection,
4953
) {
50-
this.#conversationRegistry = new ConversationRegistry(webview);
54+
this.#conversationRegistry = new ConversationRegistry(
55+
webview,
56+
configurationRegistry,
57+
taskRegistry,
58+
podmanConnection,
59+
);
5160
}
5261

53-
deleteConversation(conversationId: string): void {
62+
async deleteConversation(conversationId: string): Promise<void> {
5463
const conversation = this.#conversationRegistry.get(conversationId);
5564
this.telemetry.logUsage('playground.delete', {
5665
totalMessages: conversation.messages.length,
5766
modelId: getHash(conversation.modelId),
5867
});
59-
this.#conversationRegistry.deleteConversation(conversationId);
68+
await this.#conversationRegistry.deleteConversation(conversationId);
6069
}
6170

6271
async requestCreatePlayground(name: string, model: ModelInfo): Promise<string> {
@@ -117,11 +126,11 @@ export class PlaygroundV2Manager implements Disposable {
117126
}
118127

119128
// Create conversation
120-
const conversationId = this.#conversationRegistry.createConversation(name, model.id);
129+
const conversationId = await this.#conversationRegistry.createConversation(name, model.id);
121130

122131
// create/start inference server if necessary
123132
const servers = this.inferenceManager.getServers();
124-
const server = servers.find(s => s.models.map(mi => mi.id).includes(model.id));
133+
let server = servers.find(s => s.models.map(mi => mi.id).includes(model.id));
125134
if (!server) {
126135
await this.inferenceManager.createInferenceServer(
127136
await withDefaultConfiguration({
@@ -131,10 +140,15 @@ export class PlaygroundV2Manager implements Disposable {
131140
},
132141
}),
133142
);
143+
server = this.inferenceManager.findServerByModel(model);
134144
} else if (server.status === 'stopped') {
135145
await this.inferenceManager.startInferenceServer(server.container.containerId);
136146
}
137147

148+
if (server && server.status === 'running') {
149+
await this.#conversationRegistry.startConversationContainer(server, trackingId, conversationId);
150+
}
151+
138152
return conversationId;
139153
}
140154

packages/backend/src/registries/ConfigurationRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export class ConfigurationRegistry extends Publisher<ExtensionConfiguration> imp
6262
return path.join(this.appUserDirectory, 'models');
6363
}
6464

65+
public getConversationsPath(): string {
66+
return path.join(this.appUserDirectory, 'conversations');
67+
}
68+
6569
dispose(): void {
6670
this.#configurationDisposable?.dispose();
6771
}

packages/backend/src/registries/ConversationRegistry.ts

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
2937
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';
3047

3148
export 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

packages/backend/src/studio.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ export class Studio {
306306
this.#taskRegistry,
307307
this.#telemetry,
308308
this.#cancellationTokenRegistry,
309+
this.#configurationRegistry,
310+
this.#podmanConnection,
309311
);
310312
this.#extensionContext.subscriptions.push(this.#playgroundManager);
311313

0 commit comments

Comments
 (0)