Skip to content

Commit

Permalink
feat: dynamically register extension settings (janhq#2494)
Browse files Browse the repository at this point in the history
* feat: add extesion settings

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

---------

Signed-off-by: James <[email protected]>
Co-authored-by: James <[email protected]>
Co-authored-by: Louis <[email protected]>
  • Loading branch information
3 people authored Mar 29, 2024
1 parent ec6bcf6 commit fa35aa6
Show file tree
Hide file tree
Showing 61 changed files with 1,742 additions and 1,332 deletions.
129 changes: 127 additions & 2 deletions core/src/browser/extension.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { SettingComponentProps } from '../types'
import { getJanDataFolderPath, joinPath } from './core'
import { fs } from './fs'

export enum ExtensionTypeEnum {
Assistant = 'assistant',
Conversational = 'conversational',
Expand Down Expand Up @@ -32,6 +36,38 @@ export type InstallationState = InstallationStateTuple[number]
* This class should be extended by any class that represents an extension.
*/
export abstract class BaseExtension implements ExtensionType {
protected settingFolderName = 'settings'
protected settingFileName = 'settings.json'

/** @type {string} Name of the extension. */
name?: string

/** @type {string} The URL of the extension to load. */
url: string

/** @type {boolean} Whether the extension is activated or not. */
active

/** @type {string} Extension's description. */
description

/** @type {string} Extension's version. */
version

constructor(
url: string,
name?: string,
active?: boolean,
description?: string,
version?: string
) {
this.name = name
this.url = url
this.active = active
this.description = description
this.version = version
}

/**
* Returns the type of the extension.
* @returns {ExtensionType} The type of the extension
Expand All @@ -40,11 +76,13 @@ export abstract class BaseExtension implements ExtensionType {
type(): ExtensionTypeEnum | undefined {
return undefined
}

/**
* Called when the extension is loaded.
* Any initialization logic for the extension should be put here.
*/
abstract onLoad(): void

/**
* Called when the extension is unloaded.
* Any cleanup logic for the extension should be put here.
Expand All @@ -67,6 +105,42 @@ export abstract class BaseExtension implements ExtensionType {
return false
}

async registerSettings(settings: SettingComponentProps[]): Promise<void> {
if (!this.name) {
console.error('Extension name is not defined')
return
}

const extensionSettingFolderPath = await joinPath([
await getJanDataFolderPath(),
'settings',
this.name,
])
settings.forEach((setting) => {
setting.extensionName = this.name
})
try {
await fs.mkdir(extensionSettingFolderPath)
const settingFilePath = await joinPath([extensionSettingFolderPath, this.settingFileName])

if (await fs.existsSync(settingFilePath)) return
await fs.writeFileSync(settingFilePath, JSON.stringify(settings, null, 2))
} catch (err) {
console.error(err)
}
}

async getSetting<T>(key: string, defaultValue: T) {
const keySetting = (await this.getSettings()).find((setting) => setting.key === key)

const value = keySetting?.controllerProps.value
return (value as T) ?? defaultValue
}

onSettingUpdate<T>(key: string, value: T) {
return
}

/**
* Determine if the prerequisites for the extension are installed.
*
Expand All @@ -81,8 +155,59 @@ export abstract class BaseExtension implements ExtensionType {
*
* @returns {Promise<void>}
*/
// @ts-ignore
async install(...args): Promise<void> {
async install(): Promise<void> {
return
}

async getSettings(): Promise<SettingComponentProps[]> {
if (!this.name) return []

const settingPath = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])

try {
const content = await fs.readFileSync(settingPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
return settings
} catch (err) {
console.warn(err)
return []
}
}

async updateSettings(componentProps: Partial<SettingComponentProps>[]): Promise<void> {
if (!this.name) return

const settings = await this.getSettings()

const updatedSettings = settings.map((setting) => {
const updatedSetting = componentProps.find(
(componentProp) => componentProp.key === setting.key
)
if (updatedSetting && updatedSetting.controllerProps) {
setting.controllerProps.value = updatedSetting.controllerProps.value
}
return setting
})

const settingPath = await joinPath([
await getJanDataFolderPath(),
this.settingFolderName,
this.name,
this.settingFileName,
])

await fs.writeFileSync(settingPath, JSON.stringify(updatedSettings, null, 2))

updatedSettings.forEach((setting) => {
this.onSettingUpdate<typeof setting.controllerProps.value>(
setting.key,
setting.controllerProps.value
)
})
}
}
8 changes: 5 additions & 3 deletions core/src/browser/extensions/engines/OAIEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export abstract class OAIEngine extends AIEngine {
/*
* Inference request
*/
override inference(data: MessageRequest) {
override async inference(data: MessageRequest) {
if (data.model?.engine?.toString() !== this.provider) return

const timestamp = Date.now()
Expand Down Expand Up @@ -77,12 +77,14 @@ export abstract class OAIEngine extends AIEngine {
...data.model,
}

const header = await this.headers()

requestInference(
this.inferenceUrl,
data.messages ?? [],
model,
this.controller,
this.headers()
header
).subscribe({
next: (content: any) => {
const messageContent: ThreadContent = {
Expand Down Expand Up @@ -123,7 +125,7 @@ export abstract class OAIEngine extends AIEngine {
/**
* Headers for the inference request
*/
headers(): HeadersInit {
async headers(): Promise<HeadersInit> {
return {}
}
}
11 changes: 6 additions & 5 deletions core/src/browser/extensions/engines/RemoteOAIEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { OAIEngine } from './OAIEngine'
* Added the implementation of loading and unloading model (applicable to local inference providers)
*/
export abstract class RemoteOAIEngine extends OAIEngine {
// The inference engine
abstract apiKey: string
apiKey?: string
/**
* On extension load, subscribe to events.
*/
Expand All @@ -17,10 +16,12 @@ export abstract class RemoteOAIEngine extends OAIEngine {
/**
* Headers for the inference request
*/
override headers(): HeadersInit {
override async headers(): Promise<HeadersInit> {
return {
'Authorization': `Bearer ${this.apiKey}`,
'api-key': `${this.apiKey}`,
...(this.apiKey && {
'Authorization': `Bearer ${this.apiKey}`,
'api-key': `${this.apiKey}`,
}),
}
}
}
9 changes: 3 additions & 6 deletions core/src/node/api/common/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@ export type Handler = (route: string, args: any) => any

export class RequestHandler {
handler: Handler
adataper: RequestAdapter
adapter: RequestAdapter

constructor(handler: Handler, observer?: Function) {
this.handler = handler
this.adataper = new RequestAdapter(observer)
this.adapter = new RequestAdapter(observer)
}

handle() {
CoreRoutes.map((route) => {
this.handler(route, async (...args: any[]) => {
const values = await this.adataper.process(route, ...args)
return values
})
this.handler(route, async (...args: any[]) => this.adapter.process(route, ...args))
})
}
}
3 changes: 2 additions & 1 deletion core/src/node/api/restful/helper/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,15 @@ export const chatCompletions = async (request: any, reply: any) => {
}

const requestedModel = matchedModels[0]

const engineConfiguration = await getEngineConfiguration(requestedModel.engine)

let apiKey: string | undefined = undefined
let apiUrl: string = DEFAULT_CHAT_COMPLETION_URL

if (engineConfiguration) {
apiKey = engineConfiguration.api_key
apiUrl = engineConfiguration.full_url
apiUrl = engineConfiguration.full_url ?? DEFAULT_CHAT_COMPLETION_URL
}

const headers: Record<string, any> = {
Expand Down
30 changes: 23 additions & 7 deletions core/src/node/helper/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AppConfiguration } from '../../types'
import { AppConfiguration, SettingComponentProps } from '../../types'
import { join } from 'path'
import fs from 'fs'
import os from 'os'
Expand Down Expand Up @@ -125,14 +125,30 @@ const exec = async (command: string): Promise<string> => {
})
}

// a hacky way to get the api key. we should comes up with a better
// way to handle this
export const getEngineConfiguration = async (engineId: string) => {
if (engineId !== 'openai' && engineId !== 'groq') {
return undefined
if (engineId !== 'openai' && engineId !== 'groq') return undefined

const settingDirectoryPath = join(
getJanDataFolderPath(),
'settings',
engineId === 'openai' ? 'inference-openai-extension' : 'inference-groq-extension',
'settings.json'
)

const content = fs.readFileSync(settingDirectoryPath, 'utf-8')
const settings: SettingComponentProps[] = JSON.parse(content)
const apiKeyId = engineId === 'openai' ? 'openai-api-key' : 'groq-api-key'
const keySetting = settings.find((setting) => setting.key === apiKeyId)

let apiKey = keySetting?.controllerProps.value
if (typeof apiKey !== 'string') apiKey = ''

return {
api_key: apiKey,
full_url: undefined,
}
const directoryPath = join(getJanDataFolderPath(), 'engines')
const filePath = join(directoryPath, `${engineId}.json`)
const data = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(data)
}

/**
Expand Down
1 change: 1 addition & 0 deletions core/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './config'
export * from './huggingface'
export * from './miscellaneous'
export * from './api'
export * from './setting'
1 change: 1 addition & 0 deletions core/src/types/setting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './settingComponent'
34 changes: 34 additions & 0 deletions core/src/types/setting/settingComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type SettingComponentProps = {
key: string
title: string
description: string
controllerType: ControllerType
controllerProps: SliderComponentProps | CheckboxComponentProps | InputComponentProps

extensionName?: string
requireModelReload?: boolean
configType?: ConfigType
}

export type ConfigType = 'runtime' | 'setting'

export type ControllerType = 'slider' | 'checkbox' | 'input'

export type InputType = 'password' | 'text' | 'email' | 'number' | 'tel' | 'url'

export type InputComponentProps = {
placeholder: string
value: string
type?: InputType
}

export type SliderComponentProps = {
min: number
max: number
step: number
value: number
}

export type CheckboxComponentProps = {
value: boolean
}
6 changes: 2 additions & 4 deletions extensions/assistant-extension/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ import replace from '@rollup/plugin-replace'

const packageJson = require('./package.json')

const pkg = require('./package.json')

export default [
{
input: `src/index.ts`,
output: [{ file: pkg.main, format: 'es', sourcemap: true }],
output: [{ file: packageJson.main, format: 'es', sourcemap: true }],
// Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash')
external: [],
watch: {
Expand All @@ -36,7 +34,7 @@ export default [
// https://github.com/rollup/rollup-plugin-node-resolve#usage
resolve({
extensions: ['.js', '.ts', '.svelte'],
browser: true
browser: true,
}),

// Resolve source maps to the original source
Expand Down
Loading

0 comments on commit fa35aa6

Please sign in to comment.