Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: proxy typing + copyrights #91

Merged
merged 10 commits into from
Jan 19, 2024
Merged
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
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';
axel7083 marked this conversation as resolved.
Show resolved Hide resolved
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;
axel7083 marked this conversation as resolved.
Show resolved Hide resolved

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);

Check warning on line 69 in packages/backend/src/managers/catalogManager.ts

View workflow job for this annotation

GitHub Actions / linter, formatters and unit tests / windows-2022

Strings must use singlequote

Check warning on line 69 in packages/backend/src/managers/catalogManager.ts

View workflow job for this annotation

GitHub Actions / linter, formatters and unit tests / ubuntu-22.04

Strings must use singlequote

Check warning on line 69 in packages/backend/src/managers/catalogManager.ts

View workflow job for this annotation

GitHub Actions / linter, formatters and unit tests / macos-12

Strings must use singlequote
}

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