From 6bfaa2d328c9f5f20199c1565e010b565a58105e Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Wed, 6 Nov 2024 19:08:25 +0100 Subject: [PATCH 1/2] Add support for Hugging Face fixed #14411 Signed-off-by: Jonas Helming --- examples/browser/package.json | 1 + examples/browser/tsconfig.json | 3 + packages/ai-hugging-face/.eslintrc.js | 10 ++ packages/ai-hugging-face/README.md | 32 +++++ packages/ai-hugging-face/package.json | 49 +++++++ ...gface-frontend-application-contribution.ts | 66 +++++++++ .../browser/huggingface-frontend-module.ts | 31 +++++ .../src/browser/huggingface-preferences.ts | 42 ++++++ .../huggingface-language-models-manager.ts | 36 +++++ packages/ai-hugging-face/src/common/index.ts | 16 +++ .../src/node/huggingface-backend-module.ts | 30 +++++ .../src/node/huggingface-language-model.ts | 126 ++++++++++++++++++ ...uggingface-language-models-manager-impl.ts | 59 ++++++++ packages/ai-hugging-face/src/package.spec.ts | 28 ++++ packages/ai-hugging-face/tsconfig.json | 19 +++ tsconfig.json | 3 + yarn.lock | 12 ++ 17 files changed, 563 insertions(+) create mode 100644 packages/ai-hugging-face/.eslintrc.js create mode 100644 packages/ai-hugging-face/README.md create mode 100644 packages/ai-hugging-face/package.json create mode 100644 packages/ai-hugging-face/src/browser/huggingface-frontend-application-contribution.ts create mode 100644 packages/ai-hugging-face/src/browser/huggingface-frontend-module.ts create mode 100644 packages/ai-hugging-face/src/browser/huggingface-preferences.ts create mode 100644 packages/ai-hugging-face/src/common/huggingface-language-models-manager.ts create mode 100644 packages/ai-hugging-face/src/common/index.ts create mode 100644 packages/ai-hugging-face/src/node/huggingface-backend-module.ts create mode 100644 packages/ai-hugging-face/src/node/huggingface-language-model.ts create mode 100644 packages/ai-hugging-face/src/node/huggingface-language-models-manager-impl.ts create mode 100644 packages/ai-hugging-face/src/package.spec.ts create mode 100644 packages/ai-hugging-face/tsconfig.json diff --git a/examples/browser/package.json b/examples/browser/package.json index 3f36e92f0605e..eff0f7fd39538 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -27,6 +27,7 @@ "@theia/ai-code-completion": "1.55.0", "@theia/ai-core": "1.55.0", "@theia/ai-history": "1.55.0", + "@theia/ai-huggingface": "1.55.0", "@theia/ai-llamafile": "1.55.0", "@theia/ai-ollama": "1.55.0", "@theia/ai-openai": "1.55.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index 5f39c27826132..5c7e5e2b56ab6 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -23,6 +23,9 @@ { "path": "../../packages/ai-history" }, + { + "path": "../../packages/ai-hugging-face" + }, { "path": "../../packages/ai-llamafile" }, diff --git a/packages/ai-hugging-face/.eslintrc.js b/packages/ai-hugging-face/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-hugging-face/.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-hugging-face/README.md b/packages/ai-hugging-face/README.md new file mode 100644 index 0000000000000..b92925c32d26c --- /dev/null +++ b/packages/ai-hugging-face/README.md @@ -0,0 +1,32 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - Hugging Face AI EXTENSION

+ +
+ +
+ +## Description + +The `@theia/ai-huggingface` integrates Hugging Face's models with Theia AI. +The Hugging Face API key and the models to use can be configured via preferences. +Alternatively, the Hugging Face API key can also be provided via the `HUGGINGFACE_API_KEY` environment variable. + +## Additional Information + +- [Theia - GitHub](https://github.com/eclipse-theia/theia) +- [Theia - Website](https://theia-ide.org/) + +## License + +- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/) +- [δΈ€ (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp) + +## Trademark + +"Theia" is a trademark of the Eclipse Foundation +https://www.eclipse.org/theia diff --git a/packages/ai-hugging-face/package.json b/packages/ai-hugging-face/package.json new file mode 100644 index 0000000000000..8036bd1b8c562 --- /dev/null +++ b/packages/ai-hugging-face/package.json @@ -0,0 +1,49 @@ +{ + "name": "@theia/ai-huggingface", + "version": "1.55.0", + "description": "Theia - Hugging Face Integration", + "dependencies": { + "@theia/core": "1.55.0", + "@huggingface/inference": "^2.0.0", + "@theia/ai-core": "1.55.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/huggingface-frontend-module", + "backend": "lib/node/huggingface-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.55.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/ai-hugging-face/src/browser/huggingface-frontend-application-contribution.ts b/packages/ai-hugging-face/src/browser/huggingface-frontend-application-contribution.ts new file mode 100644 index 0000000000000..8c595133b4464 --- /dev/null +++ b/packages/ai-hugging-face/src/browser/huggingface-frontend-application-contribution.ts @@ -0,0 +1,66 @@ +// ***************************************************************************** +// 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 { HuggingFaceLanguageModelsManager, HuggingFaceModelDescription } from '../common'; +import { API_KEY_PREF, MODELS_PREF } from './huggingface-preferences'; + +@injectable() +export class HuggingFaceFrontendApplicationContribution implements FrontendApplicationContribution { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(HuggingFaceLanguageModelsManager) + protected manager: HuggingFaceLanguageModelsManager; + + protected prevModels: string[] = []; + + onStart(): void { + this.preferenceService.ready.then(() => { + const apiKey = this.preferenceService.get(API_KEY_PREF, undefined); + this.manager.setApiKey(apiKey); + + const models = this.preferenceService.get(MODELS_PREF, []); + this.manager.createOrUpdateLanguageModels(...models.map(createHuggingFaceModelDescription)); + this.prevModels = [...models]; + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === API_KEY_PREF) { + this.manager.setApiKey(event.newValue); + } else if (event.preferenceName === MODELS_PREF) { + const oldModels = new Set(this.prevModels); + const newModels = new Set(event.newValue as string[]); + + const modelsToRemove = [...oldModels].filter(model => !newModels.has(model)); + const modelsToAdd = [...newModels].filter(model => !oldModels.has(model)); + + this.manager.removeLanguageModels(...modelsToRemove.map(model => `huggingface/${model}`)); + this.manager.createOrUpdateLanguageModels(...modelsToAdd.map(createHuggingFaceModelDescription)); + this.prevModels = [...event.newValue]; + } + }); + }); + } +} + +function createHuggingFaceModelDescription(modelId: string): HuggingFaceModelDescription { + return { + id: `huggingface/${modelId}`, + model: modelId + }; +} diff --git a/packages/ai-hugging-face/src/browser/huggingface-frontend-module.ts b/packages/ai-hugging-face/src/browser/huggingface-frontend-module.ts new file mode 100644 index 0000000000000..fe391e2927e6f --- /dev/null +++ b/packages/ai-hugging-face/src/browser/huggingface-frontend-module.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// 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 { HuggingFacePreferencesSchema } from './huggingface-preferences'; +import { FrontendApplicationContribution, PreferenceContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser'; +import { HuggingFaceFrontendApplicationContribution } from './huggingface-frontend-application-contribution'; +import { HUGGINGFACE_LANGUAGE_MODELS_MANAGER_PATH, HuggingFaceLanguageModelsManager } from '../common'; + +export default new ContainerModule(bind => { + bind(PreferenceContribution).toConstantValue({ schema: HuggingFacePreferencesSchema }); + bind(HuggingFaceFrontendApplicationContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(HuggingFaceFrontendApplicationContribution); + bind(HuggingFaceLanguageModelsManager).toDynamicValue(ctx => { + const provider = ctx.container.get(RemoteConnectionProvider); + return provider.createProxy(HUGGINGFACE_LANGUAGE_MODELS_MANAGER_PATH); + }).inSingletonScope(); +}); diff --git a/packages/ai-hugging-face/src/browser/huggingface-preferences.ts b/packages/ai-hugging-face/src/browser/huggingface-preferences.ts new file mode 100644 index 0000000000000..91930ee759558 --- /dev/null +++ b/packages/ai-hugging-face/src/browser/huggingface-preferences.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// 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 { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; +import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/browser/ai-core-preferences'; + +export const API_KEY_PREF = 'ai-features.huggingFace.apiKey'; +export const MODELS_PREF = 'ai-features.huggingFace.models'; + +export const HuggingFacePreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [API_KEY_PREF]: { + type: 'string', + markdownDescription: 'Enter an API Key for your Hugging Face Account. **Please note:** By using this preference the Hugging Face API key will be stored in clear text\ + on the machine running Theia. Use the environment variable `HUGGINGFACE_API_KEY` to set the key securely.', + title: AI_CORE_PREFERENCES_TITLE, + }, + [MODELS_PREF]: { + type: 'array', + description: 'Hugging Face models to use', + title: AI_CORE_PREFERENCES_TITLE, + default: ['bigcode/starcoder'], + items: { + type: 'string' + } + } + } +}; diff --git a/packages/ai-hugging-face/src/common/huggingface-language-models-manager.ts b/packages/ai-hugging-face/src/common/huggingface-language-models-manager.ts new file mode 100644 index 0000000000000..a19937301e241 --- /dev/null +++ b/packages/ai-hugging-face/src/common/huggingface-language-models-manager.ts @@ -0,0 +1,36 @@ +// ***************************************************************************** +// 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 HUGGINGFACE_LANGUAGE_MODELS_MANAGER_PATH = '/services/huggingface/language-model-manager'; +export const HuggingFaceLanguageModelsManager = Symbol('HuggingFaceLanguageModelsManager'); + +export interface HuggingFaceModelDescription { + /** + * The identifier of the model which will be shown in the UI. + */ + id: string; + /** + * The model ID as used by the Hugging Face API. + */ + model: string; +} + +export interface HuggingFaceLanguageModelsManager { + apiKey: string | undefined; + setApiKey(key: string | undefined): void; + createOrUpdateLanguageModels(...models: HuggingFaceModelDescription[]): Promise; + removeLanguageModels(...modelIds: string[]): void; +} diff --git a/packages/ai-hugging-face/src/common/index.ts b/packages/ai-hugging-face/src/common/index.ts new file mode 100644 index 0000000000000..9fe3e98bc4b69 --- /dev/null +++ b/packages/ai-hugging-face/src/common/index.ts @@ -0,0 +1,16 @@ +// ***************************************************************************** +// 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 * from './huggingface-language-models-manager'; diff --git a/packages/ai-hugging-face/src/node/huggingface-backend-module.ts b/packages/ai-hugging-face/src/node/huggingface-backend-module.ts new file mode 100644 index 0000000000000..e5bdf7fb58e48 --- /dev/null +++ b/packages/ai-hugging-face/src/node/huggingface-backend-module.ts @@ -0,0 +1,30 @@ +// ***************************************************************************** +// 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 { HUGGINGFACE_LANGUAGE_MODELS_MANAGER_PATH, HuggingFaceLanguageModelsManager } from '../common/huggingface-language-models-manager'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; +import { HuggingFaceLanguageModelsManagerImpl } from './huggingface-language-models-manager-impl'; + +export const HuggingFaceModelFactory = Symbol('HuggingFaceModelFactory'); + +export default new ContainerModule(bind => { + bind(HuggingFaceLanguageModelsManagerImpl).toSelf().inSingletonScope(); + bind(HuggingFaceLanguageModelsManager).toService(HuggingFaceLanguageModelsManagerImpl); + bind(ConnectionHandler).toDynamicValue(ctx => + new RpcConnectionHandler(HUGGINGFACE_LANGUAGE_MODELS_MANAGER_PATH, () => ctx.container.get(HuggingFaceLanguageModelsManager)) + ).inSingletonScope(); +}); diff --git a/packages/ai-hugging-face/src/node/huggingface-language-model.ts b/packages/ai-hugging-face/src/node/huggingface-language-model.ts new file mode 100644 index 0000000000000..694e1bc17cba8 --- /dev/null +++ b/packages/ai-hugging-face/src/node/huggingface-language-model.ts @@ -0,0 +1,126 @@ +// ***************************************************************************** +// 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, + LanguageModelRequestMessage, + LanguageModelResponse, + LanguageModelStreamResponsePart, + LanguageModelTextResponse, + MessageActor +} from '@theia/ai-core'; +import { CancellationToken } from '@theia/core'; +import { HfInference } from '@huggingface/inference'; + +export const HuggingFaceModelIdentifier = Symbol('HuggingFaceModelIdentifier'); + +function toHuggingFacePrompt(messages: LanguageModelRequestMessage[]): string { + if (messages.length === 1) { + return messages[0].query; + } + return messages.map(message => `${toRoleLabel(message.actor)}: ${message.query}`).join('\n'); +} + +function toRoleLabel(actor: MessageActor): string { + switch (actor) { + case 'user': + return 'User'; + case 'ai': + return 'Assistant'; + case 'system': + return 'System'; + default: + return ''; + } +} + +export class HuggingFaceModel implements LanguageModel { + private hfInference: HfInference; + + /** + * @param id the unique id for this language model. It will be used to identify the model in the UI. + * @param model the model id as it is used by the Hugging Face API + * @param apiKey function to retrieve the API key for Hugging Face + */ + constructor(public readonly id: string, public model: string, public apiKey: () => string | undefined) { + const token = this.apiKey(); + if (!token) { + throw new Error('Please provide a Hugging Face API token.'); + } + this.hfInference = new HfInference(token); + } + + async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { + if (this.isStreamingSupported(this.model)) { + return this.handleStreamingRequest(request, cancellationToken); + } else { + return this.handleNonStreamingRequest(request); + } + } + + protected async handleNonStreamingRequest(request: LanguageModelRequest): Promise { + const response = await this.hfInference.textGeneration({ + model: this.model, + inputs: toHuggingFacePrompt(request.messages), + parameters: { + temperature: 0.1, // Controls randomness, 0.1 for consistent outputs + max_new_tokens: 200, // Limits response length + return_full_text: false, // Ensures only the generated part is returned, not the prompt + do_sample: true, // Enables sampling for more varied responses + stop: ['<|endoftext|>'] // Stop generation at this token + } + }); + + const cleanText = response.generated_text.replace(/<\|endoftext\|>/g, ''); + + return { + text: cleanText + }; + } + + protected async handleStreamingRequest(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { + const stream = this.hfInference.textGenerationStream({ + model: this.model, + inputs: toHuggingFacePrompt(request.messages), + parameters: { + temperature: 0.1, + max_new_tokens: 200, + return_full_text: false, + do_sample: true, + stop: ['<|endoftext|>'] + } + }); + + const asyncIterator = { + async *[Symbol.asyncIterator](): AsyncIterator { + for await (const chunk of stream) { + const content = chunk.token.text.replace(/<\|endoftext\|>/g, ''); + yield { content }; + if (cancellationToken?.isCancellationRequested) { + break; + } + } + } + }; + return { stream: asyncIterator }; + } + + protected isStreamingSupported(model: string): boolean { + // Assuming all models support streaming for now; can be refined if needed + return true; + } +} diff --git a/packages/ai-hugging-face/src/node/huggingface-language-models-manager-impl.ts b/packages/ai-hugging-face/src/node/huggingface-language-models-manager-impl.ts new file mode 100644 index 0000000000000..85073968a628b --- /dev/null +++ b/packages/ai-hugging-face/src/node/huggingface-language-models-manager-impl.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 { LanguageModelRegistry } from '@theia/ai-core'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { HuggingFaceModel } from './huggingface-language-model'; +import { HuggingFaceLanguageModelsManager, HuggingFaceModelDescription } from '../common'; + +@injectable() +export class HuggingFaceLanguageModelsManagerImpl implements HuggingFaceLanguageModelsManager { + + protected _apiKey: string | undefined; + + @inject(LanguageModelRegistry) + protected readonly languageModelRegistry: LanguageModelRegistry; + + get apiKey(): string | undefined { + return this._apiKey ?? process.env.HUGGINGFACE_API_KEY; + } + + async createOrUpdateLanguageModels(...modelDescriptions: HuggingFaceModelDescription[]): Promise { + for (const modelDescription of modelDescriptions) { + const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id); + const apiKeyProvider = () => this.apiKey; + + if (model) { + if (!(model instanceof HuggingFaceModel)) { + console.warn(`Hugging Face: model ${modelDescription.id} is not a Hugging Face model`); + continue; + } + model.model = modelDescription.model; + model.apiKey = apiKeyProvider; + } else { + this.languageModelRegistry.addLanguageModels([new HuggingFaceModel(modelDescription.id, modelDescription.model, apiKeyProvider)]); + } + } + } + + removeLanguageModels(...modelIds: string[]): void { + this.languageModelRegistry.removeLanguageModels(modelIds); + } + + setApiKey(apiKey: string | undefined): void { + this._apiKey = apiKey || undefined; + } +} diff --git a/packages/ai-hugging-face/src/package.spec.ts b/packages/ai-hugging-face/src/package.spec.ts new file mode 100644 index 0000000000000..8f568955d26ae --- /dev/null +++ b/packages/ai-hugging-face/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource 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-huggingface package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-hugging-face/tsconfig.json b/packages/ai-hugging-face/tsconfig.json new file mode 100644 index 0000000000000..420367fccbfb3 --- /dev/null +++ b/packages/ai-hugging-face/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 9b9553dfdd5a1..72e54f2e87b78 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -69,6 +69,9 @@ { "path": "packages/ai-history" }, + { + "path": "packages/ai-hugging-face" + }, { "path": "packages/ai-llamafile" }, diff --git a/yarn.lock b/yarn.lock index 103ac1c1f0914..f153a205443bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1023,6 +1023,18 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@huggingface/inference@^2.0.0": + version "2.8.1" + resolved "https://registry.yarnpkg.com/@huggingface/inference/-/inference-2.8.1.tgz#e119a7746faf5ce40ebf37ec97afd51286c27ecb" + integrity sha512-EfsNtY9OR6JCNaUa5bZu2mrs48iqeTz0Gutwf+fU0Kypx33xFQB4DKMhp8u4Ee6qVbLbNWvTHuWwlppLQl4p4Q== + dependencies: + "@huggingface/tasks" "^0.12.9" + +"@huggingface/tasks@^0.12.9": + version "0.12.30" + resolved "https://registry.yarnpkg.com/@huggingface/tasks/-/tasks-0.12.30.tgz#ed1295c12cd85fc1ff4621485703be92083148b0" + integrity sha512-A1ITdxbEzx9L8wKR8pF7swyrTLxWNDFIGDLUWInxvks2ruQ8PLRBZe8r0EcjC3CDdtlj9jV1V4cgV35K/iy3GQ== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" From caa370a2743d7d41849dcd91031c66485b2fa5e6 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Mon, 11 Nov 2024 23:16:18 +0100 Subject: [PATCH 2/2] Adress review comments Signed-off-by: Jonas Helming --- .../src/node/huggingface-language-model.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/ai-hugging-face/src/node/huggingface-language-model.ts b/packages/ai-hugging-face/src/node/huggingface-language-model.ts index 694e1bc17cba8..e54e463f10b74 100644 --- a/packages/ai-hugging-face/src/node/huggingface-language-model.ts +++ b/packages/ai-hugging-face/src/node/huggingface-language-model.ts @@ -49,7 +49,6 @@ function toRoleLabel(actor: MessageActor): string { } export class HuggingFaceModel implements LanguageModel { - private hfInference: HfInference; /** * @param id the unique id for this language model. It will be used to identify the model in the UI. @@ -57,23 +56,19 @@ export class HuggingFaceModel implements LanguageModel { * @param apiKey function to retrieve the API key for Hugging Face */ constructor(public readonly id: string, public model: string, public apiKey: () => string | undefined) { - const token = this.apiKey(); - if (!token) { - throw new Error('Please provide a Hugging Face API token.'); - } - this.hfInference = new HfInference(token); } async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { + const hfInference = this.initializeHfInference(); if (this.isStreamingSupported(this.model)) { - return this.handleStreamingRequest(request, cancellationToken); + return this.handleStreamingRequest(hfInference, request, cancellationToken); } else { - return this.handleNonStreamingRequest(request); + return this.handleNonStreamingRequest(hfInference, request); } } - protected async handleNonStreamingRequest(request: LanguageModelRequest): Promise { - const response = await this.hfInference.textGeneration({ + protected async handleNonStreamingRequest(hfInference: HfInference, request: LanguageModelRequest): Promise { + const response = await hfInference.textGeneration({ model: this.model, inputs: toHuggingFacePrompt(request.messages), parameters: { @@ -92,8 +87,8 @@ export class HuggingFaceModel implements LanguageModel { }; } - protected async handleStreamingRequest(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { - const stream = this.hfInference.textGenerationStream({ + protected async handleStreamingRequest(hfInference: HfInference, request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { + const stream = hfInference.textGenerationStream({ model: this.model, inputs: toHuggingFacePrompt(request.messages), parameters: { @@ -123,4 +118,12 @@ export class HuggingFaceModel implements LanguageModel { // Assuming all models support streaming for now; can be refined if needed return true; } + + private initializeHfInference(): HfInference { + const token = this.apiKey(); + if (!token) { + throw new Error('Please provide a Hugging Face API token.'); + } + return new HfInference(token); + } }