From d009cd0aaff645a24d37085ed41c55fe296a5722 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Tue, 20 Aug 2024 22:23:27 -0400 Subject: [PATCH] feat(puterai): add claude --- package-lock.json | 27 ++++- src/backend/package.json | 1 + .../src/modules/puterai/ClaudeService.js | 109 ++++++++++++++++++ .../src/modules/puterai/PuterAIModule.js | 5 + 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/backend/src/modules/puterai/ClaudeService.js diff --git a/package-lock.json b/package-lock.json index 225d2e44..f15dee87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -263,6 +263,30 @@ "node": ">=6.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.26.1.tgz", + "integrity": "sha512-HeMJP1bDFfQPQS3XTJAmfXkFBdZ88wvfkE05+vsoA9zGn5dHqEaHOPsqkazf/i0gXYg2XlLxxZrf6rUAarSqzw==", + "license": "MIT", + "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" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.45.tgz", + "integrity": "sha512-VZxPKNNhjKmaC1SUYowuXSRSMGyQGmQjvvA1xE4QZ0xce2kLtEhPDS+kqpCPBZYgqblCLQ2DAjSzmgCM5auvhA==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -16561,9 +16585,10 @@ }, "src/backend": { "name": "@heyputer/backend", - "version": "2.1.0", + "version": "2.4.1", "license": "AGPL-3.0-only", "dependencies": { + "@anthropic-ai/sdk": "^0.26.1", "@aws-sdk/client-polly": "^3.622.0", "@aws-sdk/client-textract": "^3.621.0", "@heyputer/kv.js": "^0.1.3", diff --git a/src/backend/package.json b/src/backend/package.json index 0fb81bb5..c8a2d59d 100644 --- a/src/backend/package.json +++ b/src/backend/package.json @@ -7,6 +7,7 @@ "test": "npx mocha" }, "dependencies": { + "@anthropic-ai/sdk": "^0.26.1", "@aws-sdk/client-polly": "^3.622.0", "@aws-sdk/client-textract": "^3.621.0", "@heyputer/kv.js": "^0.1.3", diff --git a/src/backend/src/modules/puterai/ClaudeService.js b/src/backend/src/modules/puterai/ClaudeService.js new file mode 100644 index 00000000..3dcba322 --- /dev/null +++ b/src/backend/src/modules/puterai/ClaudeService.js @@ -0,0 +1,109 @@ +const { default: Anthropic } = require("@anthropic-ai/sdk"); +const BaseService = require("../../services/BaseService"); +const { whatis } = require("../../util/langutil"); +const { PassThrough } = require("stream"); +const { TypedValue } = require("../../services/drivers/meta/Runtime"); + +const PUTER_PROMPT = ` + You are running on an open-source platform called Puter, + as the Claude implementation for a driver interface + called puter-chat-completion. + + The following JSON contains system messages from the + user of the driver interface (typically an app on Puter): +`.replace('\n', ' ').trim(); + +class ClaudeService extends BaseService { + static MODULES = { + Anthropic: require('@anthropic-ai/sdk'), + } + + async _init () { + this.anthropic = new Anthropic({ + apiKey: this.config.apiKey + }); + } + + static IMPLEMENTS = { + ['puter-chat-completion']: { + async complete ({ messages, stream }) { + const adapted_messages = []; + + const system_prompts = []; + let previous_was_user = false; + for ( const message of messages ) { + if ( typeof message.content === 'string' ) { + message.content = { + type: 'text', + text: message.content, + }; + } + if ( whatis(message.content) !== 'array' ) { + message.content = [message.content]; + } + if ( message.role === 'user' && previous_was_user ) { + const last_msg = adapted_messages[adapted_messages.length-1]; + last_msg.content.push( + ...(Array.isArray ? message.content : [message.content]) + ); + continue; + } + if ( message.role === 'system' ) { + system_prompts.push(...message.content); + continue; + } + adapted_messages.push(message); + if ( message.role === 'user' ) { + previous_was_user = true; + } + } + + if ( stream ) { + const stream = new PassThrough(); + const retval = new TypedValue({ + $: 'stream', + content_type: 'application/x-ndjson', + chunked: true, + }, stream); + (async () => { + const completion = await this.anthropic.messages.stream({ + model: 'claude-3-5-sonnet-20240620', + max_tokens: 1000, + temperature: 0, + system: PUTER_PROMPT + JSON.stringify(system_prompts), + messages: adapted_messages, + }); + for await ( const event of completion ) { + if ( + event.type !== 'content_block_delta' || + event.delta.type !== 'text_delta' + ) continue; + const str = JSON.stringify({ + text: event.delta.text, + }); + stream.write(str + '\n'); + } + })(); + + return retval; + } + + const msg = await this.anthropic.messages.create({ + model: 'claude-3-5-sonnet-20240620', + max_tokens: 1000, + temperature: 0, + system: PUTER_PROMPT + JSON.stringify(system_prompts), + messages: adapted_messages, + }); + return { + message: msg, + finish_reason: 'stop' + }; + } + } + } +} + +module.exports = { + ClaudeService, +}; diff --git a/src/backend/src/modules/puterai/PuterAIModule.js b/src/backend/src/modules/puterai/PuterAIModule.js index f1ccde42..977afec2 100644 --- a/src/backend/src/modules/puterai/PuterAIModule.js +++ b/src/backend/src/modules/puterai/PuterAIModule.js @@ -28,6 +28,11 @@ class PuterAIModule extends AdvancedBase { const { OpenAIImageGenerationService } = require('./OpenAIImageGenerationService'); services.registerService('openai-image-generation', OpenAIImageGenerationService); } + + if ( !! config?.services?.claude ) { + const { ClaudeService } = require('./ClaudeService'); + services.registerService('claude', ClaudeService); + } } }