Skip to content

Commit

Permalink
feat: open-webui playground prototype
Browse files Browse the repository at this point in the history
Signed-off-by: Jeff MAURY <[email protected]>
  • Loading branch information
jeffmaury committed Sep 20, 2024
1 parent 7435841 commit bf5a5f8
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 233 deletions.
Binary file added packages/backend/src/assets/webui.db
Binary file not shown.
24 changes: 19 additions & 5 deletions packages/backend/src/managers/playgroundV2Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { getRandomString } from '../utils/randomUtils';
import type { TaskRegistry } from '../registries/TaskRegistry';
import type { CancellationTokenRegistry } from '../registries/CancellationTokenRegistry';
import { getHash } from '../utils/sha';
import type { ConfigurationRegistry } from '../registries/ConfigurationRegistry';
import type { PodmanConnection } from './podmanConnection';

export class PlaygroundV2Manager implements Disposable {
#conversationRegistry: ConversationRegistry;
Expand All @@ -46,17 +48,24 @@ export class PlaygroundV2Manager implements Disposable {
private taskRegistry: TaskRegistry,
private telemetry: TelemetryLogger,
private cancellationTokenRegistry: CancellationTokenRegistry,
configurationRegistry: ConfigurationRegistry,
podmanConnection: PodmanConnection,
) {
this.#conversationRegistry = new ConversationRegistry(webview);
this.#conversationRegistry = new ConversationRegistry(
webview,
configurationRegistry,
taskRegistry,
podmanConnection,
);
}

deleteConversation(conversationId: string): void {
async deleteConversation(conversationId: string): Promise<void> {
const conversation = this.#conversationRegistry.get(conversationId);
this.telemetry.logUsage('playground.delete', {
totalMessages: conversation.messages.length,
modelId: getHash(conversation.modelId),
});
this.#conversationRegistry.deleteConversation(conversationId);
await this.#conversationRegistry.deleteConversation(conversationId);
}

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

// Create conversation
const conversationId = this.#conversationRegistry.createConversation(name, model.id);
const conversationId = await this.#conversationRegistry.createConversation(name, model.id);

// create/start inference server if necessary
const servers = this.inferenceManager.getServers();
const server = servers.find(s => s.models.map(mi => mi.id).includes(model.id));
let server = servers.find(s => s.models.map(mi => mi.id).includes(model.id));
if (!server) {
await this.inferenceManager.createInferenceServer(
await withDefaultConfiguration({
Expand All @@ -131,10 +140,15 @@ export class PlaygroundV2Manager implements Disposable {
},
}),
);
server = this.inferenceManager.findServerByModel(model);
} else if (server.status === 'stopped') {
await this.inferenceManager.startInferenceServer(server.container.containerId);
}

if (server && server.status === 'running') {
await this.#conversationRegistry.startConversationContainer(server, trackingId, conversationId);
}

return conversationId;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/registries/ConfigurationRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export class ConfigurationRegistry extends Publisher<ExtensionConfiguration> imp
return path.join(this.appUserDirectory, 'models');
}

public getConversationsPath(): string {
return path.join(this.appUserDirectory, 'conversations');
}

dispose(): void {
this.#configurationDisposable?.dispose();
}
Expand Down
120 changes: 116 additions & 4 deletions packages/backend/src/registries/ConversationRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,36 @@ import type {
Message,
PendingChat,
} from '@shared/src/models/IPlaygroundMessage';
import type { Disposable, Webview } from '@podman-desktop/api';
import {
type Disposable,
type Webview,
type ContainerCreateOptions,
containerEngine,
type ContainerProviderConnection,
type ImageInfo,
type PullEvent,
} from '@podman-desktop/api';
import { Messages } from '@shared/Messages';
import type { ConfigurationRegistry } from './ConfigurationRegistry';
import path from 'node:path';
import fs from 'node:fs';
import type { InferenceServer } from '@shared/src/models/IInference';
import { getFreeRandomPort } from '../utils/ports';
import { DISABLE_SELINUX_LABEL_SECURITY_OPTION } from '../utils/utils';
import { getImageInfo } from '../utils/inferenceUtils';
import type { TaskRegistry } from './TaskRegistry';
import type { PodmanConnection } from '../managers/podmanConnection';

export class ConversationRegistry extends Publisher<Conversation[]> implements Disposable {
#conversations: Map<string, Conversation>;
#counter: number;

constructor(webview: Webview) {
constructor(
webview: Webview,
private configurationRegistry: ConfigurationRegistry,
private taskRegistry: TaskRegistry,
private podmanConnection: PodmanConnection,
) {
super(webview, Messages.MSG_CONVERSATIONS_UPDATE, () => this.getAll());
this.#conversations = new Map<string, Conversation>();
this.#counter = 0;
Expand Down Expand Up @@ -76,13 +98,32 @@ export class ConversationRegistry extends Publisher<Conversation[]> implements D
this.notify();
}

deleteConversation(id: string): void {
async deleteConversation(id: string): Promise<void> {
const conversation = this.get(id);
if (conversation.container) {
await containerEngine.stopContainer(conversation.container?.engineId, conversation.container?.containerId);
}
await fs.promises.rm(path.join(this.configurationRegistry.getConversationsPath(), id), {
recursive: true,
force: true,
});
this.#conversations.delete(id);
this.notify();
}

createConversation(name: string, modelId: string): string {
async createConversation(name: string, modelId: string): Promise<string> {
const conversationId = this.getUniqueId();
const conversationFolder = path.join(this.configurationRegistry.getConversationsPath(), conversationId);
await fs.promises.mkdir(conversationFolder, {
recursive: true,
});
//WARNING: this will not work in production mode but didn't find how to embed binary assets
//this code get an initialized database so that default user is not admin thus did not get the initial
//welcome modal dialog
await fs.promises.copyFile(
path.join(__dirname, '..', 'src', 'assets', 'webui.db'),
path.join(conversationFolder, 'webui.db'),
);
this.#conversations.set(conversationId, {
name: name,
modelId: modelId,
Expand All @@ -93,6 +134,77 @@ export class ConversationRegistry extends Publisher<Conversation[]> implements D
return conversationId;
}

async startConversationContainer(server: InferenceServer, trackingId: string, conversationId: string): Promise<void> {
const conversation = this.get(conversationId);
const port = await getFreeRandomPort('127.0.0.1');
const connection = await this.podmanConnection.getConnectionByEngineId(server.container.engineId);
await this.pullImage(connection, 'ghcr.io/open-webui/open-webui:main', {
trackingId: trackingId,
});
const inferenceServerContainer = await containerEngine.inspectContainer(
server.container.engineId,
server.container.containerId,
);
const options: ContainerCreateOptions = {
Env: [
'DEFAULT_LOCALE=en-US',
'WEBUI_AUTH=false',
'ENABLE_OLLAMA_API=false',
`OPENAI_API_BASE_URL=http://${inferenceServerContainer.NetworkSettings.IPAddress}:8000/v1`,
'OPENAI_API_KEY=sk_dummy',
`WEBUI_URL=http://localhost:${port}`,
`DEFAULT_MODELS=/models/${server.models[0].file?.file}`,
],
Image: 'ghcr.io/open-webui/open-webui:main',
HostConfig: {
AutoRemove: true,
Mounts: [
{
Source: path.join(this.configurationRegistry.getConversationsPath(), conversationId),
Target: '/app/backend/data',
Type: 'bind',
},
],
PortBindings: {
'8080/tcp': [
{
HostPort: `${port}`,
},
],
},
SecurityOpt: [DISABLE_SELINUX_LABEL_SECURITY_OPTION],
},
};
const c = await containerEngine.createContainer(server.container.engineId, options);
conversation.container = { engineId: c.engineId, containerId: c.id, port };
}

protected pullImage(
connection: ContainerProviderConnection,
image: string,
labels: { [id: string]: string },
): Promise<ImageInfo> {
// Creating a task to follow pulling progress
const pullingTask = this.taskRegistry.createTask(`Pulling ${image}.`, 'loading', labels);

// get the default image info for this provider
return getImageInfo(connection, image, (_event: PullEvent) => {})
.catch((err: unknown) => {
pullingTask.state = 'error';
pullingTask.progress = undefined;
pullingTask.error = `Something went wrong while pulling ${image}: ${String(err)}`;
throw err;
})
.then(imageInfo => {
pullingTask.state = 'success';
pullingTask.progress = undefined;
return imageInfo;
})
.finally(() => {
this.taskRegistry.updateTask(pullingTask);
});
}

/**
* This method will be responsible for finalizing the message by concatenating all the choices
* @param conversationId
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ export class Studio {
this.#taskRegistry,
this.#telemetry,
this.#cancellationTokenRegistry,
this.#configurationRegistry,
this.#podmanConnection,
);
this.#extensionContext.subscriptions.push(this.#playgroundManager);

Expand Down
Loading

0 comments on commit bf5a5f8

Please sign in to comment.