From c55ac229b514e131804e689c7d0913e57f8ebf39 Mon Sep 17 00:00:00 2001 From: Jan Bicker Date: Thu, 28 Nov 2024 13:44:14 +0100 Subject: [PATCH] Support Anthropic as an LLM provider fixed #14613 Signed-off-by: Jonas Helming --- examples/browser/package.json | 1 + examples/browser/tsconfig.json | 3 + packages/ai-anthropic/.eslintrc.js | 10 + packages/ai-anthropic/README.md | 31 +++ packages/ai-anthropic/package.json | 49 ++++ ...ropic-frontend-application-contribution.ts | 107 +++++++++ .../src/browser/anthropic-frontend-module.ts | 31 +++ .../src/browser/anthropic-preferences.ts | 42 ++++ .../anthropic-language-models-manager.ts | 45 ++++ packages/ai-anthropic/src/common/index.ts | 16 ++ .../src/node/anthropic-backend-module.ts | 28 +++ .../src/node/anthropic-language-model.ts | 220 ++++++++++++++++++ .../anthropic-language-models-manager-impl.ts | 81 +++++++ packages/ai-anthropic/src/package.spec.ts | 28 +++ packages/ai-anthropic/tsconfig.json | 19 ++ tsconfig.json | 3 + yarn.lock | 13 ++ 17 files changed, 727 insertions(+) create mode 100644 packages/ai-anthropic/.eslintrc.js create mode 100644 packages/ai-anthropic/README.md create mode 100644 packages/ai-anthropic/package.json create mode 100644 packages/ai-anthropic/src/browser/anthropic-frontend-application-contribution.ts create mode 100644 packages/ai-anthropic/src/browser/anthropic-frontend-module.ts create mode 100644 packages/ai-anthropic/src/browser/anthropic-preferences.ts create mode 100644 packages/ai-anthropic/src/common/anthropic-language-models-manager.ts create mode 100644 packages/ai-anthropic/src/common/index.ts create mode 100644 packages/ai-anthropic/src/node/anthropic-backend-module.ts create mode 100644 packages/ai-anthropic/src/node/anthropic-language-model.ts create mode 100644 packages/ai-anthropic/src/node/anthropic-language-models-manager-impl.ts create mode 100644 packages/ai-anthropic/src/package.spec.ts create mode 100644 packages/ai-anthropic/tsconfig.json diff --git a/examples/browser/package.json b/examples/browser/package.json index 2fdea15ac3e78..0247ccf91a476 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -22,6 +22,7 @@ }, "theiaPluginsDir": "../../plugins", "dependencies": { + "@theia/ai-anthropic": "1.56.0", "@theia/ai-chat": "1.56.0", "@theia/ai-chat-ui": "1.56.0", "@theia/ai-code-completion": "1.56.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index 5c7e5e2b56ab6..2df756c8e8b8b 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -8,6 +8,9 @@ { "path": "../../dev-packages/cli" }, + { + "path": "../../packages/ai-anthropic" + }, { "path": "../../packages/ai-chat" }, diff --git a/packages/ai-anthropic/.eslintrc.js b/packages/ai-anthropic/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-anthropic/.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-anthropic/README.md b/packages/ai-anthropic/README.md new file mode 100644 index 0000000000000..d343ac4f36172 --- /dev/null +++ b/packages/ai-anthropic/README.md @@ -0,0 +1,31 @@ +
+ +
+ +theia-ext-logo + +

ECLIPSE THEIA - Anthropic EXTENSION

+ +
+ +
+ +## Description + +The `@theia/anthropic` integrates Anthropic's models with Theia AI. +The Anthropic API key and the models to use can be configured via preferences. +Alternatively the OpenAI API key can also be handed in via the `ANTHROPIC_API_KEY` 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-anthropic/package.json b/packages/ai-anthropic/package.json new file mode 100644 index 0000000000000..980b4602313ab --- /dev/null +++ b/packages/ai-anthropic/package.json @@ -0,0 +1,49 @@ +{ + "name": "@theia/ai-anthropic", + "version": "1.56.0", + "description": "Theia - Anthropic Integration", + "dependencies": { + "@theia/core": "1.56.0", + "@anthropic-ai/sdk": "^0.32.1", + "@theia/ai-core": "1.56.0" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/anthropic-frontend-module", + "backend": "lib/node/anthropic-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.56.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/ai-anthropic/src/browser/anthropic-frontend-application-contribution.ts b/packages/ai-anthropic/src/browser/anthropic-frontend-application-contribution.ts new file mode 100644 index 0000000000000..5fe9ca4aba6eb --- /dev/null +++ b/packages/ai-anthropic/src/browser/anthropic-frontend-application-contribution.ts @@ -0,0 +1,107 @@ +// ***************************************************************************** +// 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 { AnthropicLanguageModelsManager, AnthropicModelDescription } from '../common'; +import { API_KEY_PREF, MODELS_PREF } from './anthropic-preferences'; +import { PREFERENCE_NAME_REQUEST_SETTINGS, RequestSetting } from '@theia/ai-core/lib/browser/ai-core-preferences'; + +const ANTHROPIC_PROVIDER_ID = 'anthropic'; + +@injectable() +export class AnthropicFrontendApplicationContribution implements FrontendApplicationContribution { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(AnthropicLanguageModelsManager) + protected manager: AnthropicLanguageModelsManager; + + 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, []); + const requestSettings = this.getRequestSettingsPref(); + this.manager.createOrUpdateLanguageModels(...models.map(modelId => this.createAnthropicModelDescription(modelId, requestSettings))); + this.prevModels = [...models]; + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === API_KEY_PREF) { + this.manager.setApiKey(event.newValue); + } else if (event.preferenceName === MODELS_PREF) { + this.handleModelChanges(event.newValue as string[]); + } else if (event.preferenceName === PREFERENCE_NAME_REQUEST_SETTINGS) { + this.handleRequestSettingsChanges(event.newValue as RequestSetting[]); + } + }); + }); + } + + protected handleModelChanges(newModels: string[]): void { + const oldModels = new Set(this.prevModels); + const updatedModels = new Set(newModels); + + const modelsToRemove = [...oldModels].filter(model => !updatedModels.has(model)); + const modelsToAdd = [...updatedModels].filter(model => !oldModels.has(model)); + + this.manager.removeLanguageModels(...modelsToRemove.map(model => `${ANTHROPIC_PROVIDER_ID}/${model}`)); + const requestSettings = this.getRequestSettingsPref(); + this.manager.createOrUpdateLanguageModels(...modelsToAdd.map(modelId => this.createAnthropicModelDescription(modelId, requestSettings))); + this.prevModels = newModels; + } + + private getRequestSettingsPref(): RequestSetting[] { + return this.preferenceService.get(PREFERENCE_NAME_REQUEST_SETTINGS, []); + } + + protected handleRequestSettingsChanges(newSettings: RequestSetting[]): void { + const models = this.preferenceService.get(MODELS_PREF, []); + this.manager.createOrUpdateLanguageModels(...models.map(modelId => this.createAnthropicModelDescription(modelId, newSettings))); + } + + protected createAnthropicModelDescription(modelId: string, requestSettings: RequestSetting[]): AnthropicModelDescription { + const id = `${ANTHROPIC_PROVIDER_ID}/${modelId}`; + const modelRequestSetting = this.getMatchingRequestSetting(modelId, ANTHROPIC_PROVIDER_ID, requestSettings); + return { + id: id, + model: modelId, + apiKey: true, + enableStreaming: true, + defaultRequestSettings: modelRequestSetting?.requestSettings + }; + } + + protected getMatchingRequestSetting( + modelId: string, + providerId: string, + requestSettings: RequestSetting[] + ): RequestSetting | undefined { + const matchingSettings = requestSettings.filter( + setting => (!setting.providerId || setting.providerId === providerId) && setting.modelId === modelId + ); + if (matchingSettings.length > 1) { + console.warn( + `Multiple entries found for provider "${providerId}" and model "${modelId}". Using the first match.` + ); + } + return matchingSettings[0]; + } +} diff --git a/packages/ai-anthropic/src/browser/anthropic-frontend-module.ts b/packages/ai-anthropic/src/browser/anthropic-frontend-module.ts new file mode 100644 index 0000000000000..213eabb1afe51 --- /dev/null +++ b/packages/ai-anthropic/src/browser/anthropic-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 { AnthropicPreferencesSchema } from './anthropic-preferences'; +import { FrontendApplicationContribution, PreferenceContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser'; +import { AnthropicFrontendApplicationContribution } from './anthropic-frontend-application-contribution'; +import { ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, AnthropicLanguageModelsManager } from '../common'; + +export default new ContainerModule(bind => { + bind(PreferenceContribution).toConstantValue({ schema: AnthropicPreferencesSchema }); + bind(AnthropicFrontendApplicationContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(AnthropicFrontendApplicationContribution); + bind(AnthropicLanguageModelsManager).toDynamicValue(ctx => { + const provider = ctx.container.get(RemoteConnectionProvider); + return provider.createProxy(ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH); + }).inSingletonScope(); +}); diff --git a/packages/ai-anthropic/src/browser/anthropic-preferences.ts b/packages/ai-anthropic/src/browser/anthropic-preferences.ts new file mode 100644 index 0000000000000..b47622c156f83 --- /dev/null +++ b/packages/ai-anthropic/src/browser/anthropic-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.anthropic.AnthropicApiKey'; +export const MODELS_PREF = 'ai-features.anthropic.AnthropicModels'; + +export const AnthropicPreferencesSchema: PreferenceSchema = { + type: 'object', + properties: { + [API_KEY_PREF]: { + type: 'string', + markdownDescription: 'Enter an API Key of your official Anthropic Account. **Please note:** By using this preference the Anthropic API key will be stored in clear text\ + on the machine running Theia. Use the environment variable `ANTHROPIC_API_KEY` to set the key securely.', + title: AI_CORE_PREFERENCES_TITLE, + }, + [MODELS_PREF]: { + type: 'array', + description: 'Official Anthropic models to use', + title: AI_CORE_PREFERENCES_TITLE, + default: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest', 'claude-3-opus-latest'], + items: { + type: 'string' + } + }, + } +}; diff --git a/packages/ai-anthropic/src/common/anthropic-language-models-manager.ts b/packages/ai-anthropic/src/common/anthropic-language-models-manager.ts new file mode 100644 index 0000000000000..5f1af28c5e259 --- /dev/null +++ b/packages/ai-anthropic/src/common/anthropic-language-models-manager.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 +// ***************************************************************************** +export const ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH = '/services/anthropic/language-model-manager'; +export const AnthropicLanguageModelsManager = Symbol('AnthropicLanguageModelsManager'); +export interface AnthropicModelDescription { + /** + * The identifier of the model which will be shown in the UI. + */ + id: string; + /** + * The model ID as used by the Anthropic API. + */ + model: string; + /** + * The key for the model. If 'true' is provided the global Anthropic API key will be used. + */ + apiKey: string | true | undefined; + /** + * Indicate whether the streaming API shall be used. + */ + enableStreaming: boolean; + /** + * Default request settings for the Anthropic model. + */ + defaultRequestSettings?: { [key: string]: unknown }; +} +export interface AnthropicLanguageModelsManager { + apiKey: string | undefined; + setApiKey(key: string | undefined): void; + createOrUpdateLanguageModels(...models: AnthropicModelDescription[]): Promise; + removeLanguageModels(...modelIds: string[]): void +} diff --git a/packages/ai-anthropic/src/common/index.ts b/packages/ai-anthropic/src/common/index.ts new file mode 100644 index 0000000000000..e3a0ab9b8a70d --- /dev/null +++ b/packages/ai-anthropic/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 './anthropic-language-models-manager'; diff --git a/packages/ai-anthropic/src/node/anthropic-backend-module.ts b/packages/ai-anthropic/src/node/anthropic-backend-module.ts new file mode 100644 index 0000000000000..3c14a9365f471 --- /dev/null +++ b/packages/ai-anthropic/src/node/anthropic-backend-module.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from '@theia/core/shared/inversify'; +import { ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, AnthropicLanguageModelsManager } from '../common/anthropic-language-models-manager'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; +import { AnthropicLanguageModelsManagerImpl } from './anthropic-language-models-manager-impl'; + +export default new ContainerModule(bind => { + bind(AnthropicLanguageModelsManagerImpl).toSelf().inSingletonScope(); + bind(AnthropicLanguageModelsManager).toService(AnthropicLanguageModelsManagerImpl); + bind(ConnectionHandler).toDynamicValue(ctx => + new RpcConnectionHandler(ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, () => ctx.container.get(AnthropicLanguageModelsManager)) + ).inSingletonScope(); +}); diff --git a/packages/ai-anthropic/src/node/anthropic-language-model.ts b/packages/ai-anthropic/src/node/anthropic-language-model.ts new file mode 100644 index 0000000000000..dd717f4794251 --- /dev/null +++ b/packages/ai-anthropic/src/node/anthropic-language-model.ts @@ -0,0 +1,220 @@ +// ***************************************************************************** +// 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, + LanguageModelStreamResponse, + LanguageModelStreamResponsePart, + LanguageModelTextResponse, + ToolRequest +} from '@theia/ai-core'; +import { CancellationToken } from '@theia/core'; +import { Anthropic } from '@anthropic-ai/sdk'; +import { MessageParam, Tool, ToolChoiceAuto } from '@anthropic-ai/sdk/resources'; + +export const AnthropicModelIdentifier = Symbol('AnthropicModelIdentifier'); + +function transformToAnthropicParams( + messages: LanguageModelRequestMessage[] +): { messages: MessageParam[]; systemMessage?: string } { + // Extract the system message (if any), as it is a separate parameter in the Anthropic API. + const systemMessageObj = messages.find(message => message.actor === 'system'); + const systemMessage = systemMessageObj?.query; + + const convertedMessages = messages + .filter(message => message.actor !== 'system') + .map(message => ({ + role: toAnthropicRole(message), + content: message.query || '', + })); + + return { + messages: convertedMessages, + systemMessage, + }; +} + +function toAnthropicRole(message: LanguageModelRequestMessage): 'user' | 'assistant' { + switch (message.actor) { + case 'ai': + return 'assistant'; + default: + return 'user'; + } +} + +export class AnthropicModel implements LanguageModel { + + constructor( + public readonly id: string, + public model: string, + public enableStreaming: boolean, + public apiKey: () => string | undefined, + public defaultRequestSettings?: { [key: string]: unknown } + ) { } + + protected getSettings(request: LanguageModelRequest): Record { + const settings = request.settings ? request.settings : this.defaultRequestSettings; + if (!settings) { + return {}; + } + return settings; + } + + async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise { + const anthropic = this.initializeAnthropic(); + if (this.enableStreaming) { + return this.handleStreamingRequest(anthropic, request, cancellationToken); + } + return this.handleNonStreamingRequest(anthropic, request); + } + + protected async handleStreamingRequest( + anthropic: Anthropic, + request: LanguageModelRequest, + cancellationToken?: CancellationToken + ): Promise { + const settings = this.getSettings(request); + const { messages, systemMessage } = transformToAnthropicParams(request.messages); + + const tools = this.createTools(request.tools); + + const params: Anthropic.MessageCreateParams = { + max_tokens: 2048, // Setting max_tokens is mandatory for Anthropic, settings can override this default + messages, + model: this.model, + ...(systemMessage && { system: systemMessage }), + ...settings, + ...(tools.length > 0 && { + tools, + tool_choice: { type: 'auto' } as ToolChoiceAuto, + }), + }; + + const stream = anthropic.messages.stream(params); + + cancellationToken?.onCancellationRequested(() => { + stream.abort(); + }); + + const asyncIterator = { + async *[Symbol.asyncIterator](): AsyncIterator { + for await (const event of stream) { + if (event.type === 'content_block_start') { + const contentBlock = event.content_block; + + if (contentBlock.type === 'text') { + yield { content: contentBlock.text }; + } else if (contentBlock.type === 'tool_use') { + yield { + tool_calls: [ + { + id: contentBlock.id, + function: { + arguments: JSON.stringify(contentBlock.input), + name: contentBlock.name, + }, + finished: false, + }, + ], + }; + } + } else if (event.type === 'content_block_delta') { + const delta = event.delta; + + if (delta.type === 'text_delta') { + yield { content: delta.text }; + } else if (delta.type === 'input_json_delta') { + yield { + tool_calls: [ + { + id: event.index.toString(), + function: { + arguments: delta.partial_json, + }, + finished: false, + }, + ], + }; + } + } + } + }, + }; + + stream.on('error', (error: Error) => { + console.error('Error in Anthropic streaming:', error); + }); + + return { stream: asyncIterator }; + } + + protected async handleNonStreamingRequest( + anthropic: Anthropic, + request: LanguageModelRequest + ): Promise { + const settings = this.getSettings(request); + + const { messages, systemMessage } = transformToAnthropicParams(request.messages); + + const params: Anthropic.MessageCreateParams = { + max_tokens: 2048, + messages, + model: this.model, + ...(systemMessage && { system: systemMessage }), + ...settings, + }; + + const response: Anthropic.Message = await anthropic.messages.create(params); + + if (response.content[0] && response.content[0].type === 'text') { + return { + text: response.content[0].text, + }; + } + return { + text: '', + }; + } + + protected initializeAnthropic(): Anthropic { + const apiKey = this.apiKey(); + if (!apiKey) { + throw new Error('Please provide ANTHROPIC_API_KEY in preferences or via environment variable'); + } + + return new Anthropic({ apiKey: apiKey }); + } + + protected createTools(toolRequests: ToolRequest[] | undefined): Tool[] { + if (!toolRequests) { + return []; + } + return toolRequests.map(toolRequest => ({ + name: toolRequest.name, + description: toolRequest.description, + input_schema: { + type: 'object', + properties: toolRequest.parameters?.properties || undefined, + ...(toolRequest.parameters?.type && { type: toolRequest.parameters.type }), + }, + })); + } + +} diff --git a/packages/ai-anthropic/src/node/anthropic-language-models-manager-impl.ts b/packages/ai-anthropic/src/node/anthropic-language-models-manager-impl.ts new file mode 100644 index 0000000000000..0c7e0856e45a0 --- /dev/null +++ b/packages/ai-anthropic/src/node/anthropic-language-models-manager-impl.ts @@ -0,0 +1,81 @@ +// ***************************************************************************** +// 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 { AnthropicModel } from './anthropic-language-model'; +import { AnthropicLanguageModelsManager, AnthropicModelDescription } from '../common'; + +@injectable() +export class AnthropicLanguageModelsManagerImpl implements AnthropicLanguageModelsManager { + + protected _apiKey: string | undefined; + + @inject(LanguageModelRegistry) + protected readonly languageModelRegistry: LanguageModelRegistry; + + get apiKey(): string | undefined { + return this._apiKey ?? process.env.ANTHROPIC_API_KEY; + } + + async createOrUpdateLanguageModels(...modelDescriptions: AnthropicModelDescription[]): Promise { + for (const modelDescription of modelDescriptions) { + const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id); + const apiKeyProvider = () => { + if (modelDescription.apiKey === true) { + return this.apiKey; + } + if (modelDescription.apiKey) { + return modelDescription.apiKey; + } + return undefined; + }; + + if (model) { + if (!(model instanceof AnthropicModel)) { + console.warn(`Anthropic: model ${modelDescription.id} is not an Anthropic model`); + continue; + } + model.model = modelDescription.model; + model.enableStreaming = modelDescription.enableStreaming; + model.apiKey = apiKeyProvider; + model.defaultRequestSettings = modelDescription.defaultRequestSettings; + } else { + this.languageModelRegistry.addLanguageModels([ + new AnthropicModel( + modelDescription.id, + modelDescription.model, + modelDescription.enableStreaming, + apiKeyProvider, + modelDescription.defaultRequestSettings + ) + ]); + } + } + } + + removeLanguageModels(...modelIds: string[]): void { + this.languageModelRegistry.removeLanguageModels(modelIds); + } + + setApiKey(apiKey: string | undefined): void { + if (apiKey) { + this._apiKey = apiKey; + } else { + this._apiKey = undefined; + } + } +} diff --git a/packages/ai-anthropic/src/package.spec.ts b/packages/ai-anthropic/src/package.spec.ts new file mode 100644 index 0000000000000..4a86ea7c4e19c --- /dev/null +++ b/packages/ai-anthropic/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-anthropic package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-anthropic/tsconfig.json b/packages/ai-anthropic/tsconfig.json new file mode 100644 index 0000000000000..420367fccbfb3 --- /dev/null +++ b/packages/ai-anthropic/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 72e54f2e87b78..dc0d0e6811eb7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -54,6 +54,9 @@ { "path": "examples/playwright" }, + { + "path": "packages/ai-anthropic" + }, { "path": "packages/ai-chat" }, diff --git a/yarn.lock b/yarn.lock index a5a480ffa075c..fc9d7f72be143 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,6 +15,19 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@anthropic-ai/sdk@^0.32.1": + version "0.32.1" + resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.32.1.tgz#d22c8ebae2adccc59d78fb416e89de337ff09014" + integrity sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"