diff --git a/examples/browser/package.json b/examples/browser/package.json index 12b41f9507729..8172cae07e89d 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -28,6 +28,7 @@ "@theia/ai-history-ui": "1.49.0", "@theia/ai-openai": "1.49.0", "@theia/ai-terminal": "1.49.0", + "@theia/ai-llamafile": "1.49.0", "@theia/api-provider-sample": "1.49.0", "@theia/api-samples": "1.49.0", "@theia/bulk-edit": "1.49.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index bb96e14b4cc20..fc4edd13a148a 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -26,6 +26,9 @@ { "path": "../../packages/ai-history-ui" }, + { + "path": "../../packages/ai-llamafile" + }, { "path": "../../packages/ai-openai" }, diff --git a/examples/electron/package.json b/examples/electron/package.json index 5186fb2912226..bd87fc73ff392 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -34,6 +34,7 @@ "@theia/ai-history-ui": "1.49.0", "@theia/ai-openai": "1.49.0", "@theia/ai-terminal": "1.49.0", + "@theia/ai-llamafile": "1.49.0", "@theia/api-provider-sample": "1.49.0", "@theia/api-samples": "1.49.0", "@theia/bulk-edit": "1.49.0", diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json index a5340072d18ca..8a894e5d0b59e 100644 --- a/examples/electron/tsconfig.json +++ b/examples/electron/tsconfig.json @@ -29,6 +29,9 @@ { "path": "../../packages/ai-history-ui" }, + { + "path": "../../packages/ai-llamafile" + }, { "path": "../../packages/ai-openai" }, diff --git a/packages/ai-core/src/browser/frontend-language-model-registry.ts b/packages/ai-core/src/browser/frontend-language-model-registry.ts index c37841634a030..a95b1b5cce73f 100644 --- a/packages/ai-core/src/browser/frontend-language-model-registry.ts +++ b/packages/ai-core/src/browser/frontend-language-model-registry.ts @@ -88,6 +88,36 @@ export class FrontendLanguageModelRegistryImpl @inject(AISettingsService) protected settingsService: AISettingsService; + override addLanguageModels(models: LanguageModelMetaData[] | LanguageModel[]): void { + models.map(model => { + if (LanguageModel.is(model)) { + this.languageModels.push( + new Proxy( + model, + languageModelOutputHandler( + this.outputChannelManager.getChannel( + model.id + ) + ) + ) + ); + } else { + this.languageModels.push( + new Proxy( + this.createFrontendLanguageModel( + model + ), + languageModelOutputHandler( + this.outputChannelManager.getChannel( + model.id + ) + ) + ) + ); + } + }); + } + @postConstruct() protected override init(): void { this.client.setReceiver(this); @@ -97,25 +127,12 @@ export class FrontendLanguageModelRegistryImpl const promises = contributions.map(provider => provider()); const backendDescriptions = this.registryDelegate.getLanguageModelDescriptions(); + Promise.allSettled([backendDescriptions, ...promises]).then( results => { const backendDescriptionsResult = results[0]; if (backendDescriptionsResult.status === 'fulfilled') { - this.languageModels.push( - ...backendDescriptionsResult.value.map( - description => - new Proxy( - this.createFrontendLanguageModel( - description - ), - languageModelOutputHandler( - this.outputChannelManager.getChannel( - description.id - ) - ) - ) - ) - ); + this.addLanguageModels(backendDescriptionsResult.value); } else { this.logger.error( 'Failed to add language models contributed from the backend', @@ -128,19 +145,7 @@ export class FrontendLanguageModelRegistryImpl | PromiseRejectedResult | PromiseFulfilledResult; if (languageModelResult.status === 'fulfilled') { - this.languageModels.push( - ...languageModelResult.value.map( - languageModel => - new Proxy( - languageModel, - languageModelOutputHandler( - this.outputChannelManager.getChannel( - languageModel.id - ) - ) - ) - ) - ); + this.addLanguageModels(languageModelResult.value); } else { this.logger.error( 'Failed to add some language models:', diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts index e1c21bf81d1fc..3a48834b2c0fd 100644 --- a/packages/ai-core/src/common/language-model.ts +++ b/packages/ai-core/src/common/language-model.ts @@ -14,7 +14,7 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { ContributionProvider, ILogger } from '@theia/core'; +import { ContributionProvider, ILogger, isFunction, isObject } from '@theia/core'; import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; export type ChatActor = 'user' | 'ai'; @@ -89,10 +89,22 @@ export interface LanguageModelMetaData { readonly maxOutputTokens?: number; } +export namespace LanguageModelMetaData { + export function is(arg: unknown): arg is LanguageModelMetaData { + return isObject(arg) && 'id' in arg && 'providerId' in arg; + } +} + export interface LanguageModel extends LanguageModelMetaData { request(request: LanguageModelRequest): Promise; } +export namespace LanguageModel { + export function is(arg: unknown): arg is LanguageModel { + return isObject(arg) && 'id' in arg && 'providerId' in arg && isFunction(arg.request); + } +} + // See also VS Code `ILanguageModelChatSelector` interface VsCodeLanguageModelSelector { readonly identifier?: string; @@ -110,6 +122,7 @@ export interface LanguageModelSelector extends VsCodeLanguageModelSelector { export const LanguageModelRegistry = Symbol('LanguageModelRegistry'); export interface LanguageModelRegistry { + addLanguageModels(models: LanguageModel[]): void; getLanguageModels(): Promise; getLanguageModel(id: string): Promise; selectLanguageModels(request: LanguageModelSelector): Promise; @@ -143,6 +156,11 @@ export class DefaultLanguageModelRegistryImpl implements LanguageModelRegistry { }); } + addLanguageModels(models: LanguageModel[]): void { + models.map(model => this.languageModels.push(model)); + // TODO: notify frontend about new models + } + async getLanguageModels(): Promise { await this.initialized; return this.languageModels; diff --git a/packages/ai-llamafile/.eslintrc.js b/packages/ai-llamafile/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-llamafile/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + } +}; diff --git a/packages/ai-llamafile/README.md b/packages/ai-llamafile/README.md new file mode 100644 index 0000000000000..111572dd499b2 --- /dev/null +++ b/packages/ai-llamafile/README.md @@ -0,0 +1 @@ +# AI Llamafile integration diff --git a/packages/ai-llamafile/package.json b/packages/ai-llamafile/package.json new file mode 100644 index 0000000000000..48da73805c226 --- /dev/null +++ b/packages/ai-llamafile/package.json @@ -0,0 +1,52 @@ +{ + "name": "@theia/ai-llamafile", + "version": "1.49.0", + "description": "Theia - Llamafile Integration", + "dependencies": { + "@theia/core": "1.49.0", + "@theia/filesystem": "1.49.0", + "@theia/workspace": "1.49.0", + "minimatch": "^5.1.0", + "tslib": "^2.6.2", + "@theia/ai-core": "1.49.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/ai-llamafile-frontend-module", + "backend": "lib/node/ai-llamafile-backend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "lint": "theiaext lint", + "test": "theiaext test", + "watch": "theiaext watch" + }, + "devDependencies": { + "@theia/ext-scripts": "1.49.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/ai-llamafile/src/browser/ai-llamafile-frontend-module.ts b/packages/ai-llamafile/src/browser/ai-llamafile-frontend-module.ts new file mode 100644 index 0000000000000..7512fa168d455 --- /dev/null +++ b/packages/ai-llamafile/src/browser/ai-llamafile-frontend-module.ts @@ -0,0 +1,38 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { CommandContribution } from '@theia/core'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { LlamafileCommandContribution, NewLlamafileConfigQuickInputProvider } from './llamafile-command-contribution'; +import { bindViewContribution, WidgetFactory } from '@theia/core/lib/browser'; +import { LlamafileViewContribution } from './llamafile-view-contribution'; +import { LlamafileListWidget } from './llamafile-list-widget'; +import { LlamafileServerManager, LlamafileServerManagerPath } from '../common/llamafile-server-manager'; +import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; + +export default new ContainerModule(bind => { + bind(NewLlamafileConfigQuickInputProvider).toSelf().inSingletonScope(); + bind(LlamafileListWidget).toSelf(); + bind(WidgetFactory).toDynamicValue(context => ({ + id: LlamafileListWidget.ID, + createWidget: () => context.container.get(LlamafileListWidget), + })).inSingletonScope(); + bind(CommandContribution).to(LlamafileCommandContribution).inSingletonScope(); + bindViewContribution(bind, LlamafileViewContribution); + bind(LlamafileServerManager).toDynamicValue(ctx => { + const connection = ctx.container.get(RemoteConnectionProvider); + return connection.createProxy(LlamafileServerManagerPath); + }); +}); diff --git a/packages/ai-llamafile/src/browser/llamafile-command-contribution.ts b/packages/ai-llamafile/src/browser/llamafile-command-contribution.ts new file mode 100644 index 0000000000000..6481880852579 --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-command-contribution.ts @@ -0,0 +1,109 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { CommandContribution, CommandRegistry } from '@theia/core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { QuickInputService } from '@theia/core/lib/browser'; +import { FileDialogService, OpenFileDialogProps } from '@theia/filesystem/lib/browser'; +import { LlamafileListItem } from './llamafile-list-widget'; + +export const CREATE_LANGUAGE_MODEL = { + id: 'core.keyboard.languagemodel', + label: 'Create Language Model', +}; + +export const NewLlamafileEntryInput = { + id: 'llamafile.input.new.entry', + label: 'New Llamafile Entry', +}; + +@injectable() +export class NewLlamafileConfigQuickInputProvider { + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(FileDialogService) + protected readonly fileDialogService: FileDialogService; + + async askForNameAndPath(): Promise { + // Get the name input + const name = await this.quickInputService.input({ + prompt: 'Enter a name' + }); + + if (!name) { + throw new Error('Name input was canceled.'); + } + + // Get the path input using a file system picker + const path = await this.askForPath(); + + if (!path) { + throw new Error('Path selection was canceled.'); + } + + const port = await this.quickInputService.input({ + prompt: 'Enter a port' + }); + + if (!port || isNaN(Number(port))) { + throw new Error('Port input was canceled.'); + } + + return { name, path, port: Number(port), started: false, active: false }; + } + + private async askForPath(): Promise { + const props: OpenFileDialogProps = { + title: 'Select a file', + canSelectFiles: true, + canSelectFolders: false, + filters: { + 'Llamafile': ['llamafile'] + }, + canSelectMany: false + }; + + const uri = await this.fileDialogService.showOpenDialog(props); + + if (uri) { + return uri.toString(); + } + + return undefined; + } +} + +@injectable() +export class LlamafileCommandContribution implements CommandContribution { + + @inject(NewLlamafileConfigQuickInputProvider) + protected readonly quickInputProvider: NewLlamafileConfigQuickInputProvider; + + registerCommands(commandRegistry: CommandRegistry): void { + commandRegistry.registerCommand(NewLlamafileEntryInput, { + execute: async () => { + try { + return await this.quickInputProvider.askForNameAndPath(); + } catch (error) { + console.error('Input process was canceled or failed.', error); + } + } + }); + } + + +} diff --git a/packages/ai-llamafile/src/browser/llamafile-list-widget.tsx b/packages/ai-llamafile/src/browser/llamafile-list-widget.tsx new file mode 100644 index 0000000000000..33750bc8a509e --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-list-widget.tsx @@ -0,0 +1,136 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import * as React from '@theia/core/shared/react'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ReactWidget } from '@theia/core/lib/browser'; +import { LanguageModelRegistry } from '@theia/ai-core'; +import { LlamafileLanguageModel } from '../common/llamafile-language-model'; +import { NewLlamafileEntryInput } from './llamafile-command-contribution'; +import { CommandService } from '@theia/core'; +import { LlamafileServerManager } from '../common/llamafile-server-manager'; + +export interface LlamafileListItem { + name: string; + path: string; + port: number; + started: boolean; + active: boolean; +} + +// TODO: Improve UI +@injectable() +export class LlamafileListWidget extends ReactWidget { + + @inject(LanguageModelRegistry) + protected languageModelRegistry: LanguageModelRegistry; + + @inject(LlamafileServerManager) llamafileServerManager: LlamafileServerManager; + + @inject(CommandService) private commandService: CommandService; + + static readonly ID = 'llamafile:list-view'; + static readonly LABEL = 'Llamafile list view'; + + private items: LlamafileListItem[] = []; + + constructor() { + super(); + this.id = LlamafileListWidget.ID; + this.title.label = LlamafileListWidget.LABEL; + this.title.caption = LlamafileListWidget.LABEL; + this.title.closable = true; + this.update(); + } + + protected render(): React.ReactNode { + return ( +
+ { + this.items.map(item => ( +
+
+
+ Name: {item.name} +
+
+ Path: {item.path} +
+
+ Port: {item.port} +
+
+ Active: {item.active ? 'Yes' : 'No'} +
+
+ Started: {item.started ? 'Yes' : 'No'} +
+
+
+ + + +
+
+ ))} + +
+ ); + } + + addItem(): void { + // Popup dialog to get the name and path + let needsToBeActive = false; + if (this.items.length === 0) { + needsToBeActive = true; + } + this.commandService.executeCommand(NewLlamafileEntryInput.id).then(async (newItem: LlamafileListItem) => { + this.items.push(newItem); + this.languageModelRegistry.addLanguageModels( + [LlamafileLanguageModel.createNewLlamafileLanguageModel(newItem.name, newItem.path, newItem.port, this.llamafileServerManager)]); + this.update(); + if (needsToBeActive) { + this.activateServer(this.items[0].name); + } + }); + } + + async getLanguageModelForItem(name: string): Promise { + const result = await this.languageModelRegistry.getLanguageModel(name); + if (result instanceof LlamafileLanguageModel) { + return result; + } else { + return undefined; + } + } + + private startServer(name: string): void { + this.getLanguageModelForItem(name)?.then(model => model?.startServer()); + this.items.find(item => item.name === name)!.started = true; + this.update(); + } + private activateServer(name: string): void { + this.getLanguageModelForItem(name)?.then(model => model?.setAsActive()); + this.items.find(item => item.name === name)!.active = true; + this.items.find(item => item.name !== name)!.active = false; + this.update(); + } + private killServer(name: string): void { + this.getLanguageModelForItem(name)?.then(model => model?.killServer()); + this.items.find(item => item.name === name)!.started = false; + this.items.find(item => item.name === name)!.active = false; + this.update(); + } +} diff --git a/packages/ai-llamafile/src/browser/llamafile-view-contribution.ts b/packages/ai-llamafile/src/browser/llamafile-view-contribution.ts new file mode 100644 index 0000000000000..cee1389661f1b --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-view-contribution.ts @@ -0,0 +1,47 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { injectable } from '@theia/core/shared/inversify'; +import { AbstractViewContribution, CommonMenus } from '@theia/core/lib/browser'; +import { LlamafileListWidget } from './llamafile-list-widget'; +import { CommandRegistry, MenuModelRegistry } from '@theia/core'; + +@injectable() +export class LlamafileViewContribution extends AbstractViewContribution { + + constructor() { + super({ + widgetId: LlamafileListWidget.ID, + widgetName: LlamafileListWidget.LABEL, + defaultWidgetOptions: { area: 'left' }, + toggleCommandId: 'llamafile-view:toggle', + }); + } + + override registerCommands(commands: CommandRegistry): void { + super.registerCommands(commands); + commands.registerCommand({ id: 'llamafile-view:add', label: 'Add Item' }, { + execute: () => this.openView({ activate: true }).then(widget => widget.addItem()) + }); + } + + override registerMenus(menus: MenuModelRegistry): void { + super.registerMenus(menus); + menus.registerMenuAction(CommonMenus.EDIT_FIND, { + commandId: 'llamafile-view:add', + label: 'Add Item' + }); + } +} diff --git a/packages/ai-llamafile/src/common/llamafile-language-model.ts b/packages/ai-llamafile/src/common/llamafile-language-model.ts new file mode 100644 index 0000000000000..bd0cbceb8206d --- /dev/null +++ b/packages/ai-llamafile/src/common/llamafile-language-model.ts @@ -0,0 +1,114 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { LanguageModel, LanguageModelRequest, LanguageModelResponse } from '@theia/ai-core'; +import { LlamafileServerManager } from './llamafile-server-manager'; + +export class LlamafileLanguageModel implements LanguageModel { + + readonly providerId = 'llamafile'; + readonly vendor: string = 'Mozilla'; + + constructor(readonly name: string, readonly path: string, readonly port: number, readonly serverManager: LlamafileServerManager) { + } + + startServer(): void { + this.serverManager.startServer(this.name, this.path, this.port); + } + + killServer(): void { + this.serverManager.killServer(this.name); + } + + setAsActive(): void { + this.serverManager.setAsActive(this.name); + } + + isActive(): boolean { + return this.serverManager.isActive(this.name); + } + + get id(): string { + return this.name; + } + + async request(request: LanguageModelRequest): Promise { + try { + const response = await fetch(`http://localhost:${this.port}/completion`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt: request.messages[request.messages.length - 1].query, + n_predict: 200 + // stream: true + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // TODO: Get the stream working + // if (!response.body) { + // throw new Error('Response body is undefined'); + // } + + // const reader = response.body.getReader(); + // return { + // stream: { + // [Symbol.asyncIterator](): AsyncIterator { + // return { + // async next(): Promise> { + // const { value, done } = await reader.read(); + // if (done) { + // return { value: undefined, done: true }; + // } + // const text = new TextDecoder().decode(value).substring(5); + // console.log(text); + // const parsed = JSON.parse(text); + // console.log(parsed); + // return { value: parsed.content, done: false }; + // } + // }; + // } + // } + // }; + + const data = await response.json(); + if (data && data.content) { + return { + text: data.content + }; + } else { + return { + text: 'No content field found in the response.' + }; + } + } catch (error) { + console.error('Error:', error); + return { + text: `Error: ${error}` + }; + } + } + + static createNewLlamafileLanguageModel(name: string, path: string, port: number, serverManager: LlamafileServerManager): LlamafileLanguageModel { + return new LlamafileLanguageModel(name, path, port, serverManager); + } + +} diff --git a/packages/ai-llamafile/src/common/llamafile-server-manager.ts b/packages/ai-llamafile/src/common/llamafile-server-manager.ts new file mode 100644 index 0000000000000..72abb66972259 --- /dev/null +++ b/packages/ai-llamafile/src/common/llamafile-server-manager.ts @@ -0,0 +1,25 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +export const LlamafileServerManager = Symbol('LlamafileServerManager'); + +export const LlamafileServerManagerPath = '/services/llamafileservermanager'; + +export interface LlamafileServerManager { + startServer(name: string, path: string, port: number): void; + killServer(name: string): void; + setAsActive(name: string): void; + isActive(name: string): boolean; +} diff --git a/packages/ai-llamafile/src/node/ai-llamafile-backend-module.ts b/packages/ai-llamafile/src/node/ai-llamafile-backend-module.ts new file mode 100644 index 0000000000000..bc5613be73fcb --- /dev/null +++ b/packages/ai-llamafile/src/node/ai-llamafile-backend-module.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from '@theia/core/shared/inversify'; +import { BackendLlamafileServerManager } from './backend-llamafile-server-manager'; +import { LlamafileServerManager, LlamafileServerManagerPath } from '../common/llamafile-server-manager'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; + +export default new ContainerModule(bind => { + bind(LlamafileServerManager).to(BackendLlamafileServerManager).inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(ctx => new RpcConnectionHandler( + LlamafileServerManagerPath, + () => ctx.container.get(LlamafileServerManager) + )).inSingletonScope(); +}); diff --git a/packages/ai-llamafile/src/node/backend-llamafile-server-manager.ts b/packages/ai-llamafile/src/node/backend-llamafile-server-manager.ts new file mode 100644 index 0000000000000..e032cf0e3ebfa --- /dev/null +++ b/packages/ai-llamafile/src/node/backend-llamafile-server-manager.ts @@ -0,0 +1,76 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { injectable } from '@theia/core/shared/inversify'; +import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; +import pathLibrary = require('path'); +import { LlamafileServerManager } from '../common/llamafile-server-manager'; + +@injectable() +export class BackendLlamafileServerManager implements LlamafileServerManager { + + activeServer: string | undefined = undefined; + processMap: Map = new Map(); + + startServer(name: string, path: string, port: number): void { + if (!this.processMap.has(name)) { + // TODO: Make platform independent + // Remove 'file:///' and extract the file path + const filePath = path.replace('file:///', '/'); + + // Extract the directory and file name + const dir = pathLibrary.dirname(filePath); + const fileName = pathLibrary.basename(filePath); + const currentProcess = spawn(`./${fileName}`, ['--port', '' + port, '--server', '--nobrowser'], { cwd: dir }); + this.processMap.set(name, currentProcess); + currentProcess.stdout.on('data', (data: Buffer) => { + const output = data.toString(); + // TODO: Make better logging mechanism + console.log(output); + }); + currentProcess.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + // TODO: Make better logging mechanism + console.log(output); + }); + currentProcess.on('close', code => { + console.log(`LlamaFile process exited with code ${code}`); + this.processMap.delete(name); + }); + } + } + + killServer(name: string): void { + if (this.processMap.has(name)) { + const currentProcess = this.processMap.get(name); + currentProcess!.kill(); + this.processMap.delete(name); + } + if (this.activeServer === name) { + this.activeServer = undefined; + } + } + + setAsActive(name: string): void { + this.activeServer = name; + } + isActive(name: string): boolean { + if (this.activeServer === undefined) { + return false; + } + return name === this.activeServer; + } + +} diff --git a/packages/ai-llamafile/tsconfig.json b/packages/ai-llamafile/tsconfig.json new file mode 100644 index 0000000000000..61a997fc14fd1 --- /dev/null +++ b/packages/ai-llamafile/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../filesystem" + }, + { + "path": "../workspace" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 4b5a38a6b22cd..0db7264187dbf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -72,6 +72,9 @@ { "path": "packages/ai-history-ui" }, + { + "path": "packages/ai-llamafile" + }, { "path": "packages/ai-openai" },