deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// *****************************************************************************
|
||||
// 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, nls, PreferenceService } from '@theia/core';
|
||||
import { QuickInputService } from '@theia/core/lib/browser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { LlamafileManager } from '../common/llamafile-manager';
|
||||
import { PREFERENCE_LLAMAFILE } from '../common/llamafile-preferences';
|
||||
import { LlamafileEntry } from './llamafile-frontend-application-contribution';
|
||||
|
||||
export const StartLlamafileCommand = {
|
||||
id: 'llamafile.start',
|
||||
label: nls.localize('theia/ai/llamaFile/start', 'Start Llamafile'),
|
||||
};
|
||||
export const StopLlamafileCommand = {
|
||||
id: 'llamafile.stop',
|
||||
label: nls.localize('theia/ai/llamaFile/stop', '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<LlamafileEntry[]>(PREFERENCE_LLAMAFILE);
|
||||
if (llamaFiles === undefined || llamaFiles.length === 0) {
|
||||
this.messageService.error(nls.localize('theia/ai/llamafile/error/noConfigured', '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(
|
||||
nls.localize(
|
||||
'theia/ai/llamafile/error/startFailed',
|
||||
'Something went wrong during the llamafile start: {0}.\nFor more information, see the console.',
|
||||
error.message
|
||||
));
|
||||
}
|
||||
}
|
||||
}));
|
||||
commandRegistry.registerCommand(StopLlamafileCommand, this.commandHandlerFactory({
|
||||
execute: async () => {
|
||||
try {
|
||||
const llamaFiles = await this.llamafileManager.getStartedLlamafiles();
|
||||
if (llamaFiles === undefined || llamaFiles.length === 0) {
|
||||
this.messageService.error(nls.localize('theia/ai/llamafile/error/noRunning', '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(
|
||||
nls.localize(
|
||||
'theia/ai/llamafile/error/stopFailed',
|
||||
'Something went wrong during the llamafile stop: {0}.\nFor more information, see the console.',
|
||||
error.message
|
||||
));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// *****************************************************************************
|
||||
// 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 } from '@theia/core/lib/browser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { LlamafileManager, LlamafileModelDescription } from '../common/llamafile-manager';
|
||||
import { PREFERENCE_LLAMAFILE } from '../common/llamafile-preferences';
|
||||
import { PreferenceService } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class LlamafileFrontendApplicationContribution implements FrontendApplicationContribution {
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected preferenceService: PreferenceService;
|
||||
|
||||
@inject(LlamafileManager)
|
||||
protected llamafileManager: LlamafileManager;
|
||||
|
||||
private _knownLlamaFiles: Map<string, LlamafileEntry> = new Map();
|
||||
|
||||
onStart(): void {
|
||||
this.preferenceService.ready.then(() => {
|
||||
const llamafiles = this.preferenceService.get<LlamafileEntry[]>(PREFERENCE_LLAMAFILE, []);
|
||||
const validLlamafiles = llamafiles.filter(LlamafileEntry.is);
|
||||
|
||||
const LlamafileModelDescriptions = this.getLLamaFileModelDescriptions(validLlamafiles);
|
||||
|
||||
this.llamafileManager.addLanguageModels(LlamafileModelDescriptions);
|
||||
validLlamafiles.forEach(model => this._knownLlamaFiles.set(model.name, model));
|
||||
|
||||
this.preferenceService.onPreferenceChanged(event => {
|
||||
if (event.preferenceName === PREFERENCE_LLAMAFILE) {
|
||||
const currentLlamafiles = this.preferenceService.get<LlamafileEntry[]>(PREFERENCE_LLAMAFILE, []);
|
||||
const newModels = currentLlamafiles.filter(LlamafileEntry.is);
|
||||
this.handleLlamaFilePreferenceChange(newModels);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected getLLamaFileModelDescriptions(llamafiles: LlamafileEntry[]): LlamafileModelDescription[] {
|
||||
return llamafiles.map(llamafile => ({
|
||||
name: llamafile.name,
|
||||
uri: llamafile.uri,
|
||||
port: llamafile.port
|
||||
}));
|
||||
}
|
||||
|
||||
protected handleLlamaFilePreferenceChange(newModels: LlamafileEntry[]): void {
|
||||
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(newModel => LlamafileEntry.equals(newModel, llamafile)))
|
||||
.map(llamafile => llamafile.name);
|
||||
|
||||
this.llamafileManager.removeLanguageModels(llamafileIdsToRemove);
|
||||
llamafileIdsToRemove.forEach(id => this._knownLlamaFiles.delete(id));
|
||||
|
||||
this.llamafileManager.addLanguageModels(this.getLLamaFileModelDescriptions(llamafilesToAdd));
|
||||
llamafilesToAdd.forEach(model => this._knownLlamaFiles.set(model.name, model));
|
||||
}
|
||||
}
|
||||
|
||||
export interface LlamafileEntry {
|
||||
name: string;
|
||||
uri: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
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 {
|
||||
return (
|
||||
typeof entry === 'object' &&
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
entry !== null &&
|
||||
'name' in entry && typeof (entry as LlamafileEntry).name === 'string' &&
|
||||
'uri' in entry && typeof (entry as LlamafileEntry).uri === 'string' &&
|
||||
'port' in entry && typeof (entry as LlamafileEntry).port === 'number'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 '../common/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<ServiceConnectionProvider>(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<LlamafileManager>(LlamafileManagerPath, client);
|
||||
}).inSingletonScope();
|
||||
|
||||
bindAILlamafilePreferences(bind);
|
||||
});
|
||||
134
packages/ai-llamafile/src/common/llamafile-language-model.ts
Normal file
134
packages/ai-llamafile/src/common/llamafile-language-model.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
// *****************************************************************************
|
||||
// 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, LanguageModelMessage, LanguageModelRequest, LanguageModelResponse, LanguageModelStatus, LanguageModelStreamResponsePart } from '@theia/ai-core';
|
||||
import { CancellationToken } from '@theia/core';
|
||||
|
||||
const createMessageContent = (message: LanguageModelMessage): string | undefined => {
|
||||
if (LanguageModelMessage.isTextMessage(message)) {
|
||||
return message.text;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export class LlamafileLanguageModel implements LanguageModel {
|
||||
|
||||
readonly providerId = 'llamafile';
|
||||
readonly vendor: string = 'Mozilla';
|
||||
|
||||
/**
|
||||
* @param name the unique name for this language model. It will be used to identify the model in the UI.
|
||||
* @param uri the URI pointing to the Llamafile model location.
|
||||
* @param port the port on which the Llamafile model server operates.
|
||||
*/
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public status: LanguageModelStatus,
|
||||
public readonly uri: string,
|
||||
public readonly port: number,
|
||||
) { }
|
||||
|
||||
get id(): string {
|
||||
return this.name;
|
||||
}
|
||||
protected getSettings(request: LanguageModelRequest): Record<string, unknown> {
|
||||
return {
|
||||
n_predict: 200,
|
||||
stream: true,
|
||||
stop: ['</s>', 'Llama:', 'User:', '<|eot_id|>'],
|
||||
cache_prompt: true,
|
||||
...(request.settings ?? {})
|
||||
};
|
||||
}
|
||||
|
||||
async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise<LanguageModelResponse> {
|
||||
const settings = this.getSettings(request);
|
||||
try {
|
||||
let prompt = request.messages.map(message => {
|
||||
const content = createMessageContent(message);
|
||||
if (content === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
switch (message.actor) {
|
||||
case 'user':
|
||||
return `User: ${content}`;
|
||||
case 'ai':
|
||||
return `Llama: ${content}`;
|
||||
case 'system':
|
||||
return `${content.replace(/\n\n/g, '\n')}`;
|
||||
}
|
||||
}).filter(m => m !== undefined).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,
|
||||
...settings
|
||||
}),
|
||||
});
|
||||
|
||||
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<LanguageModelStreamResponsePart> {
|
||||
return {
|
||||
async next(): Promise<IteratorResult<LanguageModelStreamResponsePart>> {
|
||||
if (cancellationToken?.isCancellationRequested) {
|
||||
reader.cancel();
|
||||
return { value: undefined, done: true };
|
||||
}
|
||||
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}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
37
packages/ai-llamafile/src/common/llamafile-manager.ts
Normal file
37
packages/ai-llamafile/src/common/llamafile-manager.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// *****************************************************************************
|
||||
// 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 LlamafileModelDescription {
|
||||
name: string;
|
||||
uri: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
export interface LlamafileManager {
|
||||
startServer(name: string): Promise<void>;
|
||||
stopServer(name: string): void;
|
||||
getStartedLlamafiles(): Promise<string[]>;
|
||||
setClient(client: LlamafileServerManagerClient): void;
|
||||
addLanguageModels(llamaFiles: LlamafileModelDescription[]): Promise<void>;
|
||||
removeLanguageModels(modelIds: string[]): void;
|
||||
}
|
||||
export interface LlamafileServerManagerClient {
|
||||
log(llamafileName: string, message: string): void;
|
||||
error(llamafileName: string, message: string): void;
|
||||
}
|
||||
62
packages/ai-llamafile/src/common/llamafile-preferences.ts
Normal file
62
packages/ai-llamafile/src/common/llamafile-preferences.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
// *****************************************************************************
|
||||
// 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 { nls, PreferenceContribution, PreferenceSchema } from '@theia/core';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
|
||||
export const AI_LLAMAFILE_PREFERENCES_TITLE = nls.localize('theia/ai/llamaFile/prefs/title', '✨ AI LlamaFile');
|
||||
export const PREFERENCE_LLAMAFILE = 'ai-features.llamafile.llamafiles';
|
||||
|
||||
export const aiLlamafilePreferencesSchema: PreferenceSchema = {
|
||||
properties: {
|
||||
[PREFERENCE_LLAMAFILE]: {
|
||||
title: AI_LLAMAFILE_PREFERENCES_TITLE,
|
||||
markdownDescription: nls.localize('theia/ai/llamaFile/prefs/mdDescription', 'This setting allows you to configure and manage LlamaFile models in Theia IDE.\
|
||||
\n\
|
||||
Each entry requires a user-friendly `name`, the file `uri` pointing to your LlamaFile, and the `port` on which it will run.\
|
||||
\n\
|
||||
To start a LlamaFile, use the "Start LlamaFile" command, which enables you to select the desired model.\
|
||||
\n\
|
||||
If you edit an entry (e.g., change the port), any running instance will stop, and you will need to manually start it again.\
|
||||
\n\
|
||||
[Learn more about configuring and managing LlamaFiles in the Theia IDE documentation](https://theia-ide.org/docs/user_ai/#llamafile-models).'),
|
||||
type: 'array',
|
||||
default: [],
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: nls.localize('theia/ai/llamaFile/prefs/name/description', 'The model name to use for this Llamafile.')
|
||||
},
|
||||
uri: {
|
||||
type: 'string',
|
||||
description: nls.localize('theia/ai/llamaFile/prefs/uri/description', 'The file uri to the Llamafile.')
|
||||
},
|
||||
port: {
|
||||
type: 'number',
|
||||
description: nls.localize('theia/ai/llamaFile/prefs/port/description', 'The port to use to start the server.')
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ['experimental']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function bindAILlamafilePreferences(bind: interfaces.Bind): void {
|
||||
bind(PreferenceContribution).toConstantValue({ schema: aiLlamafilePreferencesSchema });
|
||||
}
|
||||
40
packages/ai-llamafile/src/node/llamafile-backend-module.ts
Normal file
40
packages/ai-llamafile/src/node/llamafile-backend-module.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// *****************************************************************************
|
||||
// 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';
|
||||
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
||||
import { bindAILlamafilePreferences } from '../common/llamafile-preferences';
|
||||
|
||||
// We use a connection module to handle AI services separately for each frontend.
|
||||
const llamafileConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService, bindFrontendService }) => {
|
||||
bind(LlamafileManager).to(LlamafileManagerImpl).inSingletonScope();
|
||||
bind(ConnectionHandler).toDynamicValue(ctx => new RpcConnectionHandler<LlamafileServerManagerClient>(
|
||||
LlamafileManagerPath,
|
||||
client => {
|
||||
const service = ctx.container.get<LlamafileManager>(LlamafileManager);
|
||||
service.setClient(client);
|
||||
return service;
|
||||
}
|
||||
)).inSingletonScope();
|
||||
});
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bindAILlamafilePreferences(bind);
|
||||
bind(ConnectionContainerModule).toConstantValue(llamafileConnectionModule);
|
||||
});
|
||||
152
packages/ai-llamafile/src/node/llamafile-manager-impl.ts
Normal file
152
packages/ai-llamafile/src/node/llamafile-manager-impl.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// *****************************************************************************
|
||||
// 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, LanguageModelStatus } 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 { LlamafileManager, LlamafileModelDescription, LlamafileServerManagerClient } from '../common/llamafile-manager';
|
||||
|
||||
@injectable()
|
||||
export class LlamafileManagerImpl implements LlamafileManager {
|
||||
|
||||
@inject(LanguageModelRegistry)
|
||||
protected languageModelRegistry: LanguageModelRegistry;
|
||||
|
||||
private processMap: Map<string, ChildProcessWithoutNullStreams> = new Map();
|
||||
private client: LlamafileServerManagerClient;
|
||||
|
||||
async addLanguageModels(LlamafileModelDescriptions: LlamafileModelDescription[]): Promise<void> {
|
||||
for (const llamafile of LlamafileModelDescriptions) {
|
||||
const model = await this.languageModelRegistry.getLanguageModel(llamafile.name);
|
||||
if (model) {
|
||||
if (!(model instanceof LlamafileLanguageModel)) {
|
||||
console.warn(`Llamafile: model ${model.id} is not a 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,
|
||||
this.calculateStatus(false),
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
if (this.processMap.has(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const llm = await this.getLlamafileModel(name);
|
||||
if (!llm) {
|
||||
return Promise.reject(`Llamafile ${name} not found`);
|
||||
}
|
||||
|
||||
const currentProcess = this.spawnLlamafileProcess(llm);
|
||||
this.processMap.set(name, currentProcess);
|
||||
await this.updateLanguageModelStatus(name, true);
|
||||
this.attachProcessHandlers(name, currentProcess);
|
||||
}
|
||||
|
||||
protected async getLlamafileModel(name: string): Promise<LlamafileLanguageModel | undefined> {
|
||||
const models = await this.languageModelRegistry.getLanguageModels();
|
||||
return models.find(model => model.id === name && model instanceof LlamafileLanguageModel) as LlamafileLanguageModel | undefined;
|
||||
}
|
||||
|
||||
protected spawnLlamafileProcess(llm: LlamafileLanguageModel): ChildProcessWithoutNullStreams {
|
||||
const filePath = fileURLToPath(llm.uri);
|
||||
const dir = dirname(filePath);
|
||||
const fileName = basename(filePath);
|
||||
return spawn(`./${fileName}`, ['--port', '' + llm.port, '--server', '--nobrowser'], { cwd: dir });
|
||||
}
|
||||
|
||||
protected attachProcessHandlers(name: string, currentProcess: ChildProcessWithoutNullStreams): void {
|
||||
currentProcess.stdout.on('data', (data: Buffer) => {
|
||||
this.client.log(name, data.toString());
|
||||
});
|
||||
|
||||
currentProcess.stderr.on('data', (data: Buffer) => {
|
||||
this.client.error(name, data.toString());
|
||||
});
|
||||
|
||||
currentProcess.on('close', code => {
|
||||
this.client.log(name, `LlamaFile process for file ${name} exited with code ${code}`);
|
||||
this.processMap.delete(name);
|
||||
// Set status to 'unavailable' when server stops
|
||||
this.updateLanguageModelStatus(name, false);
|
||||
});
|
||||
|
||||
currentProcess.on('error', error => {
|
||||
this.client.error(name, `Error starting LlamaFile process for file ${name}: ${error.message}`);
|
||||
this.processMap.delete(name);
|
||||
// Set status to 'unavailable' on error
|
||||
this.updateLanguageModelStatus(name, false, error.message);
|
||||
});
|
||||
}
|
||||
|
||||
protected async updateLanguageModelStatus(modelId: string, hasStarted: boolean, message?: string): Promise<void> {
|
||||
const status: LanguageModelStatus = this.calculateStatus(hasStarted, message);
|
||||
await this.languageModelRegistry.patchLanguageModel<LlamafileLanguageModel>(modelId, {
|
||||
status
|
||||
});
|
||||
}
|
||||
|
||||
protected calculateStatus(started: boolean, message?: string): LanguageModelStatus {
|
||||
if (started) {
|
||||
return { status: 'ready' };
|
||||
} else {
|
||||
return { status: 'unavailable', message: message || 'Llamafile server is not running' };
|
||||
}
|
||||
}
|
||||
|
||||
stopServer(name: string): void {
|
||||
if (this.processMap.has(name)) {
|
||||
const currentProcess = this.processMap.get(name);
|
||||
currentProcess!.kill();
|
||||
this.processMap.delete(name);
|
||||
// Set status to 'unavailable' when server is stopped
|
||||
this.updateLanguageModelStatus(name, false);
|
||||
}
|
||||
}
|
||||
|
||||
isStarted(name: string): boolean {
|
||||
return this.processMap.has(name);
|
||||
}
|
||||
|
||||
setClient(client: LlamafileServerManagerClient): void {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
}
|
||||
27
packages/ai-llamafile/src/package.spec.ts
Normal file
27
packages/ai-llamafile/src/package.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user