Skip to content

Commit

Permalink
fix: proxy typing + copyrights (#91)
Browse files Browse the repository at this point in the history
* fix: imports and typing

Signed-off-by: axel7083 <[email protected]>

* fix: moving utility methods out of the Studio API implementation

Signed-off-by: axel7083 <[email protected]>

* fix: prettier

Signed-off-by: axel7083 <[email protected]>

* fix: linter

Signed-off-by: axel7083 <[email protected]>

* fix: tests

Signed-off-by: axel7083 <[email protected]>

* fix: rebase + prettier + lint

Signed-off-by: axel7083 <[email protected]>

* fix: copyrights missing

Signed-off-by: axel7083 <[email protected]>

* fix: prettier

Signed-off-by: axel7083 <[email protected]>

* fix: prettier

Signed-off-by: axel7083 <[email protected]>

---------

Signed-off-by: axel7083 <[email protected]>
  • Loading branch information
axel7083 authored Jan 19, 2024
1 parent 06df71f commit ad6228a
Show file tree
Hide file tree
Showing 16 changed files with 390 additions and 115 deletions.
18 changes: 18 additions & 0 deletions packages/backend/src/managers/applicationManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/**********************************************************************
* 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 { Recipe } from '@shared/src/models/IRecipe';
import { arch } from 'node:os';
import type { GitManager } from './gitManager';
Expand Down
119 changes: 119 additions & 0 deletions packages/backend/src/managers/catalogManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**********************************************************************
* 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 { Catalog } from '@shared/src/models/ICatalog';
import path from 'node:path';
import { existsSync, promises } from 'node:fs';
import defaultCatalog from '../ai.json';
import type { Category } from '@shared/src/models/ICategory';
import type { Recipe } from '@shared/src/models/IRecipe';
import type { ModelInfo } from '@shared/src/models/IModelInfo';
import { MSG_NEW_CATALOG_STATE } from '@shared/Messages';
import { fs } from '@podman-desktop/api';
import type { Webview } from '@podman-desktop/api';

export class CatalogManager {
private catalog: Catalog;

constructor(
private appUserDirectory: string,
private webview: Webview,
) {
// We start with an empty catalog, for the methods to work before the catalog is loaded
this.catalog = {
categories: [],
models: [],
recipes: [],
};
}

public getCatalog(): Catalog {
return this.catalog;
}

public getCategories(): Category[] {
return this.catalog.categories;
}

public getModels(): ModelInfo[] {
return this.catalog.models;
}
public getRecipes(): Recipe[] {
return this.catalog.recipes;
}

async loadCatalog() {
const catalogPath = path.resolve(this.appUserDirectory, 'catalog.json');
if (!existsSync(catalogPath)) {
return this.setCatalog(defaultCatalog);
}

try {
this.watchCatalogFile(catalogPath); // do not await, we want to do this async
} catch (err: unknown) {
console.error("unable to watch catalog file, changes to the catalog file won't be reflected to the UI", err);
}

try {
const cat = await this.readAndAnalyzeCatalog(catalogPath);
return this.setCatalog(cat);
} catch (err: unknown) {
console.error('unable to read catalog file, reverting to default catalog', err);
}
// If something went wrong we load the default catalog
return this.setCatalog(defaultCatalog);
}

watchCatalogFile(path: string) {
const watcher = fs.createFileSystemWatcher(path);
watcher.onDidCreate(async () => {
try {
const cat = await this.readAndAnalyzeCatalog(path);
await this.setCatalog(cat);
} catch (err: unknown) {
console.error('unable to read created catalog file, continue using default catalog', err);
}
});
watcher.onDidDelete(async () => {
console.log('user catalog file deleted, reverting to default catalog');
await this.setCatalog(defaultCatalog);
});
watcher.onDidChange(async () => {
try {
const cat = await this.readAndAnalyzeCatalog(path);
await this.setCatalog(cat);
} catch (err: unknown) {
console.error('unable to read modified catalog file, reverting to default catalog', err);
}
});
}

async readAndAnalyzeCatalog(path: string): Promise<Catalog> {
const data = await promises.readFile(path, 'utf-8');
return JSON.parse(data) as Catalog;
// TODO(feloy): check version, ...
}

async setCatalog(newCatalog: Catalog) {
this.catalog = newCatalog;
await this.webview.postMessage({
id: MSG_NEW_CATALOG_STATE,
body: this.catalog,
});
}
}
18 changes: 18 additions & 0 deletions packages/backend/src/managers/gitManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/**********************************************************************
* 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 simpleGit, { type SimpleGit } from 'simple-git';

export class GitManager {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/**********************************************************************
* 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 {
provider,
containerEngine,
Expand All @@ -10,10 +28,11 @@ import type { ModelResponse } from '@shared/src/models/IModelResponse';

import path from 'node:path';
import * as http from 'node:http';
import { getFreePort } from './utils/ports';
import { getFreePort } from '../utils/ports';
import type { QueryState } from '@shared/src/models/IPlaygroundQueryState';
import { MSG_NEW_PLAYGROUND_QUERIES_STATE } from '@shared/Messages';

// TODO: this should not be hardcoded
const LOCALAI_IMAGE = 'quay.io/go-skynet/local-ai:v2.5.1';

function findFirstProvider(): ProviderContainerConnection | undefined {
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/src/models/AIConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/**********************************************************************
* 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 * as jsYaml from 'js-yaml';

export interface ContainerConfig {
Expand Down
18 changes: 18 additions & 0 deletions packages/backend/src/registries/RecipeStatusRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/**********************************************************************
* 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 { RecipeStatus } from '@shared/src/models/IRecipeStatus';
import type { TaskRegistry } from './TaskRegistry';

Expand Down
18 changes: 18 additions & 0 deletions packages/backend/src/registries/TaskRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
/**********************************************************************
* 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 { Task } from '@shared/src/models/ITask';

export class TaskRegistry {
Expand Down
58 changes: 44 additions & 14 deletions packages/backend/src/studio-api-impl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,31 +24,60 @@ import userContent from './ai-user-test.json';
import type { ApplicationManager } from './managers/applicationManager';
import type { RecipeStatusRegistry } from './registries/RecipeStatusRegistry';
import { StudioApiImpl } from './studio-api-impl';
import type { PlayGroundManager } from './playground';
import type { PlayGroundManager } from './managers/playground';
import type { TaskRegistry } from './registries/TaskRegistry';
import type { Webview } from '@podman-desktop/api';

import * as fs from 'node:fs';
import { CatalogManager } from './managers/catalogManager';

vi.mock('./ai.json', () => {
return {
default: content,
};
});

vi.mock('node:fs', () => {
return {
existsSync: vi.fn(),
promises: {
readFile: vi.fn(),
},
};
});

vi.mock('@podman-desktop/api', () => {
return {
fs: {
createFileSystemWatcher: () => ({
onDidCreate: vi.fn(),
onDidDelete: vi.fn(),
onDidChange: vi.fn(),
}),
},
};
});

let studioApiImpl: StudioApiImpl;
let catalogManager;

beforeEach(async () => {
const appUserDirectory = '.';

// Creating CatalogManager
catalogManager = new CatalogManager(appUserDirectory, {
postMessage: vi.fn(),
} as unknown as Webview);

// Creating StudioApiImpl
studioApiImpl = new StudioApiImpl(
{
appUserDirectory: '.',
appUserDirectory,
} as unknown as ApplicationManager,
{} as unknown as RecipeStatusRegistry,
{} as unknown as TaskRegistry,
{} as unknown as PlayGroundManager,
{
postMessage: vi.fn(),
} as unknown as Webview,
catalogManager,
);
vi.resetAllMocks();
vi.mock('node:fs');
Expand All @@ -57,11 +86,11 @@ beforeEach(async () => {
describe('invalid user catalog', () => {
beforeEach(async () => {
vi.spyOn(fs.promises, 'readFile').mockResolvedValue('invalid json');
await studioApiImpl.loadCatalog();
await catalogManager.loadCatalog();
});

test('expect correct model is returned with valid id', () => {
const model = studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S');
test('expect correct model is returned with valid id', async () => {
const model = await studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S');
expect(model).toBeDefined();
expect(model.name).toEqual('Llama-2-7B-Chat-GGUF');
expect(model.registry).toEqual('Hugging Face');
Expand All @@ -70,15 +99,15 @@ describe('invalid user catalog', () => {
);
});

test('expect error if id does not correspond to any model', () => {
expect(() => studioApiImpl.getModelById('unknown')).toThrowError('No model found having id unknown');
test('expect error if id does not correspond to any model', async () => {
await expect(() => studioApiImpl.getModelById('unknown')).rejects.toThrowError('No model found having id unknown');
});
});

test('expect correct model is returned from default catalog with valid id when no user catalog exists', async () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
await studioApiImpl.loadCatalog();
const model = studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S');
await catalogManager.loadCatalog();
const model = await studioApiImpl.getModelById('llama-2-7b-chat.Q5_K_S');
expect(model).toBeDefined();
expect(model.name).toEqual('Llama-2-7B-Chat-GGUF');
expect(model.registry).toEqual('Hugging Face');
Expand All @@ -90,8 +119,9 @@ test('expect correct model is returned from default catalog with valid id when n
test('expect correct model is returned with valid id when the user catalog is valid', async () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs.promises, 'readFile').mockResolvedValue(JSON.stringify(userContent));
await studioApiImpl.loadCatalog();
const model = studioApiImpl.getModelById('model1');

await catalogManager.loadCatalog();
const model = await studioApiImpl.getModelById('model1');
expect(model).toBeDefined();
expect(model.name).toEqual('Model 1');
expect(model.registry).toEqual('Hugging Face');
Expand Down
Loading

0 comments on commit ad6228a

Please sign in to comment.