Skip to content

Commit

Permalink
feat: using SnippetManager in the Service Details to create code snip…
Browse files Browse the repository at this point in the history
…pet (#556)

* feat: snippet in details

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* feat: using snippet language

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* fix: missing SnippetManager

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* test: ensuring StudioApiImpl tests works

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* fix: tests

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* tests: ensuring expected behavior

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* fix: unit tests

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* fix: RequestOptions model file directory

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* fix: comments

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

---------

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>
  • Loading branch information
axel7083 committed Mar 19, 2024
1 parent c8fc8f1 commit 6112bc2
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 6 deletions.
8 changes: 7 additions & 1 deletion packages/backend/src/managers/SnippetManager.spec.ts
Expand Up @@ -63,7 +63,13 @@ test('expect postman-code-generators to have nodejs supported.', () => {
test('expect postman-code-generators to generate proper nodejs native code', async () => {
const manager = new SnippetManager(webviewMock);

const snippet = await manager.generate('http://localhost:8080', 'nodejs', 'Request');
const snippet = await manager.generate(
{
url: 'http://localhost:8080',
},
'nodejs',
'Request',
);
expect(snippet).toBe(`var request = require('request');
var options = {
'method': 'GET',
Expand Down
5 changes: 3 additions & 2 deletions packages/backend/src/managers/SnippetManager.ts
Expand Up @@ -20,6 +20,7 @@ import { getLanguageList, convert, type Language } from 'postman-code-generators
import { Request } from 'postman-collection';
import { Publisher } from '../utils/Publisher';
import { Messages } from '@shared/Messages';
import type { RequestOptions } from '@shared/src/models/RequestOptions';

export class SnippetManager extends Publisher<Language[]> implements Disposable {
constructor(webview: Webview) {
Expand All @@ -30,9 +31,9 @@ export class SnippetManager extends Publisher<Language[]> implements Disposable
return getLanguageList();
}

generate(url: string, language: string, variant: string): Promise<string> {
generate(requestOptions: RequestOptions, language: string, variant: string): Promise<string> {
return new Promise((resolve, reject) => {
const request = new Request(url);
const request = new Request(requestOptions);
convert(language, variant, request, {}, (error: unknown, snippet: string) => {
if (error) {
reject(error);
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/studio-api-impl.spec.ts
Expand Up @@ -36,6 +36,7 @@ import type { TaskRegistry } from './registries/TaskRegistry';
import type { LocalRepositoryRegistry } from './registries/LocalRepositoryRegistry';
import type { Recipe } from '@shared/src/models/IRecipe';
import type { PlaygroundV2Manager } from './managers/playgroundV2Manager';
import type { SnippetManager } from './managers/SnippetManager';

vi.mock('./ai.json', () => {
return {
Expand Down Expand Up @@ -107,6 +108,7 @@ beforeEach(async () => {
{} as unknown as TaskRegistry,
{} as unknown as InferenceManager,
{} as unknown as PlaygroundV2Manager,
{} as unknown as SnippetManager,
);
vi.mock('node:fs');

Expand Down
12 changes: 12 additions & 0 deletions packages/backend/src/studio-api-impl.ts
Expand Up @@ -40,6 +40,9 @@ import type { Conversation } from '@shared/src/models/IPlaygroundMessage';
import type { PlaygroundV2Manager } from './managers/playgroundV2Manager';
import { getFreeRandomPort } from './utils/ports';
import { withDefaultConfiguration } from './utils/inferenceUtils';
import type { RequestOptions } from '@shared/src/models/RequestOptions';
import type { SnippetManager } from './managers/SnippetManager';
import type { Language } from 'postman-code-generators';
import type { ModelOptions } from '@shared/src/models/IModelOptions';
import type { PlaygroundV2 } from '@shared/src/models/IPlaygroundV2';

Expand All @@ -58,6 +61,7 @@ export class StudioApiImpl implements StudioAPI {
private taskRegistry: TaskRegistry,
private inferenceManager: InferenceManager,
private playgroundV2: PlaygroundV2Manager,
private snippetManager: SnippetManager,
) {}

async createPlayground(name: string, model: ModelInfo): Promise<void> {
Expand Down Expand Up @@ -86,6 +90,14 @@ export class StudioApiImpl implements StudioAPI {
return this.playgroundV2.getConversations();
}

async getSnippetLanguages(): Promise<Language[]> {
return this.snippetManager.getLanguageList();
}

createSnippet(options: RequestOptions, language: string, variant: string): Promise<string> {
return this.snippetManager.generate(options, language, variant);
}

async getInferenceServers(): Promise<InferenceServer[]> {
return this.inferenceManager.getServers();
}
Expand Down
5 changes: 4 additions & 1 deletion packages/backend/src/studio.spec.ts
Expand Up @@ -35,6 +35,7 @@ const studio = new Studio(mockedExtensionContext);
const mocks = vi.hoisted(() => ({
listContainers: vi.fn(),
getContainerConnections: vi.fn(),
postMessage: vi.fn(),
}));

vi.mock('@podman-desktop/api', async () => {
Expand All @@ -51,7 +52,7 @@ vi.mock('@podman-desktop/api', async () => {
webview: {
html: '',
onDidReceiveMessage: vi.fn(),
postMessage: vi.fn(),
postMessage: mocks.postMessage,
},
onDidChangeViewState: vi.fn(),
}),
Expand Down Expand Up @@ -85,6 +86,8 @@ beforeEach(() => {
event: vi.fn(),
fire: vi.fn(),
} as unknown as EventEmitter<unknown>);

mocks.postMessage.mockResolvedValue(undefined);
});

afterEach(() => {
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/studio.ts
Expand Up @@ -40,6 +40,7 @@ import { PodmanConnection } from './managers/podmanConnection';
import { LocalRepositoryRegistry } from './registries/LocalRepositoryRegistry';
import { InferenceManager } from './managers/inference/inferenceManager';
import { PlaygroundV2Manager } from './managers/playgroundV2Manager';
import { SnippetManager } from './managers/SnippetManager';

// TODO: Need to be configured
export const AI_STUDIO_FOLDER = path.join('podman-desktop', 'ai-studio');
Expand Down Expand Up @@ -171,6 +172,9 @@ export class Studio {

const playgroundV2 = new PlaygroundV2Manager(this.#panel.webview, this.#inferenceManager);

const snippetManager = new SnippetManager(this.#panel.webview);
snippetManager.init();

// Creating StudioApiImpl
this.studioApi = new StudioApiImpl(
applicationManager,
Expand All @@ -182,6 +186,7 @@ export class Studio {
taskRegistry,
this.#inferenceManager,
playgroundV2,
snippetManager,
);

this.catalogManager.init();
Expand Down
49 changes: 48 additions & 1 deletion packages/frontend/src/pages/InferenceServerDetails.spec.ts
Expand Up @@ -21,10 +21,12 @@ import { vi, test, expect, beforeEach } from 'vitest';
import { screen, render } from '@testing-library/svelte';
import type { InferenceServer } from '@shared/src/models/IInference';
import InferenceServerDetails from '/@/pages/InferenceServerDetails.svelte';
import type { Language } from 'postman-code-generators';

const mocks = vi.hoisted(() => {
return {
getInferenceServersMock: vi.fn(),
getSnippetLanguagesMock: vi.fn(),
};
});

Expand All @@ -37,7 +39,16 @@ vi.mock('../stores/inferenceServers', () => ({
},
}));

vi.mock('../utils/client', async () => {
vi.mock('../stores/snippetLanguages', () => ({
snippetLanguages: {
subscribe: (f: (msg: any) => void) => {
f(mocks.getSnippetLanguagesMock());
return () => {};
},
},
}));

vi.mock('../utils/client', () => {
return {
studioClient: {},
};
Expand All @@ -46,6 +57,22 @@ vi.mock('../utils/client', async () => {
beforeEach(() => {
vi.resetAllMocks();

mocks.getSnippetLanguagesMock.mockReturnValue([
{
key: 'dummyLanguageKey',
label: 'dummyLanguageLabel',
syntax_mode: 'dummySynthaxMode',
variants: [
{
key: 'dummyLanguageVariant1',
},
{
key: 'dummyLanguageVariant2',
},
],
},
] as Language[]);

mocks.getInferenceServersMock.mockReturnValue([
{
health: undefined,
Expand All @@ -68,3 +95,23 @@ test('ensure address is displayed', async () => {
const address = screen.getByText('http://localhost:9999/v1');
expect(address).toBeDefined();
});

test('language select must have the mocked snippet languages', async () => {
render(InferenceServerDetails, {
containerId: 'dummyContainerId',
});

const select: HTMLSelectElement = screen.getByLabelText('snippet language selection');
expect(select).toBeDefined();
expect(select.options.length).toBe(1);
expect(select.options[0].value).toBe('dummyLanguageKey');
});

test('variant select must be hidden when no language selected', async () => {
render(InferenceServerDetails, {
containerId: 'dummyContainerId',
});

const variantSelect = screen.queryByLabelText('snippet language variant');
expect(variantSelect).toBeNull();
});
100 changes: 99 additions & 1 deletion packages/frontend/src/pages/InferenceServerDetails.svelte
Expand Up @@ -4,13 +4,67 @@ import NavPage from '/@/lib/NavPage.svelte';
import ServiceStatus from '/@/lib/table/service/ServiceStatus.svelte';
import ServiceAction from '/@/lib/table/service/ServiceAction.svelte';
import Fa from 'svelte-fa';
import { faBuildingColumns, faCopy, faMicrochip, faScaleBalanced } from '@fortawesome/free-solid-svg-icons';
import { faBuildingColumns, faMicrochip, faScaleBalanced } from '@fortawesome/free-solid-svg-icons';
import type { InferenceServer } from '@shared/src/models/IInference';
import { snippetLanguages } from '/@/stores/snippetLanguages';
import type { LanguageVariant } from 'postman-code-generators';
import { studioClient } from '/@/utils/client';
export let containerId: string | undefined = undefined;
let service: InferenceServer | undefined;
$: service = $inferenceServers.find(server => server.container.containerId === containerId);
let selectedLanguage: string | undefined = undefined;
$: selectedLanguage;
let variants: LanguageVariant[] = [];
$: variants = $snippetLanguages.find(language => language.key === selectedLanguage)?.variants || [];
let selectedVariant: string | undefined = undefined;
$: selectedVariant;
const onLanguageChange = (): void => {
selectedVariant = variants.length > 0 ? variants[0].key : undefined;
generate();
};
let snippet: string | undefined = undefined;
$: snippet;
const generate = async () => {
if (selectedVariant === undefined || selectedLanguage === undefined) return;
snippet = await studioClient.createSnippet(
{
url: `http://localhost:${service?.connection.port || '??'}/v1/chat/completions`,
method: 'POST',
header: [
{
key: 'Content-Type',
value: 'application/json',
},
],
body: {
mode: 'raw',
raw: `{
"messages": [
{
"content": "You are a helpful assistant.",
"role": "system"
},
{
"content": "What is the capital of France?",
"role": "user"
}
]
}`,
},
},
selectedLanguage,
selectedVariant,
);
};
</script>

<NavPage title="Service Details" searchEnabled="{false}">
Expand Down Expand Up @@ -74,6 +128,50 @@ $: service = $inferenceServers.find(server => server.container.containerId === c
</div>
</div>
</div>

<!-- code client -->
<div>
<div class="flex flex-row">
<span class="text-base grow">Client code</span>

<!-- language choice -->
<select
required
aria-label="snippet language selection"
bind:value="{selectedLanguage}"
on:change="{onLanguageChange}"
id="languages"
class="border text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 block p-1 bg-charcoal-900 border-charcoal-900 placeholder-gray-700 text-white"
name="languages">
{#each $snippetLanguages as language}
<option class="my-1" value="{language.key}">{language.label}</option>
{/each}
</select>
{#if selectedVariant !== undefined}
<select
required
aria-label="snippet language variant"
id="variants"
bind:value="{selectedVariant}"
on:change="{generate}"
disabled="{variants.length === 1}"
class="border ml-1 text-sm rounded-lg focus:ring-purple-500 focus:border-purple-500 block p-1 bg-charcoal-900 border-charcoal-900 placeholder-gray-700 text-white"
name="variants">
{#each variants as variant}
<option class="my-1" value="{variant.key}">{variant.key}</option>
{/each}
</select>
{/if}
</div>

{#if snippet !== undefined}
<div class="bg-charcoal-900 rounded-md w-full p-4 mt-2">
<code class="whitespace-break-spaces">
{snippet}
</code>
</div>
{/if}
</div>
{/if}
</div>
</div>
Expand Down
28 changes: 28 additions & 0 deletions packages/frontend/src/stores/snippetLanguages.ts
@@ -0,0 +1,28 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/
import type { Readable } from 'svelte/store';
import { Messages } from '@shared/Messages';
import { studioClient } from '/@/utils/client';
import { RPCReadable } from '/@/stores/rpcReadable';
import type { Language } from 'postman-code-generators';

export const snippetLanguages: Readable<Language[]> = RPCReadable<Language[]>(
[],
[Messages.MSG_SUPPORTED_LANGUAGES_UPDATE],
studioClient.getSnippetLanguages,
);
15 changes: 15 additions & 0 deletions packages/shared/src/StudioAPI.ts
Expand Up @@ -25,6 +25,8 @@ import type { ApplicationState } from './models/IApplicationState';
import type { Task } from './models/ITask';
import type { LocalRepository } from './models/ILocalRepository';
import type { InferenceServer } from './models/IInference';
import type { RequestOptions } from './models/RequestOptions';
import type { Language } from 'postman-code-generators';
import type { CreationInferenceServerOptions } from './models/InferenceServerConfig';
import type { ModelOptions } from './models/IModelOptions';
import type { Conversation } from './models/IPlaygroundMessage';
Expand Down Expand Up @@ -147,6 +149,19 @@ export abstract class StudioAPI {
*/
abstract createPlaygroundConversation(): Promise<string>;

/**
* Return the list of supported languages to generate code from.
*/
abstract getSnippetLanguages(): Promise<Language[]>;

/**
* return a code snippet as a string matching the arguments and options provided
* @param options the options for the request
* @param language the language to use
* @param variant the variant of the language
*/
abstract createSnippet(options: RequestOptions, language: string, variant: string): Promise<string>;

abstract createPlayground(name: string, model: ModelInfo): Promise<void>;

abstract getPlaygroundsV2(): Promise<PlaygroundV2[]>;
Expand Down

0 comments on commit 6112bc2

Please sign in to comment.