diff --git a/.github/workflows/test-exports.yml b/.github/workflows/test-exports.yml index 10e37ca8dc66..14f60f54e5c0 100644 --- a/.github/workflows/test-exports.yml +++ b/.github/workflows/test-exports.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - name: Build - run: yarn workspace langchain-core build && yarn workspace langchain build + run: yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build shell: bash env: SKIP_API_DOCS: true @@ -54,7 +54,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - name: Build - run: yarn workspace langchain-core build && yarn workspace langchain build + run: yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build shell: bash env: SKIP_API_DOCS: true @@ -74,7 +74,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - name: Build - run: yarn workspace langchain-core build && yarn workspace langchain build + run: yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build shell: bash env: SKIP_API_DOCS: true @@ -94,7 +94,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - name: Build - run: yarn workspace langchain-core build && yarn workspace langchain build + run: yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build shell: bash env: SKIP_API_DOCS: true @@ -114,7 +114,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - name: Build - run: yarn workspace langchain-core build && yarn workspace langchain build + run: yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build shell: bash env: SKIP_API_DOCS: true @@ -134,7 +134,7 @@ jobs: - name: Install dependencies run: yarn install --immutable - name: Build - run: yarn workspace langchain-core build && yarn workspace langchain build + run: yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build shell: bash env: SKIP_API_DOCS: true @@ -154,7 +154,7 @@ jobs: # - name: Install dependencies # run: yarn install --immutable # - name: Build - # run: yarn workspace langchain-core build && yarn workspace langchain build + # run: yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build # shell: bash # env: # SKIP_API_DOCS: true diff --git a/docker-compose.yml b/docker-compose.yml index e3cbf9a41f87..33a5f0f91fd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - ./environment_tests/scripts:/scripts - ./langchain:/langchain - ./langchain-core:/langchain-core + - ./libs/langchain-anthropic:/langchain-anthropic command: bash /scripts/docker-ci-entrypoint.sh test-exports-esm: image: node:18 @@ -23,6 +24,7 @@ services: - ./environment_tests/scripts:/scripts - ./langchain:/langchain - ./langchain-core:/langchain-core + - ./libs/langchain-anthropic:/langchain-anthropic command: bash /scripts/docker-ci-entrypoint.sh test-exports-cjs: image: node:18 @@ -35,6 +37,7 @@ services: - ./environment_tests/scripts:/scripts - ./langchain:/langchain - ./langchain-core:/langchain-core + - ./libs/langchain-anthropic:/langchain-anthropic command: bash /scripts/docker-ci-entrypoint.sh test-exports-cf: image: node:18 @@ -47,6 +50,7 @@ services: - ./environment_tests/scripts:/scripts - ./langchain:/langchain - ./langchain-core:/langchain-core + - ./libs/langchain-anthropic:/langchain-anthropic command: bash /scripts/docker-ci-entrypoint.sh test-exports-vercel: image: node:18 @@ -59,6 +63,7 @@ services: - ./environment_tests/scripts:/scripts - ./langchain:/langchain - ./langchain-core:/langchain-core + - ./libs/langchain-anthropic:/langchain-anthropic command: bash /scripts/docker-ci-entrypoint.sh test-exports-vite: image: node:18 @@ -71,6 +76,7 @@ services: - ./environment_tests/scripts:/scripts - ./langchain:/langchain - ./langchain-core:/langchain-core + - ./libs/langchain-anthropic:/langchain-anthropic command: bash /scripts/docker-ci-entrypoint.sh # test-exports-bun: # image: oven/bun @@ -79,7 +85,8 @@ services: # - ./environment_tests/test-exports-bun:/package # - ./environment_tests/scripts:/scripts # - ./langchain:/langchain-workspace - # - ./langchain-core:/langchain-core-workspace + # - ./langchain-core:/langchain-core + # - ./libs/langchain-anthropic:/langchain-anthropic-workspace # command: bash /scripts/docker-bun-ci-entrypoint.sh success: image: alpine:3.14 diff --git a/docs/api_refs/package.json b/docs/api_refs/package.json index 5cebec86207a..5dfe6aa79552 100644 --- a/docs/api_refs/package.json +++ b/docs/api_refs/package.json @@ -6,7 +6,7 @@ "dev": "next dev -p 3001", "typedoc": "npx typedoc --options typedoc.json", "build:scripts": "node ./scripts/generate-api-refs.js && node ./scripts/update-typedoc-css.js", - "build": "yarn workspace langchain-core build && yarn workspace langchain build && yarn build:scripts && next build", + "build": "yarn workspace langchain-core build && yarn workspace @langchain/anthropic build && yarn workspace langchain build && yarn build:scripts && next build", "start": "yarn build && next start -p 3001", "lint": "next lint" }, diff --git a/environment_tests/scripts/docker-ci-entrypoint.sh b/environment_tests/scripts/docker-ci-entrypoint.sh index 9beb303f6711..c42e773f5601 100644 --- a/environment_tests/scripts/docker-ci-entrypoint.sh +++ b/environment_tests/scripts/docker-ci-entrypoint.sh @@ -19,7 +19,9 @@ cp ../root/yarn.lock ../root/.yarnrc.yml . # Avoid calling "yarn add ../langchain" as yarn berry does seem to hang for ~30s # before installation actually occurs sed -i 's/"langchain-core": "workspace:\*"/"langchain-core": "..\/langchain-core"/g' package.json +sed -i 's/"@langchain\/anthropic": "workspace:\*"/"@langchain\/anthropic": "..\/langchain-anthropic"/g' package.json sed -i 's/"langchain": "workspace:\*"/"langchain": "..\/langchain"/g' package.json + yarn install --no-immutable # Check the build command completes successfully diff --git a/environment_tests/test-exports-bun/package.json b/environment_tests/test-exports-bun/package.json index 776ecd970f6c..ac41e1658550 100644 --- a/environment_tests/test-exports-bun/package.json +++ b/environment_tests/test-exports-bun/package.json @@ -17,6 +17,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { + "@langchain/anthropic": "workspace:*", "d3-dsv": "2", "hnswlib-node": "^1.4.2", "langchain": "workspace:*", diff --git a/environment_tests/test-exports-cf/package.json b/environment_tests/test-exports-cf/package.json index 1e99b48f9d71..206838159714 100644 --- a/environment_tests/test-exports-cf/package.json +++ b/environment_tests/test-exports-cf/package.json @@ -8,6 +8,7 @@ "wrangler": "3.7.0" }, "dependencies": { + "@langchain/anthropic": "workspace:*", "langchain": "workspace:*", "langchain-core": "workspace:*" }, diff --git a/environment_tests/test-exports-cjs/package.json b/environment_tests/test-exports-cjs/package.json index ef1c1ff89149..3019c68fa662 100644 --- a/environment_tests/test-exports-cjs/package.json +++ b/environment_tests/test-exports-cjs/package.json @@ -18,6 +18,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { + "@langchain/anthropic": "workspace:*", "d3-dsv": "2", "hnswlib-node": "^1.4.2", "langchain": "workspace:*", diff --git a/environment_tests/test-exports-esbuild/package.json b/environment_tests/test-exports-esbuild/package.json index eba715c7a7ac..25b4863e2a4f 100644 --- a/environment_tests/test-exports-esbuild/package.json +++ b/environment_tests/test-exports-esbuild/package.json @@ -16,6 +16,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { + "@langchain/anthropic": "workspace:*", "d3-dsv": "2", "hnswlib-node": "^1.4.2", "langchain": "workspace:*", diff --git a/environment_tests/test-exports-esm/package.json b/environment_tests/test-exports-esm/package.json index 7661e33f0cfc..c37f54af8e0c 100644 --- a/environment_tests/test-exports-esm/package.json +++ b/environment_tests/test-exports-esm/package.json @@ -19,6 +19,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { + "@langchain/anthropic": "workspace:*", "d3-dsv": "2", "hnswlib-node": "^1.4.2", "langchain": "workspace:*", diff --git a/environment_tests/test-exports-vercel/package.json b/environment_tests/test-exports-vercel/package.json index c6f6bf1986d6..b4f0f12937cd 100644 --- a/environment_tests/test-exports-vercel/package.json +++ b/environment_tests/test-exports-vercel/package.json @@ -9,6 +9,7 @@ "test": "next lint" }, "dependencies": { + "@langchain/anthropic": "workspace:*", "@types/node": "18.15.11", "@types/react": "18.0.33", "@types/react-dom": "18.0.11", diff --git a/environment_tests/test-exports-vite/package.json b/environment_tests/test-exports-vite/package.json index 21d054971b90..c1da8133f61b 100644 --- a/environment_tests/test-exports-vite/package.json +++ b/environment_tests/test-exports-vite/package.json @@ -10,6 +10,7 @@ "test": "tsc" }, "dependencies": { + "@langchain/anthropic": "workspace:*", "langchain": "workspace:*", "langchain-core": "workspace:*" }, diff --git a/langchain/package.json b/langchain/package.json index b1892d4b2785..1c641903a9bd 100644 --- a/langchain/package.json +++ b/langchain/package.json @@ -865,6 +865,7 @@ "@google-cloud/storage": "^6.10.1", "@huggingface/inference": "^2.6.4", "@jest/globals": "^29.5.0", + "@langchain/anthropic": "workspace:*", "@mozilla/readability": "^0.4.4", "@notionhq/client": "^2.2.10", "@opensearch-project/opensearch": "^2.2.0", @@ -1378,7 +1379,7 @@ } }, "dependencies": { - "@anthropic-ai/sdk": "^0.9.1", + "@langchain/anthropic": "^0.0.2", "binary-extensions": "^2.2.0", "expr-eval": "^2.0.2", "flat": "^5.0.2", diff --git a/langchain/src/chat_models/anthropic.ts b/langchain/src/chat_models/anthropic.ts index 96771c8de6dc..e2bac02693b3 100644 --- a/langchain/src/chat_models/anthropic.ts +++ b/langchain/src/chat_models/anthropic.ts @@ -1,442 +1 @@ -import { - Anthropic, - AI_PROMPT, - HUMAN_PROMPT, - ClientOptions, -} from "@anthropic-ai/sdk"; -import type { CompletionCreateParams } from "@anthropic-ai/sdk/resources/completions"; -import type { Stream } from "@anthropic-ai/sdk/streaming"; - -import { CallbackManagerForLLMRun } from "../callbacks/manager.js"; -import { - AIMessage, - AIMessageChunk, - BaseMessage, - ChatGeneration, - ChatGenerationChunk, - ChatMessage, - ChatResult, -} from "../schema/index.js"; -import { getEnvironmentVariable } from "../util/env.js"; -import { BaseChatModel, BaseChatModelParams } from "./base.js"; -import { BaseLanguageModelCallOptions } from "../base_language/index.js"; - -/** - * Extracts the custom role of a generic chat message. - * @param message The chat message from which to extract the custom role. - * @returns The custom role of the chat message. - */ -function extractGenericMessageCustomRole(message: ChatMessage) { - if ( - message.role !== AI_PROMPT && - message.role !== HUMAN_PROMPT && - message.role !== "" - ) { - console.warn(`Unknown message role: ${message.role}`); - } - - return message.role; -} - -/** - * Gets the Anthropic prompt from a base message. - * @param message The base message from which to get the Anthropic prompt. - * @returns The Anthropic prompt from the base message. - */ -function getAnthropicPromptFromMessage(message: BaseMessage): string { - const type = message._getType(); - switch (type) { - case "ai": - return AI_PROMPT; - case "human": - return HUMAN_PROMPT; - case "system": - return ""; - case "generic": { - if (!ChatMessage.isInstance(message)) - throw new Error("Invalid generic chat message"); - return extractGenericMessageCustomRole(message); - } - default: - throw new Error(`Unknown message type: ${type}`); - } -} - -export const DEFAULT_STOP_SEQUENCES = [HUMAN_PROMPT]; - -/** - * Input to AnthropicChat class. - */ -export interface AnthropicInput { - /** Amount of randomness injected into the response. Ranges - * from 0 to 1. Use temp closer to 0 for analytical / - * multiple choice, and temp closer to 1 for creative - * and generative tasks. - */ - temperature?: number; - - /** Only sample from the top K options for each subsequent - * token. Used to remove "long tail" low probability - * responses. Defaults to -1, which disables it. - */ - topK?: number; - - /** Does nucleus sampling, in which we compute the - * cumulative distribution over all the options for each - * subsequent token in decreasing probability order and - * cut it off once it reaches a particular probability - * specified by top_p. Defaults to -1, which disables it. - * Note that you should either alter temperature or top_p, - * but not both. - */ - topP?: number; - - /** A maximum number of tokens to generate before stopping. */ - maxTokensToSample: number; - - /** A list of strings upon which to stop generating. - * You probably want `["\n\nHuman:"]`, as that's the cue for - * the next turn in the dialog agent. - */ - stopSequences?: string[]; - - /** Whether to stream the results or not */ - streaming?: boolean; - - /** Anthropic API key */ - anthropicApiKey?: string; - - /** Anthropic API URL */ - anthropicApiUrl?: string; - - /** Model name to use */ - modelName: string; - - /** Overridable Anthropic ClientOptions */ - clientOptions: ClientOptions; - - /** Holds any additional parameters that are valid to pass to {@link - * https://console.anthropic.com/docs/api/reference | - * `anthropic.complete`} that are not explicitly specified on this class. - */ - invocationKwargs?: Kwargs; -} - -/** - * A type representing additional parameters that can be passed to the - * Anthropic API. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Kwargs = Record; - -/** - * Wrapper around Anthropic large language models. - * - * To use you should have the `@anthropic-ai/sdk` package installed, with the - * `ANTHROPIC_API_KEY` environment variable set. - * - * @remarks - * Any parameters that are valid to be passed to {@link - * https://console.anthropic.com/docs/api/reference | - * `anthropic.complete`} can be passed through {@link invocationKwargs}, - * even if not explicitly available on this class. - * @example - * ```typescript - * const model = new ChatAnthropic({ - * temperature: 0.9, - * anthropicApiKey: 'YOUR-API-KEY', - * }); - * const res = await model.invoke({ input: 'Hello!' }); - * console.log(res); - * ``` - */ -export class ChatAnthropic< - CallOptions extends BaseLanguageModelCallOptions = BaseLanguageModelCallOptions - > - extends BaseChatModel - implements AnthropicInput -{ - static lc_name() { - return "ChatAnthropic"; - } - - get lc_secrets(): { [key: string]: string } | undefined { - return { - anthropicApiKey: "ANTHROPIC_API_KEY", - }; - } - - get lc_aliases(): Record { - return { - modelName: "model", - }; - } - - lc_serializable = true; - - anthropicApiKey?: string; - - apiUrl?: string; - - temperature = 1; - - topK = -1; - - topP = -1; - - maxTokensToSample = 2048; - - modelName = "claude-2"; - - invocationKwargs?: Kwargs; - - stopSequences?: string[]; - - streaming = false; - - clientOptions: ClientOptions; - - // Used for non-streaming requests - protected batchClient: Anthropic; - - // Used for streaming requests - protected streamingClient: Anthropic; - - constructor(fields?: Partial & BaseChatModelParams) { - super(fields ?? {}); - - this.anthropicApiKey = - fields?.anthropicApiKey ?? getEnvironmentVariable("ANTHROPIC_API_KEY"); - if (!this.anthropicApiKey) { - throw new Error("Anthropic API key not found"); - } - - // Support overriding the default API URL (i.e., https://api.anthropic.com) - this.apiUrl = fields?.anthropicApiUrl; - - this.modelName = fields?.modelName ?? this.modelName; - this.invocationKwargs = fields?.invocationKwargs ?? {}; - - this.temperature = fields?.temperature ?? this.temperature; - this.topK = fields?.topK ?? this.topK; - this.topP = fields?.topP ?? this.topP; - this.maxTokensToSample = - fields?.maxTokensToSample ?? this.maxTokensToSample; - this.stopSequences = fields?.stopSequences ?? this.stopSequences; - - this.streaming = fields?.streaming ?? false; - this.clientOptions = fields?.clientOptions ?? {}; - } - - /** - * Get the parameters used to invoke the model - */ - invocationParams( - options?: this["ParsedCallOptions"] - ): Omit & Kwargs { - return { - model: this.modelName, - temperature: this.temperature, - top_k: this.topK, - top_p: this.topP, - stop_sequences: - options?.stop?.concat(DEFAULT_STOP_SEQUENCES) ?? - this.stopSequences ?? - DEFAULT_STOP_SEQUENCES, - max_tokens_to_sample: this.maxTokensToSample, - stream: this.streaming, - ...this.invocationKwargs, - }; - } - - /** @ignore */ - _identifyingParams() { - return { - model_name: this.modelName, - ...this.invocationParams(), - }; - } - - /** - * Get the identifying parameters for the model - */ - identifyingParams() { - return { - model_name: this.modelName, - ...this.invocationParams(), - }; - } - - async *_streamResponseChunks( - messages: BaseMessage[], - options: this["ParsedCallOptions"], - runManager?: CallbackManagerForLLMRun - ): AsyncGenerator { - const params = this.invocationParams(options); - const stream = await this.createStreamWithRetry({ - ...params, - prompt: this.formatMessagesAsPrompt(messages), - }); - let modelSent = false; - let stopReasonSent = false; - for await (const data of stream) { - if (options.signal?.aborted) { - stream.controller.abort(); - throw new Error("AbortError: User aborted the request."); - } - const additional_kwargs: Record = {}; - if (data.model && !modelSent) { - additional_kwargs.model = data.model; - modelSent = true; - } else if (data.stop_reason && !stopReasonSent) { - additional_kwargs.stop_reason = data.stop_reason; - stopReasonSent = true; - } - const delta = data.completion ?? ""; - yield new ChatGenerationChunk({ - message: new AIMessageChunk({ - content: delta, - additional_kwargs, - }), - text: delta, - }); - await runManager?.handleLLMNewToken(delta); - if (data.stop_reason) { - break; - } - } - } - - /** - * Formats messages as a prompt for the model. - * @param messages The base messages to format as a prompt. - * @returns The formatted prompt. - */ - protected formatMessagesAsPrompt(messages: BaseMessage[]): string { - return ( - messages - .map((message) => { - const messagePrompt = getAnthropicPromptFromMessage(message); - return `${messagePrompt} ${message.content}`; - }) - .join("") + AI_PROMPT - ); - } - - /** @ignore */ - async _generate( - messages: BaseMessage[], - options: this["ParsedCallOptions"], - runManager?: CallbackManagerForLLMRun - ): Promise { - if (this.stopSequences && options.stop) { - throw new Error( - `"stopSequence" parameter found in input and default params` - ); - } - - const params = this.invocationParams(options); - let response; - if (params.stream) { - response = { - completion: "", - model: "", - stop_reason: "", - }; - const stream = await this._streamResponseChunks( - messages, - options, - runManager - ); - for await (const chunk of stream) { - response.completion += chunk.message.content; - response.model = - (chunk.message.additional_kwargs.model as string) ?? response.model; - response.stop_reason = - (chunk.message.additional_kwargs.stop_reason as string) ?? - response.stop_reason; - } - } else { - response = await this.completionWithRetry( - { - ...params, - prompt: this.formatMessagesAsPrompt(messages), - }, - { signal: options.signal } - ); - } - - const generations: ChatGeneration[] = (response.completion ?? "") - .split(AI_PROMPT) - .map((message) => ({ - text: message, - message: new AIMessage(message), - })); - - return { - generations, - }; - } - - /** - * Creates a streaming request with retry. - * @param request The parameters for creating a completion. - * @returns A streaming request. - */ - protected async createStreamWithRetry( - request: CompletionCreateParams & Kwargs - ): Promise> { - if (!this.streamingClient) { - const options = this.apiUrl ? { baseURL: this.apiUrl } : undefined; - this.streamingClient = new Anthropic({ - ...this.clientOptions, - ...options, - apiKey: this.anthropicApiKey, - maxRetries: 0, - }); - } - const makeCompletionRequest = async () => - this.streamingClient.completions.create( - { ...request, stream: true }, - { headers: request.headers } - ); - return this.caller.call(makeCompletionRequest); - } - - /** @ignore */ - protected async completionWithRetry( - request: CompletionCreateParams & Kwargs, - options: { signal?: AbortSignal } - ): Promise { - if (!this.anthropicApiKey) { - throw new Error("Missing Anthropic API key."); - } - if (!this.batchClient) { - const options = this.apiUrl ? { baseURL: this.apiUrl } : undefined; - this.batchClient = new Anthropic({ - ...this.clientOptions, - ...options, - apiKey: this.anthropicApiKey, - maxRetries: 0, - }); - } - const makeCompletionRequest = async () => - this.batchClient.completions.create( - { ...request, stream: false }, - { headers: request.headers } - ); - return this.caller.callWithOptions( - { signal: options.signal }, - makeCompletionRequest - ); - } - - _llmType() { - return "anthropic"; - } - - /** @ignore */ - _combineLLMOutput() { - return []; - } -} +export * from "@langchain/anthropic"; diff --git a/langchain/src/chat_models/tests/chatanthropic.int.test.ts b/langchain/src/chat_models/tests/chatanthropic.int.test.ts index cfc572c9584d..05470df53d0d 100644 --- a/langchain/src/chat_models/tests/chatanthropic.int.test.ts +++ b/langchain/src/chat_models/tests/chatanthropic.int.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-process-env */ import { expect, test } from "@jest/globals"; -import { HUMAN_PROMPT } from "@anthropic-ai/sdk"; +import { HUMAN_PROMPT } from "@langchain/anthropic"; import { ChatMessage, HumanMessage } from "../../schema/index.js"; import { ChatPromptValue } from "../../prompts/chat.js"; import { diff --git a/langchain/src/load/import_type.d.ts b/langchain/src/load/import_type.d.ts index ff96bdc124d3..5d5174cf122d 100644 --- a/langchain/src/load/import_type.d.ts +++ b/langchain/src/load/import_type.d.ts @@ -493,7 +493,6 @@ export interface OptionalImportMap { } export interface SecretMap { - ANTHROPIC_API_KEY?: string; AWS_ACCESS_KEY_ID?: string; AWS_SECRETE_ACCESS_KEY?: string; AWS_SECRET_ACCESS_KEY?: string; diff --git a/libs/langchain-anthropic/.eslintrc.cjs b/libs/langchain-anthropic/.eslintrc.cjs new file mode 100644 index 000000000000..4adf489ade1c --- /dev/null +++ b/libs/langchain-anthropic/.eslintrc.cjs @@ -0,0 +1,69 @@ +module.exports = { + extends: [ + "airbnb-base", + "eslint:recommended", + "prettier", + "plugin:@typescript-eslint/recommended", + ], + parserOptions: { + ecmaVersion: 12, + parser: "@typescript-eslint/parser", + project: "./tsconfig.json", + sourceType: "module", + }, + plugins: ["@typescript-eslint", "no-instanceof"], + ignorePatterns: [ + "src/utils/@cfworker", + "src/utils/fast-json-patch", + "src/utils/js-sha1", + ".eslintrc.cjs", + "scripts", + "node_modules", + "dist", + "dist-cjs", + "*.js", + "*.cjs", + "*.d.ts", + ], + rules: { + "no-process-env": 2, + "no-instanceof/no-instanceof": 2, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-shadow": 0, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/no-use-before-define": ["error", "nofunc"], + "@typescript-eslint/no-unused-vars": ["warn", { args: "none" }], + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + camelcase: 0, + "class-methods-use-this": 0, + "import/extensions": [2, "ignorePackages"], + "import/no-extraneous-dependencies": [ + "error", + { devDependencies: ["**/*.test.ts"] }, + ], + "import/no-unresolved": 0, + "import/prefer-default-export": 0, + "keyword-spacing": "error", + "max-classes-per-file": 0, + "max-len": 0, + "no-await-in-loop": 0, + "no-bitwise": 0, + "no-console": 0, + "no-restricted-syntax": 0, + "no-shadow": 0, + "no-continue": 0, + "no-void": 0, + "no-underscore-dangle": 0, + "no-use-before-define": 0, + "no-useless-constructor": 0, + "no-return-await": 0, + "consistent-return": 0, + "no-else-return": 0, + "func-names": 0, + "no-lonely-if": 0, + "prefer-rest-params": 0, + "new-cap": ["error", { properties: false, capIsNew: false }], + }, +}; diff --git a/libs/langchain-anthropic/.gitignore b/libs/langchain-anthropic/.gitignore new file mode 100644 index 000000000000..ebce63d7679c --- /dev/null +++ b/libs/langchain-anthropic/.gitignore @@ -0,0 +1,3 @@ +index.cjs +index.js +index.d.ts diff --git a/libs/langchain-anthropic/LICENSE b/libs/langchain-anthropic/LICENSE new file mode 100644 index 000000000000..d5c9d8189aa9 --- /dev/null +++ b/libs/langchain-anthropic/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) Harrison Chase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/libs/langchain-anthropic/README.md b/libs/langchain-anthropic/README.md new file mode 100644 index 000000000000..afc1b03b0bdd --- /dev/null +++ b/libs/langchain-anthropic/README.md @@ -0,0 +1 @@ +# langchain-anthropic \ No newline at end of file diff --git a/libs/langchain-anthropic/babel.config.cjs b/libs/langchain-anthropic/babel.config.cjs new file mode 100644 index 000000000000..7617b0c33f70 --- /dev/null +++ b/libs/langchain-anthropic/babel.config.cjs @@ -0,0 +1,4 @@ +// babel.config.js +module.exports = { + presets: [["@babel/preset-env", { targets: { node: true } }]], +}; diff --git a/libs/langchain-anthropic/jest.config.cjs b/libs/langchain-anthropic/jest.config.cjs new file mode 100644 index 000000000000..f11eefc468b4 --- /dev/null +++ b/libs/langchain-anthropic/jest.config.cjs @@ -0,0 +1,20 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: "ts-jest/presets/default-esm", + testEnvironment: "./jest.env.cjs", + modulePathIgnorePatterns: ["dist/", "docs/"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + transform: { + '^.+\\.tsx?$': ['@swc/jest'], + }, + transformIgnorePatterns: [ + "/node_modules/", + "\\.pnp\\.[^\\/]+$", + "./scripts/jest-setup-after-env.js", + ], + setupFiles: ["dotenv/config"], + setupFilesAfterEnv: ["./scripts/jest-setup-after-env.js"], + testTimeout: 20_000, +}; diff --git a/libs/langchain-anthropic/jest.env.cjs b/libs/langchain-anthropic/jest.env.cjs new file mode 100644 index 000000000000..2ccedccb8672 --- /dev/null +++ b/libs/langchain-anthropic/jest.env.cjs @@ -0,0 +1,12 @@ +const { TestEnvironment } = require("jest-environment-node"); + +class AdjustedTestEnvironmentToSupportFloat32Array extends TestEnvironment { + constructor(config, context) { + // Make `instanceof Float32Array` return true in tests + // to avoid https://github.com/xenova/transformers.js/issues/57 and https://github.com/jestjs/jest/issues/2549 + super(config, context); + this.global.Float32Array = Float32Array; + } +} + +module.exports = AdjustedTestEnvironmentToSupportFloat32Array; diff --git a/libs/langchain-anthropic/package.json b/libs/langchain-anthropic/package.json new file mode 100644 index 000000000000..409c3dc04bfa --- /dev/null +++ b/libs/langchain-anthropic/package.json @@ -0,0 +1,88 @@ +{ + "name": "@langchain/anthropic", + "version": "0.0.2", + "description": "Anthropic integrations for LangChain.js", + "type": "module", + "engines": { + "node": ">=18" + }, + "main": "./index.js", + "types": "./index.d.ts", + "repository": { + "type": "git", + "url": "git@github.com:langchain-ai/langchainjs.git" + }, + "scripts": { + "build": "yarn clean && yarn build:esm && yarn build:cjs && yarn build:scripts", + "build:esm": "NODE_OPTIONS=--max-old-space-size=4096 tsc --outDir dist/", + "build:cjs": "NODE_OPTIONS=--max-old-space-size=4096 tsc --outDir dist-cjs/ -p tsconfig.cjs.json && node scripts/move-cjs-to-dist.js && rimraf dist-cjs", + "build:watch": "node scripts/create-entrypoints.js && tsc --outDir dist/ --watch", + "build:scripts": "node scripts/create-entrypoints.js && node scripts/check-tree-shaking.js", + "lint": "NODE_OPTIONS=--max-old-space-size=4096 eslint src && dpdm --exit-code circular:1 --no-warning --no-tree src/*.ts src/**/*.ts", + "lint:fix": "yarn lint --fix", + "clean": "rimraf dist/ && NODE_OPTIONS=--max-old-space-size=4096 node scripts/create-entrypoints.js pre", + "prepack": "yarn build", + "release": "release-it --only-version --config .release-it.json", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts --testTimeout 30000 --maxWorkers=50%", + "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch --testPathIgnorePatterns=\\.int\\.test.ts", + "test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.cjs --testTimeout 100000", + "format": "prettier --write \"src\"", + "format:check": "prettier --check \"src\"" + }, + "author": "LangChain", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "^0.10.0", + "langchain-core": "^0.0.3" + }, + "devDependencies": { + "@jest/globals": "^29.5.0", + "@swc/core": "^1.3.90", + "@swc/jest": "^0.2.29", + "dpdm": "^3.12.0", + "eslint": "^8.33.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-no-instanceof": "^1.0.1", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^29.5.0", + "jest-environment-node": "^29.6.4", + "langchain-core": "workspace:*", + "prettier": "^2.8.3", + "release-it": "^15.10.1", + "rimraf": "^5.0.1", + "typescript": "^5.0.0" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "llm", + "ai", + "gpt3", + "chain", + "prompt", + "prompt engineering", + "chatgpt", + "machine learning", + "ml", + "openai", + "embeddings", + "vectorstores" + ], + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "require": "./index.cjs" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "index.cjs", + "index.js", + "index.d.ts" + ] +} diff --git a/libs/langchain-anthropic/scripts/check-tree-shaking.js b/libs/langchain-anthropic/scripts/check-tree-shaking.js new file mode 100644 index 000000000000..63e705e81fc1 --- /dev/null +++ b/libs/langchain-anthropic/scripts/check-tree-shaking.js @@ -0,0 +1,80 @@ +import fs from "fs/promises"; +import { rollup } from "rollup"; + +const packageJson = JSON.parse(await fs.readFile("package.json", "utf-8")); + +export function listEntrypoints() { + const exports = packageJson.exports; + const entrypoints = []; + + for (const [key, value] of Object.entries(exports)) { + if (key === "./package.json") { + continue; + } + if (typeof value === "string") { + entrypoints.push(value); + } else if (typeof value === "object" && value.import) { + entrypoints.push(value.import); + } + } + + return entrypoints; +} + +export function listExternals() { + return [ + ...Object.keys(packageJson.dependencies), + ...Object.keys(packageJson.peerDependencies ?? {}), + /node\:/, + /langchain-core\//, + ]; +} + +export async function checkTreeShaking() { + const externals = listExternals(); + const entrypoints = listEntrypoints(); + const consoleLog = console.log; + const reportMap = new Map(); + + for (const entrypoint of entrypoints) { + let sideEffects = ""; + + console.log = function (...args) { + const line = args.length ? args.join(" ") : ""; + if (line.trim().startsWith("First side effect in")) { + sideEffects += line + "\n"; + } + }; + + await rollup({ + external: externals, + input: entrypoint, + experimentalLogSideEffects: true, + }); + + reportMap.set(entrypoint, { + log: sideEffects, + hasSideEffects: sideEffects.length > 0, + }); + } + + console.log = consoleLog; + + let failed = false; + for (const [entrypoint, report] of reportMap) { + if (report.hasSideEffects) { + failed = true; + console.log("---------------------------------"); + console.log(`Tree shaking failed for ${entrypoint}`); + console.log(report.log); + } + } + + if (failed) { + process.exit(1); + } else { + console.log("Tree shaking checks passed!"); + } +} + +checkTreeShaking(); diff --git a/libs/langchain-anthropic/scripts/create-entrypoints.js b/libs/langchain-anthropic/scripts/create-entrypoints.js new file mode 100644 index 000000000000..0a752f25c083 --- /dev/null +++ b/libs/langchain-anthropic/scripts/create-entrypoints.js @@ -0,0 +1,94 @@ +import * as fs from "fs"; +import * as path from "path"; + +// This lists all the entrypoints for the library. Each key corresponds to an +// importable path, eg. `import { AgentExecutor } from "langchain/agents"`. +// The value is the path to the file in `src/` that exports the entrypoint. +// This is used to generate the `exports` field in package.json. +// Order is not important. +const entrypoints = { + "index": "index" +}; + +// Entrypoints in this list require an optional dependency to be installed. +// Therefore they are not tested in the generated test-exports-* packages. +const requiresOptionalDependency = []; + +const updateJsonFile = (relativePath, updateFunction) => { + const contents = fs.readFileSync(relativePath).toString(); + const res = updateFunction(JSON.parse(contents)); + fs.writeFileSync(relativePath, JSON.stringify(res, null, 2) + "\n"); +}; + +const generateFiles = () => { + const files = [...Object.entries(entrypoints), ["index", "index"]].flatMap( + ([key, value]) => { + const nrOfDots = key.split("/").length - 1; + const relativePath = "../".repeat(nrOfDots) || "./"; + const compiledPath = `${relativePath}dist/${value}.js`; + return [ + [ + `${key}.cjs`, + `module.exports = require('${relativePath}dist/${value}.cjs');`, + ], + [`${key}.js`, `export * from '${compiledPath}'`], + [`${key}.d.ts`, `export * from '${compiledPath}'`], + ]; + } + ); + + return Object.fromEntries(files); +}; + +const updateConfig = () => { + const generatedFiles = generateFiles(); + const filenames = Object.keys(generatedFiles); + + // Update package.json `exports` and `files` fields + updateJsonFile("./package.json", (json) => ({ + ...json, + exports: Object.assign( + Object.fromEntries( + [...Object.keys(entrypoints)].map((key) => { + let entryPoint = { + types: `./${key}.d.ts`, + import: `./${key}.js`, + require: `./${key}.cjs`, + }; + + return [key === "index" ? "." : `./${key}`, entryPoint]; + }) + ), + { "./package.json": "./package.json" } + ), + files: ["dist/", ...filenames], + })); + + // Write generated files + Object.entries(generatedFiles).forEach(([filename, content]) => { + fs.mkdirSync(path.dirname(filename), { recursive: true }); + fs.writeFileSync(filename, content); + }); + + // Update .gitignore + fs.writeFileSync("./.gitignore", filenames.join("\n") + "\n"); +}; + +const cleanGenerated = () => { + const filenames = Object.keys(generateFiles()); + filenames.forEach((fname) => { + try { + fs.unlinkSync(fname); + } catch { + // ignore error + } + }); +}; + +const command = process.argv[2]; + +if (command === "pre") { + cleanGenerated(); +} else { + updateConfig(); +} diff --git a/libs/langchain-anthropic/scripts/identify-secrets.js b/libs/langchain-anthropic/scripts/identify-secrets.js new file mode 100644 index 000000000000..c54bdd97c870 --- /dev/null +++ b/libs/langchain-anthropic/scripts/identify-secrets.js @@ -0,0 +1,77 @@ +import ts from "typescript"; +import * as fs from "fs"; + +export function identifySecrets() { + const secrets = new Set(); + + const tsConfig = ts.parseJsonConfigFileContent( + ts.readJsonConfigFile("./tsconfig.json", (p) => + fs.readFileSync(p, "utf-8") + ), + ts.sys, + "./src/" + ); + + for (const fileName of tsConfig.fileNames.filter( + (fn) => !fn.endsWith("test.ts") + )) { + const sourceFile = ts.createSourceFile( + fileName, + fs.readFileSync(fileName, "utf-8"), + tsConfig.options.target, + true + ); + sourceFile.forEachChild((node) => { + switch (node.kind) { + case ts.SyntaxKind.ClassDeclaration: + case ts.SyntaxKind.ClassExpression: { + node.forEachChild((node) => { + // look for get lc_secrets() + switch (node.kind) { + case ts.SyntaxKind.GetAccessor: { + const property = node; + if (property.name.getText() === "lc_secrets") { + // look for return { ... } + property.body.statements.forEach((stmt) => { + if ( + stmt.kind === ts.SyntaxKind.ReturnStatement && + stmt.expression.kind === + ts.SyntaxKind.ObjectLiteralExpression + ) { + // collect secret identifier + stmt.expression.properties.forEach((element) => { + if ( + element.initializer.kind === + ts.SyntaxKind.StringLiteral + ) { + const secret = element.initializer.text; + + if (secret.toUpperCase() !== secret) { + throw new Error( + `Secret identifier must be uppercase: ${secret} at ${fileName}` + ); + } + if (/\s/.test(secret)) { + throw new Error( + `Secret identifier must not contain whitespace: ${secret} at ${fileName}` + ); + } + + secrets.add(secret); + } + }); + } + }); + } + break; + } + } + }); + break; + } + } + }); + } + + return secrets; +} diff --git a/libs/langchain-anthropic/scripts/jest-setup-after-env.js b/libs/langchain-anthropic/scripts/jest-setup-after-env.js new file mode 100644 index 000000000000..30c8f7bd75ec --- /dev/null +++ b/libs/langchain-anthropic/scripts/jest-setup-after-env.js @@ -0,0 +1,3 @@ +import { awaitAllCallbacks } from "../src/callbacks/promises.js"; + +afterAll(awaitAllCallbacks); diff --git a/libs/langchain-anthropic/scripts/move-cjs-to-dist.js b/libs/langchain-anthropic/scripts/move-cjs-to-dist.js new file mode 100644 index 000000000000..1e89ccca88e9 --- /dev/null +++ b/libs/langchain-anthropic/scripts/move-cjs-to-dist.js @@ -0,0 +1,38 @@ +import { resolve, dirname, parse, format } from "node:path"; +import { readdir, readFile, writeFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +function abs(relativePath) { + return resolve(dirname(fileURLToPath(import.meta.url)), relativePath); +} + +async function moveAndRename(source, dest) { + for (const file of await readdir(abs(source), { withFileTypes: true })) { + if (file.isDirectory()) { + await moveAndRename(`${source}/${file.name}`, `${dest}/${file.name}`); + } else if (file.isFile()) { + const parsed = parse(file.name); + + // Ignore anything that's not a .js file + if (parsed.ext !== ".js") { + continue; + } + + // Rewrite any require statements to use .cjs + const content = await readFile(abs(`${source}/${file.name}`), "utf8"); + const rewritten = content.replace(/require\("(\..+?).js"\)/g, (_, p1) => { + return `require("${p1}.cjs")`; + }); + + // Rename the file to .cjs + const renamed = format({ name: parsed.name, ext: ".cjs" }); + + await writeFile(abs(`${dest}/${renamed}`), rewritten, "utf8"); + } + } +} + +moveAndRename("../dist-cjs", "../dist").catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/libs/langchain-anthropic/scripts/release-branch.sh b/libs/langchain-anthropic/scripts/release-branch.sh new file mode 100644 index 000000000000..7504238c5561 --- /dev/null +++ b/libs/langchain-anthropic/scripts/release-branch.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +if [[ $(git branch --show-current) == "main" ]]; then + git checkout -B release + git push -u origin release +fi diff --git a/libs/langchain-anthropic/src/chat_models.ts b/libs/langchain-anthropic/src/chat_models.ts new file mode 100644 index 000000000000..0e70beec5399 --- /dev/null +++ b/libs/langchain-anthropic/src/chat_models.ts @@ -0,0 +1,449 @@ +import { + Anthropic, + AI_PROMPT, + HUMAN_PROMPT, + ClientOptions, +} from "@anthropic-ai/sdk"; +import type { CompletionCreateParams } from "@anthropic-ai/sdk/resources/completions"; +import type { Stream } from "@anthropic-ai/sdk/streaming"; + +import { CallbackManagerForLLMRun } from "langchain-core/callbacks/manager"; +import { + AIMessage, + AIMessageChunk, + type BaseMessage, + ChatMessage, +} from "langchain-core/messages"; +import { + type ChatGeneration, + ChatGenerationChunk, + type ChatResult, +} from "langchain-core/outputs"; +import { getEnvironmentVariable } from "langchain-core/utils/env"; +import { + BaseChatModel, + type BaseChatModelParams, +} from "langchain-core/language_models/chat_models"; +import { type BaseLanguageModelCallOptions } from "langchain-core/language_models/base"; + +export { AI_PROMPT, HUMAN_PROMPT }; + +/** + * Extracts the custom role of a generic chat message. + * @param message The chat message from which to extract the custom role. + * @returns The custom role of the chat message. + */ +function extractGenericMessageCustomRole(message: ChatMessage) { + if ( + message.role !== AI_PROMPT && + message.role !== HUMAN_PROMPT && + message.role !== "" + ) { + console.warn(`Unknown message role: ${message.role}`); + } + + return message.role; +} + +/** + * Gets the Anthropic prompt from a base message. + * @param message The base message from which to get the Anthropic prompt. + * @returns The Anthropic prompt from the base message. + */ +function getAnthropicPromptFromMessage(message: BaseMessage): string { + const type = message._getType(); + switch (type) { + case "ai": + return AI_PROMPT; + case "human": + return HUMAN_PROMPT; + case "system": + return ""; + case "generic": { + if (!ChatMessage.isInstance(message)) + throw new Error("Invalid generic chat message"); + return extractGenericMessageCustomRole(message); + } + default: + throw new Error(`Unknown message type: ${type}`); + } +} + +export const DEFAULT_STOP_SEQUENCES = [HUMAN_PROMPT]; + +/** + * Input to AnthropicChat class. + */ +export interface AnthropicInput { + /** Amount of randomness injected into the response. Ranges + * from 0 to 1. Use temp closer to 0 for analytical / + * multiple choice, and temp closer to 1 for creative + * and generative tasks. + */ + temperature?: number; + + /** Only sample from the top K options for each subsequent + * token. Used to remove "long tail" low probability + * responses. Defaults to -1, which disables it. + */ + topK?: number; + + /** Does nucleus sampling, in which we compute the + * cumulative distribution over all the options for each + * subsequent token in decreasing probability order and + * cut it off once it reaches a particular probability + * specified by top_p. Defaults to -1, which disables it. + * Note that you should either alter temperature or top_p, + * but not both. + */ + topP?: number; + + /** A maximum number of tokens to generate before stopping. */ + maxTokensToSample: number; + + /** A list of strings upon which to stop generating. + * You probably want `["\n\nHuman:"]`, as that's the cue for + * the next turn in the dialog agent. + */ + stopSequences?: string[]; + + /** Whether to stream the results or not */ + streaming?: boolean; + + /** Anthropic API key */ + anthropicApiKey?: string; + + /** Anthropic API URL */ + anthropicApiUrl?: string; + + /** Model name to use */ + modelName: string; + + /** Overridable Anthropic ClientOptions */ + clientOptions: ClientOptions; + + /** Holds any additional parameters that are valid to pass to {@link + * https://console.anthropic.com/docs/api/reference | + * `anthropic.complete`} that are not explicitly specified on this class. + */ + invocationKwargs?: Kwargs; +} + +/** + * A type representing additional parameters that can be passed to the + * Anthropic API. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Kwargs = Record; + +/** + * Wrapper around Anthropic large language models. + * + * To use you should have the `@anthropic-ai/sdk` package installed, with the + * `ANTHROPIC_API_KEY` environment variable set. + * + * @remarks + * Any parameters that are valid to be passed to {@link + * https://console.anthropic.com/docs/api/reference | + * `anthropic.complete`} can be passed through {@link invocationKwargs}, + * even if not explicitly available on this class. + * @example + * ```typescript + * const model = new ChatAnthropic({ + * temperature: 0.9, + * anthropicApiKey: 'YOUR-API-KEY', + * }); + * const res = await model.invoke({ input: 'Hello!' }); + * console.log(res); + * ``` + */ +export class ChatAnthropic< + CallOptions extends BaseLanguageModelCallOptions = BaseLanguageModelCallOptions + > + extends BaseChatModel + implements AnthropicInput +{ + static lc_name() { + return "ChatAnthropic"; + } + + get lc_secrets(): { [key: string]: string } | undefined { + return { + anthropicApiKey: "ANTHROPIC_API_KEY", + }; + } + + get lc_aliases(): Record { + return { + modelName: "model", + }; + } + + lc_serializable = true; + + anthropicApiKey?: string; + + apiUrl?: string; + + temperature = 1; + + topK = -1; + + topP = -1; + + maxTokensToSample = 2048; + + modelName = "claude-2"; + + invocationKwargs?: Kwargs; + + stopSequences?: string[]; + + streaming = false; + + clientOptions: ClientOptions; + + // Used for non-streaming requests + protected batchClient: Anthropic; + + // Used for streaming requests + protected streamingClient: Anthropic; + + constructor(fields?: Partial & BaseChatModelParams) { + super(fields ?? {}); + + this.anthropicApiKey = + fields?.anthropicApiKey ?? getEnvironmentVariable("ANTHROPIC_API_KEY"); + if (!this.anthropicApiKey) { + throw new Error("Anthropic API key not found"); + } + + // Support overriding the default API URL (i.e., https://api.anthropic.com) + this.apiUrl = fields?.anthropicApiUrl; + + this.modelName = fields?.modelName ?? this.modelName; + this.invocationKwargs = fields?.invocationKwargs ?? {}; + + this.temperature = fields?.temperature ?? this.temperature; + this.topK = fields?.topK ?? this.topK; + this.topP = fields?.topP ?? this.topP; + this.maxTokensToSample = + fields?.maxTokensToSample ?? this.maxTokensToSample; + this.stopSequences = fields?.stopSequences ?? this.stopSequences; + + this.streaming = fields?.streaming ?? false; + this.clientOptions = fields?.clientOptions ?? {}; + } + + /** + * Get the parameters used to invoke the model + */ + invocationParams( + options?: this["ParsedCallOptions"] + ): Omit & Kwargs { + return { + model: this.modelName, + temperature: this.temperature, + top_k: this.topK, + top_p: this.topP, + stop_sequences: + options?.stop?.concat(DEFAULT_STOP_SEQUENCES) ?? + this.stopSequences ?? + DEFAULT_STOP_SEQUENCES, + max_tokens_to_sample: this.maxTokensToSample, + stream: this.streaming, + ...this.invocationKwargs, + }; + } + + /** @ignore */ + _identifyingParams() { + return { + model_name: this.modelName, + ...this.invocationParams(), + }; + } + + /** + * Get the identifying parameters for the model + */ + identifyingParams() { + return { + model_name: this.modelName, + ...this.invocationParams(), + }; + } + + async *_streamResponseChunks( + messages: BaseMessage[], + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): AsyncGenerator { + const params = this.invocationParams(options); + const stream = await this.createStreamWithRetry({ + ...params, + prompt: this.formatMessagesAsPrompt(messages), + }); + let modelSent = false; + let stopReasonSent = false; + for await (const data of stream) { + if (options.signal?.aborted) { + stream.controller.abort(); + throw new Error("AbortError: User aborted the request."); + } + const additional_kwargs: Record = {}; + if (data.model && !modelSent) { + additional_kwargs.model = data.model; + modelSent = true; + } else if (data.stop_reason && !stopReasonSent) { + additional_kwargs.stop_reason = data.stop_reason; + stopReasonSent = true; + } + const delta = data.completion ?? ""; + yield new ChatGenerationChunk({ + message: new AIMessageChunk({ + content: delta, + additional_kwargs, + }), + text: delta, + }); + await runManager?.handleLLMNewToken(delta); + if (data.stop_reason) { + break; + } + } + } + + /** + * Formats messages as a prompt for the model. + * @param messages The base messages to format as a prompt. + * @returns The formatted prompt. + */ + protected formatMessagesAsPrompt(messages: BaseMessage[]): string { + return ( + messages + .map((message) => { + const messagePrompt = getAnthropicPromptFromMessage(message); + return `${messagePrompt} ${message.content}`; + }) + .join("") + AI_PROMPT + ); + } + + /** @ignore */ + async _generate( + messages: BaseMessage[], + options: this["ParsedCallOptions"], + runManager?: CallbackManagerForLLMRun + ): Promise { + if (this.stopSequences && options.stop) { + throw new Error( + `"stopSequence" parameter found in input and default params` + ); + } + + const params = this.invocationParams(options); + let response; + if (params.stream) { + response = { + completion: "", + model: "", + stop_reason: "", + }; + const stream = await this._streamResponseChunks( + messages, + options, + runManager + ); + for await (const chunk of stream) { + response.completion += chunk.message.content; + response.model = + (chunk.message.additional_kwargs.model as string) ?? response.model; + response.stop_reason = + (chunk.message.additional_kwargs.stop_reason as string) ?? + response.stop_reason; + } + } else { + response = await this.completionWithRetry( + { + ...params, + prompt: this.formatMessagesAsPrompt(messages), + }, + { signal: options.signal } + ); + } + + const generations: ChatGeneration[] = (response.completion ?? "") + .split(AI_PROMPT) + .map((message) => ({ + text: message, + message: new AIMessage(message), + })); + + return { + generations, + }; + } + + /** + * Creates a streaming request with retry. + * @param request The parameters for creating a completion. + * @returns A streaming request. + */ + protected async createStreamWithRetry( + request: CompletionCreateParams & Kwargs + ): Promise> { + if (!this.streamingClient) { + const options = this.apiUrl ? { baseURL: this.apiUrl } : undefined; + this.streamingClient = new Anthropic({ + ...this.clientOptions, + ...options, + apiKey: this.anthropicApiKey, + maxRetries: 0, + }); + } + const makeCompletionRequest = async () => + this.streamingClient.completions.create( + { ...request, stream: true }, + { headers: request.headers } + ); + return this.caller.call(makeCompletionRequest); + } + + /** @ignore */ + protected async completionWithRetry( + request: CompletionCreateParams & Kwargs, + options: { signal?: AbortSignal } + ): Promise { + if (!this.anthropicApiKey) { + throw new Error("Missing Anthropic API key."); + } + if (!this.batchClient) { + const options = this.apiUrl ? { baseURL: this.apiUrl } : undefined; + this.batchClient = new Anthropic({ + ...this.clientOptions, + ...options, + apiKey: this.anthropicApiKey, + maxRetries: 0, + }); + } + const makeCompletionRequest = async () => + this.batchClient.completions.create( + { ...request, stream: false }, + { headers: request.headers } + ); + return this.caller.callWithOptions( + { signal: options.signal }, + makeCompletionRequest + ); + } + + _llmType() { + return "anthropic"; + } + + /** @ignore */ + _combineLLMOutput() { + return []; + } +} diff --git a/libs/langchain-anthropic/src/index.ts b/libs/langchain-anthropic/src/index.ts new file mode 100644 index 000000000000..38c7cea7f478 --- /dev/null +++ b/libs/langchain-anthropic/src/index.ts @@ -0,0 +1 @@ +export * from "./chat_models.js"; diff --git a/libs/langchain-anthropic/tsconfig.cjs.json b/libs/langchain-anthropic/tsconfig.cjs.json new file mode 100644 index 000000000000..83f5d513ef0f --- /dev/null +++ b/libs/langchain-anthropic/tsconfig.cjs.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "declaration": false + }, + "exclude": [ + "node_modules", + "dist", + "docs", + "**/tests" + ] +} \ No newline at end of file diff --git a/libs/langchain-anthropic/tsconfig.json b/libs/langchain-anthropic/tsconfig.json new file mode 100644 index 000000000000..ffc49dde54a6 --- /dev/null +++ b/libs/langchain-anthropic/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "@tsconfig/recommended", + "compilerOptions": { + "outDir": "../dist", + "rootDir": "./src", + "target": "ES2021", + "lib": [ + "ES2021", + "ES2022.Object", + "DOM" + ], + "module": "ES2020", + "moduleResolution": "nodenext", + "esModuleInterop": true, + "declaration": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "useDefineForClassFields": true, + "strictPropertyInitialization": false, + "allowJs": true, + "strict": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "docs" + ] +} diff --git a/package.json b/package.json index 926aea5eb09d..44a129a04675 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "workspaces": [ "langchain", "langchain-core", + "libs/*", "examples", "docs/*" ], diff --git a/turbo.json b/turbo.json index fd83dfb846de..6443a3e3f0a0 100644 --- a/turbo.json +++ b/turbo.json @@ -5,6 +5,11 @@ ], "pipeline": { "langchain-core#build": {}, + "libs/langchain-anthropic#build": { + "dependsOn": [ + "langchain-core#build" + ] + }, "build": { "dependsOn": [ "langchain-core#build", diff --git a/yarn.lock b/yarn.lock index e76e2fa19c7a..68b3279a84ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -211,9 +211,9 @@ __metadata: languageName: node linkType: hard -"@anthropic-ai/sdk@npm:^0.9.1": - version: 0.9.1 - resolution: "@anthropic-ai/sdk@npm:0.9.1" +"@anthropic-ai/sdk@npm:^0.10.0": + version: 0.10.0 + resolution: "@anthropic-ai/sdk@npm:0.10.0" dependencies: "@types/node": ^18.11.18 "@types/node-fetch": ^2.6.4 @@ -224,7 +224,7 @@ __metadata: formdata-node: ^4.3.2 node-fetch: ^2.6.7 web-streams-polyfill: ^3.2.1 - checksum: 0ec50abc0ffc694d903d516f4ac110aafa3a588438791851dd2c3d220da49ceaf2723e218b7f1bc13e737fee1561b18ad8c3b4cc4347e8212131f69637216413 + checksum: b7091a001c8dd51e4bf4985d05b64eed8b57015ff8211b70dbd47861c4289f8d3b8dc6dccbb71af125acd2d27483aa15cdbb1543790176fddb97d95c2f6d2471 languageName: node linkType: hard @@ -7939,6 +7939,31 @@ __metadata: languageName: node linkType: hard +"@langchain/anthropic@workspace:*, @langchain/anthropic@workspace:libs/langchain-anthropic": + version: 0.0.0-use.local + resolution: "@langchain/anthropic@workspace:libs/langchain-anthropic" + dependencies: + "@anthropic-ai/sdk": ^0.10.0 + "@jest/globals": ^29.5.0 + "@swc/core": ^1.3.90 + "@swc/jest": ^0.2.29 + dpdm: ^3.12.0 + eslint: ^8.33.0 + eslint-config-airbnb-base: ^15.0.0 + eslint-config-prettier: ^8.6.0 + eslint-plugin-import: ^2.27.5 + eslint-plugin-no-instanceof: ^1.0.1 + eslint-plugin-prettier: ^4.2.1 + jest: ^29.5.0 + jest-environment-node: ^29.6.4 + langchain-core: "workspace:*" + prettier: ^2.8.3 + release-it: ^15.10.1 + rimraf: ^5.0.1 + typescript: ^5.0.0 + languageName: unknown + linkType: soft + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.4 resolution: "@leichtgewicht/ip-codec@npm:2.0.4" @@ -22248,7 +22273,6 @@ __metadata: version: 0.0.0-use.local resolution: "langchain@workspace:langchain" dependencies: - "@anthropic-ai/sdk": ^0.9.1 "@aws-crypto/sha256-js": ^5.0.0 "@aws-sdk/client-bedrock-runtime": ^3.422.0 "@aws-sdk/client-dynamodb": ^3.310.0 @@ -22273,6 +22297,7 @@ __metadata: "@google-cloud/storage": ^6.10.1 "@huggingface/inference": ^2.6.4 "@jest/globals": ^29.5.0 + "@langchain/anthropic": "workspace:*" "@mozilla/readability": ^0.4.4 "@notionhq/client": ^2.2.10 "@opensearch-project/opensearch": ^2.2.0