Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions packages/backend/src/managers/playgroundV2Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { AiStreamProcessor } from './playground/aiSdk';
import { type McpServerManager } from './playground/McpServerManager';
import type { ToolSet } from 'ai';
import { simulateStreamingMiddleware, wrapLanguageModel } from 'ai';
import type { InferenceServer } from '@shared/models/IInference';

export class PlaygroundV2Manager implements Disposable {
readonly #conversationRegistry: ConversationRegistry;
Expand All @@ -57,7 +58,7 @@ export class PlaygroundV2Manager implements Disposable {
this.#conversationRegistry.deleteConversation(conversationId);
}

async requestCreatePlayground(name: string, model: ModelInfo): Promise<string> {
async requestCreatePlayground(name: string, model: ModelInfo, selectedService?: InferenceServer): Promise<string> {
const trackingId: string = getRandomString();
const task = this.taskRegistry.createTask('Creating Playground environment', 'loading', {
trackingId: trackingId,
Expand All @@ -67,7 +68,7 @@ export class PlaygroundV2Manager implements Disposable {
hasName: !!name,
modelId: getHash(model.id),
};
this.createPlayground(name, model, trackingId)
this.createPlayground(name, model, trackingId, selectedService)
.then((playgroundId: string) => {
this.taskRegistry.updateTask({
...task,
Expand Down Expand Up @@ -106,33 +107,51 @@ export class PlaygroundV2Manager implements Disposable {
return trackingId;
}

async createPlayground(name: string, model: ModelInfo, trackingId: string): Promise<string> {
async createPlayground(
name: string,
model: ModelInfo,
trackingId: string,
selectedService?: InferenceServer,
): Promise<string> {
if (!name) {
name = this.getFreeName();
}
if (!this.isNameFree(name)) {
throw new Error(`a playground with the name ${name} already exists`);
}

// Create conversation
const conversationId = this.#conversationRegistry.createConversation(name, model.id);
let server: InferenceServer | undefined;
let containerId: string;

// 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));
if (!server) {
await this.inferenceManager.createInferenceServer(
if (selectedService) {
// Use selected running service
server = this.inferenceManager
.getServers()
.find(s => s.container.containerId === selectedService.container.containerId);
if (!server) {
throw new Error(`Selected inference server ${selectedService.container.containerId} not found`);
}
if (server.status === 'stopped') {
await this.inferenceManager.startInferenceServer(server.container.containerId);
}
containerId = server.container.containerId;
} else {
// Create new inference server and get its containerId
containerId = await this.inferenceManager.createInferenceServer(
await withDefaultConfiguration({
modelsInfo: [model],
labels: {
trackingId: trackingId,
},
}),
);
} else if (server.status === 'stopped') {
await this.inferenceManager.startInferenceServer(server.container.containerId);
// Retrieve the server after creation
server = this.inferenceManager.getServers().find(s => s.container.containerId === containerId);
if (!server) {
throw new Error(`Inference server with containerId ${containerId} was not registered correctly.`);
}
}

const conversationId = this.#conversationRegistry.createConversation(name, model.id, containerId);
return conversationId;
}

Expand Down Expand Up @@ -192,8 +211,26 @@ export class PlaygroundV2Manager implements Disposable {
async submit(conversationId: string, userInput: string, options?: ModelOptions): Promise<number> {
const conversation = this.#conversationRegistry.get(conversationId);

console.log('conversation model id', conversation.modelId);

const servers = this.inferenceManager.getServers();
const server = servers.find(s => s.models.map(mi => mi.id).includes(conversation.modelId));

console.log('Available Inference Services:');
servers.forEach((server, index) => {
console.log(`--- Service ${index + 1} ---`);
console.log('Container ID:', server.container?.containerId);
console.log('Status:', server.status || 'unknown');
console.log('Health:', server.health?.Status ?? 'unknown');
console.log('Models:', server.models?.map(m => m.id).join(', ') || 'No models');
console.log('Inference Type:', server.type || 'N/A');
});

const server = this.inferenceManager.getServers().find(s => s.container.containerId === conversation.containerId);

console.log('picked service', server?.container.containerId);
console.log('picked service status', server?.status);
console.log('picked service health', server?.health?.Status);

if (server === undefined) throw new Error('Inference server not found.');

if (server.status !== 'running') throw new Error('Inference server is not running.');
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/registries/ConversationRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ export class ConversationRegistry extends Publisher<Conversation[]> implements D
this.notify();
}

createConversation(name: string, modelId: string): string {
createConversation(name: string, modelId: string, containerId?: string): string {
const conversationId = this.getUniqueId();
this.#conversations.set(conversationId, {
name: name,
modelId: modelId,
containerId: containerId,
messages: [],
id: conversationId,
usage: {
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/studio-api-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ export class StudioApiImpl implements StudioAPI {
});
}

async requestCreatePlayground(name: string, model: ModelInfo): Promise<string> {
async requestCreatePlayground(name: string, model: ModelInfo, selectedService?: InferenceServer): Promise<string> {
try {
return await this.playgroundV2.requestCreatePlayground(name, model);
return await this.playgroundV2.requestCreatePlayground(name, model, selectedService);
} catch (err: unknown) {
console.error('Something went wrong while trying to create playground environment', err);
throw err;
Expand Down
30 changes: 30 additions & 0 deletions packages/frontend/src/lib/select/InferenceRuntimeSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import Select from '/@/lib/select/Select.svelte';
import { InferenceType } from '@shared/models/IInference';

interface Props {
disabled?: boolean;
value: InferenceType | undefined;
}
let { value = $bindable(), disabled }: Props = $props();
const options = Object.values(InferenceType).filter(type => type !== InferenceType.NONE) as Array<InferenceType>;
function handleOnChange(nValue: { value: string } | undefined): void {
if (nValue) {
value = nValue.value as InferenceType;
} else {
value = undefined;
}
}
</script>

<Select
label="Select Inference Runtime"
name="select-inference-runtime"
disabled={disabled}
value={value ? { label: value, value: value } : undefined}
onchange={handleOnChange}
placeholder="Select Inference Runtime to use"
items={options.map(type => ({
value: type,
label: type,
}))} />
49 changes: 49 additions & 0 deletions packages/frontend/src/lib/select/ModelServiceSelect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
import Select from '/@/lib/select/Select.svelte';
import type { InferenceServer } from '@shared/models/IInference';

interface Props {
servers: InferenceServer[];
disabled?: boolean;
value: InferenceServer | undefined;
}

let { servers = [], disabled = false, value = $bindable() }: Props = $props();

// Format each server to an option for the select component
const options = servers.map(server => ({
...server,
value: server.container.containerId,
label: `${server.container.containerId.slice(0, 8)} (port ${server.connection.port}) - ${server.type} - ${server.status}`,
}));

let selected: (InferenceServer & { label: string; value: string }) | undefined = $derived.by(() => {
if (value) {
return {
...value,
label: `${value.container.containerId.slice(0, 8)} (port ${value.connection.port}) - ${value.type} - ${value.status}`,
value: value.container.containerId,
};
} else {
return undefined;
}
});

function handleOnChange(nValue: (InferenceServer & { label: string; value: string }) | undefined): void {
if (nValue) {
const { label, ...server } = nValue;
value = server as InferenceServer;
} else {
value = undefined;
}
}
</script>

<Select
label="Select Model Service"
name="select-model-service"
disabled={disabled}
value={selected}
onchange={handleOnChange}
placeholder="Select an inference server"
items={options} />
62 changes: 45 additions & 17 deletions packages/frontend/src/pages/PlaygroundCreate.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@ import { filterByLabel } from '../utils/taskUtils';
import type { Unsubscriber } from 'svelte/store';
import { Button, ErrorMessage, FormPage, Input } from '@podman-desktop/ui-svelte';
import ModelSelect from '/@/lib/select/ModelSelect.svelte';
import ModelServiceSelect from '/@/lib/select/ModelServiceSelect.svelte';
import { InferenceType } from '@shared/models/IInference';
import type { InferenceServer } from '@shared/models/IInference';
import { inferenceServers } from '/@/stores/inferenceServers';

let localModels: ModelInfo[];
let ModelServices: InferenceServer[];
$: localModels = $modelsInfo.filter(model => model.file && model.backend !== InferenceType.WHISPER_CPP);
$: availModels = $modelsInfo.filter(model => !model.file);
let model: ModelInfo | undefined = undefined;
let submitted: boolean = false;
let playgroundName: string;
let errorMsg: string | undefined = undefined;
let selectedService: InferenceServer | undefined;

// The tracking id is a unique identifier provided by the
// backend when calling requestCreateInferenceServer
Expand All @@ -34,12 +39,22 @@ $: {
if (!model && localModels.length > 0) {
model = localModels[0];
}
ModelServices = model ? $inferenceServers.filter(server => server.models.some(m => m.id === model?.id)) : [];
if (ModelServices.length > 0) {
selectedService = ModelServices[0];
} else {
selectedService = undefined;
}
}

function openModelsPage(): void {
router.goto(`/models`);
}

function openModelServicePage(): void {
router.goto(`/services`);
}

// Navigate to the new created playground environment
const openPlaygroundPage = (playgroundId: string): void => {
router.goto(`/playground/${playgroundId}`);
Expand All @@ -56,10 +71,10 @@ async function submit(): Promise<void> {
submitted = true;
try {
// Using || and not && as we want to have the empty string systemPrompt passed as undefined
trackingId = await studioClient.requestCreatePlayground(playgroundName, model);
trackingId = await studioClient.requestCreatePlayground(playgroundName, model, selectedService);
} catch (err: unknown) {
trackingId = undefined;
console.error('Something wrong while trying to create the playground.', err);
console.error('Something wrong while tryinfghfghgfhfghgg to create the playground.', err);
errorMsg = String(err);
submitted = false;
}
Expand Down Expand Up @@ -122,19 +137,17 @@ export function goToUpPage(): void {
</svelte:fragment>
<svelte:fragment slot="content">
<div class="flex flex-col w-full">
<!-- tasks tracked -->
{#if trackedTasks.length > 0}
<div class="mx-5 mt-5" role="status">
<TasksProgress tasks={trackedTasks} />
</div>
{/if}

<!-- form -->
<div class="bg-[var(--pd-content-card-bg)] m-5 pt-5 space-y-6 px-8 sm:pb-6 xl:pb-8 rounded-lg h-fit">
<div class="w-full">
<!-- playground name input -->
<label for="playgroundName" class="block mb-2 font-bold text-[var(--pd-content-card-header-text)]"
>Playground name</label>
<label for="playgroundName" class="block mb-2 font-bold text-[var(--pd-content-card-header-text)]">
Playground name
</label>
<Input
disabled={submitted}
id="playgroundName"
Expand All @@ -145,38 +158,53 @@ export function goToUpPage(): void {
placeholder="Leave blank to generate a name"
aria-label="playgroundName" />

<!-- model input -->
<label for="model" class="pt-4 block mb-2 font-bold text-[var(--pd-content-card-header-text)]">Model</label>
<label for="model" class="pt-4 block mb-2 font-bold text-[var(--pd-content-card-header-text)]"> Model </label>
<ModelSelect models={localModels} disabled={submitted} bind:value={model} />

{#if localModels.length === 0}
<div class="text-red-500 p-1 flex flex-row items-center">
<Fa size="1.1x" class="cursor-pointer text-red-500" icon={faExclamationCircle} />
<div role="alert" aria-label="Error Message Content" class="ml-2">
You don't have any models downloaded. You can download them in <a
<div class="ml-2">
You don't have any models downloaded. Download them from the
<a href="javascript:void(0);" class="underline" title="Models page" on:click={openModelsPage}
>models page</a
>.
</div>
</div>
{:else if availModels.length > 0}
<div class="text-sm p-1 flex flex-row items-center text-[var(--pd-content-card-text)]">
<Fa size="1.1x" class="cursor-pointer" icon={faInfoCircle} />
<div class="ml-2">
Other models are available on the <a
href="javascript:void(0);"
class="underline"
title="Models page"
on:click={openModelsPage}>models page</a
>.
</div>
</div>
{:else if availModels.length > 0}
{/if}
{#if ModelServices.length > 0}
<label class="pt-4 block mb-2 font-bold text-[var(--pd-content-card-header-text)]"> Model Services </label>
{#key ModelServices}
<ModelServiceSelect bind:value={selectedService} servers={ModelServices} disabled={submitted} />
{/key}
<div class="text-sm p-1 flex flex-row items-center text-[var(--pd-content-card-text)]">
<Fa size="1.1x" class="cursor-pointer" icon={faInfoCircle} />
<div role="alert" aria-label="Info Message Content" class="ml-2">
Other models are available, but must be downloaded from the <a
<div class="ml-2">
Create additional model services here <a
href="javascript:void(0);"
class="underline"
title="Models page"
on:click={openModelsPage}>models page</a
on:click={openModelServicePage}>model service page</a
>.
</div>
</div>
{/if}
</div>

{#if errorMsg !== undefined}
<ErrorMessage error={errorMsg} />
{/if}

<footer>
<div class="w-full flex flex-col">
<Button
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/StudioAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export interface StudioAPI {
*/
createSnippet(options: RequestOptions, language: string, variant: string): Promise<string>;

requestCreatePlayground(name: string, model: ModelInfo): Promise<string>;
requestCreatePlayground(name: string, model: ModelInfo, selectedService?: InferenceServer): Promise<string>;

/**
* Delete a conversation
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/models/IPlaygroundMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface Conversation {
id: string;
messages: Message[];
modelId: string;
containerId?: string;
name: string;
usage?: ModelUsage;
}
Expand Down