Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
78a3545
feat(backend): enhance config loader with embedding support and impro…
Sma1lboy Dec 16, 2024
72c6dbc
feat(backend): implement ModelStatusManager for tracking model downlo…
Sma1lboy Dec 16, 2024
1340532
chore: adding uitls ufc
Sma1lboy Dec 16, 2024
0ee6b84
Merge branch 'main' of https://github.com/Sma1lboy/codefox into feat-…
NarwhalChen Dec 25, 2024
0b412b0
feat: adding embedding provider and openai-embedding provider
NarwhalChen Dec 25, 2024
b378185
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 25, 2024
4c66d05
feat: implmenting dynamic loader for embedding and models in hugging …
NarwhalChen Dec 28, 2024
ad74c49
Merge branch 'feat-adding-embedding-config' of https://github.com/Sma…
NarwhalChen Dec 28, 2024
97cae53
Merge branch 'main' into feat-adding-embedding-config
Sma1lboy Dec 28, 2024
e579146
feat(backend): refactor model downloading and configuration handling
Sma1lboy Dec 28, 2024
8b25760
Merge branch 'feat-adding-embedding-config' of https://github.com/Sma…
NarwhalChen Dec 30, 2024
b7a8b30
to: using fastemb to implement embedding downloader
NarwhalChen Dec 30, 2024
7e68b3a
to: moving embedding provider to backend
NarwhalChen Dec 30, 2024
2c2027d
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 30, 2024
fb9be7d
feat: request llm-server with openai/some embedding provider
NarwhalChen Dec 30, 2024
0b85081
Merge branch 'feat-adding-embedding-config' of https://github.com/Sma…
NarwhalChen Dec 30, 2024
6ad8b9b
to: adding return type and comments for generateEmbResponse
NarwhalChen Dec 31, 2024
56c6ffc
fix: fixing the change of env variable
NarwhalChen Jan 6, 2025
b9c1909
fix: deleting useless embedding index.ts
NarwhalChen Jan 6, 2025
50d3bd1
Merge branch 'main' into feat-adding-embedding-config
ZHallen122 Jan 6, 2025
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
237 changes: 237 additions & 0 deletions backend/src/config/config-loader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// config-loader.ts
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Rename file to reflect implementation code

The file config-loader.spec.ts is conventionally used for test files, but it contains implementation code for the ConfigLoader class. Consider renaming it to config-loader.ts to accurately represent its contents. Additionally, create a corresponding test file config-loader.spec.ts to include unit tests for the ConfigLoader class.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider the attribution like isLocal to separate remote model or local model in chatconfig?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@NarwhalChen, adding an "isLocal" (or similarly named) attribute in the chat configuration is a reasonable approach if you’d like to distinguish between loading a model from a local path versus relying on a remote API endpoint. This attribute can help clarify configuration options in the code, guide conditional logic (e.g., deciding between file-system checks or network requests), and keep future expansions flexible (for instance, if you add multiple remote endpoints or different local model directories).

If you proceed with this, you could define something like:

interface ChatConfig {
modelName: string;
isDefault?: boolean;
isLocal?: boolean; // Example attribute
// ...other fields
}

That way, when you load or operate on the configuration, you can easily apply different behaviors based on the value of “isLocal.”

import * as fs from 'fs';
import * as path from 'path';
import * as _ from 'lodash';
import { getConfigPath } from './common-path';

export interface ChatConfig {
model: string;
endpoint?: string;
token?: string;
default?: boolean;
task?: string;
}

export interface EmbeddingConfig {
model: string;
endpoint?: string;
token?: string;
}

export interface AppConfig {
chats?: ChatConfig[];
embeddings?: EmbeddingConfig;
}

export const exampleConfigContent = `{
// Chat models configuration
// You can configure multiple chat models
"chats": [
// Example of OpenAI GPT configuration
{
"model": "gpt-3.5-turbo",
"endpoint": "https://api.openai.com/v1",
"token": "your-openai-token", // Replace with your OpenAI token
"default": true // Set as default chat model
},

// Example of local model configuration
{
"model": "llama2",
"endpoint": "http://localhost:11434/v1",
"task": "chat"
}
],

// Embedding model configuration (optional)
"embeddings": {
"model": "text-embedding-ada-002",
"endpoint": "https://api.openai.com/v1",
"token": "your-openai-token" // Replace with your OpenAI token
}
}`;

export class ConfigLoader {
private static instance: ConfigLoader;
private config: AppConfig;
private readonly configPath: string;

private constructor(configPath?: string) {
this.configPath = configPath || getConfigPath('config');
this.loadConfig();
}

public static getInstance(configPath?: string): ConfigLoader {
if (!ConfigLoader.instance) {
ConfigLoader.instance = new ConfigLoader(configPath);
}
return ConfigLoader.instance;
}

public static initConfigFile(configPath: string): void {
if (fs.existsSync(configPath)) {
throw new Error('Config file already exists');
}

const configDir = path.dirname(configPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}

fs.writeFileSync(configPath, exampleConfigContent, 'utf-8');
}

public reload(): void {
this.loadConfig();
}

private loadConfig() {
try {
const file = fs.readFileSync(this.configPath, 'utf-8');
const jsonContent = file.replace(
/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
(m, g) => (g ? '' : m),
);
this.config = JSON.parse(jsonContent);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid using regex to remove comments from JSON

Using regular expressions to strip comments from JSON can be error-prone and may not handle all edge cases correctly. Instead, consider using a JSON parser that supports comments, such as jsonc-parser, or switch to a configuration format that inherently supports comments like YAML or JSON5.

this.validateConfig();
} catch (error) {
if (
error.code === 'ENOENT' ||
error.message.includes('Unexpected end of JSON input')
) {
this.config = {};
this.saveConfig();
} else {
throw error;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Prevent data loss by not overwriting configuration on parse errors

When a JSON parsing error occurs, the current implementation initializes an empty configuration and saves it back to the file (lines 102-104). This can lead to permanent loss of the original configuration data. It's safer to notify the user about the parsing error without overwriting the existing configuration file, allowing them to fix the issue manually.

}

get<T>(path?: string): T {
if (!path) {
return this.config as unknown as T;
}
return _.get(this.config, path) as T;
}

set(path: string, value: any) {
_.set(this.config, path, value);
this.saveConfig();
}

private saveConfig() {
const configDir = path.dirname(this.configPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(
this.configPath,
JSON.stringify(this.config, null, 2),
'utf-8',
);
}

getAllChatConfigs(): ChatConfig[] {
return this.config.chats || [];
}

getChatConfig(modelName?: string): ChatConfig | null {
if (!this.config.chats || !Array.isArray(this.config.chats)) {
return null;
}

const chats = this.config.chats;

if (modelName) {
const foundChat = chats.find((chat) => chat.model === modelName);
if (foundChat) {
return foundChat;
}
}

return (
chats.find((chat) => chat.default) || (chats.length > 0 ? chats[0] : null)
);
}

addChatConfig(config: ChatConfig) {
if (!this.config.chats) {
this.config.chats = [];
}

const index = this.config.chats.findIndex(
(chat) => chat.model === config.model,
);
if (index !== -1) {
this.config.chats.splice(index, 1);
}

if (config.default) {
this.config.chats.forEach((chat) => {
chat.default = false;
});
}

this.config.chats.push(config);
this.saveConfig();
}

removeChatConfig(modelName: string): boolean {
if (!this.config.chats) {
return false;
}

const initialLength = this.config.chats.length;
this.config.chats = this.config.chats.filter(
(chat) => chat.model !== modelName,
);

if (this.config.chats.length !== initialLength) {
this.saveConfig();
return true;
}

return false;
}

getEmbeddingConfig(): EmbeddingConfig | null {
return this.config.embeddings || null;
}

validateConfig() {
if (!this.config) {
this.config = {};
}

if (typeof this.config !== 'object') {
throw new Error('Invalid configuration: Must be an object');
}

if (this.config.chats) {
if (!Array.isArray(this.config.chats)) {
throw new Error("Invalid configuration: 'chats' must be an array");
}

this.config.chats.forEach((chat, index) => {
if (!chat.model) {
throw new Error(
`Invalid chat configuration at index ${index}: 'model' is required`,
);
}
});

const defaultChats = this.config.chats.filter((chat) => chat.default);
if (defaultChats.length > 1) {
throw new Error(
'Invalid configuration: Multiple default chat configurations found',
);
}
}

if (this.config.embeddings) {
if (!this.config.embeddings.model) {
throw new Error("Invalid embedding configuration: 'model' is required");
}
}
}
}
Loading
Loading