diff --git a/examples/browser/package.json b/examples/browser/package.json index a8a39d1189d7d..5cfdc9277b381 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -27,6 +27,7 @@ "@theia/ai-code-completion": "1.54.0", "@theia/ai-core": "1.54.0", "@theia/ai-history": "1.54.0", + "@theia/ai-llamafile": "1.54.0", "@theia/ai-ollama": "1.54.0", "@theia/ai-openai": "1.54.0", "@theia/ai-terminal": "1.54.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index f6b8cad219535..5f39c27826132 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../../packages/ai-history" }, + { + "path": "../../packages/ai-llamafile" + }, { "path": "../../packages/ai-ollama" }, diff --git a/examples/electron/package.json b/examples/electron/package.json index 04482481be284..5dcb9b9007a93 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -31,6 +31,7 @@ "@theia/ai-code-completion": "1.54.0", "@theia/ai-core": "1.54.0", "@theia/ai-history": "1.54.0", + "@theia/ai-llamafile": "1.54.0", "@theia/ai-ollama": "1.54.0", "@theia/ai-openai": "1.54.0", "@theia/ai-terminal": "1.54.0", diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json index e36e0e73f4457..9199410a96828 100644 --- a/examples/electron/tsconfig.json +++ b/examples/electron/tsconfig.json @@ -26,6 +26,9 @@ { "path": "../../packages/ai-history" }, + { + "path": "../../packages/ai-llamafile" + }, { "path": "../../packages/ai-ollama" }, 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..17a4dc5464a8e --- /dev/null +++ b/packages/ai-llamafile/README.md @@ -0,0 +1,57 @@ +# AI Llamafile Integration + +The AI Llamafile package provides an integration that allows users to manage and interact with Llamafile language models within Theia IDE. + +## Features + +- Start and stop Llamafile language servers. + +## Commands + +### Start Llamafile + +- **Command ID:** `llamafile.start` +- **Label:** `Start Llamafile` +- **Functionality:** Allows you to start a Llamafile language server by selecting from a list of configured Llamafiles. + +### Stop Llamafile + +- **Command ID:** `llamafile.stop` +- **Label:** `Stop Llamafile` +- **Functionality:** Allows you to stop a running Llamafile language server by selecting from a list of currently running Llamafiles. + +## Usage + +1. **Starting a Llamafile Language Server:** + + - Use the command palette to invoke `Start Llamafile`. + - A quick pick menu will appear with a list of configured Llamafiles. + - Select a Llamafile to start its language server. + +2. **Stopping a Llamafile Language Server:** + - Use the command palette to invoke `Stop Llamafile`. + - A quick pick menu will display a list of currently running Llamafiles. + - Select a Llamafile to stop its language server. + +## Dependencies + +This extension depends on the `@theia/ai-core` package for AI-related services and functionalities. + +## Configuration + +Make sure to configure your Llamafiles properly within the preference settings. +This setting is an array of objects, where each object defines a llamafile with a user-friendly name, the file uri, and the port to start the server on. + +Example Configuration: + +```json +{ + "ai-features.llamafile.llamafiles": [ + { + "name": "MyLlamaFile", + "uri": "file:///path/to/my.llamafile", + "port": 30000 + } + ] +} +``` diff --git a/packages/ai-llamafile/package.json b/packages/ai-llamafile/package.json new file mode 100644 index 0000000000000..9518a24ad7bd6 --- /dev/null +++ b/packages/ai-llamafile/package.json @@ -0,0 +1,50 @@ +{ + "name": "@theia/ai-llamafile", + "version": "1.54.0", + "description": "Theia - Llamafile Integration", + "dependencies": { + "@theia/ai-core": "1.54.0", + "@theia/core": "1.54.0", + "@theia/output": "1.54.0", + "tslib": "^2.6.2" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/llamafile-frontend-module", + "backend": "lib/node/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.54.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} 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..eae616cfd94bd --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-command-contribution.ts @@ -0,0 +1,92 @@ +// ***************************************************************************** +// 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 { AICommandHandlerFactory } from '@theia/ai-core/lib/browser/ai-command-handler-factory'; +import { CommandContribution, CommandRegistry, MessageService } from '@theia/core'; +import { PreferenceService, QuickInputService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { LlamafileEntry, LlamafileManager } from '../common/llamafile-manager'; +import { PREFERENCE_LLAMAFILE } from './llamafile-preferences'; + +export const StartLlamafileCommand = { + id: 'llamafile.start', + label: 'Start Llamafile', +}; +export const StopLlamafileCommand = { + id: 'llamafile.stop', + label: 'Stop Llamafile', +}; + +@injectable() +export class LlamafileCommandContribution implements CommandContribution { + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(AICommandHandlerFactory) + protected readonly commandHandlerFactory: AICommandHandlerFactory; + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(MessageService) + protected messageService: MessageService; + + @inject(LlamafileManager) + protected llamafileManager: LlamafileManager; + + registerCommands(commandRegistry: CommandRegistry): void { + commandRegistry.registerCommand(StartLlamafileCommand, this.commandHandlerFactory({ + execute: async () => { + try { + const llamaFiles = this.preferenceService.get(PREFERENCE_LLAMAFILE); + if (llamaFiles === undefined || llamaFiles.length === 0) { + this.messageService.error('No Llamafiles configured.'); + return; + } + const options = llamaFiles.map(llamaFile => ({ label: llamaFile.name })); + const result = await this.quickInputService.showQuickPick(options); + if (result === undefined) { + return; + } + this.llamafileManager.startServer(result.label); + } catch (error) { + console.error('Something went wrong during the llamafile start.', error); + this.messageService.error(`Something went wrong during the llamafile start: ${error.message}.\nFor more information, see the console.`); + } + } + })); + commandRegistry.registerCommand(StopLlamafileCommand, this.commandHandlerFactory({ + execute: async () => { + try { + const llamaFiles = await this.llamafileManager.getStartedLlamafiles(); + if (llamaFiles === undefined || llamaFiles.length === 0) { + this.messageService.error('No Llamafiles running.'); + return; + } + const options = llamaFiles.map(llamaFile => ({ label: llamaFile })); + const result = await this.quickInputService.showQuickPick(options); + if (result === undefined) { + return; + } + this.llamafileManager.stopServer(result.label); + } catch (error) { + console.error('Something went wrong during the llamafile stop.', error); + this.messageService.error(`Something went wrong during the llamafile stop: ${error.message}.\nFor more information, see the console.`); + } + } + })); + } +} diff --git a/packages/ai-llamafile/src/browser/llamafile-frontend-application-contribution.ts b/packages/ai-llamafile/src/browser/llamafile-frontend-application-contribution.ts new file mode 100644 index 0000000000000..c202ca12fe54e --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-frontend-application-contribution.ts @@ -0,0 +1,59 @@ +// ***************************************************************************** +// 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 { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { LlamafileEntry, LlamafileManager } from '../common/llamafile-manager'; +import { PREFERENCE_LLAMAFILE } from './llamafile-preferences'; + +@injectable() +export class LlamafileFrontendApplicationContribution implements FrontendApplicationContribution { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(LlamafileManager) + protected llamafileManager: LlamafileManager; + + private _knownLlamaFiles: Map = new Map(); + + onStart(): void { + this.preferenceService.ready.then(() => { + const llamafiles = this.preferenceService.get(PREFERENCE_LLAMAFILE, []); + this.llamafileManager.addLanguageModels(llamafiles); + llamafiles.forEach(model => this._knownLlamaFiles.set(model.name, model)); + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === PREFERENCE_LLAMAFILE) { + // only new models which are actual LLamaFileEntries + const newModels = event.newValue.filter((llamafileEntry: unknown) => LlamafileEntry.is(llamafileEntry)) as LlamafileEntry[]; + + const llamafilesToAdd = newModels.filter(llamafile => + !this._knownLlamaFiles.has(llamafile.name) || !LlamafileEntry.equals(this._knownLlamaFiles.get(llamafile.name)!, llamafile)); + + const llamafileIdsToRemove = [...this._knownLlamaFiles.values()].filter(llamafile => + !newModels.find(a => LlamafileEntry.equals(a, llamafile))).map(a => a.name); + + this.llamafileManager.removeLanguageModels(llamafileIdsToRemove); + llamafileIdsToRemove.forEach(model => this._knownLlamaFiles.delete(model)); + + this.llamafileManager.addLanguageModels(llamafilesToAdd); + llamafilesToAdd.forEach(model => this._knownLlamaFiles.set(model.name, model)); + } + }); + }); + } +} diff --git a/packages/ai-llamafile/src/browser/llamafile-frontend-module.ts b/packages/ai-llamafile/src/browser/llamafile-frontend-module.ts new file mode 100644 index 0000000000000..5deeb0c18d982 --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-frontend-module.ts @@ -0,0 +1,45 @@ +// ***************************************************************************** +// 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 { FrontendApplicationContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { OutputChannelManager, OutputChannelSeverity } from '@theia/output/lib/browser/output-channel'; +import { LlamafileManager, LlamafileManagerPath, LlamafileServerManagerClient } from '../common/llamafile-manager'; +import { LlamafileCommandContribution } from './llamafile-command-contribution'; +import { LlamafileFrontendApplicationContribution } from './llamafile-frontend-application-contribution'; +import { bindAILlamafilePreferences } from './llamafile-preferences'; + +export default new ContainerModule(bind => { + bind(FrontendApplicationContribution).to(LlamafileFrontendApplicationContribution).inSingletonScope(); + bind(CommandContribution).to(LlamafileCommandContribution).inSingletonScope(); + bind(LlamafileManager).toDynamicValue(ctx => { + const connection = ctx.container.get(RemoteConnectionProvider); + const outputChannelManager = ctx.container.get(OutputChannelManager); + const client: LlamafileServerManagerClient = { + error: (llamafileName, message) => { + const channel = outputChannelManager.getChannel(`${llamafileName}-llamafile`); + channel.appendLine(message, OutputChannelSeverity.Error); + }, + log: (llamafileName, message) => { + const channel = outputChannelManager.getChannel(`${llamafileName}-llamafile`); + channel.appendLine(message, OutputChannelSeverity.Info); + } + }; + return connection.createProxy(LlamafileManagerPath, client); + }).inSingletonScope(); + + bindAILlamafilePreferences(bind); +}); diff --git a/packages/ai-llamafile/src/browser/llamafile-preferences.ts b/packages/ai-llamafile/src/browser/llamafile-preferences.ts new file mode 100644 index 0000000000000..fcc3255725ab7 --- /dev/null +++ b/packages/ai-llamafile/src/browser/llamafile-preferences.ts @@ -0,0 +1,60 @@ +// ***************************************************************************** +// 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 { PreferenceContribution, PreferenceSchema } from '@theia/core/lib/browser'; +import { interfaces } from '@theia/core/shared/inversify'; + +export const AI_LLAMAFILE_PREFERENCES_TITLE = '✨ AI LlamaFile'; +export const PREFERENCE_LLAMAFILE = 'ai-features.llamafile.llamafiles'; + +export const aiLlamafilePreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [PREFERENCE_LLAMAFILE]: { + title: AI_LLAMAFILE_PREFERENCES_TITLE, + markdownDescription: '❗ This setting allows you to add llamafiles.\ + \n\ + You need to provide a user friendly `name`, the file `uri` to the llamafile and the `port` to use.\ + \n\ + In order to start your llamafile you have to call the "Start Llamafile" command where you can then select the llamafile to start.\ + \n\ + If you modify an entry, e.g. change the port and the server was already running, then it will be stopped and you have to manually start it again.', + type: 'array', + default: [], + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: 'The model name to use for this Llamafile.' + }, + uri: { + type: 'string', + description: 'The file uri to the Llamafile.' + }, + port: { + type: 'number', + description: 'The port to use to start the server.' + } + } + } + } + } +}; + +export function bindAILlamafilePreferences(bind: interfaces.Bind): void { + bind(PreferenceContribution).toConstantValue({ schema: aiLlamafilePreferencesSchema }); +} 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..6f1b039013879 --- /dev/null +++ b/packages/ai-llamafile/src/common/llamafile-language-model.ts @@ -0,0 +1,102 @@ +// ***************************************************************************** +// 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, LanguageModelStreamResponsePart } from '@theia/ai-core'; + +export class LlamafileLanguageModel implements LanguageModel { + + readonly providerId = 'llamafile'; + readonly vendor: string = 'Mozilla'; + + constructor(readonly name: string, readonly uri: string, readonly port: number) { + } + + get id(): string { + return this.name; + } + + async request(request: LanguageModelRequest): Promise { + try { + let prompt = request.messages.map(message => { + switch (message.actor) { + case 'user': + return `User: ${message.query}`; + case 'ai': + return `Llama: ${message.query}`; + case 'system': + return `${message.query.replace(/\n\n/g, '\n')}`; + } + }).join('\n'); + prompt += '\nLlama:'; + const response = await fetch(`http://localhost:${this.port}/completion`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt: prompt, + n_predict: 200, + stream: true, + stop: ['', 'Llama:', 'User:', '<|eot_id|>'], + cache_prompt: true, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!response.body) { + throw new Error('Response body is undefined'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + return { + stream: { + [Symbol.asyncIterator](): AsyncIterator { + return { + async next(): Promise> { + const { value, done } = await reader.read(); + if (done) { + return { value: undefined, done: true }; + } + const read = decoder.decode(value, { stream: true }); + const chunk = read.split('\n').filter(l => l.length !== 0).reduce((acc, line) => { + try { + const parsed = JSON.parse(line.substring(6)); + acc += parsed.content; + return acc; + } catch (error) { + console.error('Error parsing JSON:', error); + return acc; + } + }, ''); + return { value: { content: chunk }, done: false }; + } + }; + } + } + }; + } catch (error) { + console.error('Error:', error); + return { + text: `Error: ${error}` + }; + } + } + +} diff --git a/packages/ai-llamafile/src/common/llamafile-manager.ts b/packages/ai-llamafile/src/common/llamafile-manager.ts new file mode 100644 index 0000000000000..561f3acdee95b --- /dev/null +++ b/packages/ai-llamafile/src/common/llamafile-manager.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// 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 LlamafileManager = Symbol('LlamafileManager'); + +export const LlamafileManagerPath = '/services/llamafilemanager'; + +export interface LlamafileManager { + startServer(name: string): Promise; + stopServer(name: string): void; + getStartedLlamafiles(): Promise; + setClient(client: LlamafileServerManagerClient): void; + addLanguageModels(llamaFiles: LlamafileEntry[]): Promise; + removeLanguageModels(modelIds: string[]): void; +} +export interface LlamafileServerManagerClient { + log(llamafileName: string, message: string): void; + error(llamafileName: string, message: string): void; +} + +export interface LlamafileEntry { + name: string; + uri: string; + port: number; +} + +export namespace LlamafileEntry { + export function equals(a: LlamafileEntry, b: LlamafileEntry): boolean { + return a.name === b.name && a.uri === b.uri && a.port === b.port; + } + export function is(entry: unknown): entry is LlamafileEntry { + // eslint-disable-next-line no-null/no-null + return typeof entry === 'object' && entry !== null + && 'name' in entry && typeof entry.name === 'string' + && 'uri' in entry && typeof entry.uri === 'string' + && 'port' in entry && typeof entry.port === 'number'; + } +} diff --git a/packages/ai-llamafile/src/node/llamafile-backend-module.ts b/packages/ai-llamafile/src/node/llamafile-backend-module.ts new file mode 100644 index 0000000000000..83ab2c182bd00 --- /dev/null +++ b/packages/ai-llamafile/src/node/llamafile-backend-module.ts @@ -0,0 +1,32 @@ +// ***************************************************************************** +// 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 { LlamafileManagerImpl } from './llamafile-manager-impl'; +import { LlamafileManager, LlamafileServerManagerClient, LlamafileManagerPath } from '../common/llamafile-manager'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; + +export default new ContainerModule(bind => { + bind(LlamafileManager).to(LlamafileManagerImpl).inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(ctx => new RpcConnectionHandler( + LlamafileManagerPath, + client => { + const service = ctx.container.get(LlamafileManager); + service.setClient(client); + return service; + } + )).inSingletonScope(); +}); diff --git a/packages/ai-llamafile/src/node/llamafile-manager-impl.ts b/packages/ai-llamafile/src/node/llamafile-manager-impl.ts new file mode 100644 index 0000000000000..3d726457f9faa --- /dev/null +++ b/packages/ai-llamafile/src/node/llamafile-manager-impl.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 { LanguageModelRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { ChildProcessWithoutNullStreams, spawn } from 'child_process'; +import { basename, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { LlamafileLanguageModel } from '../common/llamafile-language-model'; +import { LlamafileEntry, LlamafileManager, LlamafileServerManagerClient } from '../common/llamafile-manager'; + +@injectable() +export class LlamafileManagerImpl implements LlamafileManager { + + @inject(LanguageModelRegistry) + protected languageModelRegistry: LanguageModelRegistry; + + private processMap: Map = new Map(); + private client: LlamafileServerManagerClient; + + async addLanguageModels(llamaFiles: LlamafileEntry[]): Promise { + for (const llamafile of llamaFiles) { + const model = await this.languageModelRegistry.getLanguageModel(llamafile.name); + if (model) { + if (!(model instanceof LlamafileLanguageModel)) { + console.warn(`Llamafile: model ${model.id} is not an LLamafile model`); + continue; + } else { + // This can happen during the initializing of more than one frontends, changes are handled in the frontend + console.info(`Llamafile: skip creating or updating model ${llamafile.name} because it already exists.`); + } + } else { + this.languageModelRegistry.addLanguageModels([new LlamafileLanguageModel(llamafile.name, llamafile.uri, llamafile.port)]); + } + } + } + removeLanguageModels(modelIds: string[]): void { + modelIds.filter(modelId => this.isStarted(modelId)).forEach(modelId => this.stopServer(modelId)); + this.languageModelRegistry.removeLanguageModels(modelIds); + } + + async getStartedLlamafiles(): Promise { + const models = await this.languageModelRegistry.getLanguageModels(); + return models.filter(model => model instanceof LlamafileLanguageModel && this.isStarted(model.name)).map(model => model.id); + } + + async startServer(name: string): Promise { + if (!this.processMap.has(name)) { + const models = await this.languageModelRegistry.getLanguageModels(); + const llm = models.find(model => model.id === name && model instanceof LlamafileLanguageModel) as LlamafileLanguageModel | undefined; + if (llm === undefined) { + return Promise.reject(`Llamafile ${name} not found`); + } + const filePath = fileURLToPath(llm.uri); + + // Extract the directory and file name + const dir = dirname(filePath); + const fileName = basename(filePath); + const currentProcess = spawn(`./${fileName}`, ['--port', '' + llm.port, '--server', '--nobrowser'], { cwd: dir }); + this.processMap.set(name, currentProcess); + + currentProcess.stdout.on('data', (data: Buffer) => { + const output = data.toString(); + this.client.log(name, output); + }); + currentProcess.stderr.on('data', (data: Buffer) => { + const output = data.toString(); + this.client.error(name, output); + }); + currentProcess.on('close', code => { + this.client.log(name, `LlamaFile process for file ${name} exited with code ${code}`); + this.processMap.delete(name); + }); + currentProcess.on('error', error => { + this.client.error(name, `Error starting LlamaFile process for file ${name}: ${error.message}`); + this.processMap.delete(name); + }); + } + } + + stopServer(name: string): void { + if (this.processMap.has(name)) { + const currentProcess = this.processMap.get(name); + currentProcess!.kill(); + this.processMap.delete(name); + } + } + + isStarted(name: string): boolean { + return this.processMap.has(name); + } + + setClient(client: LlamafileServerManagerClient): void { + this.client = client; + } + +} diff --git a/packages/ai-llamafile/src/package.spec.ts b/packages/ai-llamafile/src/package.spec.ts new file mode 100644 index 0000000000000..ac0ffad7fa139 --- /dev/null +++ b/packages/ai-llamafile/src/package.spec.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox GmbH and others. +// +// 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 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-llamafile package', () => { + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-llamafile/tsconfig.json b/packages/ai-llamafile/tsconfig.json new file mode 100644 index 0000000000000..ed8ef9826cf57 --- /dev/null +++ b/packages/ai-llamafile/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + }, + { + "path": "../output" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index ee1bdb0025812..9b9553dfdd5a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -69,6 +69,9 @@ { "path": "packages/ai-history" }, + { + "path": "packages/ai-llamafile" + }, { "path": "packages/ai-ollama" },