diff --git a/.github/scripts/deployDomainVercel.sh b/.github/scripts/deployDomainVercel.sh new file mode 100644 index 000000000000..f27bb803c8b6 --- /dev/null +++ b/.github/scripts/deployDomainVercel.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Check if an argument is provided +if [ $# -eq 0 ]; then + echo "Error: Please provide a version string as an argument." + exit 1 +fi + +inputString=$1 + +# Check if VERCEL_TOKEN is set +if [ -z "$VERCEL_TOKEN" ]; then + echo "Error: VERCEL_TOKEN is not set." + exit 1 +fi + +# save stdout and stderr to files +vercel deploy --prebuilt --token="$VERCEL_TOKEN" >deployment-url.txt 2>error.txt + +# check the exit code +code=$? +if [ $code -eq 0 ]; then + # Set the deploymentUrl using the input string + deploymentUrl="${inputString}.api.js.langchain.com" + vercel alias $(cat deployment-url.txt) $deploymentUrl --token="$VERCEL_TOKEN" --scope="langchain" +else + # Handle the error + errorMessage=$(cat error.txt) + echo "There was an error: $errorMessage" +fi \ No newline at end of file diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml index 9174f2fc31ef..3f0a82c37fea 100644 --- a/.github/workflows/compatibility.yml +++ b/.github/workflows/compatibility.yml @@ -6,10 +6,10 @@ on: pull_request: # Only run this workflow if the following directories have changed. paths: - - 'langchain/**' - - 'langchain-core/**' - - 'libs/**' - workflow_dispatch: # Allows triggering the workflow manually in GitHub UI + - "langchain/**" + - "langchain-core/**" + - "libs/**" + workflow_dispatch: # Allows triggering the workflow manually in GitHub UI # If another push to the same PR or branch happens while this workflow is still running, # cancel the earlier run in favor of the next run. @@ -24,7 +24,7 @@ concurrency: env: PUPPETEER_SKIP_DOWNLOAD: "true" PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "true" - NODE_VERSION: "18.x" + NODE_VERSION: "20.x" COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} # Run a separate job for each check in the docker-compose file, @@ -75,42 +75,42 @@ jobs: - name: Test LangChain with lowest deps run: docker compose -f dependency_range_tests/docker-compose.yml run langchain-lowest-deps - # Community - community-latest-deps: - runs-on: ubuntu-latest - needs: get-changed-files - if: contains(needs.get-changed-files.outputs.changed_files, 'langchain-core/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-openai/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-community/') - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - cache: "yarn" - - name: Install dependencies - run: yarn install --immutable - - name: Build `@langchain/standard-tests` - run: yarn build --filter=@langchain/standard-tests - - name: Test `@langchain/community` with latest deps - run: docker compose -f dependency_range_tests/docker-compose.yml run community-latest-deps + # # Community + # community-latest-deps: + # runs-on: ubuntu-latest + # needs: get-changed-files + # if: contains(needs.get-changed-files.outputs.changed_files, 'langchain-core/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-openai/') || contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-community/') + # steps: + # - uses: actions/checkout@v4 + # - name: Use Node.js ${{ env.NODE_VERSION }} + # uses: actions/setup-node@v3 + # with: + # node-version: ${{ env.NODE_VERSION }} + # cache: "yarn" + # - name: Install dependencies + # run: yarn install --immutable + # - name: Build `@langchain/standard-tests` + # run: yarn build --filter=@langchain/standard-tests + # - name: Test `@langchain/community` with latest deps + # run: docker compose -f dependency_range_tests/docker-compose.yml run community-latest-deps - community-lowest-deps: - runs-on: ubuntu-latest - needs: get-changed-files - if: contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-community/') - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - cache: "yarn" - - name: Install dependencies - run: yarn install --immutable - - name: Build `@langchain/standard-tests` - run: yarn build --filter=@langchain/standard-tests - - name: Test `@langchain/community` with lowest deps - run: docker compose -f dependency_range_tests/docker-compose.yml run community-lowest-deps + # community-lowest-deps: + # runs-on: ubuntu-latest + # needs: get-changed-files + # if: contains(needs.get-changed-files.outputs.changed_files, 'libs/langchain-community/') + # steps: + # - uses: actions/checkout@v4 + # - name: Use Node.js ${{ env.NODE_VERSION }} + # uses: actions/setup-node@v3 + # with: + # node-version: ${{ env.NODE_VERSION }} + # cache: "yarn" + # - name: Install dependencies + # run: yarn install --immutable + # - name: Build `@langchain/standard-tests` + # run: yarn build --filter=@langchain/standard-tests + # - name: Test `@langchain/community` with lowest deps + # run: docker compose -f dependency_range_tests/docker-compose.yml run community-lowest-deps community-npm-install: runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-api-refs-prod.yml b/.github/workflows/deploy-api-refs-prod.yml index c26b880732e9..5b4a0e4bcbd0 100644 --- a/.github/workflows/deploy-api-refs-prod.yml +++ b/.github/workflows/deploy-api-refs-prod.yml @@ -3,7 +3,7 @@ name: Deploy API Refs Prod on: workflow_dispatch: # Allows triggering the workflow manually in GitHub UI push: - branches: ["main"] + branches: ["main", "v0.2"] # If another push to the same PR or branch happens while this workflow is still running, # cancel the earlier run in favor of the next run. @@ -34,6 +34,21 @@ jobs: - name: Build All Projects run: yarn turbo:command build --filter=!examples --filter=!api_refs --filter=!core_docs --filter=!create-langchain-integration - name: Build Project Artifacts - run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} - - name: Deploy Project Artifacts to Vercel - run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file + run: | + if [ ${{ github.ref }} = 'refs/heads/main' ]; then + vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + else + vercel build --token=${{ secrets.VERCEL_TOKEN }} + fi + - name: Deploy to Vercel + env: + VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} + run: | + if [ ${{ github.ref }} = 'refs/heads/main' ]; then + vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} + elif [ ${{ github.ref }} = 'refs/heads/v0.2' ]; then + .github/scripts/deployDomainVercel.sh v02 + else + echo "Error: Deployment is only allowed for 'main' or 'v0.2' branches." + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/spam-comment-filter.yml b/.github/workflows/spam-comment-filter.yml deleted file mode 100644 index 4f974e6e9941..000000000000 --- a/.github/workflows/spam-comment-filter.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Spam Comment Filter - -on: - issue_comment: - types: [created] - pull_request_review_comment: - types: [created] - -jobs: - filter_spam: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Use Node.js 18.x - uses: actions/setup-node@v3 - with: - node-version: 18.x - - name: Check issue body against regex - id: regex_check - env: - COMMENT_BODY: ${{ github.event.comment.body }} - run: | - REGEX='^download\s+(?:https?:\/\/)?[\w-]+(\.[\w-]+)+[^\s]+\s+password:\s*.+\s+in the installer menu, select\s*.+$' - if echo "$COMMENT_BODY" | tr '\n' ' ' | grep -qiP "$REGEX"; then - echo "REGEX_MATCHED=true" >> $GITHUB_OUTPUT - else - echo "REGEX_MATCHED=false" >> $GITHUB_OUTPUT - fi - - name: Install dependencies - if: steps.regex_check.outputs.REGEX_MATCHED == 'true' - run: cd ./libs/langchain-scripts && yarn workspaces focus - - name: Build scripts - if: steps.regex_check.outputs.REGEX_MATCHED == 'true' - run: cd ./libs/langchain-scripts && yarn build:internal - - name: Run spam detection script - if: steps.regex_check.outputs.REGEX_MATCHED == 'true' - env: - SPAM_COMMENT_GITHUB_TOKEN: ${{ secrets.SPAM_COMMENT_GITHUB_TOKEN }} - COMMENT_JSON: ${{ toJson(github.event.comment) }} - COMMENT_ID: ${{ github.event.comment.id }} - REPO_OWNER: ${{ github.repository_owner }} - REPO_NAME: ${{ github.event.repository.name }} - run: cd ./libs/langchain-scripts && yarn filter_spam_comment diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 000000000000..b4990557b566 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,4 @@ +node_modules/ +**/node_modules/ +.next/ +**/.next/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b49049e637a..66afe82b8794 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,9 +105,9 @@ You can invoke the script by calling `yarn release`. If new dependencies have be There are three parameters which can be passed to this script, one required and two optional. -- __Required__: ``. eg: `@langchain/core` The name of the package to release. Can be found in the `name` value of the package's `package.json` -- __Optional__: `--bump-deps` eg `--bump-deps` Will find all packages in the repo which depend on this workspace and checkout a new branch, update the dep version, run yarn install, commit & push to new branch. Generally, this is not necessary. -- __Optional__: `--tag ` eg `--tag beta` Add a tag to the NPM release. Useful if you want to push a release candidate. +- **Required**: ``. eg: `@langchain/core` The name of the package to release. Can be found in the `name` value of the package's `package.json` +- **Optional**: `--bump-deps` eg `--bump-deps` Will find all packages in the repo which depend on this workspace and checkout a new branch, update the dep version, run yarn install, commit & push to new branch. Generally, this is not necessary. +- **Optional**: `--tag ` eg `--tag beta` Add a tag to the NPM release. Useful if you want to push a release candidate. This script automatically bumps the package version, creates a new release branch with the changes, pushes the branch to GitHub, uses `release-it` to automatically release to NPM, and more depending on the flags passed. @@ -323,6 +323,10 @@ Similar to linting, we recognize documentation can be annoying. If you do not wa Documentation and the skeleton lives under the `docs/` folder. Example code is imported from under the `examples/` folder. +**If you are contributing an integration, please copy and use the appropriate template from here:** + +https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-scripts/src/cli/docs/templates + ### Running examples If you add a new major piece of functionality, it is helpful to add an diff --git a/deno.json b/deno.json index 15679236f0e4..4c6004fa02b2 100644 --- a/deno.json +++ b/deno.json @@ -1,20 +1,20 @@ { "imports": { - "langchain/": "npm:/langchain/", + "langchain/": "npm:/langchain@0.3.2/", + "@langchain/anthropic": "npm:@langchain/anthropic@0.3.0", + "@langchain/cloudflare": "npm:@langchain/cloudflare@0.1.0", + "@langchain/community/": "npm:/@langchain/community@0.3.0/", + "@langchain/openai": "npm:@langchain/openai@0.3.0", + "@langchain/cohere": "npm:@langchain/cohere@0.3.0", + "@langchain/textsplitters": "npm:@langchain/textsplitters@0.1.0", + "@langchain/google-vertexai-web": "npm:@langchain/google-vertexai-web@0.1.0", + "@langchain/mistralai": "npm:@langchain/mistralai@0.1.0", + "@langchain/core/": "npm:/@langchain/core@0.3.1/", + "@langchain/pinecone": "npm:@langchain/pinecone@0.1.0", + "@langchain/google-common": "npm:@langchain/google-common@0.1.0", + "@langchain/langgraph": "npm:/@langchain/langgraph@0.2.3", + "@langchain/langgraph/": "npm:/@langchain/langgraph@0.2.3/", "@faker-js/faker": "npm:@faker-js/faker", - "@langchain/anthropic": "npm:@langchain/anthropic", - "@langchain/cloudflare": "npm:@langchain/cloudflare", - "@langchain/community/": "npm:/@langchain/community@0.2.2/", - "@langchain/openai": "npm:@langchain/openai", - "@langchain/cohere": "npm:@langchain/cohere", - "@langchain/textsplitters": "npm:@langchain/textsplitters", - "@langchain/google-vertexai-web": "npm:@langchain/google-vertexai-web", - "@langchain/mistralai": "npm:@langchain/mistralai", - "@langchain/core/": "npm:/@langchain/core@0.2.16/", - "@langchain/pinecone": "npm:@langchain/pinecone", - "@langchain/google-common": "npm:@langchain/google-common", - "@langchain/langgraph": "npm:/@langchain/langgraph@0.0.21", - "@langchain/langgraph/": "npm:/@langchain/langgraph@0.0.21/", "@microsoft/fetch-event-source": "npm:@microsoft/fetch-event-source", "@pinecone-database/pinecone": "npm:@pinecone-database/pinecone", "cheerio": "npm:cheerio", diff --git a/dependency_range_tests/scripts/with_standard_tests/community/node/update_resolutions_npm.js b/dependency_range_tests/scripts/with_standard_tests/community/node/update_resolutions_npm.js index 9a80bc65bade..0833d484b46c 100644 --- a/dependency_range_tests/scripts/with_standard_tests/community/node/update_resolutions_npm.js +++ b/dependency_range_tests/scripts/with_standard_tests/community/node/update_resolutions_npm.js @@ -3,8 +3,8 @@ const fs = require("fs"); const communityPackageJsonPath = "/app/monorepo/libs/langchain-community/package.json"; const currentPackageJson = JSON.parse(fs.readFileSync(communityPackageJsonPath)); -if (currentPackageJson.devDependencies["@langchain/core"]) { - delete currentPackageJson.devDependencies["@langchain/core"]; +if (currentPackageJson.devDependencies) { + delete currentPackageJson.devDependencies; } fs.writeFileSync(communityPackageJsonPath, JSON.stringify(currentPackageJson, null, 2)); diff --git a/dependency_range_tests/scripts/with_standard_tests/community/npm-install.sh b/dependency_range_tests/scripts/with_standard_tests/community/npm-install.sh index 4e33fae2dd58..1f66fd5c5448 100644 --- a/dependency_range_tests/scripts/with_standard_tests/community/npm-install.sh +++ b/dependency_range_tests/scripts/with_standard_tests/community/npm-install.sh @@ -25,6 +25,5 @@ node "update_resolutions_npm.js" # Navigate back to monorepo root and install dependencies cd "$monorepo_dir" -npm install @langchain/core -npm install - +npm install @langchain/core --production +npm install --production diff --git a/docs/core_docs/.gitignore b/docs/core_docs/.gitignore index 5b3735122408..3947053a76dd 100644 --- a/docs/core_docs/.gitignore +++ b/docs/core_docs/.gitignore @@ -138,6 +138,8 @@ docs/how_to/multimodal_inputs.md docs/how_to/multimodal_inputs.mdx docs/how_to/migrate_agent.md docs/how_to/migrate_agent.mdx +docs/how_to/message_history.md +docs/how_to/message_history.mdx docs/how_to/merge_message_runs.md docs/how_to/merge_message_runs.mdx docs/how_to/logprobs.md @@ -198,14 +200,14 @@ docs/how_to/character_text_splitter.md docs/how_to/character_text_splitter.mdx docs/how_to/cancel_execution.md docs/how_to/cancel_execution.mdx +docs/how_to/callbacks_serverless.md +docs/how_to/callbacks_serverless.mdx docs/how_to/callbacks_runtime.md docs/how_to/callbacks_runtime.mdx docs/how_to/callbacks_custom_events.md docs/how_to/callbacks_custom_events.mdx docs/how_to/callbacks_constructor.md docs/how_to/callbacks_constructor.mdx -docs/how_to/callbacks_backgrounding.md -docs/how_to/callbacks_backgrounding.mdx docs/how_to/callbacks_attach.md docs/how_to/callbacks_attach.mdx docs/how_to/binding.md @@ -214,6 +216,14 @@ docs/how_to/assign.md docs/how_to/assign.mdx docs/how_to/agent_executor.md docs/how_to/agent_executor.mdx +docs/versions/migrating_memory/conversation_summary_memory.md +docs/versions/migrating_memory/conversation_summary_memory.mdx +docs/versions/migrating_memory/conversation_buffer_window_memory.md +docs/versions/migrating_memory/conversation_buffer_window_memory.mdx +docs/versions/migrating_memory/chat_history.md +docs/versions/migrating_memory/chat_history.mdx +docs/troubleshooting/errors/INVALID_TOOL_RESULTS.md +docs/troubleshooting/errors/INVALID_TOOL_RESULTS.mdx docs/integrations/vectorstores/weaviate.md docs/integrations/vectorstores/weaviate.mdx docs/integrations/vectorstores/upstash.md @@ -278,14 +288,6 @@ docs/integrations/text_embedding/bedrock.md docs/integrations/text_embedding/bedrock.mdx docs/integrations/text_embedding/azure_openai.md docs/integrations/text_embedding/azure_openai.mdx -docs/integrations/retrievers/tavily.md -docs/integrations/retrievers/tavily.mdx -docs/integrations/retrievers/kendra-retriever.md -docs/integrations/retrievers/kendra-retriever.mdx -docs/integrations/retrievers/exa.md -docs/integrations/retrievers/exa.mdx -docs/integrations/retrievers/bedrock-knowledge-bases.md -docs/integrations/retrievers/bedrock-knowledge-bases.mdx docs/integrations/llms/together.md docs/integrations/llms/together.mdx docs/integrations/llms/openai.md @@ -304,10 +306,36 @@ docs/integrations/llms/cloudflare_workersai.md docs/integrations/llms/cloudflare_workersai.mdx docs/integrations/llms/bedrock.md docs/integrations/llms/bedrock.mdx -docs/integrations/llms/arcjet.md -docs/integrations/llms/arcjet.mdx docs/integrations/llms/azure.md docs/integrations/llms/azure.mdx +docs/integrations/llms/arcjet.md +docs/integrations/llms/arcjet.mdx +docs/integrations/retrievers/tavily.md +docs/integrations/retrievers/tavily.mdx +docs/integrations/retrievers/kendra-retriever.md +docs/integrations/retrievers/kendra-retriever.mdx +docs/integrations/retrievers/exa.md +docs/integrations/retrievers/exa.mdx +docs/integrations/retrievers/bm25.md +docs/integrations/retrievers/bm25.mdx +docs/integrations/retrievers/bedrock-knowledge-bases.md +docs/integrations/retrievers/bedrock-knowledge-bases.mdx +docs/integrations/retrievers/self_query/weaviate.md +docs/integrations/retrievers/self_query/weaviate.mdx +docs/integrations/retrievers/self_query/vectara.md +docs/integrations/retrievers/self_query/vectara.mdx +docs/integrations/retrievers/self_query/supabase.md +docs/integrations/retrievers/self_query/supabase.mdx +docs/integrations/retrievers/self_query/qdrant.md +docs/integrations/retrievers/self_query/qdrant.mdx +docs/integrations/retrievers/self_query/pinecone.md +docs/integrations/retrievers/self_query/pinecone.mdx +docs/integrations/retrievers/self_query/memory.md +docs/integrations/retrievers/self_query/memory.mdx +docs/integrations/retrievers/self_query/hnswlib.md +docs/integrations/retrievers/self_query/hnswlib.mdx +docs/integrations/retrievers/self_query/chroma.md +docs/integrations/retrievers/self_query/chroma.mdx docs/integrations/chat/togetherai.md docs/integrations/chat/togetherai.mdx docs/integrations/chat/openai.md @@ -332,28 +360,12 @@ docs/integrations/chat/bedrock_converse.md docs/integrations/chat/bedrock_converse.mdx docs/integrations/chat/bedrock.md docs/integrations/chat/bedrock.mdx -docs/integrations/chat/arcjet.md -docs/integrations/chat/arcjet.mdx docs/integrations/chat/azure.md docs/integrations/chat/azure.mdx +docs/integrations/chat/arcjet.md +docs/integrations/chat/arcjet.mdx docs/integrations/chat/anthropic.md docs/integrations/chat/anthropic.mdx -docs/integrations/retrievers/self_query/weaviate.md -docs/integrations/retrievers/self_query/weaviate.mdx -docs/integrations/retrievers/self_query/vectara.md -docs/integrations/retrievers/self_query/vectara.mdx -docs/integrations/retrievers/self_query/supabase.md -docs/integrations/retrievers/self_query/supabase.mdx -docs/integrations/retrievers/self_query/qdrant.md -docs/integrations/retrievers/self_query/qdrant.mdx -docs/integrations/retrievers/self_query/pinecone.md -docs/integrations/retrievers/self_query/pinecone.mdx -docs/integrations/retrievers/self_query/memory.md -docs/integrations/retrievers/self_query/memory.mdx -docs/integrations/retrievers/self_query/hnswlib.md -docs/integrations/retrievers/self_query/hnswlib.mdx -docs/integrations/retrievers/self_query/chroma.md -docs/integrations/retrievers/self_query/chroma.mdx docs/integrations/document_loaders/web_loaders/web_puppeteer.md docs/integrations/document_loaders/web_loaders/web_puppeteer.mdx docs/integrations/document_loaders/web_loaders/web_cheerio.md @@ -375,4 +387,4 @@ docs/integrations/document_loaders/file_loaders/pdf.mdx docs/integrations/document_loaders/file_loaders/directory.md docs/integrations/document_loaders/file_loaders/directory.mdx docs/integrations/document_loaders/file_loaders/csv.md -docs/integrations/document_loaders/file_loaders/csv.mdx +docs/integrations/document_loaders/file_loaders/csv.mdx \ No newline at end of file diff --git a/docs/core_docs/docs/additional_resources/tutorials.mdx b/docs/core_docs/docs/additional_resources/tutorials.mdx index 9fbae9512796..f24629ad8fdc 100644 --- a/docs/core_docs/docs/additional_resources/tutorials.mdx +++ b/docs/core_docs/docs/additional_resources/tutorials.mdx @@ -1,6 +1,6 @@ -# Tutorials +# External guides -Below are links to tutorials and courses on LangChain.js. For written guides on common use cases for LangChain.js, check out the [tutorials](/docs/tutorials/) and [how to](/docs/how_to/) sections. +Below are links to external tutorials and courses on LangChain.js. For other written guides on common use cases for LangChain.js, check out the [tutorials](/docs/tutorials/) and [how to](/docs/how_to/) sections. --- diff --git a/docs/core_docs/docs/concepts.mdx b/docs/core_docs/docs/concepts.mdx index cc0282464339..e058ba7f5a7c 100644 --- a/docs/core_docs/docs/concepts.mdx +++ b/docs/core_docs/docs/concepts.mdx @@ -657,7 +657,7 @@ const tools = toolkit.getTools() By themselves, language models can't take actions - they just output text. A big use case for LangChain is creating **agents**. -Agents are systems that use an LLM as a reasoning enginer to determine which actions to take and what the inputs to those actions should be. +Agents are systems that use an LLM as a reasoning engineer to determine which actions to take and what the inputs to those actions should be. The results of those actions can then be fed back into the agent and it determine whether more actions are needed, or whether it is okay to finish. [LangGraph](https://github.com/langchain-ai/langgraphjs) is an extension of LangChain specifically aimed at creating highly controllable and customizable agents. diff --git a/docs/core_docs/docs/contributing/integrations.mdx b/docs/core_docs/docs/contributing/integrations.mdx index 5012a2362945..61acf1ca4f03 100644 --- a/docs/core_docs/docs/contributing/integrations.mdx +++ b/docs/core_docs/docs/contributing/integrations.mdx @@ -96,8 +96,12 @@ For information on running and implementing tests, see the [Testing guide](/docs ### Write documentation -Documentation is generated from Jupyter notebooks or `.mdx` files in the `docs/` directory. You should place the notebooks with examples -to the relevant `docs/core_docs/docs/integrations` directory in the monorepo root. +Please copy and use the appropriate template from here: + +https://github.com/langchain-ai/langchainjs/tree/main/libs/langchain-scripts/src/cli/docs/templates + +You should place the notebooks with examples +in the relevant `docs/core_docs/docs/integrations` directory in the monorepo root. ### (If Necessary) Deprecate community integration diff --git a/docs/core_docs/docs/how_to/agent_executor.ipynb b/docs/core_docs/docs/how_to/agent_executor.ipynb index f37846a3cdba..1bf617ebec21 100644 --- a/docs/core_docs/docs/how_to/agent_executor.ipynb +++ b/docs/core_docs/docs/how_to/agent_executor.ipynb @@ -103,7 +103,7 @@ { "data": { "text/plain": [ - "\u001b[32m`[{\"title\":\"Weather in San Francisco\",\"url\":\"https://www.weatherapi.com/\",\"content\":\"{'location': {'n`\u001b[39m... 1347 more characters" + "\u001b[32m`[{\"title\":\"Weather in San Francisco\",\"url\":\"https://www.weatherapi.com/\",\"content\":\"{'location': {'n`\u001b[39m... 1358 more characters" ] }, "execution_count": 1, @@ -146,7 +146,8 @@ " metadata: {\n", " source: \u001b[32m\"https://docs.smith.langchain.com/overview\"\u001b[39m,\n", " loc: { lines: { from: \u001b[33m4\u001b[39m, to: \u001b[33m4\u001b[39m } }\n", - " }\n", + " },\n", + " id: \u001b[90mundefined\u001b[39m\n", "}" ] }, @@ -186,18 +187,25 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 7, "id": "7280b031", "metadata": {}, "outputs": [], "source": [ - "import { createRetrieverTool } from \"langchain/tools/retriever\";\n", + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", "\n", - "const retrieverTool = await createRetrieverTool(retriever, {\n", - " name: \"langsmith_search\",\n", - " description:\n", - " \"Search for information about LangSmith. For any questions about LangSmith, you must use this tool!\",\n", - " });" + "const retrieverTool = tool(async ({ input }, config) => {\n", + " const docs = await retriever.invoke(input, config);\n", + " return docs.map((doc) => doc.pageContent).join(\"\\n\\n\");\n", + "}, {\n", + " name: \"langsmith_search\",\n", + " description:\n", + " \"Search for information about LangSmith. For any questions about LangSmith, you must use this tool!\",\n", + " schema: z.object({\n", + " input: z.string()\n", + " }),\n", + "});" ] }, { @@ -212,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 8, "id": "b8e8e710", "metadata": {}, "outputs": [], @@ -246,7 +254,21 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 9, + "id": "def033a4", + "metadata": {}, + "outputs": [], + "source": [ + "// @lc-docs-hide-cell\n", + "\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const model = new ChatOpenAI({ model: \"gpt-4o-mini\", temperature: 0 })" + ] + }, + { + "cell_type": "code", + "execution_count": 10, "id": "c96c960b", "metadata": {}, "outputs": [ @@ -256,18 +278,16 @@ "\u001b[32m\"Hello! How can I assist you today?\"\u001b[39m" ] }, - "execution_count": 5, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import { ChatOpenAI } from \"@langchain/openai\";\n", - "const model = new ChatOpenAI({ model: \"gpt-4\", temperature: 0 })\n", - "\n", - "import { HumanMessage } from \"@langchain/core/messages\";\n", - "\n", - "const response = await model.invoke([new HumanMessage(\"hi!\")]);\n", + "const response = await model.invoke([{\n", + " role: \"user\",\n", + " content: \"hi!\"\n", + "}]);\n", "\n", "response.content;" ] @@ -282,7 +302,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 11, "id": "ba692a74", "metadata": {}, "outputs": [], @@ -300,7 +320,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 12, "id": "b6a7e925", "metadata": {}, "outputs": [ @@ -314,7 +334,10 @@ } ], "source": [ - "const responseWithTools = await modelWithTools.invoke([new HumanMessage(\"Hi!\")])\n", + "const responseWithTools = await modelWithTools.invoke([{\n", + " role: \"user\",\n", + " content: \"Hi!\"\n", + "}])\n", "\n", "console.log(`Content: ${responseWithTools.content}`)\n", "console.log(`Tool calls: ${responseWithTools.tool_calls}`)" @@ -330,7 +353,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 13, "id": "688b465d", "metadata": {}, "outputs": [ @@ -345,14 +368,18 @@ " \"args\": {\n", " \"input\": \"current weather in San Francisco\"\n", " },\n", - " \"id\": \"call_VcSjZAZkEOx9lcHNZNXAjXkm\"\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_gtJ5rrjXswO8EIvePrxyGQbR\"\n", " }\n", "]\n" ] } ], "source": [ - "const responseWithToolCalls = await modelWithTools.invoke([new HumanMessage(\"What's the weather in SF?\")])\n", + "const responseWithToolCalls = await modelWithTools.invoke([{\n", + " role: \"user\",\n", + " content: \"What's the weather in SF?\"\n", + "}])\n", "\n", "console.log(`Content: ${responseWithToolCalls.content}`)\n", "console.log(`Tool calls: ${JSON.stringify(responseWithToolCalls.tool_calls, null, 2)}`)" @@ -382,7 +409,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 14, "id": "af83d3e3", "metadata": {}, "outputs": [ @@ -409,7 +436,8 @@ " partialVariables: undefined,\n", " templateFormat: \"f-string\",\n", " template: \"You are a helpful assistant\",\n", - " validateTemplate: true\n", + " validateTemplate: true,\n", + " additionalContentFields: undefined\n", " }\n", " },\n", " lc_runnable: true,\n", @@ -432,7 +460,8 @@ " partialVariables: undefined,\n", " templateFormat: \"f-string\",\n", " template: \"You are a helpful assistant\",\n", - " validateTemplate: true\n", + " validateTemplate: true,\n", + " additionalContentFields: undefined\n", " },\n", " messageClass: undefined,\n", " chatMessageClass: undefined\n", @@ -464,7 +493,8 @@ " partialVariables: undefined,\n", " templateFormat: \"f-string\",\n", " template: \"{input}\",\n", - " validateTemplate: true\n", + " validateTemplate: true,\n", + " additionalContentFields: undefined\n", " }\n", " },\n", " lc_runnable: true,\n", @@ -487,7 +517,8 @@ " partialVariables: undefined,\n", " templateFormat: \"f-string\",\n", " template: \"{input}\",\n", - " validateTemplate: true\n", + " validateTemplate: true,\n", + " additionalContentFields: undefined\n", " },\n", " messageClass: undefined,\n", " chatMessageClass: undefined\n", @@ -530,7 +561,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 15, "id": "89cf72b4-6046-4b47-8f27-5522d8cb8036", "metadata": {}, "outputs": [], @@ -550,7 +581,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 16, "id": "ce33904a", "metadata": {}, "outputs": [], @@ -577,7 +608,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 17, "id": "114ba50d", "metadata": {}, "outputs": [ @@ -587,7 +618,7 @@ "{ input: \u001b[32m\"hi!\"\u001b[39m, output: \u001b[32m\"Hello! How can I assist you today?\"\u001b[39m }" ] }, - "execution_count": 12, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -608,7 +639,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 18, "id": "3fa4780a", "metadata": { "scrolled": true @@ -619,13 +650,11 @@ "text/plain": [ "{\n", " input: \u001b[32m\"how can langsmith help with testing?\"\u001b[39m,\n", - " output: \u001b[32m\"LangSmith can be a valuable tool for testing in several ways:\\n\"\u001b[39m +\n", - " \u001b[32m\"\\n\"\u001b[39m +\n", - " \u001b[32m\"1. **Logging Traces**: LangSmith prov\"\u001b[39m... 960 more characters\n", + " output: \u001b[32m\"LangSmith can assist with testing in several ways, particularly for applications built using large l\"\u001b[39m... 1474 more characters\n", "}" ] }, - "execution_count": 13, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -646,7 +675,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 19, "id": "77c2f769", "metadata": {}, "outputs": [ @@ -655,11 +684,14 @@ "text/plain": [ "{\n", " input: \u001b[32m\"whats the weather in sf?\"\u001b[39m,\n", - " output: \u001b[32m\"The current weather in San Francisco, California is partly cloudy with a temperature of 12.2°C (54.0\"\u001b[39m... 176 more characters\n", + " output: \u001b[32m\"The current weather in San Francisco is as follows:\\n\"\u001b[39m +\n", + " \u001b[32m\"\\n\"\u001b[39m +\n", + " \u001b[32m\"- **Temperature**: 15.6°C (60.1°F)\\n\"\u001b[39m +\n", + " \u001b[32m\"- **Conditio\"\u001b[39m... 303 more characters\n", "}" ] }, - "execution_count": 14, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -690,7 +722,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 20, "id": "c4073e35", "metadata": {}, "outputs": [ @@ -704,7 +736,7 @@ "}" ] }, - "execution_count": 15, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -716,7 +748,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 21, "id": "550e0c6e", "metadata": {}, "outputs": [ @@ -725,58 +757,31 @@ "text/plain": [ "{\n", " chat_history: [\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"hi! my name is bob\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"hi! my name is bob\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Hello Bob! How can I assist you today?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Hello Bob! How can I assist you today?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " { role: \u001b[32m\"user\"\u001b[39m, content: \u001b[32m\"hi! my name is bob\"\u001b[39m },\n", + " {\n", + " role: \u001b[32m\"assistant\"\u001b[39m,\n", + " content: \u001b[32m\"Hello Bob! How can I assist you today?\"\u001b[39m\n", " }\n", " ],\n", " input: \u001b[32m\"what's my name?\"\u001b[39m,\n", - " output: \u001b[32m\"Your name is Bob. How can I assist you further?\"\u001b[39m\n", + " output: \u001b[32m\"Your name is Bob. How can I help you today, Bob?\"\u001b[39m\n", "}" ] }, - "execution_count": 16, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import { AIMessage, HumanMessage } from \"@langchain/core/messages\";\n", - "\n", "await agentExecutor.invoke(\n", - " {\n", - " chat_history: [\n", - " new HumanMessage(\"hi! my name is bob\"),\n", - " new AIMessage(\"Hello Bob! How can I assist you today?\"),\n", - " ],\n", - " input: \"what's my name?\",\n", - " }\n", + " {\n", + " chat_history: [\n", + " { role: \"user\", content: \"hi! my name is bob\" },\n", + " { role: \"assistant\", content: \"Hello Bob! How can I assist you today?\" },\n", + " ],\n", + " input: \"what's my name?\",\n", + " }\n", ")" ] }, @@ -797,7 +802,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 22, "id": "8edd96e6", "metadata": {}, "outputs": [ @@ -808,41 +813,23 @@ " input: \u001b[32m\"hi! I'm bob\"\u001b[39m,\n", " chat_history: [\n", " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"hi! I'm bob\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"hi! I'm bob\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", + " \"content\": \"hi! I'm bob\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Hello, Bob! How can I assist you today?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Hello, Bob! How can I assist you today?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"content\": \"Hello Bob! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", " }\n", " ],\n", - " output: \u001b[32m\"Hello, Bob! How can I assist you today?\"\u001b[39m\n", + " output: \u001b[32m\"Hello Bob! How can I assist you today?\"\u001b[39m\n", "}" ] }, - "execution_count": 17, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -855,10 +842,10 @@ "const store = {};\n", "\n", "function getMessageHistory(sessionId: string): BaseChatMessageHistory {\n", - " if (!(sessionId in store)) {\n", - " store[sessionId] = new ChatMessageHistory();\n", - " }\n", - " return store[sessionId];\n", + " if (!(sessionId in store)) {\n", + " store[sessionId] = new ChatMessageHistory();\n", + " }\n", + " return store[sessionId];\n", "}\n", "\n", "const agentWithChatHistory = new RunnableWithMessageHistory({\n", @@ -876,7 +863,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 23, "id": "ae627966", "metadata": {}, "outputs": [ @@ -887,71 +874,35 @@ " input: \u001b[32m\"what's my name?\"\u001b[39m,\n", " chat_history: [\n", " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"hi! I'm bob\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"hi! I'm bob\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", + " \"content\": \"hi! I'm bob\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Hello, Bob! How can I assist you today?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Hello, Bob! How can I assist you today?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"content\": \"Hello Bob! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", " },\n", " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"what's my name?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"what's my name?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", + " \"content\": \"what's my name?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Your name is Bob. How can I assist you further?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Your name is Bob. How can I assist you further?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"content\": \"Your name is Bob! How can I help you today, Bob?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", " }\n", " ],\n", - " output: \u001b[32m\"Your name is Bob. How can I assist you further?\"\u001b[39m\n", + " output: \u001b[32m\"Your name is Bob! How can I help you today, Bob?\"\u001b[39m\n", "}" ] }, - "execution_count": 18, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } diff --git a/docs/core_docs/docs/how_to/chatbots_memory.ipynb b/docs/core_docs/docs/how_to/chatbots_memory.ipynb index e41b3f1bb465..197a11eab4c5 100644 --- a/docs/core_docs/docs/how_to/chatbots_memory.ipynb +++ b/docs/core_docs/docs/how_to/chatbots_memory.ipynb @@ -1,18 +1,19 @@ { "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 1\n", + "---" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# How to manage memory\n", - "\n", - ":::info Prerequisites\n", - "\n", - "This guide assumes familiarity with the following:\n", - "\n", - "- [Chatbots](/docs/tutorials/chatbot)\n", - "\n", - ":::\n", + "# How to add memory to chatbots\n", "\n", "A key feature of chatbots is their ability to use content of previous conversation turns as context. This state management can take several forms, including:\n", "\n", @@ -20,18 +21,29 @@ "- The above, but trimming old messages to reduce the amount of distracting information the model has to deal with.\n", "- More complex modifications like synthesizing summaries for long running conversations.\n", "\n", - "We’ll go into more detail on a few techniques below!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ + "We'll go into more detail on a few techniques below!\n", + "\n", + ":::note\n", + "\n", + "This how-to guide previously built a chatbot using [RunnableWithMessageHistory](https://v03.api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html). You can access this version of the tutorial in the [v0.2 docs](https://js.langchain.com/v0.2/docs/how_to/chatbots_memory/).\n", + "\n", + "The LangGraph implementation offers a number of advantages over `RunnableWithMessageHistory`, including the ability to persist arbitrary components of an application's state (instead of only messages).\n", + "\n", + ":::\n", + "\n", "## Setup\n", "\n", - "You’ll need to install a few packages, and set any LLM API keys:\n", + "You'll need to install a few packages, select your chat model, and set its enviroment variable.\n", + "\n", + "```{=mdx}\n", + "import Npm2Yarn from \"@theme/Npm2Yarn\"\n", + "\n", + "\n", + " @langchain/core @langchain/langgraph\n", + "\n", + "```\n", "\n", - "Let’s also set up a chat model that we’ll use for the below examples:\n", + "Let's set up a chat model that we'll use for the below examples.\n", "\n", "```{=mdx}\n", "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", @@ -46,42 +58,53 @@ "source": [ "## Message passing\n", "\n", - "The simplest form of memory is simply passing chat history messages into a chain. Here’s an example:" + "The simplest form of memory is simply passing chat history messages into a chain. Here's an example:" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "// @lc-docs-hide-cell\n", + "\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const llm = new ChatOpenAI({ model: \"gpt-4o\" })" + ] + }, + { + "cell_type": "code", + "execution_count": 23, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m`I said \"J'adore la programmation,\" which means \"I love programming\" in French.`\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m`I said \"J'adore la programmation,\" which means \"I love programming\" in French.`\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m21\u001b[39m, promptTokens: \u001b[33m61\u001b[39m, totalTokens: \u001b[33m82\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABSxUXVIBitFRBh9MpasB5jeEHfCA\",\n", + " \"content\": \"I said \\\"J'adore la programmation,\\\" which means \\\"I love programming\\\" in French.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 18,\n", + " \"promptTokens\": 58,\n", + " \"totalTokens\": 76\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 58,\n", + " \"output_tokens\": 18,\n", + " \"total_tokens\": 76\n", + " }\n", + "}\n" + ] } ], "source": [ @@ -119,303 +142,191 @@ "We can see that by passing the previous conversation into a chain, it can use it as context to answer questions. This is the basic concept underpinning chatbot memory - the rest of the guide will demonstrate convenient techniques for passing or reformatting messages." ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Chat history\n", - "\n", - "It’s perfectly fine to store and pass messages directly as an array, but we can use LangChain’s built-in message history class to store and load messages as well. Instances of this class are responsible for storing and loading chat messages from persistent storage. LangChain integrates with many providers but for this demo we will use an ephemeral demo class.\n", - "\n", - "Here’s an example of the API:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Translate this sentence from English to French: I love programming.\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Translate this sentence from English to French: I love programming.\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"J'adore la programmation.\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"J'adore la programmation.\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " }\n", - "]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import { ChatMessageHistory } from \"langchain/stores/message/in_memory\";\n", - "\n", - "const demoEphemeralChatMessageHistory = new ChatMessageHistory();\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(\n", - " new HumanMessage(\n", - " \"Translate this sentence from English to French: I love programming.\"\n", - " )\n", - ");\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(\n", - " new AIMessage(\"J'adore la programmation.\")\n", - ");\n", - "\n", - "await demoEphemeralChatMessageHistory.getMessages();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can use it directly to store conversation turns for our chain:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m'You just asked me to translate the sentence \"I love programming\" from English to French.'\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m'You just asked me to translate the sentence \"I love programming\" from English to French.'\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m18\u001b[39m, promptTokens: \u001b[33m73\u001b[39m, totalTokens: \u001b[33m91\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await demoEphemeralChatMessageHistory.clear();\n", - "\n", - "const input1 =\n", - " \"Translate this sentence from English to French: I love programming.\";\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(new HumanMessage(input1));\n", - "\n", - "const response = await chain.invoke({\n", - " messages: await demoEphemeralChatMessageHistory.getMessages(),\n", - "});\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(response);\n", - "\n", - "const input2 = \"What did I just ask you?\";\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(new HumanMessage(input2));\n", - "\n", - "await chain.invoke({\n", - " messages: await demoEphemeralChatMessageHistory.getMessages(),\n", - "});" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Automatic history management\n", "\n", - "The previous examples pass messages to the chain explicitly. This is a completely acceptable approach, but it does require external management of new messages. LangChain also includes an wrapper for LCEL chains that can handle this process automatically called `RunnableWithMessageHistory`.\n", - "\n", - "To show how it works, let’s slightly modify the above prompt to take a final `input` variable that populates a `HumanMessage` template after the chat history. This means that we will expect a `chat_history` parameter that contains all messages BEFORE the current messages instead of all messages:" + "The previous examples pass messages to the chain (and model) explicitly. This is a completely acceptable approach, but it does require external management of new messages. LangChain also provides a way to build applications that have memory using LangGraph's persistence. You can enable persistence in LangGraph applications by providing a `checkpointer` when compiling the graph." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 24, "metadata": {}, "outputs": [], "source": [ - "const runnableWithMessageHistoryPrompt = ChatPromptTemplate.fromMessages([\n", - " [\n", - " \"system\",\n", - " \"You are a helpful assistant. Answer all questions to the best of your ability.\",\n", - " ],\n", - " new MessagesPlaceholder(\"chat_history\"),\n", - " [\"human\", \"{input}\"],\n", - "]);\n", + "import { START, END, MessagesAnnotation, StateGraph, MemorySaver } from \"@langchain/langgraph\";\n", "\n", - "const chain2 = runnableWithMessageHistoryPrompt.pipe(llm);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We’ll pass the latest input to the conversation here and let the `RunnableWithMessageHistory` class wrap our chain and do the work of appending that `input` variable to the chat history.\n", "\n", - "Next, let’s declare our wrapped chain:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "import { RunnableWithMessageHistory } from \"@langchain/core/runnables\";\n", + "// Define the function that calls the model\n", + "const callModel = async (state: typeof MessagesAnnotation.State) => {\n", + " const systemPrompt = \n", + " \"You are a helpful assistant. \" +\n", + " \"Answer all questions to the best of your ability.\";\n", + " const messages = [{ role: \"system\", content: systemPrompt }, ...state.messages];\n", + " const response = await llm.invoke(messages);\n", + " return { messages: response };\n", + "};\n", "\n", - "const demoEphemeralChatMessageHistoryForChain = new ChatMessageHistory();\n", + "const workflow = new StateGraph(MessagesAnnotation)\n", + "// Define the node and edge\n", + " .addNode(\"model\", callModel)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", "\n", - "const chainWithMessageHistory = new RunnableWithMessageHistory({\n", - " runnable: chain2,\n", - " getMessageHistory: (_sessionId) => demoEphemeralChatMessageHistoryForChain,\n", - " inputMessagesKey: \"input\",\n", - " historyMessagesKey: \"chat_history\",\n", - "});" + "// Add simple in-memory checkpointer\n", + "// highlight-start\n", + "const memory = new MemorySaver();\n", + "const app = workflow.compile({ checkpointer: memory });\n", + "// highlight-end" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This class takes a few parameters in addition to the chain that we want to wrap:\n", - "\n", - "- A factory function that returns a message history for a given session id. This allows your chain to handle multiple users at once by loading different messages for different conversations.\n", - "- An `inputMessagesKey` that specifies which part of the input should be tracked and stored in the chat history. In this example, we want to track the string passed in as input.\n", - "- A `historyMessagesKey` that specifies what the previous messages should be injected into the prompt as. Our prompt has a `MessagesPlaceholder` named `chat_history`, so we specify this property to match.\n", - " (For chains with multiple outputs) an `outputMessagesKey` which specifies which output to store as history. This is the inverse of `inputMessagesKey`.\n", - "\n", - "We can invoke this new chain as normal, with an additional `configurable` field that specifies the particular `sessionId` to pass to the factory function. This is unused for the demo, but in real-world chains, you’ll want to return a chat history corresponding to the passed session:" + " We'll pass the latest input to the conversation here and let the LangGraph keep track of the conversation history using the checkpointer:" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 25, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m`The translation of \"I love programming\" in French is \"J'adore la programmation.\"`\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m`The translation of \"I love programming\" in French is \"J'adore la programmation.\"`\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m20\u001b[39m, promptTokens: \u001b[33m39\u001b[39m, totalTokens: \u001b[33m59\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"227b82a9-4084-46a5-ac79-ab9a3faa140e\",\n", + " \"content\": \"Translate to French: I love programming.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABSxVrvztgnasTeMSFbpZQmyYqjJZ\",\n", + " \"content\": \"J'adore la programmation.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 5,\n", + " \"promptTokens\": 35,\n", + " \"totalTokens\": 40\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 35,\n", + " \"output_tokens\": 5,\n", + " \"total_tokens\": 40\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "await chainWithMessageHistory.invoke(\n", + "await app.invoke(\n", " {\n", - " input:\n", - " \"Translate this sentence from English to French: I love programming.\",\n", + " messages: [\n", + " {\n", + " role: \"user\",\n", + " content: \"Translate to French: I love programming.\"\n", + " }\n", + " ]\n", " },\n", - " { configurable: { sessionId: \"unused\" } }\n", + " {\n", + " configurable: { thread_id: \"1\" }\n", + " }\n", ");" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 26, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m'You just asked for the translation of the sentence \"I love programming\" from English to French.'\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m'You just asked for the translation of the sentence \"I love programming\" from English to French.'\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m19\u001b[39m, promptTokens: \u001b[33m74\u001b[39m, totalTokens: \u001b[33m93\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"1a0560a4-9dcb-47a1-b441-80717e229706\",\n", + " \"content\": \"Translate to French: I love programming.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABSxVrvztgnasTeMSFbpZQmyYqjJZ\",\n", + " \"content\": \"J'adore la programmation.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 5,\n", + " \"promptTokens\": 35,\n", + " \"totalTokens\": 40\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"4f233a7d-4b08-4f53-bb60-cf0141a59721\",\n", + " \"content\": \"What did I just ask you?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABSxVs5QnlPfbihTOmJrCVg1Dh7Ol\",\n", + " \"content\": \"You asked me to translate \\\"I love programming\\\" into French.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 13,\n", + " \"promptTokens\": 55,\n", + " \"totalTokens\": 68\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_9f2bfdaa89\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 55,\n", + " \"output_tokens\": 13,\n", + " \"total_tokens\": 68\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "await chainWithMessageHistory.invoke(\n", + "await app.invoke(\n", " {\n", - " input: \"What did I just ask you?\",\n", + " messages: [\n", + " {\n", + " role: \"user\",\n", + " content: \"What did I just ask you?\"\n", + " }\n", + " ]\n", " },\n", - " { configurable: { sessionId: \"unused\" } }\n", + " {\n", + " configurable: { thread_id: \"1\" }\n", + " }\n", ");" ] }, @@ -429,159 +340,98 @@ "\n", "### Trimming messages\n", "\n", - "LLMs and chat models have limited context windows, and even if you’re not directly hitting limits, you may want to limit the amount of distraction the model has to deal with. One solution is to only load and store the most recent `n` messages. Let’s use an example history with some preloaded messages:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Hey there! I'm Nemo.\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Hey there! I'm Nemo.\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Hello!\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Hello!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " },\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"How are you today?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"How are you today?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Fine thanks!\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Fine thanks!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " }\n", - "]" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await demoEphemeralChatMessageHistory.clear();\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(\n", - " new HumanMessage(\"Hey there! I'm Nemo.\")\n", - ");\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(new AIMessage(\"Hello!\"));\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(\n", - " new HumanMessage(\"How are you today?\")\n", - ");\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(new AIMessage(\"Fine thanks!\"));\n", - "\n", - "await demoEphemeralChatMessageHistory.getMessages();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let’s use this message history with the `RunnableWithMessageHistory` chain we declared above:" + "LLMs and chat models have limited context windows, and even if you're not directly hitting limits, you may want to limit the amount of distraction the model has to deal with. One solution is trim the history messages before passing them to the model. Let's use an example history with the `app` we declared above:" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 27, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Your name is Nemo!\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Your name is Nemo!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m6\u001b[39m, promptTokens: \u001b[33m66\u001b[39m, totalTokens: \u001b[33m72\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"63057c3d-f980-4640-97d6-497a9f83ddee\",\n", + " \"content\": \"Hey there! I'm Nemo.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"c9f0c20a-8f55-4909-b281-88f2a45c4f05\",\n", + " \"content\": \"Hello!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"fd7fb3a0-7bc7-4e84-99a9-731b30637b55\",\n", + " \"content\": \"How are you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"09b0debb-1d4a-4856-8821-b037f5d96ecf\",\n", + " \"content\": \"Fine thanks!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"edc13b69-25a0-40ac-81b3-175e65dc1a9a\",\n", + " \"content\": \"What's my name?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABSxWKCTdRuh2ZifXsvFHSo5z5I0J\",\n", + " \"content\": \"Your name is Nemo! How can I assist you today, Nemo?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 63,\n", + " \"totalTokens\": 77\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_a5d11b2ef2\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 63,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 77\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "const chainWithMessageHistory2 = new RunnableWithMessageHistory({\n", - " runnable: chain2,\n", - " getMessageHistory: (_sessionId) => demoEphemeralChatMessageHistory,\n", - " inputMessagesKey: \"input\",\n", - " historyMessagesKey: \"chat_history\",\n", - "});\n", + "const demoEphemeralChatHistory = [\n", + " { role: \"user\", content: \"Hey there! I'm Nemo.\" },\n", + " { role: \"assistant\", content: \"Hello!\" },\n", + " { role: \"user\", content: \"How are you today?\" },\n", + " { role: \"assistant\", content: \"Fine thanks!\" },\n", + "];\n", "\n", - "await chainWithMessageHistory2.invoke(\n", + "await app.invoke(\n", " {\n", - " input: \"What's my name?\",\n", + " messages: [\n", + " ...demoEphemeralChatHistory,\n", + " { role: \"user\", content: \"What's my name?\" }\n", + " ]\n", " },\n", - " { configurable: { sessionId: \"unused\" } }\n", + " {\n", + " configurable: { thread_id: \"2\" }\n", + " }\n", ");" ] }, @@ -589,551 +439,324 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can see the chain remembers the preloaded name.\n", + "We can see the app remembers the preloaded name.\n", "\n", - "But let’s say we have a very small context window, and we want to trim the number of messages passed to the chain to only the 2 most recent ones. We can use the `clear` method to remove messages and re-add them to the history. We don’t have to, but let’s put this method at the front of our chain to ensure it’s always called:" + "But let's say we have a very small context window, and we want to trim the number of messages passed to the model to only the 2 most recent ones. We can use the built in [trimMessages](/docs/how_to/trim_messages/) util to trim messages based on their token count before they reach our prompt. In this case we'll count each message as 1 \"token\" and keep only the last two messages:" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 28, "metadata": {}, "outputs": [], "source": [ - "import {\n", - " RunnablePassthrough,\n", - " RunnableSequence,\n", - "} from \"@langchain/core/runnables\";\n", - "\n", - "const trimMessages = async (_chainInput: Record) => {\n", - " const storedMessages = await demoEphemeralChatMessageHistory.getMessages();\n", - " if (storedMessages.length <= 2) {\n", - " return false;\n", - " }\n", - " await demoEphemeralChatMessageHistory.clear();\n", - " for (const message of storedMessages.slice(-2)) {\n", - " demoEphemeralChatMessageHistory.addMessage(message);\n", - " }\n", - " return true;\n", + "import { START, END, MessagesAnnotation, StateGraph, MemorySaver } from \"@langchain/langgraph\";\n", + "import { trimMessages } from \"@langchain/core/messages\";\n", + "\n", + "// Define trimmer\n", + "// highlight-start\n", + "// count each message as 1 \"token\" (tokenCounter: (msgs) => msgs.length) and keep only the last two messages\n", + "const trimmer = trimMessages({ strategy: \"last\", maxTokens: 2, tokenCounter: (msgs) => msgs.length });\n", + "// highlight-end\n", + "\n", + "// Define the function that calls the model\n", + "const callModel2 = async (state: typeof MessagesAnnotation.State) => {\n", + " // highlight-start\n", + " const trimmedMessages = await trimmer.invoke(state.messages);\n", + " const systemPrompt = \n", + " \"You are a helpful assistant. \" +\n", + " \"Answer all questions to the best of your ability.\";\n", + " const messages = [{ role: \"system\", content: systemPrompt }, ...trimmedMessages];\n", + " // highlight-end\n", + " const response = await llm.invoke(messages);\n", + " return { messages: response };\n", "};\n", "\n", - "const chainWithTrimming = RunnableSequence.from([\n", - " RunnablePassthrough.assign({ messages_trimmed: trimMessages }),\n", - " chainWithMessageHistory2,\n", - "]);" + "const workflow2 = new StateGraph(MessagesAnnotation)\n", + " // Define the node and edge\n", + " .addNode(\"model\", callModel2)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "// Add simple in-memory checkpointer\n", + "const app2 = workflow2.compile({ checkpointer: new MemorySaver() });" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let’s call this new chain and check the messages afterwards:" + "Let's call this new app and check the response" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 29, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m'P. Sherman is a fictional character who lives at 42 Wallaby Way, Sydney, from the movie \"Finding Nem'\u001b[39m... 3 more characters,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m'P. Sherman is a fictional character who lives at 42 Wallaby Way, Sydney, from the movie \"Finding Nem'\u001b[39m... 3 more characters,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m26\u001b[39m, promptTokens: \u001b[33m53\u001b[39m, totalTokens: \u001b[33m79\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"0d9330a0-d9d1-4aaf-8171-ca1ac6344f7c\",\n", + " \"content\": \"What is my name?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"3a24e88b-7525-4797-9fcd-d751a378d22c\",\n", + " \"content\": \"Fine thanks!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"276039c8-eba8-4c68-b015-81ec7704140d\",\n", + " \"content\": \"How are you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"2ad4f461-20e1-4982-ba3b-235cb6b02abd\",\n", + " \"content\": \"Hello!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"52213cae-953a-463d-a4a0-a7368c9ee4db\",\n", + " \"content\": \"Hey there! I'm Nemo.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABSxWe9BRDl1pmzkNIDawWwU3hvKm\",\n", + " \"content\": \"I'm sorry, but I don't have access to personal information about you unless you've shared it with me during our conversation. How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 30,\n", + " \"promptTokens\": 39,\n", + " \"totalTokens\": 69\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_3537616b13\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 39,\n", + " \"output_tokens\": 30,\n", + " \"total_tokens\": 69\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "await chainWithTrimming.invoke(\n", + "await app2.invoke(\n", " {\n", - " input: \"Where does P. Sherman live?\",\n", + " messages: [\n", + " ...demoEphemeralChatHistory,\n", + " { role: \"user\", content: \"What is my name?\" }\n", + " ]\n", " },\n", - " { configurable: { sessionId: \"unused\" } }\n", - ");" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"What's my name?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"What's my name?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Your name is Nemo!\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Your name is Nemo!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m6\u001b[39m, promptTokens: \u001b[33m66\u001b[39m, totalTokens: \u001b[33m72\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " },\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Where does P. Sherman live?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Where does P. Sherman live?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m'P. Sherman is a fictional character who lives at 42 Wallaby Way, Sydney, from the movie \"Finding Nem'\u001b[39m... 3 more characters,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m'P. Sherman is a fictional character who lives at 42 Wallaby Way, Sydney, from the movie \"Finding Nem'\u001b[39m... 3 more characters,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m26\u001b[39m, promptTokens: \u001b[33m53\u001b[39m, totalTokens: \u001b[33m79\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " }\n", - "]" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await demoEphemeralChatMessageHistory.getMessages();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "And we can see that our history has removed the two oldest messages while still adding the most recent conversation at the end. The next time the chain is called, `trimMessages` will be called again, and only the two most recent messages will be passed to the model. In this case, this means that the model will forget the name we gave it the next time we invoke it:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"I'm sorry, I don't have access to your personal information. Can I help you with anything else?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"I'm sorry, I don't have access to your personal information. Can I help you with anything else?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m22\u001b[39m, promptTokens: \u001b[33m73\u001b[39m, totalTokens: \u001b[33m95\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await chainWithTrimming.invoke(\n", " {\n", - " input: \"What is my name?\",\n", - " },\n", - " { configurable: { sessionId: \"unused\" } }\n", + " configurable: { thread_id: \"3\" }\n", + " }\n", ");" ] }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Where does P. Sherman live?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Where does P. Sherman live?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m'P. Sherman is a fictional character who lives at 42 Wallaby Way, Sydney, from the movie \"Finding Nem'\u001b[39m... 3 more characters,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m'P. Sherman is a fictional character who lives at 42 Wallaby Way, Sydney, from the movie \"Finding Nem'\u001b[39m... 3 more characters,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m26\u001b[39m, promptTokens: \u001b[33m53\u001b[39m, totalTokens: \u001b[33m79\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " },\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"What is my name?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"What is my name?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"I'm sorry, I don't have access to your personal information. Can I help you with anything else?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"I'm sorry, I don't have access to your personal information. Can I help you with anything else?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m22\u001b[39m, promptTokens: \u001b[33m73\u001b[39m, totalTokens: \u001b[33m95\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " }\n", - "]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await demoEphemeralChatMessageHistory.getMessages();" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Summary memory\n", + "We can see that `trimMessages` was called and only the two most recent messages will be passed to the model. In this case, this means that the model forgot the name we gave it.\n", "\n", - "We can use this same pattern in other ways too. For example, we could use an additional LLM call to generate a summary of the conversation before calling our chain. Let’s recreate our chat history and chatbot chain:" + "Check out our [how to guide on trimming messages](/docs/how_to/trim_messages/) for more." ] }, { - "cell_type": "code", - "execution_count": 17, + "cell_type": "markdown", "metadata": {}, - "outputs": [], "source": [ - "await demoEphemeralChatMessageHistory.clear();\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(\n", - " new HumanMessage(\"Hey there! I'm Nemo.\")\n", - ");\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(new AIMessage(\"Hello!\"));\n", - "\n", - "await demoEphemeralChatMessageHistory.addMessage(\n", - " new HumanMessage(\"How are you today?\")\n", - ");\n", + "### Summary memory\n", "\n", - "await demoEphemeralChatMessageHistory.addMessage(new AIMessage(\"Fine thanks!\"));" + "We can use this same pattern in other ways too. For example, we could use an additional LLM call to generate a summary of the conversation before calling our app. Let's recreate our chat history:" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 30, "metadata": {}, "outputs": [], "source": [ - "const runnableWithSummaryMemoryPrompt = ChatPromptTemplate.fromMessages([\n", - " [\n", - " \"system\",\n", - " \"You are a helpful assistant. Answer all questions to the best of your ability. The provided chat history includes facts about the user you are speaking with.\",\n", - " ],\n", - " new MessagesPlaceholder(\"chat_history\"),\n", - " [\"human\", \"{input}\"],\n", - "]);\n", - "\n", - "const summaryMemoryChain = runnableWithSummaryMemoryPrompt.pipe(llm);\n", - "\n", - "const chainWithMessageHistory3 = new RunnableWithMessageHistory({\n", - " runnable: summaryMemoryChain,\n", - " getMessageHistory: (_sessionId) => demoEphemeralChatMessageHistory,\n", - " inputMessagesKey: \"input\",\n", - " historyMessagesKey: \"chat_history\",\n", - "});" + "const demoEphemeralChatHistory2 = [\n", + " { role: \"user\", content: \"Hey there! I'm Nemo.\" },\n", + " { role: \"assistant\", content: \"Hello!\" },\n", + " { role: \"user\", content: \"How are you today?\" },\n", + " { role: \"assistant\", content: \"Fine thanks!\" },\n", + "];" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "And now, let’s create a function that will distill previous interactions into a summary. We can add this one to the front of the chain too:" + "And now, let's update the model-calling function to distill previous interactions into a summary:" ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 31, "metadata": {}, "outputs": [], "source": [ - "const summarizeMessages = async (_chainInput: Record) => {\n", - " const storedMessages = await demoEphemeralChatMessageHistory.getMessages();\n", - " if (storedMessages.length === 0) {\n", - " return false;\n", + "import { START, END, MessagesAnnotation, StateGraph, MemorySaver } from \"@langchain/langgraph\";\n", + "import { RemoveMessage } from \"@langchain/core/messages\";\n", + "\n", + "\n", + "// Define the function that calls the model\n", + "const callModel3 = async (state: typeof MessagesAnnotation.State) => {\n", + " const systemPrompt = \n", + " \"You are a helpful assistant. \" +\n", + " \"Answer all questions to the best of your ability. \" +\n", + " \"The provided chat history includes a summary of the earlier conversation.\";\n", + " const systemMessage = { role: \"system\", content: systemPrompt };\n", + " const messageHistory = state.messages.slice(0, -1); // exclude the most recent user input\n", + " \n", + " // Summarize the messages if the chat history reaches a certain size\n", + " if (messageHistory.length >= 4) {\n", + " const lastHumanMessage = state.messages[state.messages.length - 1];\n", + " // Invoke the model to generate conversation summary\n", + " const summaryPrompt = \n", + " \"Distill the above chat messages into a single summary message. \" +\n", + " \"Include as many specific details as you can.\";\n", + " const summaryMessage = await llm.invoke([\n", + " ...messageHistory,\n", + " { role: \"user\", content: summaryPrompt }\n", + " ]);\n", + "\n", + " // Delete messages that we no longer want to show up\n", + " const deleteMessages = state.messages.map(m => new RemoveMessage({ id: m.id }));\n", + " // Re-add user message\n", + " const humanMessage = { role: \"user\", content: lastHumanMessage.content };\n", + " // Call the model with summary & response\n", + " const response = await llm.invoke([systemMessage, summaryMessage, humanMessage]);\n", + " return { messages: [summaryMessage, humanMessage, response, ...deleteMessages] };\n", + " } else {\n", + " const response = await llm.invoke([systemMessage, ...state.messages]);\n", + " return { messages: response };\n", " }\n", - " const summarizationPrompt = ChatPromptTemplate.fromMessages([\n", - " new MessagesPlaceholder(\"chat_history\"),\n", - " [\n", - " \"user\",\n", - " \"Distill the above chat messages into a single summary message. Include as many specific details as you can.\",\n", - " ],\n", - " ]);\n", - " const summarizationChain = summarizationPrompt.pipe(llm);\n", - " const summaryMessage = await summarizationChain.invoke({\n", - " chat_history: storedMessages,\n", - " });\n", - " await demoEphemeralChatMessageHistory.clear();\n", - " demoEphemeralChatMessageHistory.addMessage(summaryMessage);\n", - " return true;\n", "};\n", "\n", - "const chainWithSummarization = RunnableSequence.from([\n", - " RunnablePassthrough.assign({\n", - " messages_summarized: summarizeMessages,\n", - " }),\n", - " chainWithMessageHistory3,\n", - "]);" + "const workflow3 = new StateGraph(MessagesAnnotation)\n", + " // Define the node and edge\n", + " .addNode(\"model\", callModel3)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "// Add simple in-memory checkpointer\n", + "const app3 = workflow3.compile({ checkpointer: new MemorySaver() });" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let’s see if it remembers the name we gave it:" + "Let's see if it remembers the name we gave it:" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 32, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m'You introduced yourself as \"Nemo.\"'\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m'You introduced yourself as \"Nemo.\"'\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m8\u001b[39m, promptTokens: \u001b[33m87\u001b[39m, totalTokens: \u001b[33m95\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABSxXjFDj6WRo7VLSneBtlAxUumPE\",\n", + " \"content\": \"Nemo greeted the assistant and asked how it was doing, to which the assistant responded that it was fine.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 22,\n", + " \"promptTokens\": 60,\n", + " \"totalTokens\": 82\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 60,\n", + " \"output_tokens\": 22,\n", + " \"total_tokens\": 82\n", + " }\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"8b1309b7-c09e-47fb-9ab3-34047f6973e3\",\n", + " \"content\": \"What did I say my name was?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABSxYAQKiBsQ6oVypO4CLFDsi1HRH\",\n", + " \"content\": \"You mentioned that your name is Nemo.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 8,\n", + " \"promptTokens\": 73,\n", + " \"totalTokens\": 81\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 73,\n", + " \"output_tokens\": 8,\n", + " \"total_tokens\": 81\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "await chainWithSummarization.invoke(\n", + "await app3.invoke(\n", " {\n", - " input: \"What did I say my name was?\",\n", + " messages: [\n", + " ...demoEphemeralChatHistory2,\n", + " { role: \"user\", content: \"What did I say my name was?\" }\n", + " ]\n", " },\n", " {\n", - " configurable: { sessionId: \"unused\" },\n", + " configurable: { thread_id: \"4\" }\n", " }\n", ");" ] }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"The conversation consists of a greeting from someone named Nemo and a general inquiry about their we\"\u001b[39m... 86 more characters,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"The conversation consists of a greeting from someone named Nemo and a general inquiry about their we\"\u001b[39m... 86 more characters,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m34\u001b[39m, promptTokens: \u001b[33m62\u001b[39m, totalTokens: \u001b[33m96\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " },\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"What did I say my name was?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"What did I say my name was?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m'You introduced yourself as \"Nemo.\"'\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m'You introduced yourself as \"Nemo.\"'\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m8\u001b[39m, promptTokens: \u001b[33m87\u001b[39m, totalTokens: \u001b[33m95\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - " }\n", - "]" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await demoEphemeralChatMessageHistory.getMessages();" - ] - }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that invoking the chain again will generate another summary generated from the initial summary plus new messages and so on. You could also design a hybrid approach where a certain number of messages are retained in chat history while others are summarized.\n", - "\n", - "## Next steps\n", - "\n", - "You've now learned how to manage memory in your chatbots\n", - "\n", - "Next, check out some of the other guides in this section, such as [how to add retrieval to your chatbot](/docs/how_to/chatbots_retrieval)." + "Note that invoking the app again will keep accumulating the history until it reaches the specified number of messages (four in our case). At that point we will generate another summary generated from the initial summary plus new messages and so on." ] } ], @@ -1144,14 +767,17 @@ "name": "deno" }, "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, "file_extension": ".ts", - "mimetype": "text/x.typescript", + "mimetype": "text/typescript", "name": "typescript", - "nb_converter": "script", - "pygments_lexer": "typescript", - "version": "5.3.3" + "version": "3.7.2" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/docs/core_docs/docs/how_to/chatbots_retrieval.ipynb b/docs/core_docs/docs/how_to/chatbots_retrieval.ipynb index a3af18fed5b4..eed68bdb0bc3 100644 --- a/docs/core_docs/docs/how_to/chatbots_retrieval.ipynb +++ b/docs/core_docs/docs/how_to/chatbots_retrieval.ipynb @@ -45,6 +45,7 @@ "outputs": [], "source": [ "// @lc-docs-hide-cell\n", + "\n", "import { ChatOpenAI } from \"@langchain/openai\";\n", "\n", "const llm = new ChatOpenAI({\n", diff --git a/docs/core_docs/docs/how_to/chatbots_tools.ipynb b/docs/core_docs/docs/how_to/chatbots_tools.ipynb index b5a3b2d0cdd1..d9f8ff25e52f 100644 --- a/docs/core_docs/docs/how_to/chatbots_tools.ipynb +++ b/docs/core_docs/docs/how_to/chatbots_tools.ipynb @@ -4,67 +4,107 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# How to use tools\n", + "# How to add tools to chatbots\n", "\n", ":::info Prerequisites\n", "\n", "This guide assumes familiarity with the following concepts:\n", "\n", "- [Chatbots](/docs/concepts/#messages)\n", - "- [Agents](https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/)\n", + "- [Agents](https://langchain-ai.github.io/langgraphjs/tutorials/multi_agent/agent_supervisor/)\n", "- [Chat history](/docs/concepts/#chat-history)\n", "\n", ":::\n", "\n", "This section will cover how to create conversational agents: chatbots that can interact with other systems and APIs using tools.\n", "\n", - "## Setup\n", + ":::note\n", + "\n", + "This how-to guide previously built a chatbot using [RunnableWithMessageHistory](https://api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html). You can access this version of the tutorial in the [v0.2 docs](https://js.langchain.com/v0.2/docs/how_to/chatbots_tools/).\n", + "\n", + "The LangGraph implementation offers a number of advantages over `RunnableWithMessageHistory`, including the ability to persist arbitrary components of an application's state (instead of only messages).\n", + "\n", + ":::\n", "\n", - "For this guide, we’ll be using an [tool calling agent](/docs/how_to/agent_executor) with a single tool for searching the web. The default will be powered by [Tavily](/docs/integrations/tools/tavily_search), but you can switch it out for any similar tool. The rest of this section will assume you’re using Tavily.\n", + "## Setup\n", "\n", - "You’ll need to [sign up for an account on the Tavily website](https://tavily.com), and install the following packages:\n", + "For this guide, we'll be using a [tool calling agent](https://langchain-ai.github.io/langgraphjs/concepts/agentic_concepts/#tool-calling-agent) with a single tool for searching the web. The default will be powered by [Tavily](/docs/integrations/tools/tavily_search), but you can switch it out for any similar tool. The rest of this section will assume you're using Tavily.\n", "\n", + "You'll need to [sign up for an account](https://tavily.com/) on the Tavily website, and install the following packages:\n", "\n", "```{=mdx}\n", "import Npm2Yarn from \"@theme/Npm2Yarn\";\n", "\n", "\n", - " @langchain/openai langchain @langchain/core\n", + " @langchain/core @langchain/langgraph @langchain/community\n", "\n", + "```\n", + "\n", + "Let’s also set up a chat model that we’ll use for the below examples.\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```\n", + "\n", + "```typescript\n", + "process.env.TAVILY_API_KEY = \"YOUR_API_KEY\";\n", "```" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Creating an agent\n", + "\n", + "Our end goal is to create an agent that can respond conversationally to user questions while looking up information as needed.\n", + "\n", + "First, let's initialize Tavily and an OpenAI chat model capable of tool calling:" + ] + }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ - "import { TavilySearchResults } from \"@langchain/community/tools/tavily_search\";\n", + "// @lc-docs-hide-cell\n", + "\n", "import { ChatOpenAI } from \"@langchain/openai\";\n", "\n", + "const llm = new ChatOpenAI({\n", + " model: \"gpt-4o\",\n", + " temperature: 0,\n", + "});" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import { TavilySearchResults } from \"@langchain/community/tools/tavily_search\";\n", + "\n", "const tools = [\n", " new TavilySearchResults({\n", " maxResults: 1,\n", " }),\n", - "];\n", - "\n", - "const llm = new ChatOpenAI({\n", - " model: \"gpt-3.5-turbo-1106\",\n", - " temperature: 0,\n", - "});" + "];" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To make our agent conversational, we must also choose a prompt with a placeholder for our chat history. Here’s an example:\n" + "To make our agent conversational, we can also specify a prompt. Here's an example:" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -78,8 +118,6 @@ " \"system\",\n", " \"You are a helpful assistant. You may not need to use tools for every query - the user may just want to chat!\",\n", " ],\n", - " [\"placeholder\", \"{messages}\"],\n", - " [\"placeholder\", \"{agent_scratchpad}\"],\n", "]);" ] }, @@ -87,30 +125,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Great! Now let’s assemble our agent:\n", - "\n", - "```{=mdx}\n", - ":::tip\n", - "As of `langchain` version `0.2.8`, the `createOpenAIToolsAgent` function now supports [OpenAI-formatted tools](https://api.js.langchain.com/interfaces/langchain_core.language_models_base.ToolDefinition.html).\n", - ":::\n", - "```\n" + "Great! Now let's assemble our agent using LangGraph's prebuilt [createReactAgent](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html), which allows you to create a [tool-calling agent](https://langchain-ai.github.io/langgraphjs/concepts/agentic_concepts/#tool-calling-agent):" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "import { AgentExecutor, createToolCallingAgent } from \"langchain/agents\";\n", - "\n", - "const agent = await createToolCallingAgent({\n", - " llm,\n", - " tools,\n", - " prompt,\n", - "});\n", + "import { createReactAgent } from \"@langchain/langgraph/prebuilt\"\n", "\n", - "const agentExecutor = new AgentExecutor({ agent, tools });" + "// messageModifier allows you to preprocess the inputs to the model inside ReAct agent\n", + "// in this case, since we're passing a prompt string, we'll just always add a SystemMessage\n", + "// with this prompt string before any other messages sent to the model\n", + "const agent = createReactAgent({ llm, tools, messageModifier: prompt })" ] }, { @@ -119,98 +148,108 @@ "source": [ "## Running the agent\n", "\n", - "Now that we’ve set up our agent, let’s try interacting with it! It can handle both trivial queries that require no lookup:\n" + "Now that we've set up our agent, let's try interacting with it! It can handle both trivial queries that require no lookup:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{\n", - " messages: [\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " }\n", - " ],\n", - " output: \u001b[32m\"Hi Nemo! It's great to meet you. How can I assist you today?\"\u001b[39m\n", - "}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"8c5fa465-e8d8-472a-9434-f574bf74537f\",\n", + " \"content\": \"I'm Nemo!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTKLLriRcZin65zLAMB3WUf9Sg1t\",\n", + " \"content\": \"How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 8,\n", + " \"promptTokens\": 93,\n", + " \"totalTokens\": 101\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_3537616b13\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 93,\n", + " \"output_tokens\": 8,\n", + " \"total_tokens\": 101\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "import { HumanMessage } from \"@langchain/core/messages\";\n", - "\n", - "await agentExecutor.invoke({\n", - " messages: [new HumanMessage(\"I'm Nemo!\")],\n", - "});" + "await agent.invoke({ messages: [{ role: \"user\", content: \"I'm Nemo!\" }]})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Or, it can use of the passed search tool to get up to date information if needed:\n" + "Or, it can use of the passed search tool to get up to date information if needed:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{\n", - " messages: [\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"What is the current conservation status of the Great Barrier Reef?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"What is the current conservation status of the Great Barrier Reef?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " }\n", - " ],\n", - " output: \u001b[32m\"The Great Barrier Reef has recorded its highest amount of coral cover since the Australian Institute\"\u001b[39m... 688 more characters\n", - "}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"65c315b6-2433-4cb1-97c7-b60b5546f518\",\n", + " \"content\": \"What is the current conservation status of the Great Barrier Reef?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTKLQn1e4axRhqIhpKMyzWWTGauO\",\n", + " \"content\": \"How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 8,\n", + " \"promptTokens\": 93,\n", + " \"totalTokens\": 101\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_3537616b13\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 93,\n", + " \"output_tokens\": 8,\n", + " \"total_tokens\": 101\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "await agentExecutor.invoke({\n", - " messages: [\n", - " new HumanMessage(\n", - " \"What is the current conservation status of the Great Barrier Reef?\"\n", - " ),\n", - " ],\n", - "});" + "await agent.invoke({ messages: [{ role: \"user\", content: \"What is the current conservation status of the Great Barrier Reef?\" }]})" ] }, { @@ -219,246 +258,233 @@ "source": [ "## Conversational responses\n", "\n", - "Because our prompt contains a placeholder for chat history messages, our agent can also take previous interactions into account and respond conversationally like a standard chatbot:\n" + "Because our prompt contains a placeholder for chat history messages, our agent can also take previous interactions into account and respond conversationally like a standard chatbot:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{\n", - " messages: [\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Hello Nemo! How can I assist you today?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Hello Nemo! How can I assist you today?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " usage_metadata: \u001b[90mundefined\u001b[39m\n", - " },\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"What is my name?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"What is my name?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " }\n", - " ],\n", - " output: \u001b[32m\"Your name is Nemo!\"\u001b[39m\n", - "}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"6433afc5-31bd-44b3-b34c-f11647e1677d\",\n", + " \"content\": \"I'm Nemo!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"f163b5f1-ea29-4d7a-9965-7c7c563d9cea\",\n", + " \"content\": \"Hello Nemo! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"382c3354-d02b-4888-98d8-44d75d045044\",\n", + " \"content\": \"What is my name?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTKMKu7ThZDZW09yMIPTq2N723Cj\",\n", + " \"content\": \"How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 8,\n", + " \"promptTokens\": 93,\n", + " \"totalTokens\": 101\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 93,\n", + " \"output_tokens\": 8,\n", + " \"total_tokens\": 101\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "import { AIMessage } from \"@langchain/core/messages\";\n", - "\n", - "await agentExecutor.invoke({\n", + "await agent.invoke({\n", " messages: [\n", - " new HumanMessage(\"I'm Nemo!\"),\n", - " new AIMessage(\"Hello Nemo! How can I assist you today?\"),\n", - " new HumanMessage(\"What is my name?\"),\n", - " ],\n", - "});" + " { role: \"user\", content: \"I'm Nemo!\" },\n", + " { role: \"user\", content: \"Hello Nemo! How can I assist you today?\" },\n", + " { role: \"user\", content: \"What is my name?\" }\n", + " ]\n", + "})" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "If preferred, you can also wrap the agent executor in a [`RunnableWithMessageHistory`](/docs/how_to/message_history/) class to internally manage history messages. Let's redeclare it this way:" + "If preferred, you can also add memory to the LangGraph agent to manage the history of messages. Let's redeclare it this way:" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ - "const agent2 = await createToolCallingAgent({\n", - " llm,\n", - " tools,\n", - " prompt,\n", - "});\n", + "import { MemorySaver } from \"@langchain/langgraph\"\n", "\n", - "const agentExecutor2 = new AgentExecutor({ agent: agent2, tools });" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Then, because our agent executor has multiple outputs, we also have to set the `outputMessagesKey` property when initializing the wrapper:\n" + "// highlight-start\n", + "const memory = new MemorySaver()\n", + "const agent2 = createReactAgent({ llm, tools, messageModifier: prompt, checkpointSaver: memory })\n", + "// highlight-end" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{\n", - " messages: [\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " }\n", - " ],\n", - " output: \u001b[32m\"Hi Nemo! It's great to meet you. How can I assist you today?\"\u001b[39m\n", - "}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"a4a4f663-8192-4179-afcc-88d9d186aa80\",\n", + " \"content\": \"I'm Nemo!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTKi4tBzOWMh3hgA46xXo7bJzb8r\",\n", + " \"content\": \"How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 8,\n", + " \"promptTokens\": 93,\n", + " \"totalTokens\": 101\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 93,\n", + " \"output_tokens\": 8,\n", + " \"total_tokens\": 101\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "import { ChatMessageHistory } from \"langchain/stores/message/in_memory\";\n", - "import { RunnableWithMessageHistory } from \"@langchain/core/runnables\";\n", - "\n", - "const demoEphemeralChatMessageHistory = new ChatMessageHistory();\n", - "\n", - "const conversationalAgentExecutor = new RunnableWithMessageHistory({\n", - " runnable: agentExecutor2,\n", - " getMessageHistory: (_sessionId) => demoEphemeralChatMessageHistory,\n", - " inputMessagesKey: \"messages\",\n", - " outputMessagesKey: \"output\",\n", - "});\n", - "\n", - "await conversationalAgentExecutor.invoke(\n", - " { messages: [new HumanMessage(\"I'm Nemo!\")] },\n", - " { configurable: { sessionId: \"unused\" } }\n", - ");" + "await agent2.invoke({ messages: [{ role: \"user\", content: \"I'm Nemo!\" }]}, { configurable: { thread_id: \"1\" } })" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And then if we rerun our wrapped agent executor:" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "{\n", - " messages: [\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"I'm Nemo!\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Hi Nemo! It's great to meet you. How can I assist you today?\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Hi Nemo! It's great to meet you. How can I assist you today?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " usage_metadata: \u001b[90mundefined\u001b[39m\n", - " },\n", - " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"What is my name?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"What is my name?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " }\n", - " ],\n", - " output: \u001b[32m\"Your name is Nemo!\"\u001b[39m\n", - "}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " messages: [\n", + " HumanMessage {\n", + " \"id\": \"c5fd303c-eb49-41a0-868e-bc8c5aa02cf6\",\n", + " \"content\": \"I'm Nemo!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTKi4tBzOWMh3hgA46xXo7bJzb8r\",\n", + " \"content\": \"How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 8,\n", + " \"promptTokens\": 93,\n", + " \"totalTokens\": 101\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"id\": \"635b17b9-2ec7-412f-bf45-85d0e9944430\",\n", + " \"content\": \"What is my name?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTKjBbmFlPb5t37aJ8p4NtoHb8YG\",\n", + " \"content\": \"How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 8,\n", + " \"promptTokens\": 93,\n", + " \"totalTokens\": 101\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 93,\n", + " \"output_tokens\": 8,\n", + " \"total_tokens\": 101\n", + " }\n", + " }\n", + " ]\n", + "}\n" + ] } ], "source": [ - "await conversationalAgentExecutor.invoke(\n", - " { messages: [new HumanMessage(\"What is my name?\")] },\n", - " { configurable: { sessionId: \"unused\" } }\n", - ");" + "await agent2.invoke({ messages: [{ role: \"user\", content: \"What is my name?\" }]}, { configurable: { thread_id: \"1\" } })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Next steps\n", + "This [LangSmith trace](https://smith.langchain.com/public/16cbcfa5-5ef1-4d4c-92c9-538a6e71f23d/r) shows what's going on under the hood.\n", + "\n", + "## Further reading\n", + "\n", + "For more on how to build agents, check these [LangGraph](https://langchain-ai.github.io/langgraphjs/) guides:\n", "\n", - "You've now learned how to create chatbots with tool-use capabilities.\n", + "* [agents conceptual guide](https://langchain-ai.github.io/langgraphjs/concepts/agentic_concepts/)\n", + "* [agents tutorials](https://langchain-ai.github.io/langgraphjs/tutorials/multi_agent/multi_agent_collaboration/)\n", + "* [createReactAgent](https://langchain-ai.github.io/langgraphjs/how-tos/create-react-agent/)\n", "\n", - "For more, check out the other guides in this section, including [how to add history to your chatbots](/docs/how_to/chatbots_memory)." + "For more on tool usage, you can also check out [this use case section](/docs/how_to#tools)." ] } ], @@ -469,14 +495,17 @@ "name": "deno" }, "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, "file_extension": ".ts", - "mimetype": "text/x.typescript", + "mimetype": "text/typescript", "name": "typescript", - "nb_converter": "script", - "pygments_lexer": "typescript", - "version": "5.3.3" + "version": "3.7.2" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/docs/core_docs/docs/how_to/graph_prompting.ipynb b/docs/core_docs/docs/how_to/graph_prompting.ipynb index 69024e9488b6..03ae3c0de203 100644 --- a/docs/core_docs/docs/how_to/graph_prompting.ipynb +++ b/docs/core_docs/docs/how_to/graph_prompting.ipynb @@ -6,7 +6,20 @@ "source": [ "# How to improve results with prompting\n", "\n", - "In this guide we’ll go over prompting strategies to improve graph database query generation. We’ll largely focus on methods for getting relevant database-specific information in your prompt." + "In this guide we’ll go over prompting strategies to improve graph database query generation. We’ll largely focus on methods for getting relevant database-specific information in your prompt.\n", + "\n", + "```{=mdx}\n", + ":::warning\n", + "\n", + "The `GraphCypherQAChain` used in this guide will execute Cypher statements against the provided database.\n", + "For production, make sure that the database connection uses credentials that are narrowly-scoped to only include necessary permissions.\n", + "\n", + "Failure to do so may result in data corruption or loss, since the calling code\n", + "may attempt commands that would result in deletion, mutation of data\n", + "if appropriately prompted or reading sensitive data if such data is present in the database.\n", + "\n", + ":::\n", + "```" ] }, { diff --git a/docs/core_docs/docs/how_to/graph_semantic.ipynb b/docs/core_docs/docs/how_to/graph_semantic.ipynb index 342f342a7731..8cf963e32632 100644 --- a/docs/core_docs/docs/how_to/graph_semantic.ipynb +++ b/docs/core_docs/docs/how_to/graph_semantic.ipynb @@ -12,7 +12,20 @@ "While that option provides excellent flexibility, the solution could be brittle and not consistently generating precise Cypher statements.\n", "Instead of generating Cypher statements, we can implement Cypher templates as tools in a semantic layer that an LLM agent can interact with.\n", "\n", - "![graph_semantic.png](../../static/img/graph_semantic.png)" + "![graph_semantic.png](../../static/img/graph_semantic.png)\n", + "\n", + "```{=mdx}\n", + ":::warning\n", + "\n", + "The code in this guide will execute Cypher statements against the provided database.\n", + "For production, make sure that the database connection uses credentials that are narrowly-scoped to only include necessary permissions.\n", + "\n", + "Failure to do so may result in data corruption or loss, since the calling code\n", + "may attempt commands that would result in deletion, mutation of data\n", + "if appropriately prompted or reading sensitive data if such data is present in the database.\n", + "\n", + ":::\n", + "```" ] }, { diff --git a/docs/core_docs/docs/how_to/installation.mdx b/docs/core_docs/docs/how_to/installation.mdx index cbeb1d54bcf5..6ad924629c33 100644 --- a/docs/core_docs/docs/how_to/installation.mdx +++ b/docs/core_docs/docs/how_to/installation.mdx @@ -270,13 +270,13 @@ You will have to make `fetch` available globally, either: You'll also need to [polyfill `ReadableStream`](https://www.npmjs.com/package/web-streams-polyfill) by installing: ```bash npm2yarn -npm i web-streams-polyfill +npm i web-streams-polyfill@4 ``` And then adding it to the global namespace in your main entrypoint: ```typescript -import "web-streams-polyfill/es6"; +import "web-streams-polyfill/polyfill"; ``` Additionally you'll have to polyfill `structuredClone`, eg. by installing `core-js` and following the instructions [here](https://github.com/zloirock/core-js). diff --git a/docs/core_docs/docs/how_to/message_history.ipynb b/docs/core_docs/docs/how_to/message_history.ipynb new file mode 100644 index 000000000000..dbca922041ff --- /dev/null +++ b/docs/core_docs/docs/how_to/message_history.ipynb @@ -0,0 +1,586 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "8165bd4c", + "metadata": { + "vscode": { + "languageId": "raw" + } + }, + "source": [ + "---\n", + "keywords: [memory]\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "f47033eb", + "metadata": {}, + "source": [ + "# How to add message history\n", + "\n", + ":::info Prerequisites\n", + "\n", + "This guide assumes familiarity with the following concepts:\n", + "\n", + "- [Chaining runnables](/docs/how_to/sequence/)\n", + "- [Prompt templates](/docs/concepts/#prompt-templates)\n", + "- [Chat Messages](/docs/concepts/#message-types)\n", + "\n", + ":::\n", + "\n", + "```{=mdx}\n", + ":::note\n", + "\n", + "This guide previously covered the [RunnableWithMessageHistory](https://api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html) abstraction. You can access this version of the guide in the [v0.2 docs](https://js.langchain.com/v0.2/docs/how_to/message_history/).\n", + "\n", + "The LangGraph implementation offers a number of advantages over `RunnableWithMessageHistory`, including the ability to persist arbitrary components of an application's state (instead of only messages).\n", + "\n", + ":::\n", + "```\n", + "\n", + "\n", + "Passing conversation state into and out a chain is vital when building a chatbot. LangGraph implements a built-in persistence layer, allowing chain states to be automatically persisted in memory, or external backends such as SQLite, Postgres or Redis. Details can be found in the LangGraph persistence documentation.\n", + "\n", + "In this guide we demonstrate how to add persistence to arbitrary LangChain runnables by wrapping them in a minimal LangGraph application. This lets us persist the message history and other elements of the chain's state, simplifying the development of multi-turn applications. It also supports multiple threads, enabling a single application to interact separately with multiple users.\n", + "\n", + "## Setup\n", + "\n", + "```{=mdx}\n", + "import Npm2Yarn from \"@theme/Npm2Yarn\";\n", + "\n", + "\n", + " @langchain/core @langchain/langgraph\n", + "\n", + "```\n", + "\n", + "Let’s also set up a chat model that we’ll use for the below examples.\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "8a4e4708", + "metadata": {}, + "outputs": [], + "source": [ + "// @lc-docs-hide-cell\n", + "\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const llm = new ChatOpenAI({\n", + " model: \"gpt-4o\",\n", + " temperature: 0,\n", + "});" + ] + }, + { + "cell_type": "markdown", + "id": "1f6121bc-2080-4ccc-acf0-f77de4bc951d", + "metadata": {}, + "source": [ + "## Example: message inputs\n", + "\n", + "Adding memory to a [chat model](/docs/concepts/#chat-models) provides a simple example. Chat models accept a list of messages as input and output a message. LangGraph includes a built-in `MessagesState` that we can use for this purpose.\n", + "\n", + "Below, we:\n", + "1. Define the graph state to be a list of messages;\n", + "2. Add a single node to the graph that calls a chat model;\n", + "3. Compile the graph with an in-memory checkpointer to store messages between runs.\n", + "\n", + ":::info\n", + "\n", + "The output of a LangGraph application is its [state](https://langchain-ai.github.io/langgraphjs/concepts/low_level/).\n", + "\n", + ":::" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "f691a73a-a866-4354-9fff-8315605e2b8f", + "metadata": {}, + "outputs": [], + "source": [ + "import { START, END, MessagesAnnotation, StateGraph, MemorySaver } from \"@langchain/langgraph\";\n", + "\n", + "// Define the function that calls the model\n", + "const callModel = async (state: typeof MessagesAnnotation.State) => {\n", + " const response = await llm.invoke(state.messages);\n", + " // Update message history with response:\n", + " return { messages: response };\n", + "};\n", + "\n", + "// Define a new graph\n", + "const workflow = new StateGraph(MessagesAnnotation)\n", + " // Define the (single) node in the graph\n", + " .addNode(\"model\", callModel)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "// Add memory\n", + "const memory = new MemorySaver();\n", + "const app = workflow.compile({ checkpointer: memory });" + ] + }, + { + "cell_type": "markdown", + "id": "c0b396a8-f81e-4139-b4b2-75adf61d8179", + "metadata": {}, + "source": [ + "When we run the application, we pass in a configuration object that specifies a `thread_id`. This ID is used to distinguish conversational threads (e.g., between different users)." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "e4309511-2140-4d91-8f5f-ea3661e6d179", + "metadata": {}, + "outputs": [], + "source": [ + "import { v4 as uuidv4 } from \"uuid\";\n", + "\n", + "const config = { configurable: { thread_id: uuidv4() } }" + ] + }, + { + "cell_type": "markdown", + "id": "108c45a2-4971-4120-ba64-9a4305a414bb", + "metadata": {}, + "source": [ + "We can then invoke the application:" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "72a5ff6c-501f-4151-8dd9-f600f70554be", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABTqCeKnMQmG9IH8dNF5vPjsgXtcM\",\n", + " \"content\": \"Hi Bob! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 10,\n", + " \"promptTokens\": 12,\n", + " \"totalTokens\": 22\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 12,\n", + " \"output_tokens\": 10,\n", + " \"total_tokens\": 22\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const input = [\n", + " {\n", + " role: \"user\",\n", + " content: \"Hi! I'm Bob.\",\n", + " }\n", + "]\n", + "const output = await app.invoke({ messages: input }, config)\n", + "// The output contains all messages in the state.\n", + "// This will long the last message in the conversation.\n", + "console.log(output.messages[output.messages.length - 1]);" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "5931fb35-0fac-40e7-8ac6-b14cb4e926cd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABTqD5jrJXeKCpvoIDp47fvgw2OPn\",\n", + " \"content\": \"Your name is Bob. How can I help you today, Bob?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 34,\n", + " \"totalTokens\": 48\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 34,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 48\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const input2 = [\n", + " {\n", + " role: \"user\",\n", + " content: \"What's my name?\",\n", + " }\n", + "]\n", + "const output2 = await app.invoke({ messages: input2 }, config)\n", + "console.log(output2.messages[output2.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "id": "91de6d12-881d-4d23-a421-f2e3bf829b79", + "metadata": {}, + "source": [ + "Note that states are separated for different threads. If we issue the same query to a thread with a new `thread_id`, the model indicates that it does not know the answer:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "6f12c26f-8913-4484-b2c5-b49eda2e6d7d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABTqDkctxwmXjeGOZpK6Km8jdCqdl\",\n", + " \"content\": \"I'm sorry, but I don't have access to personal information about users. How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 21,\n", + " \"promptTokens\": 11,\n", + " \"totalTokens\": 32\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 11,\n", + " \"output_tokens\": 21,\n", + " \"total_tokens\": 32\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const config2 = { configurable: { thread_id: uuidv4() } }\n", + "const input3 = [\n", + " {\n", + " role: \"user\",\n", + " content: \"What's my name?\",\n", + " }\n", + "]\n", + "const output3 = await app.invoke({ messages: input3 }, config2)\n", + "console.log(output3.messages[output3.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "id": "6749ea95-3382-4843-bb96-cfececb9e4e5", + "metadata": {}, + "source": [ + "## Example: object inputs\n", + "\n", + "LangChain runnables often accept multiple inputs via separate keys in a single object argument. A common example is a prompt template with multiple parameters.\n", + "\n", + "Whereas before our runnable was a chat model, here we chain together a prompt template and chat model." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "6e7a402a-0994-4fc5-a607-fb990a248aa4", + "metadata": {}, + "outputs": [], + "source": [ + "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\";\n", + "\n", + "const prompt = ChatPromptTemplate.fromMessages([\n", + " [\"system\", \"Answer in {language}.\"],\n", + " new MessagesPlaceholder(\"messages\"),\n", + "])\n", + "\n", + "const runnable = prompt.pipe(llm);" + ] + }, + { + "cell_type": "markdown", + "id": "f83107bd-ae61-45e1-a57e-94ab043aad4b", + "metadata": {}, + "source": [ + "For this scenario, we define the graph state to include these parameters (in addition to the message history). We then define a single-node graph in the same way as before.\n", + "\n", + "Note that in the below state:\n", + "- Updates to the `messages` list will append messages;\n", + "- Updates to the `language` string will overwrite the string." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "267429ea-be0f-4f80-8daf-c63d881a1436", + "metadata": {}, + "outputs": [], + "source": [ + "import { START, END, StateGraph, MemorySaver, MessagesAnnotation, Annotation } from \"@langchain/langgraph\";\n", + "\n", + "// Define the State\n", + "// highlight-next-line\n", + "const GraphAnnotation = Annotation.Root({\n", + " // highlight-next-line\n", + " language: Annotation(),\n", + " // Spread `MessagesAnnotation` into the state to add the `messages` field.\n", + " // highlight-next-line\n", + " ...MessagesAnnotation.spec,\n", + "})\n", + "\n", + "\n", + "// Define the function that calls the model\n", + "const callModel2 = async (state: typeof GraphAnnotation.State) => {\n", + " const response = await runnable.invoke(state);\n", + " // Update message history with response:\n", + " return { messages: [response] };\n", + "};\n", + "\n", + "const workflow2 = new StateGraph(GraphAnnotation)\n", + " .addNode(\"model\", callModel2)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "const app2 = workflow2.compile({ checkpointer: new MemorySaver() });" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "f3844fb4-58d7-43c8-b427-6d9f64d7411b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABTqFnCASRB5UhZ7XAbbf5T0Bva4U\",\n", + " \"content\": \"Lo siento, pero no tengo suficiente información para saber tu nombre. ¿Cómo te llamas?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 19,\n", + " \"promptTokens\": 19,\n", + " \"totalTokens\": 38\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 19,\n", + " \"output_tokens\": 19,\n", + " \"total_tokens\": 38\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const config3 = { configurable: { thread_id: uuidv4() } }\n", + "const input4 = {\n", + " messages: [\n", + " {\n", + " role: \"user\",\n", + " content: \"What's my name?\",\n", + " }\n", + " ],\n", + " language: \"Spanish\",\n", + "} \n", + "const output4 = await app2.invoke(input4, config3)\n", + "console.log(output4.messages[output4.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "id": "7df47824-ef18-4a6e-a416-345ec9203f88", + "metadata": {}, + "source": [ + "## Managing message history\n", + "\n", + "The message history (and other elements of the application state) can be accessed via `.getState`:" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "1cbd6d82-43c1-4d11-98af-5c3ad9cd9b3b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Language: Spanish\n", + "[\n", + " HumanMessage {\n", + " \"content\": \"What's my name?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTqFnCASRB5UhZ7XAbbf5T0Bva4U\",\n", + " \"content\": \"Lo siento, pero no tengo suficiente información para saber tu nombre. ¿Cómo te llamas?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 19,\n", + " \"promptTokens\": 19,\n", + " \"totalTokens\": 38\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "const state = (await app2.getState(config3)).values\n", + "\n", + "console.log(`Language: ${state.language}`);\n", + "console.log(state.messages)" + ] + }, + { + "cell_type": "markdown", + "id": "acfbccda-0bd6-4c4d-ae6e-8118520314e1", + "metadata": {}, + "source": [ + "We can also update the state via `.updateState`. For example, we can manually append a new message:" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "e98310d7-8ab1-461d-94a7-dd419494ab8d", + "metadata": {}, + "outputs": [], + "source": [ + "const _ = await app2.updateState(config3, { messages: [{ role: \"user\", content: \"test\" }]})" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "74ab3691-6f3b-49c5-aad0-2a90fc2a1e6a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Language: Spanish\n", + "[\n", + " HumanMessage {\n", + " \"content\": \"What's my name?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABTqFnCASRB5UhZ7XAbbf5T0Bva4U\",\n", + " \"content\": \"Lo siento, pero no tengo suficiente información para saber tu nombre. ¿Cómo te llamas?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 19,\n", + " \"promptTokens\": 19,\n", + " \"totalTokens\": 38\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"content\": \"test\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "const state2 = (await app2.getState(config3)).values\n", + "\n", + "console.log(`Language: ${state2.language}`);\n", + "console.log(state2.messages)" + ] + }, + { + "cell_type": "markdown", + "id": "e4a1ea00-d7ff-4f18-b9ec-9aec5909d027", + "metadata": {}, + "source": [ + "For details on managing state, including deleting messages, see the LangGraph documentation:\n", + "\n", + "- [How to delete messages](https://langchain-ai.github.io/langgraphjs/how-tos/delete-messages/)\n", + "- [How to view and update past graph state](https://langchain-ai.github.io/langgraphjs/how-tos/time-travel/)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "TypeScript", + "language": "typescript", + "name": "tslab" + }, + "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, + "file_extension": ".ts", + "mimetype": "text/typescript", + "name": "typescript", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/core_docs/docs/how_to/message_history.mdx b/docs/core_docs/docs/how_to/message_history.mdx deleted file mode 100644 index 2712135ff482..000000000000 --- a/docs/core_docs/docs/how_to/message_history.mdx +++ /dev/null @@ -1,206 +0,0 @@ -# How to add message history - -:::info Prerequisites - -This guide assumes familiarity with the following concepts: - -- [LangChain Expression Language (LCEL)](/docs/concepts/#langchain-expression-language) -- [Chaining runnables](/docs/how_to/sequence/) -- [Configuring chain parameters at runtime](/docs/how_to/binding) -- [Prompt templates](/docs/concepts/#prompt-templates) -- [Chat Messages](/docs/concepts/#message-types) - -::: - -The `RunnableWithMessageHistory` lets us add message history to certain types of chains. - -Specifically, it can be used for any Runnable that takes as input one of - -- a sequence of [`BaseMessages`](/docs/concepts/#message-types) -- a dict with a key that takes a sequence of `BaseMessage` -- a dict with a key that takes the latest message(s) as a string or sequence of `BaseMessage`, and a separate key that takes historical messages - -And returns as output one of - -- a string that can be treated as the contents of an `AIMessage` -- a sequence of `BaseMessage` -- a dict with a key that contains a sequence of `BaseMessage` - -Let's take a look at some examples to see how it works. - -## Setup - -We'll use Upstash to store our chat message histories and Anthropic's claude-2 model so we'll need to install the following dependencies: - -```bash npm2yarn -npm install @langchain/anthropic @langchain/community @langchain/core @upstash/redis -``` - -You'll need to set environment variables for `ANTHROPIC_API_KEY` and grab your Upstash REST url and secret token. - -### [LangSmith](https://smith.langchain.com/) - -LangSmith is especially useful for something like message history injection, where it can be hard to otherwise understand what the inputs are to various parts of the chain. - -Note that LangSmith is not needed, but it is helpful. -If you do want to use LangSmith, after you sign up at the link above, make sure to uncoment the below and set your environment variables to start logging traces: - -```bash -export LANGCHAIN_TRACING_V2="true" -export LANGCHAIN_API_KEY="" - -# Reduce tracing latency if you are not in a serverless environment -# export LANGCHAIN_CALLBACKS_BACKGROUND=true -``` - -Let's create a simple runnable that takes a dict as input and returns a `BaseMessage`. - -In this case the `"question"` key in the input represents our input message, and the `"history"` key is where our historical messages will be injected. - -```typescript -import { - ChatPromptTemplate, - MessagesPlaceholder, -} from "@langchain/core/prompts"; -import { ChatAnthropic } from "@langchain/anthropic"; -import { UpstashRedisChatMessageHistory } from "@langchain/community/stores/message/upstash_redis"; -// For demos, you can also use an in-memory store: -// import { ChatMessageHistory } from "langchain/stores/message/in_memory"; - -const prompt = ChatPromptTemplate.fromMessages([ - ["system", "You're an assistant who's good at {ability}"], - new MessagesPlaceholder("history"), - ["human", "{question}"], -]); - -const chain = prompt.pipe( - new ChatAnthropic({ model: "claude-3-sonnet-20240229" }) -); -``` - -### Adding message history - -To add message history to our original chain we wrap it in the `RunnableWithMessageHistory` class. - -Crucially, we also need to define a `getMessageHistory()` method that takes a `sessionId` string and based on it returns a `BaseChatMessageHistory`. Given the same input, this method should return an equivalent output. - -In this case, we'll also want to specify `inputMessagesKey` (the key to be treated as the latest input message) and `historyMessagesKey` (the key to add historical messages to). - -```typescript -import { RunnableWithMessageHistory } from "@langchain/core/runnables"; - -const chainWithHistory = new RunnableWithMessageHistory({ - runnable: chain, - getMessageHistory: (sessionId) => - new UpstashRedisChatMessageHistory({ - sessionId, - config: { - url: process.env.UPSTASH_REDIS_REST_URL!, - token: process.env.UPSTASH_REDIS_REST_TOKEN!, - }, - }), - inputMessagesKey: "question", - historyMessagesKey: "history", -}); -``` - -## Invoking with config - -Whenever we call our chain with message history, we need to include an additional config object that contains the `session_id` - -```typescript -{ - configurable: { - sessionId: ""; - } -} -``` - -Given the same configuration, our chain should be pulling from the same chat message history. - -```typescript -const result = await chainWithHistory.invoke( - { - ability: "math", - question: "What does cosine mean?", - }, - { - configurable: { - sessionId: "foobarbaz", - }, - } -); - -console.log(result); - -/* - AIMessage { - content: 'Cosine refers to one of the basic trigonometric functions. Specifically:\n' + - '\n' + - '- Cosine is one of the three main trigonometric functions, along with sine and tangent. It is often abbreviated as cos.\n' + - '\n' + - '- For a right triangle with sides a, b, and c (where c is the hypotenuse), cosine represents the ratio of the length of the adjacent side (a) to the length of the hypotenuse (c). So cos(A) = a/c, where A is the angle opposite side a.\n' + - '\n' + - '- On the Cartesian plane, cosine represents the x-coordinate of a point on the unit circle for a given angle. So if you take an angle θ on the unit circle, the cosine of θ gives you the x-coordinate of where the terminal side of that angle intersects the circle.\n' + - '\n' + - '- The cosine function has a periodic waveform that oscillates between 1 and -1. Its graph forms a cosine wave.\n' + - '\n' + - 'So in essence, cosine helps relate an angle in a right triangle to the ratio of two of its sides. Along with sine and tangent, it is foundational to trigonometry and mathematical modeling of periodic functions.', - name: undefined, - additional_kwargs: { - id: 'msg_01QnnAkKEz7WvhJrwLWGbLBm', - type: 'message', - role: 'assistant', - model: 'claude-3-sonnet-20240229', - stop_reason: 'end_turn', - stop_sequence: null - } - } -*/ - -const result2 = await chainWithHistory.invoke( - { - ability: "math", - question: "What's its inverse?", - }, - { - configurable: { - sessionId: "foobarbaz", - }, - } -); - -console.log(result2); - -/* - AIMessage { - content: 'The inverse of the cosine function is the arcsine or inverse sine function, often written as sin−1(x) or sin^{-1}(x).\n' + - '\n' + - 'Some key properties of the inverse cosine function:\n' + - '\n' + - '- It accepts values between -1 and 1 as inputs and returns angles from 0 to π radians (0 to 180 degrees). This is the inverse of the regular cosine function, which takes angles and returns the cosine ratio.\n' + - '\n' + - '- It is also called cos−1(x) or cos^{-1}(x) (read as "cosine inverse of x").\n' + - '\n' + - '- The notation sin−1(x) is usually preferred over cos−1(x) since it relates more directly to the unit circle definition of cosine. sin−1(x) gives the angle whose sine equals x.\n' + - '\n' + - '- The arcsine function is one-to-one on the domain [-1, 1]. This means every output angle maps back to exactly one input ratio x. This one-to-one mapping is what makes it the mathematical inverse of cosine.\n' + - '\n' + - 'So in summary, arcsine or inverse sine, written as sin−1(x) or sin^{-1}(x), gives you the angle whose cosine evaluates to the input x, undoing the cosine function. It is used throughout trigonometry and calculus.', - additional_kwargs: { - id: 'msg_01PYRhpoUudApdJvxug6R13W', - type: 'message', - role: 'assistant', - model: 'claude-3-sonnet-20240229', - stop_reason: 'end_turn', - stop_sequence: null - } - } -*/ -``` - -:::tip -[Langsmith trace](https://smith.langchain.com/public/50377a89-d0b8-413b-8cd7-8e6618835e00/r) -::: - -Looking at the Langsmith trace for the second call, we can see that when constructing the prompt, a "history" variable has been injected which is a list of two messages (our first input and first output). diff --git a/docs/core_docs/docs/how_to/migrate_agent.ipynb b/docs/core_docs/docs/how_to/migrate_agent.ipynb index e4cb32b8ad30..33fbf3332002 100644 --- a/docs/core_docs/docs/how_to/migrate_agent.ipynb +++ b/docs/core_docs/docs/how_to/migrate_agent.ipynb @@ -42,7 +42,7 @@ "\n", "#### Prerequisites\n", "\n", - "This how-to guide uses Anthropic's `\"claude-3-haiku-20240307\"` as the LLM. If you are running this guide as a notebook, set your Anthropic API key to run." + "This how-to guide uses OpenAI's `\"gpt-4o-mini\"` as the LLM. If you are running this guide as a notebook, set your OpenAI API key as shown below:" ] }, { @@ -54,7 +54,7 @@ }, "outputs": [], "source": [ - "// process.env.ANTHROPIC_API_KEY = \"sk-...\";\n", + "// process.env.OPENAI_API_KEY = \"...\";\n", "\n", "// Optional, add tracing in LangSmith\n", "// process.env.LANGCHAIN_API_KEY = \"ls...\";\n", @@ -86,7 +86,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "1222c5e2", "metadata": { "lines_to_next_cell": 2 @@ -95,11 +95,10 @@ "source": [ "import { tool } from \"@langchain/core/tools\";\n", "import { z } from \"zod\";\n", - "import { ChatAnthropic } from \"@langchain/anthropic\";\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", "\n", - "const llm = new ChatAnthropic({\n", - " model: \"claude-3-haiku-20240307\",\n", - " temperature: 0,\n", + "const llm = new ChatOpenAI({\n", + " model: \"gpt-4o-mini\",\n", "});\n", "\n", "const magicTool = tool(async ({ input }: { input: number }) => {\n", @@ -130,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "e52bf891", "metadata": { "lines_to_next_cell": 2 @@ -141,11 +140,11 @@ "text/plain": [ "{\n", " input: \u001b[32m\"what is the value of magic_function(3)?\"\u001b[39m,\n", - " output: \u001b[32m\"The value of magic_function(3) is 5.\"\u001b[39m\n", + " output: \u001b[32m\"The value of `magic_function(3)` is 5.\"\u001b[39m\n", "}" ] }, - "execution_count": 2, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -164,8 +163,15 @@ " [\"placeholder\", \"{agent_scratchpad}\"],\n", "]);\n", "\n", - "const agent = createToolCallingAgent({ llm, tools, prompt });\n", - "const agentExecutor = new AgentExecutor({ agent, tools });\n", + "const agent = createToolCallingAgent({\n", + " llm,\n", + " tools,\n", + " prompt\n", + "});\n", + "const agentExecutor = new AgentExecutor({\n", + " agent,\n", + " tools,\n", + "});\n", "\n", "await agentExecutor.invoke({ input: query });" ] @@ -185,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "dcda7082", "metadata": {}, "outputs": [ @@ -196,124 +202,77 @@ "{\n", " messages: [\n", " HumanMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: \"what is the value of magic_function(3)?\",\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"what is the value of magic_function(3)?\",\n", - " name: undefined,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", + " \"id\": \"eeef343c-80d1-4ccb-86af-c109343689cd\",\n", + " \"content\": \"what is the value of magic_function(3)?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: [ [Object] ],\n", - " additional_kwargs: {\n", - " id: \"msg_015jSku8UgrtRQ2kNQuTsvi1\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: [Object]\n", - " },\n", - " tool_calls: [ [Object] ],\n", - " invalid_tool_calls: [],\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: [\n", - " {\n", - " type: \"tool_use\",\n", - " id: \"toolu_01WCezi2ywMPnRm1xbrXYPoB\",\n", - " name: \"magic_function\",\n", - " input: [Object]\n", - " }\n", - " ],\n", - " name: undefined,\n", - " additional_kwargs: {\n", - " id: \"msg_015jSku8UgrtRQ2kNQuTsvi1\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 365, output_tokens: 53 }\n", + " \"id\": \"chatcmpl-A7exs2uRqEipaZ7MtRbXnqu0vT0Da\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_MtwWLn000BQHeSYQKsbxYNR0\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", " },\n", - " response_metadata: {\n", - " id: \"msg_015jSku8UgrtRQ2kNQuTsvi1\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 365, output_tokens: 53 }\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 55,\n", + " \"totalTokens\": 69\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", " },\n", - " tool_calls: [\n", + " \"tool_calls\": [\n", " {\n", - " name: \"magic_function\",\n", - " args: [Object],\n", - " id: \"toolu_01WCezi2ywMPnRm1xbrXYPoB\"\n", + " \"name\": \"magic_function\",\n", + " \"args\": {\n", + " \"input\": 3\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_MtwWLn000BQHeSYQKsbxYNR0\"\n", " }\n", " ],\n", - " invalid_tool_calls: []\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 55,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 69\n", + " }\n", " },\n", " ToolMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " name: \"magic_function\",\n", - " content: \"5\",\n", - " tool_call_id: \"toolu_01WCezi2ywMPnRm1xbrXYPoB\",\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"5\",\n", - " name: \"magic_function\",\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_call_id: \"toolu_01WCezi2ywMPnRm1xbrXYPoB\"\n", + " \"id\": \"1001bf20-7cde-4f8b-81f1-1faa654a8bb4\",\n", + " \"content\": \"5\",\n", + " \"name\": \"magic_function\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_MtwWLn000BQHeSYQKsbxYNR0\"\n", " },\n", " AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: \"The value of magic_function(3) is 5.\",\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {\n", - " id: \"msg_01FbyPvpxtczu2Cmd4vKcPQm\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: [Object]\n", + " \"id\": \"chatcmpl-A7exsTk3ilzGzC8DuY8GpnKOaGdvx\",\n", + " \"content\": \"The value of `magic_function(3)` is 5.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 78,\n", + " \"totalTokens\": 92\n", " },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"The value of magic_function(3) is 5.\",\n", - " name: undefined,\n", - " additional_kwargs: {\n", - " id: \"msg_01FbyPvpxtczu2Cmd4vKcPQm\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 431, output_tokens: 17 }\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_54e2f484be\"\n", " },\n", - " response_metadata: {\n", - " id: \"msg_01FbyPvpxtczu2Cmd4vKcPQm\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 431, output_tokens: 17 }\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 78,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 92\n", + " }\n", " }\n", " ]\n", "}\n" @@ -322,13 +281,18 @@ ], "source": [ "import { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n", - "import { HumanMessage } from \"@langchain/core/messages\";\n", "\n", - "const app = createReactAgent({ llm, tools });\n", + "const app = createReactAgent({\n", + " llm,\n", + " tools,\n", + "});\n", "\n", "let agentOutput = await app.invoke({\n", " messages: [\n", - " new HumanMessage(query)\n", + " {\n", + " role: \"user\",\n", + " content: query\n", + " },\n", " ],\n", "});\n", "\n", @@ -337,7 +301,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "b0a390a2", "metadata": {}, "outputs": [ @@ -347,186 +311,110 @@ "{\n", " messages: [\n", " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"what is the value of magic_function(3)?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"what is the value of magic_function(3)?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", + " \"id\": \"eeef343c-80d1-4ccb-86af-c109343689cd\",\n", + " \"content\": \"what is the value of magic_function(3)?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: [ \u001b[36m[Object]\u001b[39m ],\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_015jSku8UgrtRQ2kNQuTsvi1\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"tool_use\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: \u001b[36m[Object]\u001b[39m\n", - " },\n", - " tool_calls: [ \u001b[36m[Object]\u001b[39m ],\n", - " invalid_tool_calls: [],\n", - " response_metadata: {}\n", + " \"id\": \"chatcmpl-A7exs2uRqEipaZ7MtRbXnqu0vT0Da\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_MtwWLn000BQHeSYQKsbxYNR0\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: [\n", - " {\n", - " type: \u001b[32m\"tool_use\"\u001b[39m,\n", - " id: \u001b[32m\"toolu_01WCezi2ywMPnRm1xbrXYPoB\"\u001b[39m,\n", - " name: \u001b[32m\"magic_function\"\u001b[39m,\n", - " input: \u001b[36m[Object]\u001b[39m\n", - " }\n", - " ],\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_015jSku8UgrtRQ2kNQuTsvi1\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"tool_use\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m365\u001b[39m, output_tokens: \u001b[33m53\u001b[39m }\n", - " },\n", - " response_metadata: {\n", - " id: \u001b[32m\"msg_015jSku8UgrtRQ2kNQuTsvi1\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"tool_use\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m365\u001b[39m, output_tokens: \u001b[33m53\u001b[39m }\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 55,\n", + " \"totalTokens\": 69\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", " },\n", - " tool_calls: [\n", + " \"tool_calls\": [\n", " {\n", - " name: \u001b[32m\"magic_function\"\u001b[39m,\n", - " args: \u001b[36m[Object]\u001b[39m,\n", - " id: \u001b[32m\"toolu_01WCezi2ywMPnRm1xbrXYPoB\"\u001b[39m\n", + " \"name\": \"magic_function\",\n", + " \"args\": {\n", + " \"input\": 3\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_MtwWLn000BQHeSYQKsbxYNR0\"\n", " }\n", " ],\n", - " invalid_tool_calls: []\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 55,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 69\n", + " }\n", " },\n", " ToolMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " name: \u001b[32m\"magic_function\"\u001b[39m,\n", - " content: \u001b[32m\"5\"\u001b[39m,\n", - " tool_call_id: \u001b[32m\"toolu_01WCezi2ywMPnRm1xbrXYPoB\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"5\"\u001b[39m,\n", - " name: \u001b[32m\"magic_function\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_call_id: \u001b[32m\"toolu_01WCezi2ywMPnRm1xbrXYPoB\"\u001b[39m\n", + " \"id\": \"1001bf20-7cde-4f8b-81f1-1faa654a8bb4\",\n", + " \"content\": \"5\",\n", + " \"name\": \"magic_function\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_MtwWLn000BQHeSYQKsbxYNR0\"\n", " },\n", " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"The value of magic_function(3) is 5.\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_01FbyPvpxtczu2Cmd4vKcPQm\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: \u001b[36m[Object]\u001b[39m\n", + " \"id\": \"chatcmpl-A7exsTk3ilzGzC8DuY8GpnKOaGdvx\",\n", + " \"content\": \"The value of `magic_function(3)` is 5.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 78,\n", + " \"totalTokens\": 92\n", " },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"The value of magic_function(3) is 5.\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_01FbyPvpxtczu2Cmd4vKcPQm\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m431\u001b[39m, output_tokens: \u001b[33m17\u001b[39m }\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_54e2f484be\"\n", " },\n", - " response_metadata: {\n", - " id: \u001b[32m\"msg_01FbyPvpxtczu2Cmd4vKcPQm\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m431\u001b[39m, output_tokens: \u001b[33m17\u001b[39m }\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 78,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 92\n", + " }\n", " },\n", " HumanMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Pardon?\"\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Pardon?\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", + " \"id\": \"1f2a9f41-c8ff-48fe-9d93-e663ee9279ff\",\n", + " \"content\": \"Pardon?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"I apologize for the confusion. Let me explain the steps I took to arrive at the result:\\n\"\u001b[39m +\n", - " \u001b[32m\"\\n\"\u001b[39m +\n", - " \u001b[32m\"1. You aske\"\u001b[39m... 52 more characters,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_012yLSnnf1c64NWKS9K58hcN\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: \u001b[36m[Object]\u001b[39m\n", + " \"id\": \"chatcmpl-A7exyTe9Ofs63Ex3sKwRx3wWksNup\",\n", + " \"content\": \"The result of calling the `magic_function` with an input of 3 is 5.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 20,\n", + " \"promptTokens\": 102,\n", + " \"totalTokens\": 122\n", " },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"I apologize for the confusion. Let me explain the steps I took to arrive at the result:\\n\"\u001b[39m +\n", - " \u001b[32m\"\\n\"\u001b[39m +\n", - " \u001b[32m\"1. You aske\"\u001b[39m... 52 more characters,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_012yLSnnf1c64NWKS9K58hcN\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m455\u001b[39m, output_tokens: \u001b[33m137\u001b[39m }\n", - " },\n", - " response_metadata: {\n", - " id: \u001b[32m\"msg_012yLSnnf1c64NWKS9K58hcN\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m455\u001b[39m, output_tokens: \u001b[33m137\u001b[39m }\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 102,\n", + " \"output_tokens\": 20,\n", + " \"total_tokens\": 122\n", + " }\n", " }\n", " ]\n", "}" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -538,7 +426,7 @@ "agentOutput = await app.invoke({\n", " messages: [\n", " ...messageHistory,\n", - " new HumanMessage(newQuery)\n", + " { role: \"user\", content: newQuery }\n", " ],\n", "});\n" ] @@ -571,7 +459,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "4c5266cc", "metadata": { "lines_to_next_cell": 2 @@ -582,11 +470,11 @@ "text/plain": [ "{\n", " input: \u001b[32m\"what is the value of magic_function(3)?\"\u001b[39m,\n", - " output: \u001b[32m\"El valor de magic_function(3) es 5.\"\u001b[39m\n", + " output: \u001b[32m\"El valor de `magic_function(3)` es 5.\"\u001b[39m\n", "}" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -631,7 +519,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "38a751ba", "metadata": { "lines_to_next_cell": 2 @@ -641,54 +529,34 @@ "data": { "text/plain": [ "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"El valor de magic_function(3) es 5.\"\u001b[39m,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_01P5VUYbBZoeMaReqBgqFJZa\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m444\u001b[39m, output_tokens: \u001b[33m17\u001b[39m }\n", + " \"id\": \"chatcmpl-A7ey8LGWAs8ldrRRcO5wlHM85w9T8\",\n", + " \"content\": \"El valor de `magic_function(3)` es 5.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 89,\n", + " \"totalTokens\": 103\n", " },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"El valor de magic_function(3) es 5.\"\u001b[39m,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: {\n", - " id: \u001b[32m\"msg_01P5VUYbBZoeMaReqBgqFJZa\"\u001b[39m,\n", - " type: \u001b[32m\"message\"\u001b[39m,\n", - " role: \u001b[32m\"assistant\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m444\u001b[39m, output_tokens: \u001b[33m17\u001b[39m }\n", - " },\n", - " response_metadata: {\n", - " id: \u001b[32m\"msg_01P5VUYbBZoeMaReqBgqFJZa\"\u001b[39m,\n", - " model: \u001b[32m\"claude-3-haiku-20240307\"\u001b[39m,\n", - " stop_reason: \u001b[32m\"end_turn\"\u001b[39m,\n", - " stop_sequence: \u001b[1mnull\u001b[22m,\n", - " usage: { input_tokens: \u001b[33m444\u001b[39m, output_tokens: \u001b[33m17\u001b[39m }\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 89,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 103\n", + " }\n", "}" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "import { SystemMessage } from \"@langchain/core/messages\";\n", - "\n", "const systemMessage = \"You are a helpful assistant. Respond only in Spanish.\";\n", "\n", "// This could also be a SystemMessage object\n", @@ -702,7 +570,7 @@ "\n", "agentOutput = await appWithSystemMessage.invoke({\n", " messages: [\n", - " new HumanMessage(query)\n", + " { role: \"user\", content: query }\n", " ],\n", "});\n", "agentOutput.messages[agentOutput.messages.length - 1];" @@ -721,7 +589,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "c7120cdd", "metadata": {}, "outputs": [ @@ -731,13 +599,13 @@ "text": [ "{\n", " input: \"what is the value of magic_function(3)?\",\n", - " output: \"5. ¡Pandemonium!\"\n", + " output: \"El valor de magic_function(3) es 5. ¡Pandemonium!\"\n", "}\n" ] } ], "source": [ - "import { BaseMessage, SystemMessage } from \"@langchain/core/messages\";\n", + "import { BaseMessage, SystemMessage, HumanMessage } from \"@langchain/core/messages\";\n", "\n", "const modifyMessages = (messages: BaseMessage[]) => {\n", " return [\n", @@ -754,7 +622,7 @@ "});\n", "\n", "agentOutput = await appWithMessagesModifier.invoke({\n", - " messages: [new HumanMessage(query)],\n", + " messages: [{ role: \"user\", content: query }],\n", "});\n", "\n", "console.log({\n", @@ -776,7 +644,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "4d67ba36", "metadata": {}, "outputs": [ @@ -784,11 +652,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "The magic_function takes an input number and applies some magic to it, returning the output. For an input of 3, the output is 5.\n", + "The output of the magic function for the input 3 is 5.\n", "---\n", - "Okay, I remember your name is Polly.\n", + "Yes, your name is Polly! How can I assist you today?\n", "---\n", - "So the output of the magic_function with an input of 3 is 5.\n" + "The output of the magic function for the input 3 is 5.\n" ] } ], @@ -844,7 +712,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "bbc64438", "metadata": {}, "outputs": [ @@ -852,25 +720,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "The magic_function takes an input number and applies some magic to it, returning the output. For an input of 3, the magic_function returns 5.\n", + "Hi Polly! The output of the magic function for the input 3 is 5.\n", "---\n", - "Ah yes, I remember your name is Polly! It's nice to meet you Polly.\n", + "Yes, your name is Polly!\n", "---\n", - "So the magic_function returned an output of 5 for an input of 3.\n" + "The output of the magic function for the input 3 was 5.\n" ] } ], "source": [ "import { MemorySaver } from \"@langchain/langgraph\";\n", "\n", - "const memory = new MemorySaver();\n", + "const checkpointer = new MemorySaver();\n", "const appWithMemory = createReactAgent({\n", - " llm,\n", - " tools,\n", - " checkpointSaver: memory\n", + " llm: llm,\n", + " tools: tools,\n", + " checkpointSaver: checkpointer\n", "});\n", "\n", - "const config = {\n", + "const langGraphConfig = {\n", " configurable: {\n", " thread_id: \"test-thread\",\n", " },\n", @@ -879,12 +747,13 @@ "agentOutput = await appWithMemory.invoke(\n", " {\n", " messages: [\n", - " new HumanMessage(\n", - " \"Hi, I'm polly! What's the output of magic_function of 3?\",\n", - " ),\n", + " {\n", + " role: \"user\",\n", + " content: \"Hi, I'm polly! What's the output of magic_function of 3?\",\n", + " }\n", " ],\n", " },\n", - " config,\n", + " langGraphConfig,\n", ");\n", "\n", "console.log(agentOutput.messages[agentOutput.messages.length - 1].content);\n", @@ -893,10 +762,10 @@ "agentOutput = await appWithMemory.invoke(\n", " {\n", " messages: [\n", - " new HumanMessage(\"Remember my name?\")\n", + " { role: \"user\", content: \"Remember my name?\" }\n", " ]\n", " },\n", - " config,\n", + " langGraphConfig,\n", ");\n", "\n", "console.log(agentOutput.messages[agentOutput.messages.length - 1].content);\n", @@ -905,10 +774,10 @@ "agentOutput = await appWithMemory.invoke(\n", " {\n", " messages: [\n", - " new HumanMessage(\"what was that output again?\")\n", + " { role: \"user\", content: \"what was that output again?\" }\n", " ]\n", " },\n", - " config,\n", + " langGraphConfig,\n", ");\n", "\n", "console.log(agentOutput.messages[agentOutput.messages.length - 1].content);" @@ -929,7 +798,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "5c928049", "metadata": { "lines_to_next_cell": 2 @@ -945,16 +814,61 @@ " action: {\n", " tool: \"magic_function\",\n", " toolInput: { input: 3 },\n", - " toolCallId: \"toolu_01KCJJ8kyiY5LV4RHbVPzK8v\",\n", - " log: 'Invoking \"magic_function\" with {\"input\":3}\\n' +\n", - " '[{\"type\":\"tool_use\",\"id\":\"toolu_01KCJJ8kyiY5LV4RHbVPzK8v\"'... 46 more characters,\n", - " messageLog: [ [AIMessageChunk] ]\n", + " toolCallId: \"call_IQZr1yy2Ug6904VkQg6pWGgR\",\n", + " log: 'Invoking \"magic_function\" with {\"input\":3}\\n',\n", + " messageLog: [\n", + " AIMessageChunk {\n", + " \"id\": \"chatcmpl-A7eziUrDmLSSMoiOskhrfbsHqx4Sd\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"index\": 0,\n", + " \"id\": \"call_IQZr1yy2Ug6904VkQg6pWGgR\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"prompt\": 0,\n", + " \"completion\": 0,\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"magic_function\",\n", + " \"args\": {\n", + " \"input\": 3\n", + " },\n", + " \"id\": \"call_IQZr1yy2Ug6904VkQg6pWGgR\",\n", + " \"type\": \"tool_call\"\n", + " }\n", + " ],\n", + " \"tool_call_chunks\": [\n", + " {\n", + " \"name\": \"magic_function\",\n", + " \"args\": \"{\\\"input\\\":3}\",\n", + " \"id\": \"call_IQZr1yy2Ug6904VkQg6pWGgR\",\n", + " \"index\": 0,\n", + " \"type\": \"tool_call_chunk\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 61,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 75\n", + " }\n", + " }\n", + " ]\n", " },\n", " observation: \"5\"\n", " }\n", " ]\n", "}\n", - "{ output: \"The value of magic_function(3) is 5.\" }\n" + "{ output: \"The value of `magic_function(3)` is 5.\" }\n" ] } ], @@ -978,7 +892,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "2be89a30", "metadata": {}, "outputs": [ @@ -990,35 +904,42 @@ " agent: {\n", " messages: [\n", " AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: [Array],\n", - " additional_kwargs: [Object],\n", - " tool_calls: [Array],\n", - " invalid_tool_calls: [],\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: [ [Object] ],\n", - " name: undefined,\n", - " additional_kwargs: {\n", - " id: \"msg_01WWYeJvJroT82QhJQZKdwSt\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: [Object]\n", + " \"id\": \"chatcmpl-A7ezu8hirCENjdjR2GpLjkzXFTEmp\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_KhhNL0m3mlPoJiboFMoX8hzk\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", " },\n", - " response_metadata: {\n", - " id: \"msg_01WWYeJvJroT82QhJQZKdwSt\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: [Object]\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 55,\n", + " \"totalTokens\": 69\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", " },\n", - " tool_calls: [ [Object] ],\n", - " invalid_tool_calls: []\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"magic_function\",\n", + " \"args\": {\n", + " \"input\": 3\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_KhhNL0m3mlPoJiboFMoX8hzk\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 55,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 69\n", + " }\n", " }\n", " ]\n", " }\n", @@ -1027,20 +948,11 @@ " tools: {\n", " messages: [\n", " ToolMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " name: \"magic_function\",\n", - " content: \"5\",\n", - " tool_call_id: \"toolu_01X9pwxuroTWNVqiwQTL1U8C\",\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"5\",\n", - " name: \"magic_function\",\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_call_id: \"toolu_01X9pwxuroTWNVqiwQTL1U8C\"\n", + " \"content\": \"5\",\n", + " \"name\": \"magic_function\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_KhhNL0m3mlPoJiboFMoX8hzk\"\n", " }\n", " ]\n", " }\n", @@ -1049,35 +961,25 @@ " agent: {\n", " messages: [\n", " AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: \"The value of magic_function(3) is 5.\",\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: [Object],\n", - " response_metadata: {}\n", + " \"id\": \"chatcmpl-A7ezuTrh8GC550eKa1ZqRZGjpY5zh\",\n", + " \"content\": \"The value of `magic_function(3)` is 5.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 78,\n", + " \"totalTokens\": 92\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"The value of magic_function(3) is 5.\",\n", - " name: undefined,\n", - " additional_kwargs: {\n", - " id: \"msg_012kQPkxt2CrsFw4CsdfNTWr\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: [Object]\n", - " },\n", - " response_metadata: {\n", - " id: \"msg_012kQPkxt2CrsFw4CsdfNTWr\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: [Object]\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 78,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 92\n", + " }\n", " }\n", " ]\n", " }\n", @@ -1087,7 +989,7 @@ ], "source": [ "const langGraphStream = await app.stream(\n", - " { messages: [new HumanMessage(query)] },\n", + " { messages: [{ role: \"user\", content: query }] },\n", " { streamMode: \"updates\" },\n", ");\n", "\n", @@ -1110,7 +1012,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "77ce2771", "metadata": { "lines_to_next_cell": 2 @@ -1125,21 +1027,53 @@ " action: {\n", " tool: \"magic_function\",\n", " toolInput: { input: 3 },\n", - " toolCallId: \"toolu_0126dJXbjwLC5daAScz8bw1k\",\n", - " log: 'Invoking \"magic_function\" with {\"input\":3}\\n' +\n", - " '[{\"type\":\"tool_use\",\"id\":\"toolu_0126dJXbjwLC5daAScz8bw1k\"'... 46 more characters,\n", + " toolCallId: \"call_mbg1xgLEYEEWClbEaDe7p5tK\",\n", + " log: 'Invoking \"magic_function\" with {\"input\":3}\\n',\n", " messageLog: [\n", " AIMessageChunk {\n", - " lc_serializable: true,\n", - " lc_kwargs: [Object],\n", - " lc_namespace: [Array],\n", - " content: [Array],\n", - " name: undefined,\n", - " additional_kwargs: [Object],\n", - " response_metadata: {},\n", - " tool_calls: [Array],\n", - " invalid_tool_calls: [],\n", - " tool_call_chunks: [Array]\n", + " \"id\": \"chatcmpl-A7f0NdSRSUJsBP6ENTpiQD4LzpBAH\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"index\": 0,\n", + " \"id\": \"call_mbg1xgLEYEEWClbEaDe7p5tK\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"prompt\": 0,\n", + " \"completion\": 0,\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_54e2f484be\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"magic_function\",\n", + " \"args\": {\n", + " \"input\": 3\n", + " },\n", + " \"id\": \"call_mbg1xgLEYEEWClbEaDe7p5tK\",\n", + " \"type\": \"tool_call\"\n", + " }\n", + " ],\n", + " \"tool_call_chunks\": [\n", + " {\n", + " \"name\": \"magic_function\",\n", + " \"args\": \"{\\\"input\\\":3}\",\n", + " \"id\": \"call_mbg1xgLEYEEWClbEaDe7p5tK\",\n", + " \"index\": 0,\n", + " \"type\": \"tool_call_chunk\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 61,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 75\n", + " }\n", " }\n", " ]\n", " },\n", @@ -1176,7 +1110,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "id": "2f9cdfa8", "metadata": { "lines_to_next_cell": 2 @@ -1188,137 +1122,77 @@ "text": [ "[\n", " HumanMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: \"what is the value of magic_function(3)?\",\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"what is the value of magic_function(3)?\",\n", - " name: undefined,\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", + " \"id\": \"46a825b2-13a3-4f19-b1aa-7716c53eb247\",\n", + " \"content\": \"what is the value of magic_function(3)?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", " },\n", " AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: [\n", - " {\n", - " type: \"tool_use\",\n", - " id: \"toolu_01L2N6TKrZxyUWRCQZ5qLYVj\",\n", - " name: \"magic_function\",\n", - " input: [Object]\n", - " }\n", - " ],\n", - " additional_kwargs: {\n", - " id: \"msg_01BhXyjA2PTwGC5J3JNnfAXY\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 365, output_tokens: 53 }\n", - " },\n", - " tool_calls: [\n", + " \"id\": \"chatcmpl-A7f0iUuWktC8gXztWZCjofqyCozY2\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", " {\n", - " name: \"magic_function\",\n", - " args: [Object],\n", - " id: \"toolu_01L2N6TKrZxyUWRCQZ5qLYVj\"\n", + " \"id\": \"call_ndsPDU58wsMeGaqr41cSlLlF\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", " }\n", - " ],\n", - " invalid_tool_calls: [],\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: [\n", - " {\n", - " type: \"tool_use\",\n", - " id: \"toolu_01L2N6TKrZxyUWRCQZ5qLYVj\",\n", - " name: \"magic_function\",\n", - " input: { input: 3 }\n", - " }\n", - " ],\n", - " name: undefined,\n", - " additional_kwargs: {\n", - " id: \"msg_01BhXyjA2PTwGC5J3JNnfAXY\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 365, output_tokens: 53 }\n", + " ]\n", " },\n", - " response_metadata: {\n", - " id: \"msg_01BhXyjA2PTwGC5J3JNnfAXY\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"tool_use\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 365, output_tokens: 53 }\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 55,\n", + " \"totalTokens\": 69\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_483d39d857\"\n", " },\n", - " tool_calls: [\n", + " \"tool_calls\": [\n", " {\n", - " name: \"magic_function\",\n", - " args: { input: 3 },\n", - " id: \"toolu_01L2N6TKrZxyUWRCQZ5qLYVj\"\n", + " \"name\": \"magic_function\",\n", + " \"args\": {\n", + " \"input\": 3\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_ndsPDU58wsMeGaqr41cSlLlF\"\n", " }\n", " ],\n", - " invalid_tool_calls: []\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 55,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 69\n", + " }\n", " },\n", " ToolMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " name: \"magic_function\",\n", - " content: \"5\",\n", - " tool_call_id: \"toolu_01L2N6TKrZxyUWRCQZ5qLYVj\",\n", - " additional_kwargs: {},\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"5\",\n", - " name: \"magic_function\",\n", - " additional_kwargs: {},\n", - " response_metadata: {},\n", - " tool_call_id: \"toolu_01L2N6TKrZxyUWRCQZ5qLYVj\"\n", + " \"id\": \"ac6aa309-bbfb-46cd-ba27-cbdbfd848705\",\n", + " \"content\": \"5\",\n", + " \"name\": \"magic_function\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_ndsPDU58wsMeGaqr41cSlLlF\"\n", " },\n", " AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: \"The value of magic_function(3) is 5.\",\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: {\n", - " id: \"msg_01ABtcXJ4CwMHphYYmffQZoF\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 431, output_tokens: 17 }\n", + " \"id\": \"chatcmpl-A7f0i7iHyDUV6is6sgwtcXivmFZ1x\",\n", + " \"content\": \"The value of `magic_function(3)` is 5.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 78,\n", + " \"totalTokens\": 92\n", " },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"The value of magic_function(3) is 5.\",\n", - " name: undefined,\n", - " additional_kwargs: {\n", - " id: \"msg_01ABtcXJ4CwMHphYYmffQZoF\",\n", - " type: \"message\",\n", - " role: \"assistant\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 431, output_tokens: 17 }\n", - " },\n", - " response_metadata: {\n", - " id: \"msg_01ABtcXJ4CwMHphYYmffQZoF\",\n", - " model: \"claude-3-haiku-20240307\",\n", - " stop_reason: \"end_turn\",\n", - " stop_sequence: null,\n", - " usage: { input_tokens: 431, output_tokens: 17 }\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_54e2f484be\"\n", " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 78,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 92\n", + " }\n", " }\n", "]\n" ] @@ -1327,7 +1201,7 @@ "source": [ "agentOutput = await app.invoke({\n", " messages: [\n", - " new HumanMessage(query)\n", + " { role: \"user\", content: query },\n", " ]\n", "});\n", "\n", @@ -1349,229 +1223,20 @@ "limit, so we will need to multiply by two (and add one) to get equivalent\n", "results.\n", "\n", - "If the recursion limit is reached, LangGraph raises a specific exception type,\n", - "that we can catch and manage similarly to AgentExecutor.\n" + "Here's an example of how you'd set this parameter with the legacy `AgentExecutor`:" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "1cca9d11", "metadata": { "lines_to_next_cell": 2 }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[32m[chain/start]\u001b[39m [\u001b[90m\u001b[1m1:chain:AgentExecutor\u001b[22m\u001b[39m] Entering Chain run with input: {\n", - " \"input\": \"what is the value of magic_function(3)?\"\n", - "}\n", - "\u001b[32m[chain/start]\u001b[39m [\u001b[90m1:chain:AgentExecutor > \u001b[1m2:chain:ToolCallingAgent\u001b[22m\u001b[39m] Entering Chain run with input: {\n", - " \"input\": \"what is the value of magic_function(3)?\",\n", - " \"steps\": []\n", - "}\n", - "\u001b[32m[chain/start]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m3:chain:RunnableAssign\u001b[22m\u001b[39m] Entering Chain run with input: {\n", - " \"input\": \"\"\n", - "}\n", - "\u001b[32m[chain/start]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > 3:chain:RunnableAssign > \u001b[1m4:chain:RunnableMap\u001b[22m\u001b[39m] Entering Chain run with input: {\n", - " \"input\": \"\"\n", - "}\n", - "\u001b[32m[chain/start]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > 3:chain:RunnableAssign > 4:chain:RunnableMap > \u001b[1m5:chain:RunnableLambda\u001b[22m\u001b[39m] Entering Chain run with input: {\n", - " \"input\": \"\"\n", - "}\n", - "\u001b[36m[chain/end]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > 3:chain:RunnableAssign > 4:chain:RunnableMap > \u001b[1m5:chain:RunnableLambda\u001b[22m\u001b[39m] [0ms] Exiting Chain run with output: {\n", - " \"output\": []\n", - "}\n", - "\u001b[36m[chain/end]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > 3:chain:RunnableAssign > \u001b[1m4:chain:RunnableMap\u001b[22m\u001b[39m] [1ms] Exiting Chain run with output: {\n", - " \"agent_scratchpad\": []\n", - "}\n", - "\u001b[36m[chain/end]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m3:chain:RunnableAssign\u001b[22m\u001b[39m] [1ms] Exiting Chain run with output: {\n", - " \"input\": \"what is the value of magic_function(3)?\",\n", - " \"steps\": [],\n", - " \"agent_scratchpad\": []\n", - "}\n", - "\u001b[32m[chain/start]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m6:prompt:ChatPromptTemplate\u001b[22m\u001b[39m] Entering Chain run with input: {\n", - " \"input\": \"what is the value of magic_function(3)?\",\n", - " \"steps\": [],\n", - " \"agent_scratchpad\": []\n", - "}\n", - "\u001b[36m[chain/end]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m6:prompt:ChatPromptTemplate\u001b[22m\u001b[39m] [0ms] Exiting Chain run with output: {\n", - " \"lc\": 1,\n", - " \"type\": \"constructor\",\n", - " \"id\": [\n", - " \"langchain_core\",\n", - " \"prompt_values\",\n", - " \"ChatPromptValue\"\n", - " ],\n", - " \"kwargs\": {\n", - " \"messages\": [\n", - " {\n", - " \"lc\": 1,\n", - " \"type\": \"constructor\",\n", - " \"id\": [\n", - " \"langchain_core\",\n", - " \"messages\",\n", - " \"SystemMessage\"\n", - " ],\n", - " \"kwargs\": {\n", - " \"content\": \"You are a helpful assistant. Respond only in Spanish.\",\n", - " \"additional_kwargs\": {},\n", - " \"response_metadata\": {}\n", - " }\n", - " },\n", - " {\n", - " \"lc\": 1,\n", - " \"type\": \"constructor\",\n", - " \"id\": [\n", - " \"langchain_core\",\n", - " \"messages\",\n", - " \"HumanMessage\"\n", - " ],\n", - " \"kwargs\": {\n", - " \"content\": \"what is the value of magic_function(3)?\",\n", - " \"additional_kwargs\": {},\n", - " \"response_metadata\": {}\n", - " }\n", - " }\n", - " ]\n", - " }\n", - "}\n", - "\u001b[32m[llm/start]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m7:llm:ChatAnthropic\u001b[22m\u001b[39m] Entering LLM run with input: {\n", - " \"messages\": [\n", - " [\n", - " {\n", - " \"lc\": 1,\n", - " \"type\": \"constructor\",\n", - " \"id\": [\n", - " \"langchain_core\",\n", - " \"messages\",\n", - " \"SystemMessage\"\n", - " ],\n", - " \"kwargs\": {\n", - " \"content\": \"You are a helpful assistant. Respond only in Spanish.\",\n", - " \"additional_kwargs\": {},\n", - " \"response_metadata\": {}\n", - " }\n", - " },\n", - " {\n", - " \"lc\": 1,\n", - " \"type\": \"constructor\",\n", - " \"id\": [\n", - " \"langchain_core\",\n", - " \"messages\",\n", - " \"HumanMessage\"\n", - " ],\n", - " \"kwargs\": {\n", - " \"content\": \"what is the value of magic_function(3)?\",\n", - " \"additional_kwargs\": {},\n", - " \"response_metadata\": {}\n", - " }\n", - " }\n", - " ]\n", - " ]\n", - "}\n", - "\u001b[36m[llm/end]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m7:llm:ChatAnthropic\u001b[22m\u001b[39m] [1.56s] Exiting LLM run with output: {\n", - " \"generations\": [\n", - " [\n", - " {\n", - " \"text\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\",\n", - " \"message\": {\n", - " \"lc\": 1,\n", - " \"type\": \"constructor\",\n", - " \"id\": [\n", - " \"langchain_core\",\n", - " \"messages\",\n", - " \"AIMessageChunk\"\n", - " ],\n", - " \"kwargs\": {\n", - " \"content\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\",\n", - " \"additional_kwargs\": {\n", - " \"id\": \"msg_011b4GnLtiCRnCzZiqUBAZeH\",\n", - " \"type\": \"message\",\n", - " \"role\": \"assistant\",\n", - " \"model\": \"claude-3-haiku-20240307\",\n", - " \"stop_reason\": \"end_turn\",\n", - " \"stop_sequence\": null,\n", - " \"usage\": {\n", - " \"input_tokens\": 378,\n", - " \"output_tokens\": 59\n", - " }\n", - " },\n", - " \"tool_call_chunks\": [],\n", - " \"tool_calls\": [],\n", - " \"invalid_tool_calls\": [],\n", - " \"response_metadata\": {}\n", - " }\n", - " }\n", - " }\n", - " ]\n", - " ]\n", - "}\n", - "\u001b[32m[chain/start]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m8:parser:ToolCallingAgentOutputParser\u001b[22m\u001b[39m] Entering Chain run with input: {\n", - " \"lc\": 1,\n", - " \"type\": \"constructor\",\n", - " \"id\": [\n", - " \"langchain_core\",\n", - " \"messages\",\n", - " \"AIMessageChunk\"\n", - " ],\n", - " \"kwargs\": {\n", - " \"content\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\",\n", - " \"additional_kwargs\": {\n", - " \"id\": \"msg_011b4GnLtiCRnCzZiqUBAZeH\",\n", - " \"type\": \"message\",\n", - " \"role\": \"assistant\",\n", - " \"model\": \"claude-3-haiku-20240307\",\n", - " \"stop_reason\": \"end_turn\",\n", - " \"stop_sequence\": null,\n", - " \"usage\": {\n", - " \"input_tokens\": 378,\n", - " \"output_tokens\": 59\n", - " }\n", - " },\n", - " \"tool_call_chunks\": [],\n", - " \"tool_calls\": [],\n", - " \"invalid_tool_calls\": [],\n", - " \"response_metadata\": {}\n", - " }\n", - "}\n", - "\u001b[36m[chain/end]\u001b[39m [\u001b[90m1:chain:AgentExecutor > 2:chain:ToolCallingAgent > \u001b[1m8:parser:ToolCallingAgentOutputParser\u001b[22m\u001b[39m] [0ms] Exiting Chain run with output: {\n", - " \"returnValues\": {\n", - " \"output\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\"\n", - " },\n", - " \"log\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\"\n", - "}\n", - "\u001b[36m[chain/end]\u001b[39m [\u001b[90m1:chain:AgentExecutor > \u001b[1m2:chain:ToolCallingAgent\u001b[22m\u001b[39m] [1.56s] Exiting Chain run with output: {\n", - " \"returnValues\": {\n", - " \"output\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\"\n", - " },\n", - " \"log\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\"\n", - "}\n", - "\u001b[36m[chain/end]\u001b[39m [\u001b[90m\u001b[1m1:chain:AgentExecutor\u001b[22m\u001b[39m] [1.56s] Exiting Chain run with output: {\n", - " \"input\": \"what is the value of magic_function(3)?\",\n", - " \"output\": \"Lo siento, pero la función \\\"magic_function\\\" espera un parámetro de tipo \\\"string\\\", no un número entero. Por favor, proporciona una entrada de tipo cadena de texto para que pueda aplicar la función mágica.\"\n", - "}\n" - ] - }, - { - "data": { - "text/plain": [ - "{\n", - " input: \u001b[32m\"what is the value of magic_function(3)?\"\u001b[39m,\n", - " output: \u001b[32m'Lo siento, pero la función \"magic_function\" espera un parámetro de tipo \"string\", no un número enter'\u001b[39m... 103 more characters\n", - "}" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "const badMagicTool = tool(async ({ input }) => {\n", - " return \"Sorry, there was an error. Please try again.\";\n", + "const badMagicTool = tool(async ({ input: _input }) => {\n", + " return \"Sorry, there was a temporary error. Please try again with the same input.\";\n", "}, {\n", " name: \"magic_function\",\n", " description: \"Applies a magic function to an input.\",\n", @@ -1593,12 +1258,20 @@ " maxIterations: 2,\n", "});\n", "\n", - "await spanishAgentExecutorWithMaxIterations.invoke({ input: query });\n" + "await spanishAgentExecutorWithMaxIterations.invoke({ input: query });" + ] + }, + { + "cell_type": "markdown", + "id": "245e064c", + "metadata": {}, + "source": [ + "If the recursion limit is reached in LangGraph.js, the framework will raise a specific exception type that we can catch and manage similarly to AgentExecutor." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "id": "2f5e7d58", "metadata": {}, "outputs": [ @@ -1620,7 +1293,7 @@ "try {\n", " await appWithBadTools.invoke({\n", " messages: [\n", - " new HumanMessage(query)\n", + " { role: \"user\", content: query }\n", " ]\n", " }, {\n", " recursionLimit: RECURSION_LIMIT,\n", @@ -1645,14 +1318,6 @@ "\n", "Next, check out other [LangGraph how-to guides](https://langchain-ai.github.io/langgraphjs/how-tos/)." ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3f5bf788", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/docs/core_docs/docs/how_to/output_parser_structured.ipynb b/docs/core_docs/docs/how_to/output_parser_structured.ipynb index c7860f637fa9..a6252a3aa1b9 100644 --- a/docs/core_docs/docs/how_to/output_parser_structured.ipynb +++ b/docs/core_docs/docs/how_to/output_parser_structured.ipynb @@ -140,7 +140,7 @@ "id": "75976cd6-78e2-458b-821f-3ddf3683466b", "metadata": {}, "source": [ - "Output parsers implement the [Runnable interface](/docs/how_to/#langchain-expression-language-lcel), the basic building block of the [LangChain Expression Language (LCEL)](/docs/how_to/#langchain-expression-language-lcel). This means they support `invoke`, `stream`, `batch`, `streamLog` calls.\n", + "Output parsers implement the [Runnable interface](/docs/how_to/#langchain-expression-language-lcel), the basic building block of [LangChain Expression Language (LCEL)](/docs/how_to/#langchain-expression-language-lcel). This means they support `invoke`, `stream`, `batch`, `streamLog` calls.\n", "\n", "## Validation\n", "\n", diff --git a/docs/core_docs/docs/how_to/qa_chat_history_how_to.ipynb b/docs/core_docs/docs/how_to/qa_chat_history_how_to.ipynb index cead1afc3fa2..4ed8e4bd849e 100644 --- a/docs/core_docs/docs/how_to/qa_chat_history_how_to.ipynb +++ b/docs/core_docs/docs/how_to/qa_chat_history_how_to.ipynb @@ -4,24 +4,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# How to add chat history to a question-answering chain\n", + "# How to add chat history\n", "\n", - ":::info Prerequisites\n", "\n", - "This guide assumes familiarity with the following:\n", + ":::note\n", "\n", - "- [Retrieval-augmented generation](/docs/tutorials/rag/)\n", + "This tutorial previously built a chatbot using [RunnableWithMessageHistory](https://api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html). You can access this version of the tutorial in the [v0.2 docs](https://js.langchain.com/v0.2/docs/how_to/qa_chat_history_how_to/).\n", + "\n", + "The LangGraph implementation offers a number of advantages over `RunnableWithMessageHistory`, including the ability to persist arbitrary components of an application's state (instead of only messages).\n", "\n", ":::\n", "\n", "In many Q&A applications we want to allow the user to have a back-and-forth conversation, meaning the application needs some sort of \"memory\" of past questions and answers, and some logic for incorporating those into its current thinking.\n", "\n", - "In this guide we focus on **adding logic for incorporating historical messages, and NOT on chat history management.** Chat history management is [covered here](/docs/how_to/message_history).\n", + "In this guide we focus on **adding logic for incorporating historical messages.**\n", + "\n", + "This is largely a condensed version of the [Conversational RAG tutorial](/docs/tutorials/qa_chat_history).\n", + "\n", + "We will cover two approaches:\n", "\n", - "We'll work off of the Q&A app we built over the [LLM Powered Autonomous Agents](https://lilianweng.github.io/posts/2023-06-23-agent/) blog post by Lilian Weng. We'll need to update two things about our existing app:\n", + "1. [Chains](/docs/how_to/qa_chat_history_how_to#chains), in which we always execute a retrieval step;\n", + "2. [Agents](/docs/how_to/qa_chat_history_how_to#agents), in which we give an LLM discretion over whether and how to execute a retrieval step (or multiple steps).\n", "\n", - "1. **Prompt**: Update our prompt to support historical messages as an input.\n", - "2. **Contextualizing questions**: Add a sub-chain that takes the latest user question and reformulates it in the context of the chat history. This is needed in case the latest question references some context from past messages. For example, if a user asks a follow-up question like \"Can you elaborate on the second point?\", this cannot be understood without the context of the previous message. Therefore we can't effectively perform retrieval with a question like this." + "For the external knowledge source, we will use the same [LLM Powered Autonomous Agents](https://lilianweng.github.io/posts/2023-06-23-agent/) blog post by Lilian Weng from the [RAG tutorial](/docs/tutorials/rag)." ] }, { @@ -36,7 +41,7 @@ "We’ll use the following packages:\n", "\n", "```bash\n", - "npm install --save langchain @langchain/openai cheerio\n", + "npm install --save langchain @langchain/openai langchain cheerio uuid\n", "```\n", "\n", "We need to set environment variable `OPENAI_API_KEY`:\n", @@ -66,6 +71,43 @@ "```" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chains {#chains}\n", + "\n", + "In a conversational RAG application, queries issued to the retriever should be informed by the context of the conversation. LangChain provides a [createHistoryAwareRetriever](https://api.js.langchain.com/functions/langchain.chains_history_aware_retriever.createHistoryAwareRetriever.html) constructor to simplify this. It constructs a chain that accepts keys `input` and `chat_history` as input, and has the same output schema as a retriever. `createHistoryAwareRetriever` requires as inputs: \n", + "\n", + "1. LLM;\n", + "2. Retriever;\n", + "3. Prompt.\n", + "\n", + "First we obtain these objects:\n", + "\n", + "### LLM\n", + "\n", + "We can use any supported chat model:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\"\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "// @lc-docs-hide-cell\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const llm = new ChatOpenAI({ model: \"gpt-4o\" });" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -75,21 +117,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "import \"cheerio\";\n", "import { CheerioWebBaseLoader } from \"@langchain/community/document_loaders/web/cheerio\";\n", "import { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n", "import { MemoryVectorStore } from \"langchain/vectorstores/memory\"\n", - "import { OpenAIEmbeddings, ChatOpenAI } from \"@langchain/openai\";\n", - "import { pull } from \"langchain/hub\";\n", - "import { ChatPromptTemplate } from \"@langchain/core/prompts\";\n", - "import { RunnableSequence, RunnablePassthrough } from \"@langchain/core/runnables\";\n", - "import { StringOutputParser } from \"@langchain/core/output_parsers\";\n", - "\n", - "import { createStuffDocumentsChain } from \"langchain/chains/combine_documents\";\n", + "import { OpenAIEmbeddings } from \"@langchain/openai\";\n", "\n", "const loader = new CheerioWebBaseLoader(\n", " \"https://lilianweng.github.io/posts/2023-06-23-agent/\"\n", @@ -102,14 +137,112 @@ "const vectorStore = await MemoryVectorStore.fromDocuments(splits, new OpenAIEmbeddings());\n", "\n", "// Retrieve and generate using the relevant snippets of the blog.\n", - "const retriever = vectorStore.asRetriever();\n", - "// Tip - you can edit this!\n", - "const prompt = await pull(\"rlm/rag-prompt\");\n", - "const llm = new ChatOpenAI({ model: \"gpt-3.5-turbo\", temperature: 0 });\n", - "const ragChain = await createStuffDocumentsChain({\n", + "const retriever = vectorStore.asRetriever();" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Prompt\n", + "\n", + "We'll use a prompt that includes a `MessagesPlaceholder` variable under the name \"chat_history\". This allows us to pass in a list of Messages to the prompt using the \"chat_history\" input key, and these messages will be inserted after the system message and before the human message containing the latest question." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\";\n", + "\n", + "const contextualizeQSystemPrompt = (\n", + " \"Given a chat history and the latest user question \" +\n", + " \"which might reference context in the chat history, \" +\n", + " \"formulate a standalone question which can be understood \" +\n", + " \"without the chat history. Do NOT answer the question, \" +\n", + " \"just reformulate it if needed and otherwise return it as is.\"\n", + ")\n", + "\n", + "const contextualizeQPrompt = ChatPromptTemplate.fromMessages(\n", + " [\n", + " [\"system\", contextualizeQSystemPrompt],\n", + " new MessagesPlaceholder(\"chat_history\"),\n", + " [\"human\", \"{input}\"],\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Assembling the chain\n", + "\n", + "We can then instantiate the history-aware retriever:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import { createHistoryAwareRetriever } from \"langchain/chains/history_aware_retriever\";\n", + "\n", + "const historyAwareRetriever = await createHistoryAwareRetriever({\n", " llm,\n", - " prompt,\n", - " outputParser: new StringOutputParser(),\n", + " retriever,\n", + " rephrasePrompt: contextualizeQPrompt\n", + "});\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This chain prepends a rephrasing of the input query to our retriever, so that the retrieval incorporates the context of the conversation.\n", + "\n", + "Now we can build our full QA chain.\n", + "\n", + "As in the [RAG tutorial](/docs/tutorials/rag), we will use [createStuffDocumentsChain](https://api.js.langchain.com/functions/langchain.chains_combine_documents.createStuffDocumentsChain.html) to generate a `questionAnswerChain`, with input keys `context`, `chat_history`, and `input`-- it accepts the retrieved context alongside the conversation history and query to generate an answer.\n", + "\n", + "We build our final `ragChain` with [createRetrievalChain](https://api.js.langchain.com/functions/langchain.chains_retrieval.createRetrievalChain.html). This chain applies the `historyAwareRetriever` and `questionAnswerChain` in sequence, retaining intermediate outputs such as the retrieved context for convenience. It has input keys `input` and `chat_history`, and includes `input`, `chat_history`, `context`, and `answer` in its output." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import { createStuffDocumentsChain } from \"langchain/chains/combine_documents\";\n", + "import { createRetrievalChain } from \"langchain/chains/retrieval\";\n", + "\n", + "const systemPrompt = \n", + " \"You are an assistant for question-answering tasks. \" +\n", + " \"Use the following pieces of retrieved context to answer \" +\n", + " \"the question. If you don't know the answer, say that you \" +\n", + " \"don't know. Use three sentences maximum and keep the \" +\n", + " \"answer concise.\" +\n", + " \"\\n\\n\" +\n", + " \"{context}\";\n", + "\n", + "const qaPrompt = ChatPromptTemplate.fromMessages([\n", + " [\"system\", systemPrompt],\n", + " new MessagesPlaceholder(\"chat_history\"),\n", + " [\"human\", \"{input}\"],\n", + "]);\n", + "\n", + "const questionAnswerChain = await createStuffDocumentsChain({\n", + " llm,\n", + " prompt: qaPrompt,\n", + "});\n", + "\n", + "const ragChain = await createRetrievalChain({\n", + " retriever: historyAwareRetriever,\n", + " combineDocsChain: questionAnswerChain,\n", "});" ] }, @@ -117,278 +250,885 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's see what this prompt actually looks like" + "### Stateful Management of chat history\n", + "\n", + "We have added application logic for incorporating chat history, but we are still manually plumbing it through our application. In production, the Q&A application we usually persist the chat history into a database, and be able to read and update it appropriately.\n", + "\n", + "[LangGraph](https://langchain-ai.github.io/langgraphjs/) implements a built-in [persistence layer](https://langchain-ai.github.io/langgraphjs/concepts/persistence/), making it ideal for chat applications that support multiple conversational turns.\n", + "\n", + "Wrapping our chat model in a minimal LangGraph application allows us to automatically persist the message history, simplifying the development of multi-turn applications.\n", + "\n", + "LangGraph comes with a simple [in-memory checkpointer](https://langchain-ai.github.io/langgraphjs/reference/classes/checkpoint.MemorySaver.html), which we use below. See its documentation for more detail, including how to use different persistence backends (e.g., SQLite or Postgres).\n", + "\n", + "For a detailed walkthrough of how to manage message history, head to the How to add message history (memory) guide." ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import { AIMessage, BaseMessage, HumanMessage } from \"@langchain/core/messages\";\n", + "import { StateGraph, START, END, MemorySaver, messagesStateReducer, Annotation } from \"@langchain/langgraph\";\n", + "\n", + "// Define the State interface\n", + "const GraphAnnotation = Annotation.Root({\n", + " input: Annotation(),\n", + " chat_history: Annotation({\n", + " reducer: messagesStateReducer,\n", + " default: () => [],\n", + " }),\n", + " context: Annotation(),\n", + " answer: Annotation(),\n", + "})\n", + "\n", + "// Define the call_model function\n", + "async function callModel(state: typeof GraphAnnotation.State) {\n", + " const response = await ragChain.invoke(state);\n", + " return {\n", + " chat_history: [\n", + " new HumanMessage(state.input),\n", + " new AIMessage(response.answer),\n", + " ],\n", + " context: response.context,\n", + " answer: response.answer,\n", + " };\n", + "}\n", + "\n", + "// Create the workflow\n", + "const workflow = new StateGraph(GraphAnnotation)\n", + " .addNode(\"model\", callModel)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "// Compile the graph with a checkpointer object\n", + "const memory = new MemorySaver();\n", + "const app = workflow.compile({ checkpointer: memory });" + ] + }, + { + "cell_type": "code", + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\n", - "Question: {question} \n", - "Context: {context} \n", - "Answer:\n" + "Task Decomposition is the process of breaking down a complicated task into smaller, simpler, and more manageable steps. Techniques like Chain of Thought (CoT) and Tree of Thoughts expand on this by enabling agents to think step by step or explore multiple reasoning possibilities at each step. This allows for a more structured and interpretable approach to handling complex tasks.\n" ] } ], "source": [ - "console.log(prompt.promptMessages.map((msg) => msg.prompt.template).join(\"\\n\"));" + "import { v4 as uuidv4 } from \"uuid\";\n", + "\n", + "const threadId = uuidv4();\n", + "const config = { configurable: { thread_id: threadId } };\n", + "\n", + "const result = await app.invoke(\n", + " { input: \"What is Task Decomposition?\" },\n", + " config,\n", + ")\n", + "console.log(result.answer);" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 10, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"Task Decomposition involves breaking down complex tasks into smaller and simpler steps to make them \"\u001b[39m... 243 more characters" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "One way of doing task decomposition is by using an LLM with simple prompting, such as asking \"Steps for XYZ.\\n1.\" or \"What are the subgoals for achieving XYZ?\" This method leverages direct prompts to guide the model in breaking down tasks.\n" + ] } ], "source": [ - "await ragChain.invoke({\n", - " context: await retriever.invoke(\"What is Task Decomposition?\"),\n", - " question: \"What is Task Decomposition?\"\n", - "});" + "const result2 = await app.invoke(\n", + " { input: \"What is one way of doing it?\" },\n", + " config,\n", + ")\n", + "console.log(result2.answer);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Contextualizing the question\n", + "The conversation history can be inspected via the state of the application:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "HumanMessage {\n", + " \"content\": \"What is Task Decomposition?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + "}\n", + "AIMessage {\n", + " \"content\": \"Task Decomposition is the process of breaking down a complicated task into smaller, simpler, and more manageable steps. Techniques like Chain of Thought (CoT) and Tree of Thoughts expand on this by enabling agents to think step by step or explore multiple reasoning possibilities at each step. This allows for a more structured and interpretable approach to handling complex tasks.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + "}\n", + "HumanMessage {\n", + " \"content\": \"What is one way of doing it?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + "}\n", + "AIMessage {\n", + " \"content\": \"One way of doing task decomposition is by using an LLM with simple prompting, such as asking \\\"Steps for XYZ.\\\\n1.\\\" or \\\"What are the subgoals for achieving XYZ?\\\" This method leverages direct prompts to guide the model in breaking down tasks.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + "}\n" + ] + } + ], + "source": [ + "const chatHistory = (await app.getState(config)).values.chat_history;\n", + "for (const message of chatHistory) {\n", + " console.log(message);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tying it together\n", "\n", - "First we'll need to define a sub-chain that takes historical messages and the latest user question, and reformulates the question if it makes reference to any information in the historical information.\n", + "![](../../static/img/conversational_retrieval_chain.png)\n", "\n", - "We'll use a prompt that includes a `MessagesPlaceholder` variable under the name \"chat_history\". This allows us to pass in a list of Messages to the prompt using the \"chat_history\" input key, and these messages will be inserted after the system message and before the human message containing the latest question." + "For convenience, we tie together all of the necessary steps in a single code cell:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Task Decomposition is the process of breaking a complicated task into smaller, simpler steps to enhance model performance on complex tasks. Techniques like Chain of Thought (CoT) and Tree of Thoughts (ToT) are used for this, with CoT focusing on step-by-step thinking and ToT exploring multiple reasoning possibilities at each step. Decomposition can be carried out by the LLM itself, using task-specific instructions, or through human inputs.\n", + "One way of doing task decomposition is by prompting the LLM with simple instructions such as \"Steps for XYZ.\\n1.\" or \"What are the subgoals for achieving XYZ?\" This encourages the model to break down the task into smaller, manageable steps on its own.\n" + ] + } + ], "source": [ + "import { CheerioWebBaseLoader } from \"@langchain/community/document_loaders/web/cheerio\";\n", + "import { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n", + "import { MemoryVectorStore } from \"langchain/vectorstores/memory\"\n", + "import { OpenAIEmbeddings, ChatOpenAI } from \"@langchain/openai\";\n", "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\";\n", + "import { createHistoryAwareRetriever } from \"langchain/chains/history_aware_retriever\";\n", + "import { createStuffDocumentsChain } from \"langchain/chains/combine_documents\";\n", + "import { createRetrievalChain } from \"langchain/chains/retrieval\";\n", + "import { AIMessage, BaseMessage, HumanMessage } from \"@langchain/core/messages\";\n", + "import { StateGraph, START, END, MemorySaver, messagesStateReducer, Annotation } from \"@langchain/langgraph\";\n", + "import { v4 as uuidv4 } from \"uuid\";\n", "\n", - "const contextualizeQSystemPrompt = `Given a chat history and the latest user question\n", - "which might reference context in the chat history, formulate a standalone question\n", - "which can be understood without the chat history. Do NOT answer the question,\n", - "just reformulate it if needed and otherwise return it as is.`;\n", + "const llm2 = new ChatOpenAI({ model: \"gpt-4o\" });\n", "\n", - "const contextualizeQPrompt = ChatPromptTemplate.fromMessages([\n", - " [\"system\", contextualizeQSystemPrompt],\n", + "const loader2 = new CheerioWebBaseLoader(\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\"\n", + ");\n", + "\n", + "const docs2 = await loader2.load();\n", + "\n", + "const textSplitter2 = new RecursiveCharacterTextSplitter({ chunkSize: 1000, chunkOverlap: 200 });\n", + "const splits2 = await textSplitter2.splitDocuments(docs2);\n", + "const vectorStore2 = await MemoryVectorStore.fromDocuments(splits2, new OpenAIEmbeddings());\n", + "\n", + "// Retrieve and generate using the relevant snippets of the blog.\n", + "const retriever2 = vectorStore2.asRetriever();\n", + "\n", + "const contextualizeQSystemPrompt2 =\n", + " \"Given a chat history and the latest user question \" +\n", + " \"which might reference context in the chat history, \" +\n", + " \"formulate a standalone question which can be understood \" +\n", + " \"without the chat history. Do NOT answer the question, \" +\n", + " \"just reformulate it if needed and otherwise return it as is.\";\n", + "\n", + "const contextualizeQPrompt2 = ChatPromptTemplate.fromMessages(\n", + " [\n", + " [\"system\", contextualizeQSystemPrompt2],\n", + " new MessagesPlaceholder(\"chat_history\"),\n", + " [\"human\", \"{input}\"],\n", + " ]\n", + ")\n", + "\n", + "const historyAwareRetriever2 = await createHistoryAwareRetriever({\n", + " llm: llm2,\n", + " retriever: retriever2,\n", + " rephrasePrompt: contextualizeQPrompt2\n", + "});\n", + "\n", + "const systemPrompt2 = \n", + " \"You are an assistant for question-answering tasks. \" +\n", + " \"Use the following pieces of retrieved context to answer \" +\n", + " \"the question. If you don't know the answer, say that you \" +\n", + " \"don't know. Use three sentences maximum and keep the \" +\n", + " \"answer concise.\" +\n", + " \"\\n\\n\" +\n", + " \"{context}\";\n", + "\n", + "const qaPrompt2 = ChatPromptTemplate.fromMessages([\n", + " [\"system\", systemPrompt2],\n", " new MessagesPlaceholder(\"chat_history\"),\n", - " [\"human\", \"{question}\"]\n", + " [\"human\", \"{input}\"],\n", "]);\n", - "const contextualizeQChain = contextualizeQPrompt.pipe(llm).pipe(new StringOutputParser());" + "\n", + "const questionAnswerChain2 = await createStuffDocumentsChain({\n", + " llm: llm2,\n", + " prompt: qaPrompt2,\n", + "});\n", + "\n", + "const ragChain2 = await createRetrievalChain({\n", + " retriever: historyAwareRetriever2,\n", + " combineDocsChain: questionAnswerChain2,\n", + "});\n", + "\n", + "// Define the State interface\n", + "const GraphAnnotation2 = Annotation.Root({\n", + " input: Annotation(),\n", + " chat_history: Annotation({\n", + " reducer: messagesStateReducer,\n", + " default: () => [],\n", + " }),\n", + " context: Annotation(),\n", + " answer: Annotation(),\n", + "})\n", + "\n", + "// Define the call_model function\n", + "async function callModel2(state: typeof GraphAnnotation2.State) {\n", + " const response = await ragChain2.invoke(state);\n", + " return {\n", + " chat_history: [\n", + " new HumanMessage(state.input),\n", + " new AIMessage(response.answer),\n", + " ],\n", + " context: response.context,\n", + " answer: response.answer,\n", + " };\n", + "}\n", + "\n", + "// Create the workflow\n", + "const workflow2 = new StateGraph(GraphAnnotation2)\n", + " .addNode(\"model\", callModel2)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "// Compile the graph with a checkpointer object\n", + "const memory2 = new MemorySaver();\n", + "const app2 = workflow2.compile({ checkpointer: memory2 });\n", + "\n", + "const threadId2 = uuidv4();\n", + "const config2 = { configurable: { thread_id: threadId2 } };\n", + "\n", + "const result3 = await app2.invoke(\n", + " { input: \"What is Task Decomposition?\" },\n", + " config2,\n", + ")\n", + "console.log(result3.answer);\n", + "\n", + "const result4 = await app2.invoke(\n", + " { input: \"What is one way of doing it?\" },\n", + " config2,\n", + ")\n", + "console.log(result4.answer);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Using this chain we can ask follow-up questions that reference past messages and have them reformulated into standalone questions:" + "## Agents {#agents}\n", + "\n", + "Agents leverage the reasoning capabilities of LLMs to make decisions during execution. Using agents allow you to offload some discretion over the retrieval process. Although their behavior is less predictable than chains, they offer some advantages in this context:\n", + "- Agents generate the input to the retriever directly, without necessarily needing us to explicitly build in contextualization, as we did above;\n", + "- Agents can execute multiple retrieval steps in service of a query, or refrain from executing a retrieval step altogether (e.g., in response to a generic greeting from a user).\n", + "\n", + "### Retrieval tool\n", + "\n", + "Agents can access \"tools\" and manage their execution. In this case, we will convert our retriever into a LangChain tool to be wielded by the agent:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 13, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[32m'What is the definition of \"large\" in this context?'\u001b[39m" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "import { AIMessage, HumanMessage } from \"@langchain/core/messages\";\n", + "import { createRetrieverTool } from \"langchain/tools/retriever\";\n", "\n", - "await contextualizeQChain.invoke({\n", - " chat_history: [\n", - " new HumanMessage(\"What does LLM stand for?\"),\n", - " new AIMessage(\"Large language model\") \n", - " ],\n", - " question: \"What is meant by large\",\n", - "})" + "const tool = createRetrieverTool(\n", + " retriever,\n", + " {\n", + " name: \"blog_post_retriever\",\n", + " description: \"Searches and returns excerpts from the Autonomous Agents blog post.\",\n", + " }\n", + ")\n", + "const tools = [tool]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Chain with chat history\n", + "### Agent constructor\n", "\n", - "And now we can build our full QA chain. \n", + "Now that we have defined the tools and the LLM, we can create the agent. We will be using [LangGraph](/docs/concepts/#langgraph) to construct the agent. \n", + "Currently we are using a high level interface to construct the agent, but the nice thing about LangGraph is that this high-level interface is backed by a low-level, highly controllable API in case you want to modify the agent logic." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "import { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n", "\n", - "Notice we add some routing functionality to only run the \"condense question chain\" when our chat history isn't empty. Here we're taking advantage of the fact that if a function in an LCEL chain returns another chain, that chain will itself be invoked." + "const agentExecutor = createReactAgent({ llm, tools })" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now try it out. Note that so far it is not stateful (we still need to add in memory)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: \"Task decomposition involves breaking down a complex task into smaller and simpler steps to make it m\"... 358 more characters,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: undefined, tool_calls: undefined },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"Task decomposition involves breaking down a complex task into smaller and simpler steps to make it m\"... 358 more characters,\n", - " name: undefined,\n", - " additional_kwargs: { function_call: undefined, tool_calls: undefined },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: 83, promptTokens: 701, totalTokens: 784 },\n", - " finish_reason: \"stop\"\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}\n" + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-AB7xlcJBGSKSp1GvgDY9FP8KvXxwB\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_Ev0nA6nzGwOeMC5upJUUxTuw\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 19,\n", + " \"promptTokens\": 66,\n", + " \"totalTokens\": 85\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"blog_post_retriever\",\n", + " \"args\": {\n", + " \"query\": \"Task Decomposition\"\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_Ev0nA6nzGwOeMC5upJUUxTuw\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 66,\n", + " \"output_tokens\": 19,\n", + " \"total_tokens\": 85\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " tools: {\n", + " messages: [\n", + " ToolMessage {\n", + " \"content\": \"Fig. 1. Overview of a LLM-powered autonomous agent system.\\nComponent One: Planning#\\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\\nTask Decomposition#\\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\\nTree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\\n\\nTask decomposition can be done (1) by LLM with simple prompting like \\\"Steps for XYZ.\\\\n1.\\\", \\\"What are the subgoals for achieving XYZ?\\\", (2) by using task-specific instructions; e.g. \\\"Write a story outline.\\\" for writing a novel, or (3) with human inputs.\\nAnother quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\\nSelf-Reflection#\\n\\nAgent System Overview\\n \\n Component One: Planning\\n \\n \\n Task Decomposition\\n \\n Self-Reflection\\n \\n \\n Component Two: Memory\\n \\n \\n Types of Memory\\n \\n Maximum Inner Product Search (MIPS)\\n \\n \\n Component Three: Tool Use\\n \\n Case Studies\\n \\n \\n Scientific Discovery Agent\\n \\n Generative Agents Simulation\\n \\n Proof-of-Concept Examples\\n \\n \\n Challenges\\n \\n Citation\\n \\n References\\n\\n(3) Task execution: Expert models execute on the specific tasks and log results.\\nInstruction:\\n\\nWith the input and the inference results, the AI assistant needs to describe the process and results. The previous stages can be formed as - User Input: {{ User Input }}, Task Planning: {{ Tasks }}, Model Selection: {{ Model Assignment }}, Task Execution: {{ Predictions }}. You must first answer the user's request in a straightforward manner. Then describe the task process and show your analysis and model inference results to the user in the first person. If inference results contain a file path, must tell the user the complete file path.\",\n", + " \"name\": \"blog_post_retriever\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_Ev0nA6nzGwOeMC5upJUUxTuw\"\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-AB7xmiPNPbMX2KvZKHM2oPfcoFMnY\",\n", + " \"content\": \"**Task Decomposition** involves breaking down a complicated or large task into smaller, more manageable subtasks. Here are some insights based on current techniques and research:\\n\\n1. **Chain of Thought (CoT)**:\\n - Introduced by Wei et al. (2022), this technique prompts the model to \\\"think step by step\\\".\\n - It helps decompose hard tasks into several simpler steps.\\n - Enhances the interpretability of the model's thought process.\\n\\n2. **Tree of Thoughts (ToT)**:\\n - An extension of CoT by Yao et al. (2023).\\n - Decomposes problems into multiple thought steps and generates several possibilities at each step.\\n - Utilizes tree structures through BFS (Breadth-First Search) or DFS (Depth-First Search) with evaluation by a classifier or majority vote.\\n\\n3. **Methods of Task Decomposition**:\\n - **Simple Prompting**: Asking the model directly, e.g., \\\"Steps for XYZ.\\\\n1.\\\" or \\\"What are the subgoals for achieving XYZ?\\\".\\n - **Task-Specific Instructions**: Tailoring instructions to the task, such as \\\"Write a story outline\\\" for writing a novel.\\n - **Human Inputs**: Receiving inputs from humans to refine the process.\\n\\n4. **LLM+P Approach**:\\n - Suggested by Liu et al. (2023), combines language models with an external classical planner.\\n - Uses Planning Domain Definition Language (PDDL) for long-horizon planning:\\n 1. Translates the problem into a PDDL problem.\\n 2. Requests an external planner to generate a PDDL plan.\\n 3. Translates the PDDL plan back into natural language.\\n - This method offloads the planning complexity to a specialized tool, especially relevant for domains utilizing robotic setups.\\n\\nTask Decomposition is a fundamental component of planning in autonomous agent systems, aiding in the efficient accomplishment of complex tasks by breaking them into smaller, actionable steps.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 411,\n", + " \"promptTokens\": 732,\n", + " \"totalTokens\": 1143\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 732,\n", + " \"output_tokens\": 411,\n", + " \"total_tokens\": 1143\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" ] - }, - { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Common ways of task decomposition include using simple prompting techniques like Chain of Thought (C\"\u001b[39m... 353 more characters,\n", - " tool_calls: [],\n", - " invalid_tool_calls: [],\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {}\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Common ways of task decomposition include using simple prompting techniques like Chain of Thought (C\"\u001b[39m... 353 more characters,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m },\n", - " response_metadata: {\n", - " tokenUsage: { completionTokens: \u001b[33m81\u001b[39m, promptTokens: \u001b[33m779\u001b[39m, totalTokens: \u001b[33m860\u001b[39m },\n", - " finish_reason: \u001b[32m\"stop\"\u001b[39m\n", - " },\n", - " tool_calls: [],\n", - " invalid_tool_calls: []\n", - "}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ - "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\"\n", - "import { RunnablePassthrough, RunnableSequence } from \"@langchain/core/runnables\";\n", - "import { formatDocumentsAsString } from \"langchain/util/document\";\n", + "const query = \"What is Task Decomposition?\"\n", "\n", - "const qaSystemPrompt = `You are an assistant for question-answering tasks.\n", - "Use the following pieces of retrieved context to answer the question.\n", - "If you don't know the answer, just say that you don't know.\n", - "Use three sentences maximum and keep the answer concise.\n", + "for await (const s of await agentExecutor.stream(\n", + " { messages: [{ role: \"user\", content: query }] },\n", + ")){\n", + " console.log(s)\n", + " console.log(\"----\")\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "LangGraph comes with built in persistence, so we don't need to use `ChatMessageHistory`! Rather, we can pass in a checkpointer to our LangGraph agent directly.\n", "\n", - "{context}`\n", + "Distinct conversations are managed by specifying a key for a conversation thread in the config object, as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "import { MemorySaver } from \"@langchain/langgraph\";\n", "\n", - "const qaPrompt = ChatPromptTemplate.fromMessages([\n", - " [\"system\", qaSystemPrompt],\n", - " new MessagesPlaceholder(\"chat_history\"),\n", - " [\"human\", \"{question}\"]\n", - "]);\n", + "const memory3 = new MemorySaver();\n", "\n", - "const contextualizedQuestion = (input: Record) => {\n", - " if (\"chat_history\" in input) {\n", - " return contextualizeQChain;\n", - " }\n", - " return input.question;\n", - "};\n", - "\n", - "const ragChain = RunnableSequence.from([\n", - " RunnablePassthrough.assign({\n", - " context: async (input: Record) => {\n", - " if (\"chat_history\" in input) {\n", - " const chain = contextualizedQuestion(input);\n", - " return chain.pipe(retriever).pipe(formatDocumentsAsString);\n", - " }\n", - " return \"\";\n", - " },\n", - " }),\n", - " qaPrompt,\n", - " llm\n", - "]);\n", + "const agentExecutor2 = createReactAgent({ llm, tools, checkpointSaver: memory3 })" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is all we need to construct a conversational RAG agent.\n", "\n", - "const chat_history = [];\n", + "Let's observe its behavior. Note that if we input a query that does not require a retrieval step, the agent does not execute one:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-AB7y8P8AGHkxOwKpwMc3qj6r0skYr\",\n", + " \"content\": \"Hello, Bob! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 12,\n", + " \"promptTokens\": 64,\n", + " \"totalTokens\": 76\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 64,\n", + " \"output_tokens\": 12,\n", + " \"total_tokens\": 76\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" + ] + } + ], + "source": [ + "const threadId3 = uuidv4();\n", + "const config3 = { configurable: { thread_id: threadId3 } };\n", "\n", - "const question = \"What is task decomposition?\";\n", - "const aiMsg = await ragChain.invoke({ question, chat_history });\n", + "for await (const s of await agentExecutor2.stream({ messages: [{ role: \"user\", content: \"Hi! I'm bob\" }] }, config3)) {\n", + " console.log(s)\n", + " console.log(\"----\")\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Further, if we input a query that does require a retrieval step, the agent generates the input to the tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-AB7y8Do2IHJ2rnUvvMU3pTggmuZud\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_3tSaOZ3xdKY4miIJdvBMR80V\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 19,\n", + " \"promptTokens\": 89,\n", + " \"totalTokens\": 108\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"blog_post_retriever\",\n", + " \"args\": {\n", + " \"query\": \"Task Decomposition\"\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_3tSaOZ3xdKY4miIJdvBMR80V\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 89,\n", + " \"output_tokens\": 19,\n", + " \"total_tokens\": 108\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " tools: {\n", + " messages: [\n", + " ToolMessage {\n", + " \"content\": \"Fig. 1. Overview of a LLM-powered autonomous agent system.\\nComponent One: Planning#\\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\\nTask Decomposition#\\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\\nTree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\\n\\nTask decomposition can be done (1) by LLM with simple prompting like \\\"Steps for XYZ.\\\\n1.\\\", \\\"What are the subgoals for achieving XYZ?\\\", (2) by using task-specific instructions; e.g. \\\"Write a story outline.\\\" for writing a novel, or (3) with human inputs.\\nAnother quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\\nSelf-Reflection#\\n\\nAgent System Overview\\n \\n Component One: Planning\\n \\n \\n Task Decomposition\\n \\n Self-Reflection\\n \\n \\n Component Two: Memory\\n \\n \\n Types of Memory\\n \\n Maximum Inner Product Search (MIPS)\\n \\n \\n Component Three: Tool Use\\n \\n Case Studies\\n \\n \\n Scientific Discovery Agent\\n \\n Generative Agents Simulation\\n \\n Proof-of-Concept Examples\\n \\n \\n Challenges\\n \\n Citation\\n \\n References\\n\\n(3) Task execution: Expert models execute on the specific tasks and log results.\\nInstruction:\\n\\nWith the input and the inference results, the AI assistant needs to describe the process and results. The previous stages can be formed as - User Input: {{ User Input }}, Task Planning: {{ Tasks }}, Model Selection: {{ Model Assignment }}, Task Execution: {{ Predictions }}. You must first answer the user's request in a straightforward manner. Then describe the task process and show your analysis and model inference results to the user in the first person. If inference results contain a file path, must tell the user the complete file path.\",\n", + " \"name\": \"blog_post_retriever\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_3tSaOZ3xdKY4miIJdvBMR80V\"\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-AB7y9tpoTvM3lsrhoxCWkkerk9fb2\",\n", + " \"content\": \"Task decomposition is a methodology used to break down complex tasks into smaller, more manageable steps. Here’s an overview of various approaches to task decomposition:\\n\\n1. **Chain of Thought (CoT)**: This technique prompts a model to \\\"think step by step,\\\" which aids in transforming big tasks into multiple smaller tasks. This method enhances the model’s performance on complex tasks by making the problem more manageable and interpretable.\\n\\n2. **Tree of Thoughts (ToT)**: An extension of Chain of Thought, this approach explores multiple reasoning possibilities at each step, effectively creating a tree structure. The search process can be carried out using Breadth-First Search (BFS) or Depth-First Search (DFS), with each state evaluated by either a classifier or a majority vote.\\n\\n3. **Simple Prompting**: Involves straightforward instructions to decompose a task, such as starting with \\\"Steps for XYZ. 1.\\\" or asking \\\"What are the subgoals for achieving XYZ?\\\". This can also include task-specific instructions like \\\"Write a story outline\\\" for writing a novel.\\n\\n4. **LLM+P**: Combines Large Language Models (LLMs) with an external classical planner. The problem is translated into a Planning Domain Definition Language (PDDL) format, an external planner generates a plan, and then the plan is translated back into natural language. This approach highlights a synergy between modern AI techniques and traditional planning strategies.\\n\\nThese approaches allow complex problems to be approached and solved more efficiently by focusing on manageable sub-tasks.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 311,\n", + " \"promptTokens\": 755,\n", + " \"totalTokens\": 1066\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 755,\n", + " \"output_tokens\": 311,\n", + " \"total_tokens\": 1066\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" + ] + } + ], + "source": [ + "const query2 = \"What is Task Decomposition?\"\n", "\n", - "console.log(aiMsg)\n", + "for await (const s of await agentExecutor2.stream({ messages: [{ role: \"user\", content: query2 }] }, config3)) {\n", + " console.log(s)\n", + " console.log(\"----\")\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Above, instead of inserting our query verbatim into the tool, the agent stripped unnecessary words like \"what\" and \"is\".\n", "\n", - "chat_history.push(aiMsg);\n", + "This same principle allows the agent to use the context of the conversation when necessary:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-AB7yDE4rCOXTPZ3595GknUgVzASmt\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_cWnDZq2aloVtMB4KjZlTxHmZ\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 21,\n", + " \"promptTokens\": 1089,\n", + " \"totalTokens\": 1110\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"blog_post_retriever\",\n", + " \"args\": {\n", + " \"query\": \"common ways of task decomposition\"\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_cWnDZq2aloVtMB4KjZlTxHmZ\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 1089,\n", + " \"output_tokens\": 21,\n", + " \"total_tokens\": 1110\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " tools: {\n", + " messages: [\n", + " ToolMessage {\n", + " \"content\": \"Fig. 1. Overview of a LLM-powered autonomous agent system.\\nComponent One: Planning#\\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\\nTask Decomposition#\\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\\nTree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\\n\\nTask decomposition can be done (1) by LLM with simple prompting like \\\"Steps for XYZ.\\\\n1.\\\", \\\"What are the subgoals for achieving XYZ?\\\", (2) by using task-specific instructions; e.g. \\\"Write a story outline.\\\" for writing a novel, or (3) with human inputs.\\nAnother quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\\nSelf-Reflection#\\n\\nAgent System Overview\\n \\n Component One: Planning\\n \\n \\n Task Decomposition\\n \\n Self-Reflection\\n \\n \\n Component Two: Memory\\n \\n \\n Types of Memory\\n \\n Maximum Inner Product Search (MIPS)\\n \\n \\n Component Three: Tool Use\\n \\n Case Studies\\n \\n \\n Scientific Discovery Agent\\n \\n Generative Agents Simulation\\n \\n Proof-of-Concept Examples\\n \\n \\n Challenges\\n \\n Citation\\n \\n References\\n\\nResources:\\n1. Internet access for searches and information gathering.\\n2. Long Term memory management.\\n3. GPT-3.5 powered Agents for delegation of simple tasks.\\n4. File output.\\n\\nPerformance Evaluation:\\n1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities.\\n2. Constructively self-criticize your big-picture behavior constantly.\\n3. Reflect on past decisions and strategies to refine your approach.\\n4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.\",\n", + " \"name\": \"blog_post_retriever\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_cWnDZq2aloVtMB4KjZlTxHmZ\"\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-AB7yGASxz0Z0g2jiCxwx4gYHYJTi4\",\n", + " \"content\": \"According to the blog post, there are several common methods of task decomposition:\\n\\n1. **Simple Prompting by LLMs**: This involves straightforward instructions to decompose a task. Examples include:\\n - \\\"Steps for XYZ. 1.\\\"\\n - \\\"What are the subgoals for achieving XYZ?\\\"\\n - Task-specific instructions like \\\"Write a story outline\\\" for writing a novel.\\n\\n2. **Human Inputs**: Decomposition can be guided by human insights and instructions.\\n\\n3. **Chain of Thought (CoT)**: This technique prompts a model to think step-by-step, enabling it to break down complex tasks into smaller, more manageable tasks. CoT has become a standard method to enhance model performance on intricate tasks.\\n\\n4. **Tree of Thoughts (ToT)**: An extension of CoT, this approach decomposes the problem into multiple thought steps and generates several thoughts per step, forming a tree structure. The search process can be performed using Breadth-First Search (BFS) or Depth-First Search (DFS), with each state evaluated by a classifier or through a majority vote.\\n\\n5. **LLM+P (Large Language Model plus Planner)**: This method integrates LLMs with an external classical planner. It involves:\\n - Translating the problem into “Problem PDDL” (Planning Domain Definition Language).\\n - Using an external planner to generate a PDDL plan based on an existing “Domain PDDL”.\\n - Translating the PDDL plan back into natural language.\\n \\nBy utilizing these methods, tasks can be effectively decomposed into more manageable parts, allowing for more efficient problem-solving and planning.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 334,\n", + " \"promptTokens\": 1746,\n", + " \"totalTokens\": 2080\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_52a7f40b0b\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 1746,\n", + " \"output_tokens\": 334,\n", + " \"total_tokens\": 2080\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" + ] + } + ], + "source": [ + "const query3 = \"What according to the blog post are common ways of doing it? redo the search\"\n", "\n", - "const secondQuestion = \"What are common ways of doing it?\";\n", - "await ragChain.invoke({ question: secondQuestion, chat_history });" + "for await (const s of await agentExecutor2.stream({ messages: [{ role: \"user\", content: query3 }] }, config3)) {\n", + " console.log(s)\n", + " console.log(\"----\")\n", + "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "See the first [LangSmith trace here](https://smith.langchain.com/public/527981c6-5018-4b68-a11a-ebcde77843e7/r) and the [second trace here](https://smith.langchain.com/public/7b97994a-ab9f-4bf3-a2e4-abb609e5610a/r)" + "Note that the agent was able to infer that \"it\" in our query refers to \"task decomposition\", and generated a reasonable search query as a result-- in this case, \"common ways of task decomposition\"." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tying it together\n", + "\n", + "For convenience, we tie together all of the necessary steps in a single code cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "import { createRetrieverTool } from \"langchain/tools/retriever\";\n", + "import { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n", + "import { MemorySaver } from \"@langchain/langgraph\";\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "import { CheerioWebBaseLoader } from \"@langchain/community/document_loaders/web/cheerio\";\n", + "import { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n", + "import { MemoryVectorStore } from \"langchain/vectorstores/memory\"\n", + "import { OpenAIEmbeddings } from \"@langchain/openai\";\n", + "\n", + "const llm3 = new ChatOpenAI({ model: \"gpt-4o\" });\n", + "\n", + "const loader3 = new CheerioWebBaseLoader(\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\"\n", + ");\n", + "\n", + "const docs3 = await loader3.load();\n", + "\n", + "const textSplitter3 = new RecursiveCharacterTextSplitter({ chunkSize: 1000, chunkOverlap: 200 });\n", + "const splits3 = await textSplitter3.splitDocuments(docs3);\n", + "const vectorStore3 = await MemoryVectorStore.fromDocuments(splits3, new OpenAIEmbeddings());\n", + "\n", + "// Retrieve and generate using the relevant snippets of the blog.\n", + "const retriever3 = vectorStore3.asRetriever();\n", + "\n", + "const tool2 = createRetrieverTool(\n", + " retriever3,\n", + " {\n", + " name: \"blog_post_retriever\",\n", + " description: \"Searches and returns excerpts from the Autonomous Agents blog post.\",\n", + " }\n", + ")\n", + "const tools2 = [tool2]\n", + "const memory4 = new MemorySaver();\n", + "\n", + "const agentExecutor3 = createReactAgent({ llm: llm3, tools: tools2, checkpointSaver: memory4 })" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here we've gone over how to add application logic for incorporating historical outputs, but we're still manually updating the chat history and inserting it into each input. In a real Q&A application we'll want some way of persisting chat history and some way of automatically inserting and updating it.\n", + "## Next steps\n", + "\n", + "We've covered the steps to build a basic conversational Q&A application:\n", "\n", - "For this we can use:\n", + "- We used chains to build a predictable application that generates search queries for each user input;\n", + "- We used agents to build an application that \"decides\" when and how to generate search queries.\n", "\n", - "- [BaseChatMessageHistory](https://api.js.langchain.com/classes/langchain_core.chat_history.BaseChatMessageHistory.html): Store chat history.\n", - "- [RunnableWithMessageHistory](/docs/how_to/message_history/): Wrapper for an LCEL chain and a `BaseChatMessageHistory` that handles injecting chat history into inputs and updating it after each invocation.\n", + "To explore different types of retrievers and retrieval strategies, visit the [retrievers](/docs/how_to#retrievers) section of the how-to guides.\n", "\n", - "For a detailed walkthrough of how to use these classes together to create a stateful conversational chain, head to the [How to add message history (memory)](/docs/how_to/message_history/) LCEL page." + "For a detailed walkthrough of LangChain's conversation memory abstractions, visit the [How to add message history (memory)](/docs/how_to/message_history) LCEL page.\n" ] } ], "metadata": { "kernelspec": { - "display_name": "Deno", + "display_name": "TypeScript", "language": "typescript", - "name": "deno" + "name": "tslab" }, "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, "file_extension": ".ts", - "mimetype": "text/x.typescript", + "mimetype": "text/typescript", "name": "typescript", - "nb_converter": "script", - "pygments_lexer": "typescript", - "version": "5.3.3" + "version": "3.7.2" } }, "nbformat": 4, diff --git a/docs/core_docs/docs/how_to/routing.mdx b/docs/core_docs/docs/how_to/routing.mdx index 1a92671ad5ee..be0aa6c3ff53 100644 --- a/docs/core_docs/docs/how_to/routing.mdx +++ b/docs/core_docs/docs/how_to/routing.mdx @@ -65,4 +65,4 @@ import BranchExample from "@examples/guides/expression_language/how_to_routing_r You've now learned how to add routing to your composed LCEL chains. -Next, check out the other [how-to guides on runnables](/docs/how_to/#langchain-expression-language-lcel) in this section. +Next, check out the other [how-to guides on runnables](/docs/how_to/#langchain-expression-language) in this section. diff --git a/docs/core_docs/docs/how_to/streaming.ipynb b/docs/core_docs/docs/how_to/streaming.ipynb index d83b0a29f741..f9817104e5ed 100644 --- a/docs/core_docs/docs/how_to/streaming.ipynb +++ b/docs/core_docs/docs/how_to/streaming.ipynb @@ -11,7 +11,7 @@ "This guide assumes familiarity with the following concepts:\n", "\n", "- [Chat models](/docs/concepts/#chat-models)\n", - "- [LangChain Expression Language](/docs/concepts/#langchain-expression-language-lcel)\n", + "- [LangChain Expression Language](/docs/concepts/#langchain-expression-language)\n", "- [Output parsers](/docs/concepts/#output-parsers)\n", "\n", ":::\n", diff --git a/docs/core_docs/docs/how_to/tool_runtime.ipynb b/docs/core_docs/docs/how_to/tool_runtime.ipynb index ac23dde615b4..4f27f5ef13e4 100644 --- a/docs/core_docs/docs/how_to/tool_runtime.ipynb +++ b/docs/core_docs/docs/how_to/tool_runtime.ipynb @@ -28,17 +28,13 @@ "\n", "Most of the time, such values should not be controlled by the LLM. In fact, allowing the LLM to control the user ID may lead to a security risk.\n", "\n", - "Instead, the LLM should only control the parameters of the tool that are meant to be controlled by the LLM, while other parameters (such as user ID) should be fixed by the application logic.\n", - "\n", - "This how-to guide shows a design pattern that creates the tool dynamically at run time and binds to them appropriate values." + "Instead, the LLM should only control the parameters of the tool that are meant to be controlled by the LLM, while other parameters (such as user ID) should be fixed by the application logic." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can bind them to chat models as follows:\n", - "\n", "```{=mdx}\n", "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", "\n", @@ -48,11 +44,209 @@ "```" ] }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "// @lc-docs-hide-cell\n", + "\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const llm = new ChatOpenAI({ model: \"gpt-4o-mini\" })" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Passing request time information\n", + "## Using context variables\n", + "\n", + "```{=mdx}\n", + ":::caution Compatibility\n", + "This functionality was added in `@langchain/core>=0.3.10`. If you are using the LangSmith SDK separately in your project, we also recommend upgrading to `langsmith>=0.1.65`. Please make sure your packages are up to date.\n", + "\n", + "It also requires [`async_hooks`](https://nodejs.org/api/async_hooks.html) support, which is not supported in all environments.\n", + ":::\n", + "```\n", + "\n", + "One way to solve this problem is by using **context variables**. Context variables are a powerful feature that allows you to set values at a higher level of your application, then access them within child runnable (such as tools) called from that level.\n", + "\n", + "They work outside of traditional scoping rules, so you don't need to have a direct reference to the declared variable to access its value.\n", + "\n", + "Below, we declare a tool that updates a central `userToPets` state based on a context variable called `userId`. Note that this `userId` is not part of the tool schema or input:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "import { getContextVariable } from \"@langchain/core/context\";\n", + "\n", + "let userToPets: Record = {};\n", + "\n", + "const updateFavoritePets = tool(async (input) => {\n", + " const userId = getContextVariable(\"userId\");\n", + " if (userId === undefined) {\n", + " throw new Error(`No \"userId\" found in current context. Remember to call \"setContextVariable('userId', value)\";`);\n", + " }\n", + " userToPets[userId] = input.pets;\n", + " return \"update_favorite_pets called.\"\n", + "}, {\n", + " name: \"update_favorite_pets\",\n", + " description: \"add to the list of favorite pets.\",\n", + " schema: z.object({\n", + " pets: z.array(z.string())\n", + " }),\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you were to invoke the above tool before setting a context variable at a higher level, `userId` would be `undefined`:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Error: No \"userId\" found in current context. Remember to call \"setContextVariable('userId', value)\";\n", + " at updateFavoritePets.name (evalmachine.:14:15)\n", + " at /Users/jacoblee/langchain/langchainjs/langchain-core/dist/tools/index.cjs:329:33\n", + " at AsyncLocalStorage.run (node:async_hooks:346:14)\n", + " at AsyncLocalStorageProvider.runWithConfig (/Users/jacoblee/langchain/langchainjs/langchain-core/dist/singletons/index.cjs:58:24)\n", + " at /Users/jacoblee/langchain/langchainjs/langchain-core/dist/tools/index.cjs:325:68\n", + " at new Promise ()\n", + " at DynamicStructuredTool.func (/Users/jacoblee/langchain/langchainjs/langchain-core/dist/tools/index.cjs:321:20)\n", + " at DynamicStructuredTool._call (/Users/jacoblee/langchain/langchainjs/langchain-core/dist/tools/index.cjs:283:21)\n", + " at DynamicStructuredTool.call (/Users/jacoblee/langchain/langchainjs/langchain-core/dist/tools/index.cjs:111:33)\n", + " at async evalmachine.:3:22\n" + ] + } + ], + "source": [ + "await updateFavoritePets.invoke({ pets: [\"cat\", \"dog\" ]})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead, set a context variable with a parent of where the tools are invoked:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import { setContextVariable } from \"@langchain/core/context\";\n", + "import { BaseChatModel } from \"@langchain/core/language_models/chat_models\";\n", + "import { RunnableLambda } from \"@langchain/core/runnables\";\n", + "\n", + "const handleRunTimeRequestRunnable = RunnableLambda.from(async (params: {\n", + " userId: string;\n", + " query: string;\n", + " llm: BaseChatModel;\n", + "}) => {\n", + " const { userId, query, llm } = params;\n", + " if (!llm.bindTools) {\n", + " throw new Error(\"Language model does not support tools.\");\n", + " }\n", + " // Set a context variable accessible to any child runnables called within this one.\n", + " // You can also set context variables at top level that act as globals.\n", + " setContextVariable(\"userId\", userId);\n", + " const tools = [updateFavoritePets];\n", + " const llmWithTools = llm.bindTools(tools);\n", + " const modelResponse = await llmWithTools.invoke(query);\n", + " // For simplicity, skip checking the tool call's name field and assume\n", + " // that the model is calling the \"updateFavoritePets\" tool\n", + " if (modelResponse.tool_calls.length > 0) {\n", + " return updateFavoritePets.invoke(modelResponse.tool_calls[0]);\n", + " } else {\n", + " return \"No tool invoked.\";\n", + " }\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And when our method invokes the tools, you will see that the tool properly access the previously set `userId` context variable and runs successfully:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ToolMessage {\n", + " \"content\": \"update_favorite_pets called.\",\n", + " \"name\": \"update_favorite_pets\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_vsD2DbSpDquOtmFlOtbUME6h\"\n", + "}\n" + ] + } + ], + "source": [ + "await handleRunTimeRequestRunnable.invoke({\n", + " userId: \"brace\",\n", + " query: \"my favorite animals are cats and parrots.\",\n", + " llm: llm\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And have additionally updated the `userToPets` object with a key matching the `userId` we passed, `\"brace\"`:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{ brace: [ 'cats', 'parrots' ] }\n" + ] + } + ], + "source": [ + "console.log(userToPets);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Without context variables\n", + "\n", + "If you are on an earlier version of core or an environment that does not support `async_hooks`, you can use the following design pattern that creates the tool dynamically at run time and binds to them appropriate values.\n", "\n", "The idea is to create the tool dynamically at request time, and bind to it the appropriate information. For example,\n", "this information may be the user ID as resolved from the request itself." @@ -60,14 +254,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "import { z } from \"zod\";\n", "import { tool } from \"@langchain/core/tools\";\n", "\n", - "const userToPets: Record = {};\n", + "userToPets = {};\n", "\n", "function generateToolsForUser(userId: string) {\n", " const updateFavoritePets = tool(async (input) => {\n", @@ -80,27 +274,8 @@ " pets: z.array(z.string())\n", " }),\n", " });\n", - "\n", - " const deleteFavoritePets = tool(async () => {\n", - " if (userId in userToPets) {\n", - " delete userToPets[userId];\n", - " }\n", - " return \"delete_favorite_pets called.\";\n", - " }, {\n", - " name: \"delete_favorite_pets\",\n", - " description: \"Delete the list of favorite pets.\",\n", - " schema: z.object({}),\n", - " });\n", - "\n", - " const listFavoritePets = tool(async () => {\n", - " return JSON.stringify(userToPets[userId] ?? []);\n", - " }, {\n", - " name: \"list_favorite_pets\",\n", - " description: \"List favorite pets if any.\",\n", - " schema: z.object({}),\n", - " });\n", - "\n", - " return [updateFavoritePets, deleteFavoritePets, listFavoritePets];\n", + " // You can declare and return additional tools as well:\n", + " return [updateFavoritePets];\n", "}" ] }, @@ -108,35 +283,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Verify that the tools work correctly" + "Verify that the tool works correctly" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{ brace: [ 'cat', 'dog' ] }\n", - "[\"cat\",\"dog\"]\n" + "{ cobb: [ 'tiger', 'wolf' ] }\n" ] } ], "source": [ - "const [updatePets, deletePets, listPets] = generateToolsForUser(\"brace\");\n", + "const [updatePets] = generateToolsForUser(\"cobb\");\n", "\n", - "await updatePets.invoke({ pets: [\"cat\", \"dog\"] });\n", + "await updatePets.invoke({ pets: [\"tiger\", \"wolf\"] });\n", "\n", - "console.log(userToPets);\n", - "console.log(await listPets.invoke({}));" + "console.log(userToPets);" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -156,12 +329,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This code will allow the LLM to invoke the tools, but the LLM is **unaware** of the fact that a **user ID** even exists! You can see that `user_id` is not among the params the LLM generates:" + "This code will allow the LLM to invoke the tools, but the LLM is **unaware** of the fact that a **user ID** even exists. You can see that `user_id` is not among the params the LLM generates:" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 11, "metadata": {}, "outputs": [ { @@ -170,16 +343,16 @@ "text": [ "{\n", " name: 'update_favorite_pets',\n", - " args: { pets: [ 'cats', 'parrots' ] },\n", + " args: { pets: [ 'tigers', 'wolves' ] },\n", " type: 'tool_call',\n", - " id: 'call_97h0nQ3B3cr0m58HOwq9ZyUz'\n", + " id: 'call_FBF4D51SkVK2clsLOQHX6wTv'\n", "}\n" ] } ], "source": [ "const aiMessage = await handleRunTimeRequest(\n", - " \"brace\", \"my favorite animals are cats and parrots.\", llm,\n", + " \"cobb\", \"my favorite pets are tigers and wolves.\", llm,\n", ");\n", "console.log(aiMessage.tool_calls[0]);" ] diff --git a/docs/core_docs/docs/integrations/chat/cohere.ipynb b/docs/core_docs/docs/integrations/chat/cohere.ipynb index 375ad61993f9..fd14be747f9f 100644 --- a/docs/core_docs/docs/integrations/chat/cohere.ipynb +++ b/docs/core_docs/docs/integrations/chat/cohere.ipynb @@ -11,6 +11,7 @@ "source": [ "---\n", "sidebar_label: Cohere\n", + "lc_docs_skip_validation: true\n", "---" ] }, @@ -107,6 +108,40 @@ "})" ] }, + { + "cell_type": "markdown", + "id": "cd31a8b7", + "metadata": {}, + "source": [ + "### Custom client for Cohere on Azure, Cohere on AWS Bedrock, and Standalone Cohere Instance.\n", + "\n", + "We can instantiate a custom `CohereClient` and pass it to the ChatCohere constructor.\n", + "\n", + "**Note:** If a custom client is provided both `COHERE_API_KEY` environment variable and `apiKey` parameter in the constructor will be ignored." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d92326b8", + "metadata": {}, + "outputs": [], + "source": [ + "import { ChatCohere } from \"@langchain/cohere\";\n", + "import { CohereClient } from \"cohere-ai\";\n", + "\n", + "const client = new CohereClient({\n", + " token: \"\",\n", + " environment: \"\", //optional\n", + " // other params\n", + "});\n", + "\n", + "const llmWithCustomClient = new ChatCohere({\n", + " client,\n", + " // other params...\n", + "});" + ] + }, { "cell_type": "markdown", "id": "2b4f3e15", diff --git a/docs/core_docs/docs/integrations/chat/groq.ipynb b/docs/core_docs/docs/integrations/chat/groq.ipynb index 4bdc2ea7f6f3..b6a58553ad3b 100644 --- a/docs/core_docs/docs/integrations/chat/groq.ipynb +++ b/docs/core_docs/docs/integrations/chat/groq.ipynb @@ -145,11 +145,11 @@ ], "source": [ "const aiMsg = await llm.invoke([\n", - " [\n", - " \"system\",\n", - " \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n", - " ],\n", - " [\"human\", \"I love programming.\"],\n", + " {\n", + " role: \"system\",\n", + " content: \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n", + " },\n", + " { role: \"user\", content: \"I love programming.\" },\n", "])\n", "aiMsg" ] @@ -174,6 +174,50 @@ "console.log(aiMsg.content)" ] }, + { + "cell_type": "markdown", + "id": "ce0414fe", + "metadata": {}, + "source": [ + "## Json invocation" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3f0a7a2a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " aiInvokeMsgContent: '{\\n\"result\": 6\\n}',\n", + " aiBindMsg: '{\\n\"result\": 6\\n}'\n", + "}\n" + ] + } + ], + "source": [ + "const messages = [\n", + " {\n", + " role: \"system\",\n", + " content: \"You are a math tutor that handles math exercises and makes output in json in format { result: number }.\",\n", + " },\n", + " { role: \"user\", content: \"2 + 2 * 2\" },\n", + "];\n", + "\n", + "const aiInvokeMsg = await llm.invoke(messages, { response_format: { type: \"json_object\" } });\n", + "\n", + "// if you want not to pass response_format in every invoke, you can bind it to the instance\n", + "const llmWithResponseFormat = llm.bind({ response_format: { type: \"json_object\" } });\n", + "const aiBindMsg = await llmWithResponseFormat.invoke(messages);\n", + "\n", + "// they are the same\n", + "console.log({ aiInvokeMsgContent: aiInvokeMsg.content, aiBindMsg: aiBindMsg.content });" + ] + }, { "cell_type": "markdown", "id": "18e2bfc0-7e78-4528-a73f-499ac150dca8", @@ -186,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "e197d1d7-a070-4c96-9f8a-a0e86d046e0b", "metadata": {}, "outputs": [ diff --git a/docs/core_docs/docs/integrations/chat/llama_cpp.mdx b/docs/core_docs/docs/integrations/chat/llama_cpp.mdx index f68a709fb8e4..cd71225ef640 100644 --- a/docs/core_docs/docs/integrations/chat/llama_cpp.mdx +++ b/docs/core_docs/docs/integrations/chat/llama_cpp.mdx @@ -12,14 +12,14 @@ This module is based on the [node-llama-cpp](https://github.com/withcatai/node-l ## Setup -You'll need to install the [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) module to communicate with your local model. +You'll need to install major version `2` of the [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) module to communicate with your local model. import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; ```bash npm2yarn -npm install -S node-llama-cpp @langchain/community @langchain/core +npm install -S node-llama-cpp@2 @langchain/community @langchain/core ``` You will also need a local Llama 2 model (or a model supported by [node-llama-cpp](https://github.com/withcatai/node-llama-cpp)). You will need to pass the path to this model to the LlamaCpp module as a part of the parameters (see example). diff --git a/docs/core_docs/docs/integrations/chat/openai.ipynb b/docs/core_docs/docs/integrations/chat/openai.ipynb index 61dc4103ddd0..083e64b88c27 100644 --- a/docs/core_docs/docs/integrations/chat/openai.ipynb +++ b/docs/core_docs/docs/integrations/chat/openai.ipynb @@ -97,9 +97,9 @@ "import { ChatOpenAI } from \"@langchain/openai\" \n", "\n", "const llm = new ChatOpenAI({\n", - " model: \"gpt-4o\",\n", - " temperature: 0,\n", - " // other params...\n", + " model: \"gpt-4o\",\n", + " temperature: 0,\n", + " // other params...\n", "})" ] }, @@ -124,23 +124,24 @@ "output_type": "stream", "text": [ "AIMessage {\n", - " \"id\": \"chatcmpl-9rB4GvhlRb0x3hxupLBQYOKKmTxvV\",\n", + " \"id\": \"chatcmpl-ADItECqSPuuEuBHHPjeCkh9wIO1H5\",\n", " \"content\": \"J'adore la programmation.\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", - " \"completionTokens\": 8,\n", + " \"completionTokens\": 5,\n", " \"promptTokens\": 31,\n", - " \"totalTokens\": 39\n", + " \"totalTokens\": 36\n", " },\n", - " \"finish_reason\": \"stop\"\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_5796ac6771\"\n", " },\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": [],\n", " \"usage_metadata\": {\n", " \"input_tokens\": 31,\n", - " \"output_tokens\": 8,\n", - " \"total_tokens\": 39\n", + " \"output_tokens\": 5,\n", + " \"total_tokens\": 36\n", " }\n", "}\n" ] @@ -148,11 +149,14 @@ ], "source": [ "const aiMsg = await llm.invoke([\n", - " [\n", - " \"system\",\n", - " \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n", - " ],\n", - " [\"human\", \"I love programming.\"],\n", + " {\n", + " role: \"system\",\n", + " content: \"You are a helpful assistant that translates English to French. Translate the user sentence.\",\n", + " },\n", + " {\n", + " role: \"user\",\n", + " content: \"I love programming.\"\n", + " },\n", "])\n", "aiMsg" ] @@ -196,23 +200,24 @@ "output_type": "stream", "text": [ "AIMessage {\n", - " \"id\": \"chatcmpl-9rB4JD9rVBLzTuMee9AabulowEH0d\",\n", - " \"content\": \"Ich liebe das Programmieren.\",\n", + " \"id\": \"chatcmpl-ADItFaWFNqkSjSmlxeGk6HxcBHzVN\",\n", + " \"content\": \"Ich liebe Programmieren.\",\n", " \"additional_kwargs\": {},\n", " \"response_metadata\": {\n", " \"tokenUsage\": {\n", - " \"completionTokens\": 6,\n", + " \"completionTokens\": 5,\n", " \"promptTokens\": 26,\n", - " \"totalTokens\": 32\n", + " \"totalTokens\": 31\n", " },\n", - " \"finish_reason\": \"stop\"\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_5796ac6771\"\n", " },\n", " \"tool_calls\": [],\n", " \"invalid_tool_calls\": [],\n", " \"usage_metadata\": {\n", " \"input_tokens\": 26,\n", - " \"output_tokens\": 6,\n", - " \"total_tokens\": 32\n", + " \"output_tokens\": 5,\n", + " \"total_tokens\": 31\n", " }\n", "}\n" ] @@ -222,22 +227,22 @@ "import { ChatPromptTemplate } from \"@langchain/core/prompts\"\n", "\n", "const prompt = ChatPromptTemplate.fromMessages(\n", + " [\n", " [\n", - " [\n", - " \"system\",\n", - " \"You are a helpful assistant that translates {input_language} to {output_language}.\",\n", - " ],\n", - " [\"human\", \"{input}\"],\n", - " ]\n", + " \"system\",\n", + " \"You are a helpful assistant that translates {input_language} to {output_language}.\",\n", + " ],\n", + " [\"human\", \"{input}\"],\n", + " ]\n", ")\n", "\n", "const chain = prompt.pipe(llm);\n", "await chain.invoke(\n", - " {\n", - " input_language: \"English\",\n", - " output_language: \"German\",\n", - " input: \"I love programming.\",\n", - " }\n", + " {\n", + " input_language: \"English\",\n", + " output_language: \"German\",\n", + " input: \"I love programming.\",\n", + " }\n", ")" ] }, @@ -384,7 +389,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 8, "id": "2b675330", "metadata": {}, "outputs": [ @@ -396,7 +401,7 @@ " content: [\n", " {\n", " token: 'Hello',\n", - " logprob: -0.0005151443,\n", + " logprob: -0.0004740447,\n", " bytes: [ 72, 101, 108, 108, 111 ],\n", " top_logprobs: []\n", " },\n", @@ -408,25 +413,25 @@ " },\n", " {\n", " token: ' How',\n", - " logprob: -0.000035477897,\n", + " logprob: -0.000030113732,\n", " bytes: [ 32, 72, 111, 119 ],\n", " top_logprobs: []\n", " },\n", " {\n", " token: ' can',\n", - " logprob: -0.0006658526,\n", + " logprob: -0.0004797665,\n", " bytes: [ 32, 99, 97, 110 ],\n", " top_logprobs: []\n", " },\n", " {\n", " token: ' I',\n", - " logprob: -0.0000010280384,\n", + " logprob: -7.89631e-7,\n", " bytes: [ 32, 73 ],\n", " top_logprobs: []\n", " },\n", " {\n", " token: ' assist',\n", - " logprob: -0.10124119,\n", + " logprob: -0.114006,\n", " bytes: [\n", " 32, 97, 115,\n", " 115, 105, 115,\n", @@ -436,23 +441,24 @@ " },\n", " {\n", " token: ' you',\n", - " logprob: -5.5122365e-7,\n", + " logprob: -4.3202e-7,\n", " bytes: [ 32, 121, 111, 117 ],\n", " top_logprobs: []\n", " },\n", " {\n", " token: ' today',\n", - " logprob: -0.000052643223,\n", + " logprob: -0.00004501419,\n", " bytes: [ 32, 116, 111, 100, 97, 121 ],\n", " top_logprobs: []\n", " },\n", " {\n", " token: '?',\n", - " logprob: -0.000012352386,\n", + " logprob: -0.000010206721,\n", " bytes: [ 63 ],\n", " top_logprobs: []\n", " }\n", - " ]\n", + " ],\n", + " refusal: null\n", "}\n" ] } @@ -489,24 +495,26 @@ "id": "3392390e", "metadata": {}, "source": [ - "### ``strict: true``\n", + "## ``strict: true``\n", + "\n", + "As of Aug 6, 2024, OpenAI supports a `strict` argument when calling tools that will enforce that the tool argument schema is respected by the model. See more here: https://platform.openai.com/docs/guides/function-calling.\n", "\n", "```{=mdx}\n", "\n", ":::info Requires ``@langchain/openai >= 0.2.6``\n", "\n", - "As of Aug 6, 2024, OpenAI supports a `strict` argument when calling tools that will enforce that the tool argument schema is respected by the model. See more here: https://platform.openai.com/docs/guides/function-calling\n", - "\n", "**Note**: If ``strict: true`` the tool definition will also be validated, and a subset of JSON schema are accepted. Crucially, schema cannot have optional args (those with default values). Read the full docs on what types of schema are supported here: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas. \n", ":::\n", "\n", "\n", - "```" + "```\n", + "\n", + "Here's an example with tool calling. Passing an extra `strict: true` argument to `.bindTools` will pass the param through to all tool definitions:" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 9, "id": "90f0d465", "metadata": {}, "outputs": [ @@ -517,9 +525,9 @@ "[\n", " {\n", " name: 'get_current_weather',\n", - " args: { location: 'Hanoi' },\n", + " args: { location: 'current' },\n", " type: 'tool_call',\n", - " id: 'call_aB85ybkLCoccpzqHquuJGH3d'\n", + " id: 'call_hVFyYNRwc6CoTgr9AQFQVjm9'\n", " }\n", "]\n" ] @@ -552,6 +560,627 @@ "console.dir(strictTrueResult.tool_calls, { depth: null });" ] }, + { + "cell_type": "markdown", + "id": "6c46a668", + "metadata": {}, + "source": [ + "If you only want to apply this parameter to a select number of tools, you can also pass OpenAI formatted tool schemas directly:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e2da9ead", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\n", + " {\n", + " name: 'get_current_weather',\n", + " args: { location: 'London' },\n", + " type: 'tool_call',\n", + " id: 'call_EOSejtax8aYtqpchY8n8O82l'\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "import { zodToJsonSchema } from \"zod-to-json-schema\";\n", + "\n", + "const toolSchema = {\n", + " type: \"function\",\n", + " function: {\n", + " name: \"get_current_weather\",\n", + " description: \"Get the current weather\",\n", + " strict: true,\n", + " parameters: zodToJsonSchema(\n", + " z.object({\n", + " location: z.string(),\n", + " })\n", + " ),\n", + " },\n", + "};\n", + "\n", + "const llmWithStrictTrueTools = new ChatOpenAI({\n", + " model: \"gpt-4o\",\n", + "}).bindTools([toolSchema], {\n", + " strict: true,\n", + "});\n", + "\n", + "const weatherToolResult = await llmWithStrictTrueTools.invoke([{\n", + " role: \"user\",\n", + " content: \"What is the current weather in London?\"\n", + "}])\n", + "\n", + "weatherToolResult.tool_calls;" + ] + }, + { + "cell_type": "markdown", + "id": "045668fe", + "metadata": {}, + "source": [ + "### Structured output\n", + "\n", + "We can also pass `strict: true` to the [`.withStructuredOutput()`](https://js.langchain.com/docs/how_to/structured_output/#the-.withstructuredoutput-method). Here's an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "8e8171a5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{ traits: [ `6'5\" tall`, 'love fruit' ] }\n" + ] + } + ], + "source": [ + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const traitSchema = z.object({\n", + " traits: z.array(z.string()).describe(\"A list of traits contained in the input\"),\n", + "});\n", + "\n", + "const structuredLlm = new ChatOpenAI({\n", + " model: \"gpt-4o-mini\",\n", + "}).withStructuredOutput(traitSchema, {\n", + " name: \"extract_traits\",\n", + " strict: true,\n", + "});\n", + "\n", + "await structuredLlm.invoke([{\n", + " role: \"user\",\n", + " content: `I am 6'5\" tall and love fruit.`\n", + "}]);" + ] + }, + { + "cell_type": "markdown", + "id": "af20e756", + "metadata": {}, + "source": [ + "## Prompt caching\n", + "\n", + "Newer OpenAI models will automatically [cache parts of your prompt](https://openai.com/index/api-prompt-caching/) if your inputs are above a certain size (1024 tokens at the time of writing) in order to reduce costs for use-cases that require long context.\n", + "\n", + "**Note:** The number of tokens cached for a given query is not yet standardized in `AIMessage.usage_metadata`, and is instead contained in the `AIMessage.response_metadata` field.\n", + "\n", + "Here's an example" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cb4e4fd0", + "metadata": {}, + "outputs": [], + "source": [ + "// @lc-docs-hide-cell\n", + "\n", + "const CACHED_TEXT = `## Components\n", + "\n", + "LangChain provides standard, extendable interfaces and external integrations for various components useful for building with LLMs.\n", + "Some components LangChain implements, some components we rely on third-party integrations for, and others are a mix.\n", + "\n", + "### Chat models\n", + "\n", + "\n", + "\n", + "Language models that use a sequence of messages as inputs and return chat messages as outputs (as opposed to using plain text).\n", + "These are generally newer models (older models are generally \\`LLMs\\`, see below).\n", + "Chat models support the assignment of distinct roles to conversation messages, helping to distinguish messages from the AI, users, and instructions such as system messages.\n", + "\n", + "Although the underlying models are messages in, message out, the LangChain wrappers also allow these models to take a string as input.\n", + "This gives them the same interface as LLMs (and simpler to use).\n", + "When a string is passed in as input, it will be converted to a \\`HumanMessage\\` under the hood before being passed to the underlying model.\n", + "\n", + "LangChain does not host any Chat Models, rather we rely on third party integrations.\n", + "\n", + "We have some standardized parameters when constructing ChatModels:\n", + "\n", + "- \\`model\\`: the name of the model\n", + "\n", + "Chat Models also accept other parameters that are specific to that integration.\n", + "\n", + ":::important\n", + "Some chat models have been fine-tuned for **tool calling** and provide a dedicated API for it.\n", + "Generally, such models are better at tool calling than non-fine-tuned models, and are recommended for use cases that require tool calling.\n", + "Please see the [tool calling section](/docs/concepts/#functiontool-calling) for more information.\n", + ":::\n", + "\n", + "For specifics on how to use chat models, see the [relevant how-to guides here](/docs/how_to/#chat-models).\n", + "\n", + "#### Multimodality\n", + "\n", + "Some chat models are multimodal, accepting images, audio and even video as inputs.\n", + "These are still less common, meaning model providers haven't standardized on the \"best\" way to define the API.\n", + "Multimodal outputs are even less common. As such, we've kept our multimodal abstractions fairly light weight\n", + "and plan to further solidify the multimodal APIs and interaction patterns as the field matures.\n", + "\n", + "In LangChain, most chat models that support multimodal inputs also accept those values in OpenAI's content blocks format.\n", + "So far this is restricted to image inputs. For models like Gemini which support video and other bytes input, the APIs also support the native, model-specific representations.\n", + "\n", + "For specifics on how to use multimodal models, see the [relevant how-to guides here](/docs/how_to/#multimodal).\n", + "\n", + "### LLMs\n", + "\n", + "\n", + "\n", + ":::caution\n", + "Pure text-in/text-out LLMs tend to be older or lower-level. Many popular models are best used as [chat completion models](/docs/concepts/#chat-models),\n", + "even for non-chat use cases.\n", + "\n", + "You are probably looking for [the section above instead](/docs/concepts/#chat-models).\n", + ":::\n", + "\n", + "Language models that takes a string as input and returns a string.\n", + "These are traditionally older models (newer models generally are [Chat Models](/docs/concepts/#chat-models), see above).\n", + "\n", + "Although the underlying models are string in, string out, the LangChain wrappers also allow these models to take messages as input.\n", + "This gives them the same interface as [Chat Models](/docs/concepts/#chat-models).\n", + "When messages are passed in as input, they will be formatted into a string under the hood before being passed to the underlying model.\n", + "\n", + "LangChain does not host any LLMs, rather we rely on third party integrations.\n", + "\n", + "For specifics on how to use LLMs, see the [relevant how-to guides here](/docs/how_to/#llms).\n", + "\n", + "### Message types\n", + "\n", + "Some language models take an array of messages as input and return a message.\n", + "There are a few different types of messages.\n", + "All messages have a \\`role\\`, \\`content\\`, and \\`response_metadata\\` property.\n", + "\n", + "The \\`role\\` describes WHO is saying the message.\n", + "LangChain has different message classes for different roles.\n", + "\n", + "The \\`content\\` property describes the content of the message.\n", + "This can be a few different things:\n", + "\n", + "- A string (most models deal this type of content)\n", + "- A List of objects (this is used for multi-modal input, where the object contains information about that input type and that input location)\n", + "\n", + "#### HumanMessage\n", + "\n", + "This represents a message from the user.\n", + "\n", + "#### AIMessage\n", + "\n", + "This represents a message from the model. In addition to the \\`content\\` property, these messages also have:\n", + "\n", + "**\\`response_metadata\\`**\n", + "\n", + "The \\`response_metadata\\` property contains additional metadata about the response. The data here is often specific to each model provider.\n", + "This is where information like log-probs and token usage may be stored.\n", + "\n", + "**\\`tool_calls\\`**\n", + "\n", + "These represent a decision from an language model to call a tool. They are included as part of an \\`AIMessage\\` output.\n", + "They can be accessed from there with the \\`.tool_calls\\` property.\n", + "\n", + "This property returns a list of \\`ToolCall\\`s. A \\`ToolCall\\` is an object with the following arguments:\n", + "\n", + "- \\`name\\`: The name of the tool that should be called.\n", + "- \\`args\\`: The arguments to that tool.\n", + "- \\`id\\`: The id of that tool call.\n", + "\n", + "#### SystemMessage\n", + "\n", + "This represents a system message, which tells the model how to behave. Not every model provider supports this.\n", + "\n", + "#### ToolMessage\n", + "\n", + "This represents the result of a tool call. In addition to \\`role\\` and \\`content\\`, this message has:\n", + "\n", + "- a \\`tool_call_id\\` field which conveys the id of the call to the tool that was called to produce this result.\n", + "- an \\`artifact\\` field which can be used to pass along arbitrary artifacts of the tool execution which are useful to track but which should not be sent to the model.\n", + "\n", + "#### (Legacy) FunctionMessage\n", + "\n", + "This is a legacy message type, corresponding to OpenAI's legacy function-calling API. \\`ToolMessage\\` should be used instead to correspond to the updated tool-calling API.\n", + "\n", + "This represents the result of a function call. In addition to \\`role\\` and \\`content\\`, this message has a \\`name\\` parameter which conveys the name of the function that was called to produce this result.\n", + "\n", + "### Prompt templates\n", + "\n", + "\n", + "\n", + "Prompt templates help to translate user input and parameters into instructions for a language model.\n", + "This can be used to guide a model's response, helping it understand the context and generate relevant and coherent language-based output.\n", + "\n", + "Prompt Templates take as input an object, where each key represents a variable in the prompt template to fill in.\n", + "\n", + "Prompt Templates output a PromptValue. This PromptValue can be passed to an LLM or a ChatModel, and can also be cast to a string or an array of messages.\n", + "The reason this PromptValue exists is to make it easy to switch between strings and messages.\n", + "\n", + "There are a few different types of prompt templates:\n", + "\n", + "#### String PromptTemplates\n", + "\n", + "These prompt templates are used to format a single string, and generally are used for simpler inputs.\n", + "For example, a common way to construct and use a PromptTemplate is as follows:\n", + "\n", + "\\`\\`\\`typescript\n", + "import { PromptTemplate } from \"@langchain/core/prompts\";\n", + "\n", + "const promptTemplate = PromptTemplate.fromTemplate(\n", + " \"Tell me a joke about {topic}\"\n", + ");\n", + "\n", + "await promptTemplate.invoke({ topic: \"cats\" });\n", + "\\`\\`\\`\n", + "\n", + "#### ChatPromptTemplates\n", + "\n", + "These prompt templates are used to format an array of messages. These \"templates\" consist of an array of templates themselves.\n", + "For example, a common way to construct and use a ChatPromptTemplate is as follows:\n", + "\n", + "\\`\\`\\`typescript\n", + "import { ChatPromptTemplate } from \"@langchain/core/prompts\";\n", + "\n", + "const promptTemplate = ChatPromptTemplate.fromMessages([\n", + " [\"system\", \"You are a helpful assistant\"],\n", + " [\"user\", \"Tell me a joke about {topic}\"],\n", + "]);\n", + "\n", + "await promptTemplate.invoke({ topic: \"cats\" });\n", + "\\`\\`\\`\n", + "\n", + "In the above example, this ChatPromptTemplate will construct two messages when called.\n", + "The first is a system message, that has no variables to format.\n", + "The second is a HumanMessage, and will be formatted by the \\`topic\\` variable the user passes in.\n", + "\n", + "#### MessagesPlaceholder\n", + "\n", + "\n", + "\n", + "This prompt template is responsible for adding an array of messages in a particular place.\n", + "In the above ChatPromptTemplate, we saw how we could format two messages, each one a string.\n", + "But what if we wanted the user to pass in an array of messages that we would slot into a particular spot?\n", + "This is how you use MessagesPlaceholder.\n", + "\n", + "\\`\\`\\`typescript\n", + "import {\n", + " ChatPromptTemplate,\n", + " MessagesPlaceholder,\n", + "} from \"@langchain/core/prompts\";\n", + "import { HumanMessage } from \"@langchain/core/messages\";\n", + "\n", + "const promptTemplate = ChatPromptTemplate.fromMessages([\n", + " [\"system\", \"You are a helpful assistant\"],\n", + " new MessagesPlaceholder(\"msgs\"),\n", + "]);\n", + "\n", + "promptTemplate.invoke({ msgs: [new HumanMessage({ content: \"hi!\" })] });\n", + "\\`\\`\\`\n", + "\n", + "This will produce an array of two messages, the first one being a system message, and the second one being the HumanMessage we passed in.\n", + "If we had passed in 5 messages, then it would have produced 6 messages in total (the system message plus the 5 passed in).\n", + "This is useful for letting an array of messages be slotted into a particular spot.\n", + "\n", + "An alternative way to accomplish the same thing without using the \\`MessagesPlaceholder\\` class explicitly is:\n", + "\n", + "\\`\\`\\`typescript\n", + "const promptTemplate = ChatPromptTemplate.fromMessages([\n", + " [\"system\", \"You are a helpful assistant\"],\n", + " [\"placeholder\", \"{msgs}\"], // <-- This is the changed part\n", + "]);\n", + "\\`\\`\\`\n", + "\n", + "For specifics on how to use prompt templates, see the [relevant how-to guides here](/docs/how_to/#prompt-templates).\n", + "\n", + "### Example Selectors\n", + "\n", + "One common prompting technique for achieving better performance is to include examples as part of the prompt.\n", + "This gives the language model concrete examples of how it should behave.\n", + "Sometimes these examples are hardcoded into the prompt, but for more advanced situations it may be nice to dynamically select them.\n", + "Example Selectors are classes responsible for selecting and then formatting examples into prompts.\n", + "\n", + "For specifics on how to use example selectors, see the [relevant how-to guides here](/docs/how_to/#example-selectors).\n", + "\n", + "### Output parsers\n", + "\n", + "\n", + "\n", + ":::note\n", + "\n", + "The information here refers to parsers that take a text output from a model try to parse it into a more structured representation.\n", + "More and more models are supporting function (or tool) calling, which handles this automatically.\n", + "It is recommended to use function/tool calling rather than output parsing.\n", + "See documentation for that [here](/docs/concepts/#function-tool-calling).\n", + "\n", + ":::\n", + "\n", + "Responsible for taking the output of a model and transforming it to a more suitable format for downstream tasks.\n", + "Useful when you are using LLMs to generate structured data, or to normalize output from chat models and LLMs.\n", + "\n", + "There are two main methods an output parser must implement:\n", + "\n", + "- \"Get format instructions\": A method which returns a string containing instructions for how the output of a language model should be formatted.\n", + "- \"Parse\": A method which takes in a string (assumed to be the response from a language model) and parses it into some structure.\n", + "\n", + "And then one optional one:\n", + "\n", + "- \"Parse with prompt\": A method which takes in a string (assumed to be the response from a language model) and a prompt (assumed to be the prompt that generated such a response) and parses it into some structure. The prompt is largely provided in the event the OutputParser wants to retry or fix the output in some way, and needs information from the prompt to do so.\n", + "\n", + "Output parsers accept a string or \\`BaseMessage\\` as input and can return an arbitrary type.\n", + "\n", + "LangChain has many different types of output parsers. This is a list of output parsers LangChain supports. The table below has various pieces of information:\n", + "\n", + "**Name**: The name of the output parser\n", + "\n", + "**Supports Streaming**: Whether the output parser supports streaming.\n", + "\n", + "**Input Type**: Expected input type. Most output parsers work on both strings and messages, but some (like OpenAI Functions) need a message with specific arguments.\n", + "\n", + "**Output Type**: The output type of the object returned by the parser.\n", + "\n", + "**Description**: Our commentary on this output parser and when to use it.\n", + "\n", + "The current date is ${new Date().toISOString()}`;\n", + "\n", + "// Noop statement to hide output\n", + "void 0;" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7a43595c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "USAGE: {\n", + " prompt_tokens: 2624,\n", + " completion_tokens: 263,\n", + " total_tokens: 2887,\n", + " prompt_tokens_details: { cached_tokens: 0 },\n", + " completion_tokens_details: { reasoning_tokens: 0 }\n", + "}\n" + ] + } + ], + "source": [ + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const modelWithCaching = new ChatOpenAI({\n", + " model: \"gpt-4o-mini-2024-07-18\",\n", + "});\n", + "\n", + "// CACHED_TEXT is some string longer than 1024 tokens\n", + "const LONG_TEXT = `You are a pirate. Always respond in pirate dialect.\n", + "\n", + "Use the following as context when answering questions:\n", + "\n", + "${CACHED_TEXT}`;\n", + "\n", + "const longMessages = [\n", + " {\n", + " role: \"system\",\n", + " content: LONG_TEXT,\n", + " },\n", + " {\n", + " role: \"user\",\n", + " content: \"What types of messages are supported in LangChain?\",\n", + " },\n", + "];\n", + "\n", + "const originalRes = await modelWithCaching.invoke(longMessages);\n", + "\n", + "console.log(\"USAGE:\", originalRes.response_metadata.usage);" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "76c8005e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "USAGE: {\n", + " prompt_tokens: 2624,\n", + " completion_tokens: 272,\n", + " total_tokens: 2896,\n", + " prompt_tokens_details: { cached_tokens: 2432 },\n", + " completion_tokens_details: { reasoning_tokens: 0 }\n", + "}\n" + ] + } + ], + "source": [ + "const resWitCaching = await modelWithCaching.invoke(longMessages);\n", + "\n", + "console.log(\"USAGE:\", resWitCaching.response_metadata.usage);" + ] + }, + { + "cell_type": "markdown", + "id": "cc8b3c94", + "metadata": {}, + "source": [ + "## Audio output\n", + "\n", + "Some OpenAI models (such as `gpt-4o-audio-preview`) support generating audio output. This example shows how to use that feature:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b4d579b7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " id: 'audio_67129e9466f48190be70372922464162',\n", + " data: 'UklGRgZ4BABXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguMjkuMTAwAGRhdGHA',\n", + " expires_at: 1729277092,\n", + " transcript: \"Why did the cat sit on the computer's keyboard? Because it wanted to keep an eye on the mouse!\"\n", + "}\n" + ] + } + ], + "source": [ + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const modelWithAudioOutput = new ChatOpenAI({\n", + " model: \"gpt-4o-audio-preview\",\n", + " // You may also pass these fields to `.bind` as a call argument.\n", + " modalities: [\"text\", \"audio\"], // Specifies that the model should output audio.\n", + " audio: {\n", + " voice: \"alloy\",\n", + " format: \"wav\",\n", + " },\n", + "});\n", + "\n", + "const audioOutputResult = await modelWithAudioOutput.invoke(\"Tell me a joke about cats.\");\n", + "const castAudioContent = audioOutputResult.additional_kwargs.audio as Record;\n", + "\n", + "console.log({\n", + " ...castAudioContent,\n", + " data: castAudioContent.data.slice(0, 100) // Sliced for brevity\n", + "})" + ] + }, + { + "cell_type": "markdown", + "id": "bfea3608", + "metadata": {}, + "source": [ + "We see that the audio data is returned inside the `data` field. We are also provided an `expires_at` date field. This field represents the date the audio response will no longer be accessible on the server for use in multi-turn conversations.\n", + "\n", + "### Streaming Audio Output\n", + "\n", + "OpenAI also supports streaming audio output. Here's an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0fa68183", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " id: 'audio_67129e976ce081908103ba4947399a3eaudio_67129e976ce081908103ba4947399a3e',\n", + " transcript: 'Why was the cat sitting on the computer? Because it wanted to keep an eye on the mouse!',\n", + " index: 0,\n", + " data: 'CgAGAAIADAAAAA0AAwAJAAcACQAJAAQABQABAAgABQAPAAAACAADAAUAAwD8/wUA+f8MAPv/CAD7/wUA///8/wUA/f8DAPj/AgD6',\n", + " expires_at: 1729277096\n", + "}\n" + ] + } + ], + "source": [ + "import { AIMessageChunk } from \"@langchain/core/messages\";\n", + "import { concat } from \"@langchain/core/utils/stream\"\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const modelWithStreamingAudioOutput = new ChatOpenAI({\n", + " model: \"gpt-4o-audio-preview\",\n", + " modalities: [\"text\", \"audio\"],\n", + " audio: {\n", + " voice: \"alloy\",\n", + " format: \"pcm16\", // Format must be `pcm16` for streaming\n", + " },\n", + "});\n", + "\n", + "const audioOutputStream = await modelWithStreamingAudioOutput.stream(\"Tell me a joke about cats.\");\n", + "let finalAudioOutputMsg: AIMessageChunk | undefined;\n", + "for await (const chunk of audioOutputStream) {\n", + " finalAudioOutputMsg = finalAudioOutputMsg ? concat(finalAudioOutputMsg, chunk) : chunk;\n", + "}\n", + "const castStreamedAudioContent = finalAudioOutputMsg?.additional_kwargs.audio as Record;\n", + "\n", + "console.log({\n", + " ...castStreamedAudioContent,\n", + " data: castStreamedAudioContent.data.slice(0, 100) // Sliced for brevity\n", + "})" + ] + }, + { + "cell_type": "markdown", + "id": "e8b84aac", + "metadata": {}, + "source": [ + "### Audio input\n", + "\n", + "These models also support passing audio as input. For this, you must specify `input_audio` fields as seen below:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1a69dad8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "That's a great joke! It's always fun to imagine why cats do the funny things they do. Keeping an eye on the \"mouse\" is a creatively punny way to describe it!\n" + ] + } + ], + "source": [ + "import { HumanMessage } from \"@langchain/core/messages\";\n", + "\n", + "const userInput = new HumanMessage({\n", + " content: [{\n", + " type: \"input_audio\",\n", + " input_audio: {\n", + " data: castAudioContent.data, // Re-use the base64 data from the first example\n", + " format: \"wav\",\n", + " },\n", + " }]\n", + "})\n", + "\n", + "// Re-use the same model instance\n", + "const userInputAudioRes = await modelWithAudioOutput.invoke([userInput]);\n", + "\n", + "console.log((userInputAudioRes.additional_kwargs.audio as Record).transcript);" + ] + }, { "cell_type": "markdown", "id": "3a5bb5ca-c3ae-4a58-be67-2cd18574b9a3", diff --git a/docs/core_docs/docs/integrations/document_compressors/cohere_rerank.mdx b/docs/core_docs/docs/integrations/document_compressors/cohere_rerank.mdx index 2114d3d34f3c..af34c3c549dd 100644 --- a/docs/core_docs/docs/integrations/document_compressors/cohere_rerank.mdx +++ b/docs/core_docs/docs/integrations/document_compressors/cohere_rerank.mdx @@ -33,3 +33,11 @@ import ExampleCompressor from "@examples/document_compressors/cohere_rerank_comp From the results, we can see it returned the top 3 documents, and assigned a `relevanceScore` to each. As expected, the document with the highest `relevanceScore` is the one that references Washington, D.C., with a score of `98.7%`! + +### Usage with `CohereClient` + +If you are using Cohere on Azure, AWS Bedrock or a standalone instance you can use the `CohereClient` to create a `CohereRerank` instance with your endpoint. + +import ExampleClient from "@examples/document_compressors/cohere_rerank_custom_client.ts"; + +{ExampleClient} diff --git a/docs/core_docs/docs/integrations/document_loaders/file_loaders/epub.mdx b/docs/core_docs/docs/integrations/document_loaders/file_loaders/epub.mdx index e3bf219617b0..01847609a549 100644 --- a/docs/core_docs/docs/integrations/document_loaders/file_loaders/epub.mdx +++ b/docs/core_docs/docs/integrations/document_loaders/file_loaders/epub.mdx @@ -9,7 +9,7 @@ This example goes over how to load data from EPUB files. By default, one documen # Setup ```bash npm2yarn -npm install @langchian/community @langchain/core epub2 html-to-text +npm install @langchain/community @langchain/core epub2 html-to-text ``` # Usage, one document per chapter diff --git a/docs/core_docs/docs/integrations/document_loaders/web_loaders/apify_dataset.mdx b/docs/core_docs/docs/integrations/document_loaders/web_loaders/apify_dataset.mdx index 4759c110beb1..8e3d4da49562 100644 --- a/docs/core_docs/docs/integrations/document_loaders/web_loaders/apify_dataset.mdx +++ b/docs/core_docs/docs/integrations/document_loaders/web_loaders/apify_dataset.mdx @@ -10,7 +10,7 @@ This guide shows how to use [Apify](https://apify.com) with LangChain to load do ## Overview [Apify](https://apify.com) is a cloud platform for web scraping and data extraction, -which provides an [ecosystem](https://apify.com/store) of more than a thousand +which provides an [ecosystem](https://apify.com/store) of more than two thousand ready-made apps called _Actors_ for various web scraping, crawling, and data extraction use cases. This guide shows how to load documents @@ -19,11 +19,14 @@ storage built for storing structured web scraping results, such as a list of products or Google SERPs, and then export them to various formats like JSON, CSV, or Excel. -Datasets are typically used to save results of Actors. +Datasets are typically used to save results of different Actors. For example, [Website Content Crawler](https://apify.com/apify/website-content-crawler) Actor deeply crawls websites such as documentation, knowledge bases, help centers, or blogs, and then stores the text content of webpages into a dataset, -from which you can feed the documents into a vector index and answer questions from it. +from which you can feed the documents into a vector database and use it for information retrieval. +Another example is the [RAG Web Browser](https://apify.com/apify/rag-web-browser) Actor, +which queries Google Search, scrapes the top N pages from the results, and returns the cleaned +content in Markdown format for further processing by a large language model. ## Setup @@ -38,18 +41,21 @@ import IntegrationInstallTooltip from "@mdx_components/integration_install_toolt ```bash npm2yarn -npm install @langchain/openai @langchain/community @langchain/core +npm install hnswlib-node @langchain/openai @langchain/community @langchain/core ``` -You'll also need to sign up and retrieve your [Apify API token](https://console.apify.com/account/integrations). +You'll also need to sign up and retrieve your [Apify API token](https://console.apify.com/settings/integrations). ## Usage -### From a New Dataset +### From a New Dataset (Crawl a Website and Store the data in Apify Dataset) If you don't already have an existing dataset on the Apify platform, you'll need to initialize the document loader by calling an Actor and waiting for the results. +In the example below, we use the [Website Content Crawler](https://apify.com/apify/website-content-crawler) Actor to crawl +LangChain documentation, store the results in Apify Dataset, and then load the dataset using the `ApifyDatasetLoader`. +For this demonstration, we'll use a fast Cheerio crawler type and limit the number of crawled pages to 10. -**Note:** Calling an Actor can take a significant amount of time, on the order of hours, or even days for large sites! +**Note:** Running the Website Content Crawler may take some time, depending on the size of the website. For large sites, it can take several hours or even days! Here's an example: @@ -60,7 +66,7 @@ import NewExample from "@examples/document_loaders/apify_dataset_new.ts"; ## From an Existing Dataset -If you already have an existing dataset on the Apify platform, you can initialize the document loader with the constructor directly: +If you've already run an Actor and have an existing dataset on the Apify platform, you can initialize the document loader directly using the constructor import ExistingExample from "@examples/document_loaders/apify_dataset_existing.ts"; diff --git a/docs/core_docs/docs/integrations/document_loaders/web_loaders/firecrawl.ipynb b/docs/core_docs/docs/integrations/document_loaders/web_loaders/firecrawl.ipynb index 7f6ca8fa5d96..4126bc084b0a 100644 --- a/docs/core_docs/docs/integrations/document_loaders/web_loaders/firecrawl.ipynb +++ b/docs/core_docs/docs/integrations/document_loaders/web_loaders/firecrawl.ipynb @@ -37,7 +37,7 @@ "\n", "## Setup\n", "\n", - "To access `FireCrawlLoader` document loader you'll need to install the `@langchain/community` integration, and the `@mendable/firecrawl-js` package. Then create a **[FireCrawl](https://firecrawl.dev)** account and get an API key.\n", + "To access `FireCrawlLoader` document loader you'll need to install the `@langchain/community` integration, and the `@mendable/firecrawl-js@0.0.36` package. Then create a **[FireCrawl](https://firecrawl.dev)** account and get an API key.\n", "\n", "### Credentials\n", "\n", @@ -67,7 +67,7 @@ "\n", "\n", "\n", - " @langchain/community @langchain/core @mendable/firecrawl-js\n", + " @langchain/community @langchain/core @mendable/firecrawl-js@0.0.36\n", "\n", "\n", "```" diff --git a/docs/core_docs/docs/integrations/llms/cohere.ipynb b/docs/core_docs/docs/integrations/llms/cohere.ipynb index 63ac3e60d16f..7e4f20391841 100644 --- a/docs/core_docs/docs/integrations/llms/cohere.ipynb +++ b/docs/core_docs/docs/integrations/llms/cohere.ipynb @@ -3,10 +3,15 @@ { "cell_type": "raw", "id": "67db2992", - "metadata": {}, + "metadata": { + "vscode": { + "languageId": "raw" + } + }, "source": [ "---\n", "sidebar_label: Cohere\n", + "lc_docs_skip_validation: true\n", "---" ] }, @@ -106,6 +111,40 @@ "})" ] }, + { + "cell_type": "markdown", + "id": "2518004d", + "metadata": {}, + "source": [ + "### Custom client for Cohere on Azure, Cohere on AWS Bedrock, and Standalone Cohere Instance.\n", + "\n", + "We can instantiate a custom `CohereClient` and pass it to the ChatCohere constructor.\n", + "\n", + "**Note:** If a custom client is provided both `COHERE_API_KEY` environment variable and `apiKey` parameter in the constructor will be ignored." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79da9b26", + "metadata": {}, + "outputs": [], + "source": [ + "import { Cohere } from \"@langchain/cohere\";\n", + "import { CohereClient } from \"cohere-ai\";\n", + "\n", + "const client = new CohereClient({\n", + " token: \"\",\n", + " environment: \"\", //optional\n", + " // other params\n", + "});\n", + "\n", + "const llmWithCustomClient = new Cohere({\n", + " client,\n", + " // other params...\n", + "});" + ] + }, { "cell_type": "markdown", "id": "0ee90032", diff --git a/docs/core_docs/docs/integrations/llms/jigsawstack.mdx b/docs/core_docs/docs/integrations/llms/jigsawstack.mdx new file mode 100644 index 000000000000..1060f55ee323 --- /dev/null +++ b/docs/core_docs/docs/integrations/llms/jigsawstack.mdx @@ -0,0 +1,43 @@ +# JigsawStack Prompt Engine + +LangChain.js supports calling JigsawStack [Prompt Engine](https://docs.jigsawstack.com/api-reference/prompt-engine/run-direct) LLMs. + +## Setup + +- Set up an [account](https://jigsawstack.com/dashboard) (Get started for free) +- Create and retrieve your [API key](https://jigsawstack.com/dashboard) + +## Credentials + +```bash +export JIGSAWSTACK_API_KEY="your-api-key" +``` + +## Usage + +import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; + + + +```bash npm2yarn +npm install @langchain/jigsawstack +``` + +import CodeBlock from "@theme/CodeBlock"; + +```ts +import { JigsawStackPromptEngine } from "@langchain/jigsawstack"; + +export const run = async () => { + const model = new JigsawStackPromptEngine(); + const res = await model.invoke( + "Tell me about the leaning tower of pisa?\nAnswer:" + ); + console.log({ res }); +}; +``` + +## Related + +- LLM [conceptual guide](/docs/concepts/#llms) +- LLM [how-to guides](/docs/how_to/#llms) diff --git a/docs/core_docs/docs/integrations/llms/llama_cpp.mdx b/docs/core_docs/docs/integrations/llms/llama_cpp.mdx index 98371877d8a4..fe0673b79207 100644 --- a/docs/core_docs/docs/integrations/llms/llama_cpp.mdx +++ b/docs/core_docs/docs/integrations/llms/llama_cpp.mdx @@ -12,10 +12,10 @@ This module is based on the [node-llama-cpp](https://github.com/withcatai/node-l ## Setup -You'll need to install the [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) module to communicate with your local model. +You'll need to install major version `2` of the [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) module to communicate with your local model. ```bash npm2yarn -npm install -S node-llama-cpp +npm install -S node-llama-cpp@2 ``` import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; diff --git a/docs/core_docs/docs/integrations/llms/raycast.mdx b/docs/core_docs/docs/integrations/llms/raycast.mdx index 20df4bdfdee6..12ec3de9d69f 100644 --- a/docs/core_docs/docs/integrations/llms/raycast.mdx +++ b/docs/core_docs/docs/integrations/llms/raycast.mdx @@ -17,9 +17,18 @@ npm install @langchain/community @langchain/core ``` import CodeBlock from "@theme/CodeBlock"; -import RaycastAIExample from "@examples/models/llm/raycast.ts"; -{RaycastAIExample} +```ts +import { RaycastAI } from "@langchain/community/llms/raycast"; + +import { Tool } from "@langchain/core/tools"; + +const model = new RaycastAI({ + rateLimitPerMinute: 10, // It is 10 by default so you can omit this line + model: "", + creativity: 0, // `creativity` is a term used by Raycast which is equivalent to `temperature` in some other LLMs +}); +``` ## Related diff --git a/docs/core_docs/docs/integrations/text_embedding/cohere.ipynb b/docs/core_docs/docs/integrations/text_embedding/cohere.ipynb index 764b1e13ac6b..2e0ab4847719 100644 --- a/docs/core_docs/docs/integrations/text_embedding/cohere.ipynb +++ b/docs/core_docs/docs/integrations/text_embedding/cohere.ipynb @@ -11,6 +11,7 @@ "source": [ "---\n", "sidebar_label: Cohere\n", + "lc_docs_skip_validation: true\n", "---" ] }, @@ -91,6 +92,40 @@ "});" ] }, + { + "cell_type": "markdown", + "id": "b6470d5e", + "metadata": {}, + "source": [ + "### Custom client for Cohere on Azure, Cohere on AWS Bedrock, and Standalone Cohere Instance.\n", + "\n", + "We can instantiate a custom `CohereClient` and pass it to the ChatCohere constructor.\n", + "\n", + "**Note:** If a custom client is provided both `COHERE_API_KEY` environment variable and apiKey parameter in the constructor will be ignored" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a241b0f0", + "metadata": {}, + "outputs": [], + "source": [ + "import { CohereEmbeddings } from \"@langchain/cohere\";\n", + "import { CohereClient } from \"cohere-ai\";\n", + "\n", + "const client = new CohereClient({\n", + " token: \"\",\n", + " environment: \"\", //optional\n", + " // other params\n", + "});\n", + "\n", + "const embeddingsWithCustomClient = new CohereEmbeddings({\n", + " client,\n", + " // other params...\n", + "});" + ] + }, { "cell_type": "markdown", "id": "77d271b6", diff --git a/docs/core_docs/docs/integrations/text_embedding/llama_cpp.mdx b/docs/core_docs/docs/integrations/text_embedding/llama_cpp.mdx index 824db38cd589..59349a1e88bc 100644 --- a/docs/core_docs/docs/integrations/text_embedding/llama_cpp.mdx +++ b/docs/core_docs/docs/integrations/text_embedding/llama_cpp.mdx @@ -12,10 +12,10 @@ This module is based on the [node-llama-cpp](https://github.com/withcatai/node-l ## Setup -You'll need to install the [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) module to communicate with your local model. +You'll need to install major version `2` of the [node-llama-cpp](https://github.com/withcatai/node-llama-cpp) module to communicate with your local model. ```bash npm2yarn -npm install -S node-llama-cpp +npm install -S node-llama-cpp@2 ``` import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; diff --git a/docs/core_docs/docs/integrations/tools/jigsawstack.mdx b/docs/core_docs/docs/integrations/tools/jigsawstack.mdx new file mode 100644 index 000000000000..9c96f4c4792c --- /dev/null +++ b/docs/core_docs/docs/integrations/tools/jigsawstack.mdx @@ -0,0 +1,163 @@ +--- +hide_table_of_contents: true +--- + +import CodeBlock from "@theme/CodeBlock"; + +# JigsawStack Tool + +The JigsawStack Tool provides your agent with the following capabilities: + +- JigsawStackAIScrape: Scrape web content using advanced AI. + +- JigsawStackAISearch: Perform AI-powered web searches and retrieve high-quality results. + +- JigsawStackSpeechToText - Transcribe video and audio files using the Whisper large V3 AI model. + +- JigsawStackVOCR - Recognize, describe, and extract data from images using a prompt. + +- JigsawStackTextToSQL - Generate semantically correct SQL queries from text. + +## Setup + +- Set up an [account](https://jigsawstack.com/dashboard) (Get started for free) +- Create and retrieve your [API key](https://jigsawstack.com/dashboard) + +## Credentials + +```bash +export JIGSAWSTACK_API_KEY="your-api-key" +``` + +## Usage, standalone + +import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; + + + +```bash npm2yarn +npm install @langchain/openai +``` + +```js +import { + JigsawStackAIScrape, + JigsawStackAISearch, + JigsawStackSpeechToText, + JigsawStackVOCR, + JigsawStackTextToSQL, +} from "@langchain/jigsawstack"; + +export const run = async () => { + // AI Scrape Tool + const aiScrapeTool = new JigsawStackAIScrape({ + params: { + element_prompts: ["Pro plan"], + }, + }); + const result = await aiScrapeTool.invoke("https://jigsawstack.com/pricing"); + + console.log({ result }); + + // AI Search Tool + + const aiSearchTool = new JigsawStackAISearch(); + const doc = await aiSearchTool.invoke("The leaning tower of pisa"); + console.log({ doc }); + + // VOCR Tool + + const vocrTool = new JigsawStackVOCR({ + params: { + prompt: "Describe the image in detail", + }, + }); + const data = await vocrTool.invoke( + "https://rogilvkqloanxtvjfrkm.supabase.co/storage/v1/object/public/demo/Collabo%201080x842.jpg?t=2024-03-22T09%3A22%3A48.442Z" + ); + + console.log({ data }); + + // Speech-to-Text Tool + const sttTool = new JigsawStackSpeechToText(); + await sttTool.invoke( + "https://rogilvkqloanxtvjfrkm.supabase.co/storage/v1/object/public/demo/Video%201737458382653833217.mp4?t=2024-03-22T09%3A50%3A49.894" + ); + + // Text-to-SQL Tool + const sqlTool = new JigsawStackTextToSQL({ + params: { + sql_schema: + "CREATE TABLE Transactions (transaction_id INT PRIMARY KEY, user_id INT NOT NULL,total_amount DECIMAL(10, 2 NOT NULL, transaction_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,status VARCHAR(20) DEFAULT 'pending',FOREIGN KEY(user_id) REFERENCES Users(user_id))", + }, + }); + + await sqlTool.invoke( + "Generate a query to get transactions that amount exceed 10000 and sort by when created" + ); +}; +``` + +## Usage, in an Agent + +```js +import { ChatOpenAI } from "@langchain/openai"; +import { initializeAgentExecutorWithOptions } from "langchain/agents"; +import { + JigsawStackAIScrape, + JigsawStackAISearch, + JigsawStackVOCR, + JigsawStackSpeechToText, + JigsawStackTextToSQL, +} from "@langchain/jigsawstack"; + +const model = new ChatOpenAI({ + temperature: 0, +}); + +// add the tools that you need +const tools = [ + new JigsawStackAIScrape(), + new JigsawStackAISearch(), + new JigsawStackVOCR(), + new JigsawStackSpeechToText(), + new JigsawStackTextToSQL(), +]; + +const executor = await initializeAgentExecutorWithOptions(tools, model, { + agentType: "zero-shot-react-description", + verbose: true, +}); + +const res = await executor.invoke({ + input: `Kokkalo Restaurant Santorini`, +}); + +console.log(res.output); + +/* +{ + "query": "Kokkalo Restaurant Santorini", + "ai_overview": "Kokkalo Restaurant, located in Fira, Santorini, offers a unique dining experience that blends traditional Greek cuisine with modern culinary trends. Here are some key details about the restaurant:\n\n- **Location**: Situated on the main road of Firostefani, Kokkalo is surrounded by the picturesque Cycladic architecture and provides stunning views of the Aegean Sea.\n- **Cuisine**: The restaurant specializes in authentic Greek dishes, crafted from high-quality, locally sourced ingredients. The menu is designed to engage all senses and features a variety of Mediterranean flavors.\n- **Ambiance**: Kokkalo boasts a chic and modern décor, creating a welcoming atmosphere for guests. The staff is known for their professionalism and attentiveness, enhancing the overall dining experience.\n- **Culinary Experience**: The name \"Kokkalo,\" meaning \"bone\" in Greek, symbolizes the strong foundation of the restaurant's culinary philosophy. Guests can expect a bold and unforgettable culinary journey.\n- **Cooking Classes**: Kokkalo also offers cooking lessons, allowing visitors to learn how to prepare traditional Greek dishes, providing a unique souvenir of their time in Santorini.\n- **Contact Information**: \n - Address: 25 Martiou str, Fira, Santorini 84 700, Cyclades, Greece\n - Phone: +30 22860 25407\n - Email: reservation@kokkalosantorini.com\n\nFor more information, you can visit their [official website](https://www.santorini-view.com/restaurants/kokkalo-restaurant/) or their [Facebook page](https://www.facebook.com/kokkalorestaurant/).", + "is_safe": true, + "results": [ + { + "title": "Kokkalo restaurant, Restaurants in Firostefani Santorini Greece", + "url": "http://www.travel-to-santorini.com/restaurants/firostefani/thebonerestaurant/", + "description": "Details Contact : George Grafakos Address : Firostefani, Opposite of Fira Primary School Zipcode : 84700 City : Santorni Phone : +30 22860 25407 Send an email", + "content": null, + "site_name": "Travel-to-santorini", + "site_long_name": "travel-to-santorini.com", + "language": "en", + "is_safe": true, + "favicon": "https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://travel-to-santorini.com&size=96" + } + ] +} +*/ +``` + +## Related + +- Tool [conceptual guide](/docs/concepts/#tools) +- Tool [how-to guides](/docs/how_to/#tools) diff --git a/docs/core_docs/docs/integrations/vectorstores/couchbase.mdx b/docs/core_docs/docs/integrations/vectorstores/couchbase.mdx index 912638369b7b..d02eba470fba 100644 --- a/docs/core_docs/docs/integrations/vectorstores/couchbase.mdx +++ b/docs/core_docs/docs/integrations/vectorstores/couchbase.mdx @@ -178,11 +178,77 @@ chunk the text into 500 character chunks with no overlaps and index all these ch After the data is indexed, we perform a simple query to find the top 4 chunks that are similar to the query "What did president say about Ketanji Brown Jackson". -At the emd, also shows how to get similarity score +At the end, it also shows how to get similarity score + +```ts +import { OpenAIEmbeddings } from "@langchain/openai"; +import { + CouchbaseVectorStoreArgs, + CouchbaseVectorStore, +} from "@langchain/community/vectorstores/couchbase"; +import { Cluster } from "couchbase"; +import { TextLoader } from "langchain/document_loaders/fs/text"; +import { CharacterTextSplitter } from "@langchain/textsplitters"; + +const connectionString = + process.env.COUCHBASE_DB_CONN_STR ?? "couchbase://localhost"; +const databaseUsername = process.env.COUCHBASE_DB_USERNAME ?? "Administrator"; +const databasePassword = process.env.COUCHBASE_DB_PASSWORD ?? "Password"; + +// Load documents from file +const loader = new TextLoader("./state_of_the_union.txt"); +const rawDocuments = await loader.load(); +const splitter = new CharacterTextSplitter({ + chunkSize: 500, + chunkOverlap: 0, +}); +const docs = await splitter.splitDocuments(rawDocuments); + +const couchbaseClient = await Cluster.connect(connectionString, { + username: databaseUsername, + password: databasePassword, + configProfile: "wanDevelopment", +}); + +// Open AI API Key is required to use OpenAIEmbeddings, some other embeddings may also be used +const embeddings = new OpenAIEmbeddings({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const couchbaseConfig: CouchbaseVectorStoreArgs = { + cluster: couchbaseClient, + bucketName: "testing", + scopeName: "_default", + collectionName: "_default", + indexName: "vector-index", + textKey: "text", + embeddingKey: "embedding", +}; + +const store = await CouchbaseVectorStore.fromDocuments( + docs, + embeddings, + couchbaseConfig +); -import SimilaritySearch from "@examples/indexes/vector_stores/couchbase/similaritySearch.ts"; +const query = "What did president say about Ketanji Brown Jackson"; -{SimilaritySearch} +const resultsSimilaritySearch = await store.similaritySearch(query); +console.log("resulting documents: ", resultsSimilaritySearch[0]); + +// Similarity Search With Score +const resultsSimilaritySearchWithScore = await store.similaritySearchWithScore( + query, + 1 +); +console.log("resulting documents: ", resultsSimilaritySearchWithScore[0][0]); +console.log("resulting scores: ", resultsSimilaritySearchWithScore[0][1]); + +const result = await store.similaritySearch(query, 1, { + fields: ["metadata.source"], +}); +console.log(result[0]); +``` ## Specifying Fields to Return diff --git a/docs/core_docs/docs/integrations/vectorstores/libsql.mdx b/docs/core_docs/docs/integrations/vectorstores/libsql.mdx new file mode 100644 index 000000000000..2d836b54ae04 --- /dev/null +++ b/docs/core_docs/docs/integrations/vectorstores/libsql.mdx @@ -0,0 +1,169 @@ +# libSQL + +[Turso](https://turso.tech) is a SQLite-compatible database built on [libSQL](https://docs.turso.tech/libsql), the Open Contribution fork of SQLite. Vector Similiarity Search is built into Turso and libSQL as a native datatype, enabling you to store and query vectors directly in the database. + +LangChain.js supports using a local libSQL, or remote Turso database as a vector store, and provides a simple API to interact with it. + +This guide provides a quick overview for getting started with libSQL vector stores. For detailed documentation of all libSQL features and configurations head to the API reference. + +## Overview + +## Integration details + +| Class | Package | JS support | Package latest | +| ------------------- | ---------------------- | ---------- | ----------------------------------------------------------------- | +| `LibSQLVectorStore` | `@langchain/community` | ✅ | ![npm version](https://img.shields.io/npm/v/@langchain/community) | + +## Setup + +To use libSQL vector stores, you'll need to create a Turso account or set up a local SQLite database, and install the `@langchain/community` integration package. + +This guide will also use OpenAI embeddings, which require you to install the `@langchain/openai` integration package. You can also use other supported embeddings models if you wish. + +You can use local SQLite when working with the libSQL vector store, or use a hosted Turso Database. + +import IntegrationInstallTooltip from "@mdx_components/integration_install_tooltip.mdx"; + + + +```bash npm2yarn +npm install @libsql/client @langchain/openai @langchain/community +``` + +Now it's time to create a database. You can create one locally, or use a hosted Turso database. + +### Local libSQL + +Create a new local SQLite file and connect to the shell: + +```bash +sqlite3 file.db +``` + +### Hosted Turso + +Visit [sqlite.new](https://sqlite.new) to create a new database, give it a name, and create a database auth token. + +Make sure to copy the database auth token, and the database URL, it should look something like: + +```text +libsql://[database-name]-[your-username].turso.io +``` + +### Setup the table and index + +Execute the following SQL command to create a new table or add the embedding column to an existing table. + +Make sure to mopdify the following parts of the SQL: + +- `TABLE_NAME` is the name of the table you want to create. +- `content` is used to store the `Document.pageContent` values. +- `metadata` is used to store the `Document.metadata` object. +- `EMBEDDING_COLUMN` is used to store the vector values, use the dimensions size used by the model you plan to use (1536 for OpenAI). + +```sql +CREATE TABLE IF NOT EXISTS TABLE_NAME ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT, + metadata TEXT, + EMBEDDING_COLUMN F32_BLOB(1536) -- 1536-dimensional f32 vector for OpenAI +); +``` + +Now create an index on the `EMBEDDING_COLUMN` column: + +```sql +CREATE INDEX IF NOT EXISTS idx_TABLE_NAME_EMBEDDING_COLUMN ON TABLE_NAME(libsql_vector_idx(EMBEDDING_COLUMN)); +``` + +Make sure to replace the `TABLE_NAME` and `EMBEDDING_COLUMN` with the values you used in the previous step. + +## Instantiation + +To initialize a new `LibSQL` vector store, you need to provide the database URL and Auth Token when working remotely, or by passing the filename for a local SQLite. + +```typescript +import { LibSQLVectorStore } from "@langchain/community/vectorstores/libsql"; +import { OpenAIEmbeddings } from "@langchain/openai"; +import { createClient } from "@libsql/client"; + +const embeddings = new OpenAIEmbeddings({ + model: "text-embedding-3-small", +}); + +const libsqlClient = createClient({ + url: "libsql://[database-name]-[your-username].turso.io", + authToken: "...", +}); + +// Local instantiation +// const libsqlClient = createClient({ +// url: "file:./dev.db", +// }); + +const vectorStore = new LibSQLVectorStore(embeddings, { + db: libsqlClient, + tableName: "TABLE_NAME", + embeddingColumn: "EMBEDDING_COLUMN", + dimensions: 1536, +}); +``` + +## Manage vector store + +### Add items to vector store + +```typescript +import type { Document } from "@langchain/core/documents"; + +const documents: Document[] = [ + { pageContent: "Hello", metadata: { topic: "greeting" } }, + { pageContent: "Bye bye", metadata: { topic: "greeting" } }, +]; + +await vectorStore.addDocuments(documents); +``` + +### Delete items from vector store + +```typescript +await vectorStore.deleteDocuments({ ids: [1, 2] }); +``` + +## Query vector store + +Once you have inserted the documents, you can query the vector store. + +### Query directly + +Performing a simple similarity search can be done as follows: + +```typescript +const resultOne = await vectorStore.similaritySearch("hola", 1); + +for (const doc of similaritySearchResults) { + console.log(`${doc.pageContent} [${JSON.stringify(doc.metadata, null)}]`); +} +``` + +For similarity search with scores: + +```typescript +const similaritySearchWithScoreResults = + await vectorStore.similaritySearchWithScore("hola", 1); + +for (const [doc, score] of similaritySearchWithScoreResults) { + console.log( + `${score.toFixed(3)} ${doc.pageContent} [${JSON.stringify(doc.metadata)}` + ); +} +``` + +## API reference + +For detailed documentation of all `LibSQLVectorStore` features and configurations head to the API reference. + +## Related + +- Vector store [conceptual guide](/docs/concepts/#vectorstores) +- Vector store [how-to guides](/docs/how_to/#vectorstores) diff --git a/docs/core_docs/docs/troubleshooting/errors/INVALID_PROMPT_INPUT.mdx b/docs/core_docs/docs/troubleshooting/errors/INVALID_PROMPT_INPUT.mdx new file mode 100644 index 000000000000..95d5c37b1517 --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/INVALID_PROMPT_INPUT.mdx @@ -0,0 +1,57 @@ +# INVALID_PROMPT_INPUT + +A [prompt template](/docs/concepts#prompt-templates) received missing or invalid input variables. + +One unexpected way this can occur is if you add a JSON object directly into a prompt template: + +```ts +import { PromptTemplate } from "@langchain/core/prompts"; +import { ChatOpenAI } from "@langchain/openai"; + +const prompt = PromptTemplate.fromTemplate(`You are a helpful assistant. + +Here is an example of how you should respond: + +{ + "firstName": "John", + "lastName": "Doe", + "age": 21 +} + +Now, answer the following question: + +{question}`); +``` + +You might think that the above prompt template should require a single input key named `question`, but the JSON object will be +interpreted as an additional variable because the curly braces (`{`) are not escaped, and should be preceded by a second brace instead, like this: + +```ts +import { PromptTemplate } from "@langchain/core/prompts"; +import { ChatOpenAI } from "@langchain/openai"; + +const prompt = PromptTemplate.fromTemplate(`You are a helpful assistant. + +Here is an example of how you should respond: + +{{ + "firstName": "John", + "lastName": "Doe", + "age": 21 +}} + +Now, answer the following question: + +{question}`); +``` + +## Troubleshooting + +The following may help resolve this error: + +- Double-check your prompt template to ensure that it is correct. + - If you are using default formatting and you are using curly braces `{` anywhere in your template, they should be double escaped like this: `{{`, as shown above. +- If you are using a [`MessagesPlaceholder`](/docs/concepts/#messagesplaceholder), make sure that you are passing in an array of messages or message-like objects. + - If you are using shorthand tuples to declare your prompt template, make sure that the variable name is wrapped in curly braces (`["placeholder", "{messages}"]`). +- Try viewing the inputs into your prompt template using [LangSmith](https://docs.smith.langchain.com/) or log statements to confirm they appear as expected. +- If you are pulling a prompt from the [LangChain Prompt Hub](https://smith.langchain.com/prompts), try pulling and logging it or running it in isolation with a sample input to confirm that it is what you expect. diff --git a/docs/core_docs/docs/troubleshooting/errors/INVALID_TOOL_RESULTS.ipynb b/docs/core_docs/docs/troubleshooting/errors/INVALID_TOOL_RESULTS.ipynb new file mode 100644 index 000000000000..4047de8f4de9 --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/INVALID_TOOL_RESULTS.ipynb @@ -0,0 +1,448 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# INVALID_TOOL_RESULTS\n", + "\n", + "You are passing too many, too few, or mismatched [`ToolMessages`](https://api.js.langchain.com/classes/_langchain_core.messages_tool.ToolMessage.html) to a model.\n", + "\n", + "When [using a model to call tools](/docs/concepts#functiontool-calling), the [`AIMessage`](https://api.js.langchain.com/classes/_langchain_core.messages.AIMessage.html)\n", + "the model responds with will contain a `tool_calls` array. To continue the flow, the next messages you pass back to the model must\n", + "be exactly one `ToolMessage` for each item in that array containing the result of that tool call. Each `ToolMessage` must have a `tool_call_id` field\n", + "that matches one of the `tool_calls` on the `AIMessage`.\n", + "\n", + "For example, given the following response from a model:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-AIgT1xUd6lkWAutThiiBsqjq7Ykj1\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_BknYpnY7xiARM17TPYqL7luj\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " },\n", + " {\n", + " \"id\": \"call_EHf8MIcTdsLCZcFVlcH4hxJw\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"promptTokens\": 42,\n", + " \"completionTokens\": 37,\n", + " \"totalTokens\": 79\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"usage\": {\n", + " \"prompt_tokens\": 42,\n", + " \"completion_tokens\": 37,\n", + " \"total_tokens\": 79,\n", + " \"prompt_tokens_details\": {\n", + " \"cached_tokens\": 0\n", + " },\n", + " \"completion_tokens_details\": {\n", + " \"reasoning_tokens\": 0\n", + " }\n", + " },\n", + " \"system_fingerprint\": \"fp_e2bde53e6e\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"foo\",\n", + " \"args\": {},\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_BknYpnY7xiARM17TPYqL7luj\"\n", + " },\n", + " {\n", + " \"name\": \"foo\",\n", + " \"args\": {},\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_EHf8MIcTdsLCZcFVlcH4hxJw\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"output_tokens\": 37,\n", + " \"input_tokens\": 42,\n", + " \"total_tokens\": 79,\n", + " \"input_token_details\": {\n", + " \"cache_read\": 0\n", + " },\n", + " \"output_token_details\": {\n", + " \"reasoning\": 0\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "import { BaseMessageLike } from \"@langchain/core/messages\";\n", + "\n", + "const model = new ChatOpenAI({\n", + " model: \"gpt-4o-mini\",\n", + "});\n", + "\n", + "const dummyTool = tool(\n", + " async () => {\n", + " return \"action complete!\";\n", + " },\n", + " {\n", + " name: \"foo\",\n", + " schema: z.object({}),\n", + " }\n", + ");\n", + "\n", + "const modelWithTools = model.bindTools([dummyTool]);\n", + "\n", + "const chatHistory: BaseMessageLike[] = [\n", + " {\n", + " role: \"user\",\n", + " content: `Call tool \"foo\" twice with no arguments`,\n", + " },\n", + "];\n", + "\n", + "const responseMessage = await modelWithTools.invoke(chatHistory);\n", + "\n", + "console.log(responseMessage);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calling the model with only one tool response would result in an error:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "BadRequestError: 400 An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_EHf8MIcTdsLCZcFVlcH4hxJw\n", + " at APIError.generate (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/error.js:45:20)\n", + " at OpenAI.makeStatusError (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/core.js:291:33)\n", + " at OpenAI.makeRequest (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/core.js:335:30)\n", + " at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n", + " at async /Users/jacoblee/langchain/langchainjs/libs/langchain-openai/dist/chat_models.cjs:1441:29\n", + " at async RetryOperation._fn (/Users/jacoblee/langchain/langchainjs/node_modules/p-retry/index.js:50:12) {\n", + " status: 400,\n", + " headers: {\n", + " 'access-control-expose-headers': 'X-Request-ID',\n", + " 'alt-svc': 'h3=\":443\"; ma=86400',\n", + " 'cf-cache-status': 'DYNAMIC',\n", + " 'cf-ray': '8d31d4d95e2a0c96-EWR',\n", + " connection: 'keep-alive',\n", + " 'content-length': '315',\n", + " 'content-type': 'application/json',\n", + " date: 'Tue, 15 Oct 2024 18:21:53 GMT',\n", + " 'openai-organization': 'langchain',\n", + " 'openai-processing-ms': '16',\n", + " 'openai-version': '2020-10-01',\n", + " server: 'cloudflare',\n", + " 'set-cookie': '__cf_bm=e5.GX1bHiMVgr76YSvAKuECCGG7X_RXF0jDGSMXFGfU-1729016513-1.0.1.1-ZBYeVqX.M6jSNJB.wS696fEhX7V.es._M0WcWtQ9Qx8doEA5qMVKNE5iX6i7UKyPCg2GvDfM.MoDwRCXKMSkEA; path=/; expires=Tue, 15-Oct-24 18:51:53 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None, _cfuvid=J8gS08GodUA9hRTYuElen0YOCzMO3d4LW0ZT0k_kyj4-1729016513560-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None',\n", + " 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',\n", + " 'x-content-type-options': 'nosniff',\n", + " 'x-ratelimit-limit-requests': '30000',\n", + " 'x-ratelimit-limit-tokens': '150000000',\n", + " 'x-ratelimit-remaining-requests': '29999',\n", + " 'x-ratelimit-remaining-tokens': '149999967',\n", + " 'x-ratelimit-reset-requests': '2ms',\n", + " 'x-ratelimit-reset-tokens': '0s',\n", + " 'x-request-id': 'req_f810058e7f047fafcb713575c4419161'\n", + " },\n", + " request_id: 'req_f810058e7f047fafcb713575c4419161',\n", + " error: {\n", + " message: \"An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. The following tool_call_ids did not have response messages: call_EHf8MIcTdsLCZcFVlcH4hxJw\",\n", + " type: 'invalid_request_error',\n", + " param: 'messages',\n", + " code: null\n", + " },\n", + " code: null,\n", + " param: 'messages',\n", + " type: 'invalid_request_error',\n", + " attemptNumber: 1,\n", + " retriesLeft: 6\n", + "}\n" + ] + } + ], + "source": [ + "const toolResponse1 = await dummyTool.invoke(responseMessage.tool_calls![0]);\n", + "\n", + "chatHistory.push(responseMessage);\n", + "chatHistory.push(toolResponse1);\n", + "\n", + "await modelWithTools.invoke(chatHistory);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we add a second response, the call will succeed as expected because we now have one tool response per tool call:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-AIgTPDBm1epnnLHx0tPFTgpsf8Ay6\",\n", + " \"content\": \"The tool \\\"foo\\\" was called twice, and both times returned the result: \\\"action complete!\\\".\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"promptTokens\": 98,\n", + " \"completionTokens\": 21,\n", + " \"totalTokens\": 119\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"usage\": {\n", + " \"prompt_tokens\": 98,\n", + " \"completion_tokens\": 21,\n", + " \"total_tokens\": 119,\n", + " \"prompt_tokens_details\": {\n", + " \"cached_tokens\": 0\n", + " },\n", + " \"completion_tokens_details\": {\n", + " \"reasoning_tokens\": 0\n", + " }\n", + " },\n", + " \"system_fingerprint\": \"fp_e2bde53e6e\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"output_tokens\": 21,\n", + " \"input_tokens\": 98,\n", + " \"total_tokens\": 119,\n", + " \"input_token_details\": {\n", + " \"cache_read\": 0\n", + " },\n", + " \"output_token_details\": {\n", + " \"reasoning\": 0\n", + " }\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const toolResponse2 = await dummyTool.invoke(responseMessage.tool_calls![1]);\n", + "\n", + "chatHistory.push(toolResponse2);\n", + "\n", + "await modelWithTools.invoke(chatHistory);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But if we add a duplicate, extra tool response, the call will fail again:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "BadRequestError: 400 Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.\n", + " at APIError.generate (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/error.js:45:20)\n", + " at OpenAI.makeStatusError (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/core.js:291:33)\n", + " at OpenAI.makeRequest (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/core.js:335:30)\n", + " at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n", + " at async /Users/jacoblee/langchain/langchainjs/libs/langchain-openai/dist/chat_models.cjs:1441:29\n", + " at async RetryOperation._fn (/Users/jacoblee/langchain/langchainjs/node_modules/p-retry/index.js:50:12) {\n", + " status: 400,\n", + " headers: {\n", + " 'access-control-expose-headers': 'X-Request-ID',\n", + " 'alt-svc': 'h3=\":443\"; ma=86400',\n", + " 'cf-cache-status': 'DYNAMIC',\n", + " 'cf-ray': '8d31d57dff5e0f3b-EWR',\n", + " connection: 'keep-alive',\n", + " 'content-length': '233',\n", + " 'content-type': 'application/json',\n", + " date: 'Tue, 15 Oct 2024 18:22:19 GMT',\n", + " 'openai-organization': 'langchain',\n", + " 'openai-processing-ms': '36',\n", + " 'openai-version': '2020-10-01',\n", + " server: 'cloudflare',\n", + " 'set-cookie': '__cf_bm=QUsNlSGxVeIbscI0rm2YR3U9aUFLNxxqh1i_3aYBGN4-1729016539-1.0.1.1-sKRUvxHkQXvlb5LaqASkGtIwPMWUF5x9kF0ut8NLP6e0FVKEhdIEkEe6lYA1toW45JGTwp98xahaX7wt9CO4AA; path=/; expires=Tue, 15-Oct-24 18:52:19 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None, _cfuvid=J6fN8u8HUieCeyLDI59mi_0r_W0DgiO207wEtvrmT9Y-1729016539919-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None',\n", + " 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',\n", + " 'x-content-type-options': 'nosniff',\n", + " 'x-ratelimit-limit-requests': '30000',\n", + " 'x-ratelimit-limit-tokens': '150000000',\n", + " 'x-ratelimit-remaining-requests': '29999',\n", + " 'x-ratelimit-remaining-tokens': '149999956',\n", + " 'x-ratelimit-reset-requests': '2ms',\n", + " 'x-ratelimit-reset-tokens': '0s',\n", + " 'x-request-id': 'req_aebfebbb9af2feaf2e9683948e431676'\n", + " },\n", + " request_id: 'req_aebfebbb9af2feaf2e9683948e431676',\n", + " error: {\n", + " message: \"Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.\",\n", + " type: 'invalid_request_error',\n", + " param: 'messages.[4].role',\n", + " code: null\n", + " },\n", + " code: null,\n", + " param: 'messages.[4].role',\n", + " type: 'invalid_request_error',\n", + " attemptNumber: 1,\n", + " retriesLeft: 6\n", + "}\n" + ] + } + ], + "source": [ + "const duplicateToolResponse2 = await dummyTool.invoke(responseMessage.tool_calls![1]);\n", + "\n", + "chatHistory.push(duplicateToolResponse2);\n", + "\n", + "await modelWithTools.invoke(chatHistory);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You should additionally not pass `ToolMessages` back to to a model if they are not preceded by an `AIMessage` with tool calls. For example, this will fail:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "BadRequestError: 400 Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.\n", + " at APIError.generate (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/error.js:45:20)\n", + " at OpenAI.makeStatusError (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/core.js:291:33)\n", + " at OpenAI.makeRequest (/Users/jacoblee/langchain/langchainjs/libs/langchain-openai/node_modules/openai/core.js:335:30)\n", + " at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n", + " at async /Users/jacoblee/langchain/langchainjs/libs/langchain-openai/dist/chat_models.cjs:1441:29\n", + " at async RetryOperation._fn (/Users/jacoblee/langchain/langchainjs/node_modules/p-retry/index.js:50:12) {\n", + " status: 400,\n", + " headers: {\n", + " 'access-control-expose-headers': 'X-Request-ID',\n", + " 'alt-svc': 'h3=\":443\"; ma=86400',\n", + " 'cf-cache-status': 'DYNAMIC',\n", + " 'cf-ray': '8d31d5da7fba19aa-EWR',\n", + " connection: 'keep-alive',\n", + " 'content-length': '233',\n", + " 'content-type': 'application/json',\n", + " date: 'Tue, 15 Oct 2024 18:22:34 GMT',\n", + " 'openai-organization': 'langchain',\n", + " 'openai-processing-ms': '25',\n", + " 'openai-version': '2020-10-01',\n", + " server: 'cloudflare',\n", + " 'set-cookie': '__cf_bm=qK6.PWACr7IYuMafLpxumD4CrFnwHQiJn4TiGkrNTBk-1729016554-1.0.1.1-ECIk0cvh1wOfsK41a1Ce7npngsUDRRG93_yinP4.kVIWu1eX0CFG19iZ8yfGXedyPo6Wh1CKTGLk_3Qwrg.blA; path=/; expires=Tue, 15-Oct-24 18:52:34 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None, _cfuvid=IVTqysqHo4VUVJ.tVTcGg0rnXGWTbSSzX5mcUVrw8BU-1729016554732-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None',\n", + " 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',\n", + " 'x-content-type-options': 'nosniff',\n", + " 'x-ratelimit-limit-requests': '30000',\n", + " 'x-ratelimit-limit-tokens': '150000000',\n", + " 'x-ratelimit-remaining-requests': '29999',\n", + " 'x-ratelimit-remaining-tokens': '149999978',\n", + " 'x-ratelimit-reset-requests': '2ms',\n", + " 'x-ratelimit-reset-tokens': '0s',\n", + " 'x-request-id': 'req_59339f8163ef5bd3f0308a212611dfea'\n", + " },\n", + " request_id: 'req_59339f8163ef5bd3f0308a212611dfea',\n", + " error: {\n", + " message: \"Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'.\",\n", + " type: 'invalid_request_error',\n", + " param: 'messages.[0].role',\n", + " code: null\n", + " },\n", + " code: null,\n", + " param: 'messages.[0].role',\n", + " type: 'invalid_request_error',\n", + " attemptNumber: 1,\n", + " retriesLeft: 6\n", + "}\n" + ] + } + ], + "source": [ + "await modelWithTools.invoke([{\n", + " role: \"tool\",\n", + " content: \"action completed!\",\n", + " tool_call_id: \"dummy\",\n", + "}])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "See [this guide](/docs/how_to/tool_results_pass_to_model/) for more details on tool calling.\n", + "\n", + "## Troubleshooting\n", + "\n", + "The following may help resolve this error:\n", + "\n", + "- If you are using a custom executor rather than a prebuilt one like LangGraph's [`ToolNode`](https://langchain-ai.github.io/langgraphjs/reference/classes/langgraph_prebuilt.ToolNode.html)\n", + " or the legacy LangChain [AgentExecutor](/docs/how_to/agent_executor), verify that you are invoking and returning the result for one tool per tool call.\n", + "- If you are using [few-shot tool call examples](/docs/how_to/tools_few_shot) with messages that you manually create, and you want to simulate a failure,\n", + " you still need to pass back a `ToolMessage` whose content indicates that failure.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "TypeScript", + "language": "typescript", + "name": "tslab" + }, + "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, + "file_extension": ".ts", + "mimetype": "text/typescript", + "name": "typescript", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/core_docs/docs/troubleshooting/errors/MESSAGE_COERCION_FAILURE.mdx b/docs/core_docs/docs/troubleshooting/errors/MESSAGE_COERCION_FAILURE.mdx new file mode 100644 index 000000000000..8ee5544bc201 --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/MESSAGE_COERCION_FAILURE.mdx @@ -0,0 +1,33 @@ +# MESSAGE_COERCION_FAILURE + +Several modules in LangChain take [`MessageLike`](https://api.js.langchain.com/types/_langchain_core.messages.BaseMessageLike.html) +objects in place of formal [`BaseMessage`](/docs/concepts#message-types) classes. These include OpenAI style message objects (`{ role: "user", content: "Hello world!" }`), +tuples, and plain strings (which are converted to [`HumanMessages`](/docs/concepts#humanmessage)). + +If one of these modules receives a value outside of one of these formats, you will receive an error like the following: + +```ts +const badlyFormattedMessageObject = { + role: "foo", + randomNonContentValue: "bar", +}; + +await model.invoke([badlyFormattedMessageObject]); +``` + +``` +Error: Unable to coerce message from array: only human, AI, system, or tool message coercion is currently supported. + +Received: { + "role": "foo", + "randomNonContentValue": "bar", +} +``` + +## Troubleshooting + +The following may help resolve this error: + +- Ensure that all inputs to chat models are an array of LangChain message classes or a supported message-like. + - Check that there is no stringification or other unexpected transformation occuring. +- Check the error's stack trace and add log or debugger statements. diff --git a/docs/core_docs/docs/troubleshooting/errors/MODEL_AUTHENTICATION.mdx b/docs/core_docs/docs/troubleshooting/errors/MODEL_AUTHENTICATION.mdx new file mode 100644 index 000000000000..ca22f8a03a2b --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/MODEL_AUTHENTICATION.mdx @@ -0,0 +1,20 @@ +# MODEL_AUTHENTICATION + +Your model provider is denying you access to their service. + +## Troubleshooting + +The following may help resolve this error: + +- Confirm that your API key or other credentials are correct. +- If you are relying on an environment variable to authenticate, confirm that the variable name is correct and that it has a value set. + - Note that some environments, like Cloudflare Workers, do not support environment variables. + - For some models, you can try explicitly passing an `apiKey` parameter to rule out any environment variable issues like this: + +```ts +const model = new ChatOpenAI({ + apiKey: "YOUR_KEY_HERE", +}); +``` + +- If you are using a proxy or other custom endpoint, make sure that your custom provider does not expect an alternative authentication scheme. diff --git a/docs/core_docs/docs/troubleshooting/errors/MODEL_NOT_FOUND.mdx b/docs/core_docs/docs/troubleshooting/errors/MODEL_NOT_FOUND.mdx new file mode 100644 index 000000000000..32b8d6896a6a --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/MODEL_NOT_FOUND.mdx @@ -0,0 +1,10 @@ +# MODEL_NOT_FOUND + +The model name you have specified is not acknowledged by your provider. + +## Troubleshooting + +The following may help resolve this error: + +- Double check the model string you are passing in. +- If you are using a proxy or other alternative host with a model wrapper, confirm that the permitted model names are not restricted or altered. diff --git a/docs/core_docs/docs/troubleshooting/errors/MODEL_RATE_LIMIT.mdx b/docs/core_docs/docs/troubleshooting/errors/MODEL_RATE_LIMIT.mdx new file mode 100644 index 000000000000..78108c7c8077 --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/MODEL_RATE_LIMIT.mdx @@ -0,0 +1,14 @@ +# MODEL_RATE_LIMIT + +You have hit the maximum number of requests that a model provider allows over a given time period and are being temporarily blocked. +Generally, this error is temporary and your limit will reset after a certain amount of time. + +## Troubleshooting + +The following may help resolve this error: + +- Contact your model provider and ask for a rate limit increase. +- If many of your incoming requests are the same, utilize [model response caching](/docs/how_to/chat_model_caching/). +- Spread requests across different providers if your application allows it. +- Set a higher number of [max retries](https://api.js.langchain.com/interfaces/_langchain_core.language_models_base.BaseLanguageModelParams.html#maxRetries) when initializing your model. + LangChain will use an exponential backoff strategy for requests that fail in this way, so the retry may occur when your limits have reset. diff --git a/docs/core_docs/docs/troubleshooting/errors/OUTPUT_PARSING_FAILURE.mdx b/docs/core_docs/docs/troubleshooting/errors/OUTPUT_PARSING_FAILURE.mdx new file mode 100644 index 000000000000..f27147d080ee --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/OUTPUT_PARSING_FAILURE.mdx @@ -0,0 +1,36 @@ +# OUTPUT_PARSING_FAILURE + +An [output parser](/docs/concepts#output-parsers) was unable to handle model output as expected. + +To illustrate this, let's say you have an output parser that expects a chat model to output JSON surrounded by a markdown code tag (triple backticks). Here would be an example of good input: + +````ts +AIMessage { + content: "```\n{\"foo\": \"bar\"}\n```" +} +```` + +Internally, our output parser might try to strip out the markdown fence and newlines and then run `JSON.parse()`. + +If instead the chat model generated an output with malformed JSON like this: + +````ts +AIMessage { + content: "```\n{\"foo\":\n```" +} +```` + +When our output parser attempts to parse this, the `JSON.parse()` call will fail. + +Note that some prebuilt constructs like [legacy LangChain agents](/docs/how_to/agent_executor) and chains may use output parsers internally, +so you may see this error even if you're not visibly instantiating and using an output parser. + +## Troubleshooting + +The following may help resolve this error: + +- Consider using [tool calling or other structured output techniques](/docs/how_to/structured_output/) if possible without an output parser to reliably output parseable values. + - If you are using a prebuilt chain or agent, use [LangGraph](https://langchain-ai.github.io/langgraphjs/) to compose your logic explicitly instead. +- Add more precise formatting instructions to your prompt. In the above example, adding `"You must always return valid JSON fenced by a markdown code block. Do not return any additional text."` to your input may help steer the model to returning the expected format. +- If you are using a smaller or less capable model, try using a more capable one. +- Add [LLM-powered retries](/docs/how_to/output_parser_fixing/). diff --git a/docs/core_docs/docs/troubleshooting/errors/index.mdx b/docs/core_docs/docs/troubleshooting/errors/index.mdx new file mode 100644 index 000000000000..aad0308b9c49 --- /dev/null +++ b/docs/core_docs/docs/troubleshooting/errors/index.mdx @@ -0,0 +1,12 @@ +# Error reference + +This page contains guides around resolving common errors you may find while building with LangChain. +Errors referenced below will have an `lc_error_code` property corresponding to one of the below codes when they are thrown in code. + +- [INVALID_PROMPT_INPUT](/docs/troubleshooting/errors/INVALID_PROMPT_INPUT) +- [INVALID_TOOL_RESULTS](/docs/troubleshooting/errors/INVALID_TOOL_RESULTS) +- [MESSAGE_COERCION_FAILURE](/docs/troubleshooting/errors/MESSAGE_COERCION_FAILURE) +- [MODEL_AUTHENTICATION](/docs/troubleshooting/errors/MODEL_AUTHENTICATION) +- [MODEL_NOT_FOUND](/docs/troubleshooting/errors/MODEL_NOT_FOUND) +- [MODEL_RATE_LIMIT](/docs/troubleshooting/errors/MODEL_RATE_LIMIT) +- [OUTPUT_PARSING_FAILURE](/docs/troubleshooting/errors/OUTPUT_PARSING_FAILURE) diff --git a/docs/core_docs/docs/tutorials/chatbot.ipynb b/docs/core_docs/docs/tutorials/chatbot.ipynb index 61bd171e59db..91490c7fa1d0 100644 --- a/docs/core_docs/docs/tutorials/chatbot.ipynb +++ b/docs/core_docs/docs/tutorials/chatbot.ipynb @@ -2,10 +2,15 @@ "cells": [ { "cell_type": "raw", - "metadata": {}, + "metadata": { + "vscode": { + "languageId": "raw" + } + }, "source": [ "---\n", "sidebar_position: 1\n", + "keywords: [conversationchain]\n", "---" ] }, @@ -13,14 +18,8 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Build a Chatbot" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Overview\n", + "# Build a Chatbot\n", + "\n", "\n", ":::info Prerequisites\n", "\n", @@ -30,34 +29,57 @@ "- [Prompt Templates](/docs/concepts/#prompt-templates)\n", "- [Chat History](/docs/concepts/#chat-history)\n", "\n", + "This guide requires `langgraph >= 0.2.28`.\n", + "\n", + ":::\n", + "\n", + "\n", + "```{=mdx}\n", + "\n", + ":::note\n", + "\n", + "This tutorial previously built a chatbot using [RunnableWithMessageHistory](https://api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html). You can access this version of the tutorial in the [v0.2 docs](https://js.langchain.com/v0.2/docs/tutorials/chatbot/).\n", + "\n", + "The LangGraph implementation offers a number of advantages over `RunnableWithMessageHistory`, including the ability to persist arbitrary components of an application's state (instead of only messages).\n", + "\n", ":::\n", "\n", + "```\n", + "\n", + "## Overview\n", + "\n", "We'll go over an example of how to design and implement an LLM-powered chatbot. \n", "This chatbot will be able to have a conversation and remember previous interactions.\n", "\n", + "\n", "Note that this chatbot that we build will only use the language model to have a conversation.\n", "There are several other related concepts that you may be looking for:\n", "\n", "- [Conversational RAG](/docs/tutorials/qa_chat_history): Enable a chatbot experience over an external source of data\n", - "- [Agents](https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/): Build a chatbot that can take actions\n", + "- [Agents](https://langchain-ai.github.io/langgraphjs/tutorials/multi_agent/agent_supervisor/): Build a chatbot that can take actions\n", "\n", "This tutorial will cover the basics which will be helpful for those two more advanced topics, but feel free to skip directly to there should you choose.\n", "\n", "## Setup\n", "\n", + "### Jupyter Notebook\n", + "\n", + "This guide (and most of the other guides in the documentation) uses [Jupyter notebooks](https://jupyter.org/) and assumes the reader is as well. Jupyter notebooks are perfect for learning how to work with LLM systems because oftentimes things can go wrong (unexpected output, API down, etc) and going through guides in an interactive environment is a great way to better understand them.\n", + "\n", + "This and other tutorials are perhaps most conveniently run in a Jupyter notebook. See [here](https://jupyter.org/install) for instructions on how to install.\n", + "\n", "### Installation\n", "\n", - "To install LangChain run:\n", + "For this tutorial we will need `@langchain/core` and `langgraph`:\n", "\n", "```{=mdx}\n", "import Npm2Yarn from \"@theme/Npm2Yarn\"\n", "\n", "\n", - " langchain @langchain/core\n", + " @langchain/core @langchain/langgraph uuid\n", "\n", "```\n", "\n", - "\n", "For more details, see our [Installation guide](/docs/how_to/installation).\n", "\n", "### LangSmith\n", @@ -68,35 +90,25 @@ "\n", "After you sign up at the link above, make sure to set your environment variables to start logging traces:\n", "\n", - "```shell\n", - "export LANGCHAIN_TRACING_V2=\"true\"\n", - "export LANGCHAIN_API_KEY=\"...\"\n", - "\n", - "# Reduce tracing latency if you are not in a serverless environment\n", - "# export LANGCHAIN_CALLBACKS_BACKGROUND=true\n", + "```typescript\n", + "process.env.LANGCHAIN_TRACING_V2 = \"true\"\n", + "process.env.LANGCHAIN_API_KEY = \"...\"\n", "```\n", "\n", "## Quickstart\n", "\n", - "First up, let's learn how to use a language model by itself. LangChain supports many different language models that you can use interchangably - select the one you want to use below!\n", + "First up, let's learn how to use a language model by itself. LangChain supports many different language models that you can use interchangeably - select the one you want to use below!\n", "\n", "```{=mdx}\n", "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", "\n", - "\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's first use the model directly. `ChatModel`s are instances of LangChain \"Runnables\", which means they expose a standard interface for interacting with them. To just simply call the model, we can pass in a list of messages to the `.invoke` method." + "\n", + "```\n" ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -104,51 +116,51 @@ "\n", "import { ChatOpenAI } from \"@langchain/openai\";\n", "\n", - "const model = new ChatOpenAI({\n", - " model: \"gpt-4o-mini\",\n", - " temperature: 0,\n", - "});" + "const llm = new ChatOpenAI({ model: \"gpt-4o-mini\" })" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's first use the model directly. `ChatModel`s are instances of LangChain \"Runnables\", which means they expose a standard interface for interacting with them. To just simply call the model, we can pass in a list of messages to the `.invoke` method." ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 28, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " \"id\": \"chatcmpl-A64of8iD4GIFNSYlOaFHxPdCeyl9E\",\n", - " \"content\": \"Hi Bob! How can I assist you today?\",\n", - " \"additional_kwargs\": {},\n", - " \"response_metadata\": {\n", - " \"tokenUsage\": {\n", - " \"completionTokens\": 10,\n", - " \"promptTokens\": 11,\n", - " \"totalTokens\": 21\n", - " },\n", - " \"finish_reason\": \"stop\"\n", - " },\n", - " \"tool_calls\": [],\n", - " \"invalid_tool_calls\": [],\n", - " \"usage_metadata\": {\n", - " \"input_tokens\": 11,\n", - " \"output_tokens\": 10,\n", - " \"total_tokens\": 21\n", - " }\n", - "}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXeSO4JQpxO96lj7iudUptJ6nfW\",\n", + " \"content\": \"Hi Bob! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 10,\n", + " \"promptTokens\": 10,\n", + " \"totalTokens\": 20\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 10,\n", + " \"output_tokens\": 10,\n", + " \"total_tokens\": 20\n", + " }\n", + "}\n" + ] } ], "source": [ - "import { HumanMessage } from \"@langchain/core/messages\";\n", - "\n", - "await model.invoke([new HumanMessage({ content: \"Hi! I'm Bob\" })]);" + "await llm.invoke([{ role: \"user\", content: \"Hi im bob\" }])" ] }, { @@ -160,48 +172,46 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 29, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " \"id\": \"chatcmpl-A64ogC7owxmPla3ggZERNCFZpVHSp\",\n", - " \"content\": \"I'm sorry, but I don't have access to personal information about users unless it has been shared with me in the course of our conversation. If you'd like to tell me your name, feel free!\",\n", - " \"additional_kwargs\": {},\n", - " \"response_metadata\": {\n", - " \"tokenUsage\": {\n", - " \"completionTokens\": 39,\n", - " \"promptTokens\": 11,\n", - " \"totalTokens\": 50\n", - " },\n", - " \"finish_reason\": \"stop\"\n", - " },\n", - " \"tool_calls\": [],\n", - " \"invalid_tool_calls\": [],\n", - " \"usage_metadata\": {\n", - " \"input_tokens\": 11,\n", - " \"output_tokens\": 39,\n", - " \"total_tokens\": 50\n", - " }\n", - "}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXe1Zih4gMe3XgotWL83xeWub2h\",\n", + " \"content\": \"I'm sorry, but I don't have access to personal information about individuals unless it has been shared with me during our conversation. If you'd like to tell me your name, feel free to do so!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 39,\n", + " \"promptTokens\": 10,\n", + " \"totalTokens\": 49\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 10,\n", + " \"output_tokens\": 39,\n", + " \"total_tokens\": 49\n", + " }\n", + "}\n" + ] } ], "source": [ - "await model.invoke([new HumanMessage({ content: \"What's my name?\" })])" + "await llm.invoke([{ role: \"user\", content: \"Whats my name\" }])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's take a look at the example [LangSmith trace](https://smith.langchain.com/public/e5a0ae1b-32b9-4beb-836d-38f40bfa6762/r)\n", + "Let's take a look at the example [LangSmith trace](https://smith.langchain.com/public/3b768e44-a319-453a-bd6e-30f9df75f16a/r)\n", "\n", "We can see that it doesn't take the previous conversation turn into context, and cannot answer the question.\n", "This makes for a terrible chatbot experience!\n", @@ -211,49 +221,43 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 30, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "AIMessage {\n", - " \"id\": \"chatcmpl-A64ohhg3P4BuIiw8mUCLI3zYHNOvS\",\n", - " \"content\": \"Your name is Bob! How can I help you today, Bob?\",\n", - " \"additional_kwargs\": {},\n", - " \"response_metadata\": {\n", - " \"tokenUsage\": {\n", - " \"completionTokens\": 14,\n", - " \"promptTokens\": 33,\n", - " \"totalTokens\": 47\n", - " },\n", - " \"finish_reason\": \"stop\"\n", - " },\n", - " \"tool_calls\": [],\n", - " \"invalid_tool_calls\": [],\n", - " \"usage_metadata\": {\n", - " \"input_tokens\": 33,\n", - " \"output_tokens\": 14,\n", - " \"total_tokens\": 47\n", - " }\n", - "}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXfX4Fnp247rOxyPlBUYMQgahj2\",\n", + " \"content\": \"Your name is Bob! How can I help you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 12,\n", + " \"promptTokens\": 33,\n", + " \"totalTokens\": 45\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 33,\n", + " \"output_tokens\": 12,\n", + " \"total_tokens\": 45\n", + " }\n", + "}\n" + ] } ], "source": [ - "import { AIMessage } from \"@langchain/core/messages\"\n", - "\n", - "await model.invoke(\n", - " [\n", - " new HumanMessage({ content: \"Hi! I'm Bob\" }),\n", - " new AIMessage({ content: \"Hello Bob! How can I assist you today?\" }),\n", - " new HumanMessage({ content: \"What's my name?\" }),\n", - " ]\n", - ");" + "await llm.invoke([\n", + " { role: \"user\", content: \"Hi! I'm Bob\" },\n", + " { role: \"assistant\", content: \"Hello Bob! How can I assist you today?\" },\n", + " { role: \"user\", content: \"What's my name?\" }\n", + "]);" ] }, { @@ -270,153 +274,208 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Message History\n", + "## Message persistence\n", + "\n", + "[LangGraph](https://langchain-ai.github.io/langgraphjs/) implements a built-in persistence layer, making it ideal for chat applications that support multiple conversational turns.\n", + "\n", + "Wrapping our chat model in a minimal LangGraph application allows us to automatically persist the message history, simplifying the development of multi-turn applications.\n", "\n", - "We can use a Message History class to wrap our model and make it stateful.\n", - "This will keep track of inputs and outputs of the model, and store them in some datastore.\n", - "Future interactions will then load those messages and pass them into the chain as part of the input.\n", - "Let's see how to use this!" + "LangGraph comes with a simple in-memory checkpointer, which we use below." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 31, "metadata": {}, + "outputs": [], "source": [ - "We import the relevant classes and set up our chain which wraps the model and adds in this message history. A key part here is the function we pass into as the `getSessionHistory()`. This function is expected to take in a `sessionId` and return a Message History object. This `sessionId` is used to distinguish between separate conversations, and should be passed in as part of the config when calling the new chain.\n", + "import { START, END, MessagesAnnotation, StateGraph, MemorySaver } from \"@langchain/langgraph\";\n", "\n", - "Let's also create a simple chain by adding a prompt to help with formatting:" + "// Define the function that calls the model\n", + "const callModel = async (state: typeof MessagesAnnotation.State) => {\n", + " const response = await llm.invoke(state.messages);\n", + " return { messages: response };\n", + "};\n", + "\n", + "// Define a new graph\n", + "const workflow = new StateGraph(MessagesAnnotation)\n", + " // Define the node and edge\n", + " .addNode(\"model\", callModel)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "// Add memory\n", + "const memory = new MemorySaver();\n", + "const app = workflow.compile({ checkpointer: memory });" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to create a `config` that we pass into the runnable every time. This config contains information that is not part of the input directly, but is still useful. In this case, we want to include a `thread_id`. This should look like:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 32, "metadata": {}, "outputs": [], "source": [ - "// We use an ephemeral, in-memory chat history for this demo.\n", - "import { InMemoryChatMessageHistory } from \"@langchain/core/chat_history\";\n", - "import { ChatPromptTemplate } from \"@langchain/core/prompts\";\n", - "import { RunnableWithMessageHistory } from \"@langchain/core/runnables\";\n", + "import { v4 as uuidv4 } from \"uuid\";\n", "\n", - "const messageHistories: Record = {};\n", - "\n", - "const prompt = ChatPromptTemplate.fromMessages([\n", - " [\"system\", `You are a helpful assistant who remembers all details the user shares with you.`],\n", - " [\"placeholder\", \"{chat_history}\"],\n", - " [\"human\", \"{input}\"],\n", - "]);\n", - "\n", - "const chain = prompt.pipe(model);\n", - "\n", - "const withMessageHistory = new RunnableWithMessageHistory({\n", - " runnable: chain,\n", - " getMessageHistory: async (sessionId) => {\n", - " if (messageHistories[sessionId] === undefined) {\n", - " messageHistories[sessionId] = new InMemoryChatMessageHistory();\n", - " }\n", - " return messageHistories[sessionId];\n", - " },\n", - " inputMessagesKey: \"input\",\n", - " historyMessagesKey: \"chat_history\",\n", - "});" + "const config = { configurable: { thread_id: uuidv4() } };" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We now need to create a `config` that we pass into the runnable every time. This config contains information that is not part of the input directly, but is still useful. In this case, we want to include a `session_id`. This should look like:" + "This enables us to support multiple conversation threads with a single application, a common requirement when your application has multiple users.\n", + "\n", + "We can then invoke the application:" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 33, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"Hi Bob! How can I assist you today?\"\u001b[39m" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXfjqCno78CGXCHoAgamqXG1pnZ\",\n", + " \"content\": \"Hi Bob! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 10,\n", + " \"promptTokens\": 12,\n", + " \"totalTokens\": 22\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 12,\n", + " \"output_tokens\": 10,\n", + " \"total_tokens\": 22\n", + " }\n", + "}\n" + ] } ], "source": [ - "const config = {\n", - " configurable: {\n", - " sessionId: \"abc2\"\n", + "const input = [\n", + " {\n", + " role: \"user\",\n", + " content: \"Hi! I'm Bob.\",\n", " }\n", - "};\n", - "\n", - "const response = await withMessageHistory.invoke({\n", - " input: \"Hi! I'm Bob\",\n", - "}, config);\n", - "\n", - "response.content;" + "]\n", + "const output = await app.invoke({ messages: input }, config)\n", + "// The output contains all messages in the state.\n", + "// This will long the last message in the conversation.\n", + "console.log(output.messages[output.messages.length - 1]);" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 34, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"Your name is Bob. How can I help you today?\"\u001b[39m" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXgzHFHk4KsaNmDJyvflHq4JY2L\",\n", + " \"content\": \"Your name is Bob! How can I help you today, Bob?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 14,\n", + " \"promptTokens\": 34,\n", + " \"totalTokens\": 48\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 34,\n", + " \"output_tokens\": 14,\n", + " \"total_tokens\": 48\n", + " }\n", + "}\n" + ] } ], "source": [ - "const followupResponse = await withMessageHistory.invoke({\n", - " input: \"What's my name?\",\n", - "}, config);\n", - "\n", - "followupResponse.content" + "const input2 = [\n", + " {\n", + " role: \"user\",\n", + " content: \"What's my name?\",\n", + " }\n", + "]\n", + "const output2 = await app.invoke({ messages: input2 }, config)\n", + "console.log(output2.messages[output2.messages.length - 1]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Great! Our chatbot now remembers things about us. If we change the config to reference a different `session_id`, we can see that it starts the conversation fresh." + "Great! Our chatbot now remembers things about us. If we change the config to reference a different `thread_id`, we can see that it starts the conversation fresh." ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 35, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"I'm sorry, but I don't have your name. If you tell me, I'll remember it for our future conversations\"\u001b[39m... 1 more character" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXhT4EVx8mGgmKXJ1s132qEluxR\",\n", + " \"content\": \"I'm sorry, but I don’t have access to personal data about individuals unless it has been shared in the course of our conversation. Therefore, I don't know your name. How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 41,\n", + " \"promptTokens\": 11,\n", + " \"totalTokens\": 52\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 11,\n", + " \"output_tokens\": 41,\n", + " \"total_tokens\": 52\n", + " }\n", + "}\n" + ] } ], "source": [ - "const config2 = {\n", - " configurable: {\n", - " sessionId: \"abc3\"\n", + "const config2 = { configurable: { thread_id: uuidv4() } }\n", + "const input3 = [\n", + " {\n", + " role: \"user\",\n", + " content: \"What's my name?\",\n", " }\n", - "};\n", - "\n", - "const response2 = await withMessageHistory.invoke({\n", - " input: \"What's my name?\",\n", - "}, config2);\n", - "\n", - "response2.content" + "]\n", + "const output3 = await app.invoke({ messages: input3 }, config2)\n", + "console.log(output3.messages[output3.messages.length - 1]);" ] }, { @@ -428,338 +487,623 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 36, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"Your name is Bob. What would you like to talk about?\"\u001b[39m" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXhZmtzvV3kqKig47xxhKEnvVfH\",\n", + " \"content\": \"Your name is Bob! If there's anything else you'd like to talk about or ask, feel free!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 20,\n", + " \"promptTokens\": 60,\n", + " \"totalTokens\": 80\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 60,\n", + " \"output_tokens\": 20,\n", + " \"total_tokens\": 80\n", + " }\n", + "}\n" + ] } ], "source": [ - "const config3 = {\n", - " configurable: {\n", - " sessionId: \"abc2\"\n", - " }\n", - "};\n", + "const output4 = await app.invoke({ messages: input2 }, config)\n", + "console.log(output4.messages[output4.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is how we can support a chatbot having conversations with many users!\n", + "\n", + "Right now, all we've done is add a simple persistence layer around the model. We can start to make the more complicated and personalized by adding in a prompt template.\n", "\n", - "const response3 = await withMessageHistory.invoke({\n", - " input: \"What's my name?\",\n", - "}, config3);\n", + "## Prompt templates\n", "\n", - "response3.content" + "Prompt Templates help to turn raw user information into a format that the LLM can work with. In this case, the raw user input is just a message, which we are passing to the LLM. Let's now make that a bit more complicated. First, let's add in a system message with some custom instructions (but still taking messages as input). Next, we'll add in more input besides just the messages.\n", + "\n", + "To add in a system message, we will create a `ChatPromptTemplate`. We will utilize `MessagesPlaceholder` to pass all the messages in." ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 37, "metadata": {}, + "outputs": [], "source": [ - "This is how we can support a chatbot having conversations with many users!" + "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\";\n", + "\n", + "const prompt = ChatPromptTemplate.fromMessages([\n", + " [\"system\", \"You talk like a pirate. Answer all questions to the best of your ability.\"],\n", + " new MessagesPlaceholder(\"messages\"),\n", + "]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Managing Conversation History\n", - "\n", - "One important concept to understand when building chatbots is how to manage conversation history. If left unmanaged, the list of messages will grow unbounded and potentially overflow the context window of the LLM. Therefore, it is important to add a step that limits the size of the messages you are passing in.\n", - "\n", - "**Importantly, you will want to do this BEFORE the prompt template but AFTER you load previous messages from Message History.**\n", - "\n", - "We can do this by adding a simple step in front of the prompt that modifies the `chat_history` key appropriately, and then wrap that new chain in the Message History class. First, let's define a function that will modify the messages passed in. Let's make it so that it selects the 10 most recent messages. We can then create a new chain by adding that at the start." + "We can now update our application to incorporate this template:" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 38, "metadata": {}, "outputs": [], "source": [ - "import type { BaseMessage } from \"@langchain/core/messages\";\n", - "import { RunnablePassthrough, RunnableSequence } from \"@langchain/core/runnables\";\n", - "\n", - "type ChainInput = {\n", - " chat_history: BaseMessage[];\n", - " input: string;\n", - "}\n", + "import { START, END, MessagesAnnotation, StateGraph, MemorySaver } from \"@langchain/langgraph\";\n", + "\n", + "// Define the function that calls the model\n", + "const callModel2 = async (state: typeof MessagesAnnotation.State) => {\n", + " // highlight-start\n", + " const chain = prompt.pipe(llm);\n", + " const response = await chain.invoke(state);\n", + " // highlight-end\n", + " // Update message history with response:\n", + " return { messages: [response] };\n", + "};\n", "\n", - "const filterMessages = (input: ChainInput) => input.chat_history.slice(-10);\n", + "// Define a new graph\n", + "const workflow2 = new StateGraph(MessagesAnnotation)\n", + " // Define the (single) node in the graph\n", + " .addNode(\"model\", callModel2)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", "\n", - "const chain2 = RunnableSequence.from([\n", - " RunnablePassthrough.assign({\n", - " chat_history: filterMessages\n", - " }),\n", - " prompt,\n", - " model,\n", - "]);" + "// Add memory\n", + "const app2 = workflow2.compile({ checkpointer: new MemorySaver() });" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's now try it out! If we create a list of messages more than 10 messages long, we can see what it no longer remembers information in the early messages." + "We invoke the application in the same way:" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 39, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXio2Vy1YNRDiFdKKEyN3Yw1B9I\",\n", + " \"content\": \"Ahoy, Jim! What brings ye to these treacherous waters today? Speak up, matey!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 22,\n", + " \"promptTokens\": 32,\n", + " \"totalTokens\": 54\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 32,\n", + " \"output_tokens\": 22,\n", + " \"total_tokens\": 54\n", + " }\n", + "}\n" + ] + } + ], "source": [ - "const messages = [\n", - " new HumanMessage({ content: \"hi! I'm bob\" }),\n", - " new AIMessage({ content: \"hi!\" }),\n", - " new HumanMessage({ content: \"I like vanilla ice cream\" }),\n", - " new AIMessage({ content: \"nice\" }),\n", - " new HumanMessage({ content: \"whats 2 + 2\" }),\n", - " new AIMessage({ content: \"4\" }),\n", - " new HumanMessage({ content: \"thanks\" }),\n", - " new AIMessage({ content: \"No problem!\" }),\n", - " new HumanMessage({ content: \"having fun?\" }),\n", - " new AIMessage({ content: \"yes!\" }),\n", - " new HumanMessage({ content: \"That's great!\" }),\n", - " new AIMessage({ content: \"yes it is!\" }),\n", - "];" + "const config3 = { configurable: { thread_id: uuidv4() } }\n", + "const input4 = [\n", + " {\n", + " role: \"user\",\n", + " content: \"Hi! I'm Jim.\",\n", + " }\n", + "]\n", + "const output5 = await app2.invoke({ messages: input4 }, config3)\n", + "console.log(output5.messages[output5.messages.length - 1]);" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 40, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"You haven't shared your name with me yet. What is it?\"\u001b[39m" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXjZNHiT5g7eTf52auWGXDUUcDs\",\n", + " \"content\": \"Ye be callin' yerself Jim, if me memory serves me right! Arrr, what else can I do fer ye, matey?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 31,\n", + " \"promptTokens\": 67,\n", + " \"totalTokens\": 98\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_3a215618e8\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 67,\n", + " \"output_tokens\": 31,\n", + " \"total_tokens\": 98\n", + " }\n", + "}\n" + ] } ], "source": [ - "const response4 = await chain2.invoke(\n", + "const input5 = [\n", " {\n", - " chat_history: messages,\n", - " input: \"what's my name?\"\n", + " role: \"user\",\n", + " content: \"What is my name?\"\n", " }\n", - ")\n", - "response4.content" + "]\n", + "const output6 = await app2.invoke({ messages: input5 }, config3)\n", + "console.log(output6.messages[output6.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Awesome! Let's now make our prompt a little bit more complicated. Let's assume that the prompt template now looks something like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "const prompt2 = ChatPromptTemplate.fromMessages([\n", + " [\"system\", \"You are a helpful assistant. Answer all questions to the best of your ability in {language}.\"],\n", + " new MessagesPlaceholder(\"messages\"),\n", + "]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "But if we ask about information that is within the last ten messages, it still remembers it" + "Note that we have added a new `language` input to the prompt. Our application now has two parameters-- the input `messages` and `language`. We should update our application's state to reflect this:" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "import { START, END, StateGraph, MemorySaver, MessagesAnnotation, Annotation } from \"@langchain/langgraph\";\n", + "\n", + "// Define the State\n", + "const GraphAnnotation = Annotation.Root({\n", + " ...MessagesAnnotation.spec,\n", + " language: Annotation(),\n", + "});\n", + "\n", + "// Define the function that calls the model\n", + "const callModel3 = async (state: typeof GraphAnnotation.State) => {\n", + " const chain = prompt2.pipe(llm);\n", + " const response = await chain.invoke(state);\n", + " return { messages: [response] };\n", + "};\n", + "\n", + "const workflow3 = new StateGraph(GraphAnnotation)\n", + " .addNode(\"model\", callModel3)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "const app3 = workflow3.compile({ checkpointer: new MemorySaver() });" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 43, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"Your favorite ice cream is vanilla!\"\u001b[39m" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXkq2ZV9xmOBSM2iJbYSn8Epvqa\",\n", + " \"content\": \"¡Hola, Bob! ¿En qué puedo ayudarte hoy?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 12,\n", + " \"promptTokens\": 32,\n", + " \"totalTokens\": 44\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 32,\n", + " \"output_tokens\": 12,\n", + " \"total_tokens\": 44\n", + " }\n", + "}\n" + ] } ], "source": [ - "const response5 = await chain2.invoke(\n", - " {\n", - " chat_history: messages,\n", - " input: \"what's my fav ice cream\"\n", - " }\n", - ")\n", - "response5.content" + "const config4 = { configurable: { thread_id: uuidv4() } }\n", + "const input6 = {\n", + " messages: [\n", + " {\n", + " role: \"user\",\n", + " content: \"Hi im bob\"\n", + " }\n", + " ],\n", + " language: \"Spanish\"\n", + "}\n", + "const output7 = await app3.invoke(input6, config4)\n", + "console.log(output7.messages[output7.messages.length - 1]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's now wrap this chain in a `RunnableWithMessageHistory` constructor. For demo purposes, we will also slightly modify our `getMessageHistory()` method to always start new sessions with the previously declared list of 10 messages to simulate several conversation turns:" + "Note that the entire state is persisted, so we can omit parameters like `language` if no changes are desired:" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 44, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"You haven't shared your name with me yet. What is it?\"\u001b[39m" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUXk9Ccr1dhmA9lZ1VmZ998PFyJF\",\n", + " \"content\": \"Tu nombre es Bob. ¿Hay algo más en lo que te pueda ayudar?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 16,\n", + " \"promptTokens\": 57,\n", + " \"totalTokens\": 73\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 57,\n", + " \"output_tokens\": 16,\n", + " \"total_tokens\": 73\n", + " }\n", + "}\n" + ] } ], "source": [ - "const messageHistories2: Record = {};\n", - "\n", - "const withMessageHistory2 = new RunnableWithMessageHistory({\n", - " runnable: chain2,\n", - " getMessageHistory: async (sessionId) => {\n", - " if (messageHistories2[sessionId] === undefined) {\n", - " const messageHistory = new InMemoryChatMessageHistory();\n", - " await messageHistory.addMessages(messages);\n", - " messageHistories2[sessionId] = messageHistory;\n", + "const input7 = {\n", + " messages: [\n", + " {\n", + " role: \"user\",\n", + " content: \"What is my name?\"\n", " }\n", - " return messageHistories2[sessionId];\n", - " },\n", - " inputMessagesKey: \"input\",\n", - " historyMessagesKey: \"chat_history\",\n", - "})\n", - "\n", - "const config4 = {\n", - " configurable: {\n", - " sessionId: \"abc4\"\n", - " }\n", - "};\n", - "\n", - "const response7 = await withMessageHistory2.invoke(\n", - " {\n", - " input: \"whats my name?\",\n", - " chat_history: [],\n", - " },\n", - " config4,\n", - ")\n", - "\n", - "response7.content" + " ],\n", + "}\n", + "const output8 = await app3.invoke(input7, config4)\n", + "console.log(output8.messages[output8.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To help you understand what's happening internally, check out [this LangSmith trace](https://smith.langchain.com/public/d61630b7-6a52-4dc9-974c-8452008c498a/r)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "There's now two new messages in the chat history. This means that even more information that used to be accessible in our conversation history is no longer available!" + "## Managing Conversation History\n", + "\n", + "One important concept to understand when building chatbots is how to manage conversation history. If left unmanaged, the list of messages will grow unbounded and potentially overflow the context window of the LLM. Therefore, it is important to add a step that limits the size of the messages you are passing in.\n", + "\n", + "**Importantly, you will want to do this BEFORE the prompt template but AFTER you load previous messages from Message History.**\n", + "\n", + "We can do this by adding a simple step in front of the prompt that modifies the `messages` key appropriately, and then wrap that new chain in the Message History class. \n", + "\n", + "LangChain comes with a few built-in helpers for [managing a list of messages](/docs/how_to/#messages). In this case we'll use the [trimMessages](/docs/how_to/trim_messages/) helper to reduce how many messages we're sending to the model. The trimmer allows us to specify how many tokens we want to keep, along with other parameters like if we want to always keep the system message and whether to allow partial messages:" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 54, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "\u001b[32m\"You haven't mentioned your favorite ice cream yet. What is it?\"\u001b[39m" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "[\n", + " SystemMessage {\n", + " \"content\": \"you're a good assistant\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " HumanMessage {\n", + " \"content\": \"I like vanilla ice cream\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"content\": \"nice\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"content\": \"whats 2 + 2\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"content\": \"4\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"content\": \"thanks\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"content\": \"no problem!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " },\n", + " HumanMessage {\n", + " \"content\": \"having fun?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + " },\n", + " AIMessage {\n", + " \"content\": \"yes!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + " }\n", + "]\n" + ] } ], "source": [ - "const response8 = await withMessageHistory2.invoke({\n", - " input: \"whats my favorite ice cream?\",\n", - " chat_history: [],\n", - "}, config4);\n", + "import { SystemMessage, HumanMessage, AIMessage, trimMessages } from \"@langchain/core/messages\"\n", + "\n", + "const trimmer = trimMessages({\n", + " maxTokens: 10,\n", + " strategy: \"last\",\n", + " tokenCounter: (msgs) => msgs.length,\n", + " includeSystem: true,\n", + " allowPartial: false,\n", + " startOn: \"human\",\n", + "})\n", "\n", - "response8.content" + "const messages = [\n", + " new SystemMessage(\"you're a good assistant\"),\n", + " new HumanMessage(\"hi! I'm bob\"),\n", + " new AIMessage(\"hi!\"),\n", + " new HumanMessage(\"I like vanilla ice cream\"),\n", + " new AIMessage(\"nice\"),\n", + " new HumanMessage(\"whats 2 + 2\"),\n", + " new AIMessage(\"4\"),\n", + " new HumanMessage(\"thanks\"),\n", + " new AIMessage(\"no problem!\"),\n", + " new HumanMessage(\"having fun?\"),\n", + " new AIMessage(\"yes!\"),\n", + "]\n", + "\n", + "await trimmer.invoke(messages)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "If you take a look at LangSmith, you can see exactly what is happening under the hood in the [LangSmith trace](https://smith.langchain.com/public/ebc2e1e7-0703-43f7-a476-8cb8cbd7f61a/r). Navigate to the chat model call to see exactly which messages are getting filtered out." + "To use it in our chain, we just need to run the trimmer before we pass the `messages` input to our prompt. " ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 55, "metadata": {}, + "outputs": [], "source": [ - "## Streaming\n", + "const callModel4 = async (state: typeof GraphAnnotation.State) => {\n", + " const chain = prompt2.pipe(llm);\n", + " // highlight-start\n", + " const trimmedMessage = await trimmer.invoke(state.messages);\n", + " const response = await chain.invoke({ messages: trimmedMessage, language: state.language });\n", + " // highlight-end\n", + " return { messages: [response] };\n", + "};\n", "\n", - "Now we've got a functional chatbot. However, one *really* important UX consideration for chatbot application is streaming. LLMs can sometimes take a while to respond, and so in order to improve the user experience one thing that most application do is stream back each token as it is generated. This allows the user to see progress.\n", "\n", - "It's actually super easy to do this!\n", + "const workflow4 = new StateGraph(GraphAnnotation)\n", + " .addNode(\"model\", callModel4)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", "\n", - "All chains expose a `.stream()` method, and ones that use message history are no different. We can simply use that method to get back a streaming response." + "const app4 = workflow4.compile({ checkpointer: new MemorySaver() });" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now if we try asking the model our name, it won't know it since we trimmed that part of the chat history:" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 56, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "| \n", - "| Hi\n", - "| Todd\n", - "| !\n", - "| Here\n", - "| ’s\n", - "| a\n", - "| joke\n", - "| for\n", - "| you\n", - "| :\n", - "| \n", - "\n", - "\n", - "| Why\n", - "| did\n", - "| the\n", - "| scare\n", - "| crow\n", - "| win\n", - "| an\n", - "| award\n", - "| ?\n", - "| \n", - "\n", - "\n", - "| Because\n", - "| he\n", - "| was\n", - "| outstanding\n", - "| in\n", - "| his\n", - "| field\n", - "| !\n", - "| \n" + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUdCOvzRAvgoxd2sf93oGKQfA9vh\",\n", + " \"content\": \"I don’t know your name, but I’d be happy to learn it if you’d like to share!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 22,\n", + " \"promptTokens\": 97,\n", + " \"totalTokens\": 119\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 97,\n", + " \"output_tokens\": 22,\n", + " \"total_tokens\": 119\n", + " }\n", + "}\n" ] } ], "source": [ - "const config5 = {\n", - " configurable: {\n", - " sessionId: \"abc6\"\n", - " }\n", - "};\n", + "const config5 = { configurable: { thread_id: uuidv4() }}\n", + "const input8 = {\n", + " // highlight-next-line\n", + " messages: [...messages, new HumanMessage(\"What is my name?\")],\n", + " language: \"English\"\n", + "}\n", "\n", - "const stream = await withMessageHistory2.stream({\n", - " input: \"hi! I'm todd. tell me a joke\",\n", - " chat_history: [],\n", - "}, config5);\n", + "const output9 = await app4.invoke(\n", + " input8,\n", + " config5,\n", + ")\n", + "console.log(output9.messages[output9.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But if we ask about information that is within the last few messages, it remembers:" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-ABUdChq5JOMhcFA1dB7PvCHLyliwM\",\n", + " \"content\": \"You asked for the solution to the math problem \\\"what's 2 + 2,\\\" and I answered that it equals 4.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 27,\n", + " \"promptTokens\": 99,\n", + " \"totalTokens\": 126\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_1bb46167f9\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 99,\n", + " \"output_tokens\": 27,\n", + " \"total_tokens\": 126\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const config6 = { configurable: { thread_id: uuidv4() }}\n", + "const input9 = {\n", + " // highlight-next-line\n", + " messages: [...messages, new HumanMessage(\"What math problem did I ask?\")],\n", + " language: \"English\"\n", + "}\n", "\n", - "for await (const chunk of stream) {\n", - " console.log(\"|\", chunk.content);\n", - "}" + "const output10 = await app4.invoke(\n", + " input9,\n", + " config6,\n", + ")\n", + "console.log(output10.messages[output10.messages.length - 1]);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you take a look at LangSmith, you can see exactly what is happening under the hood in the [LangSmith trace](https://smith.langchain.com/public/bf1b1a10-0fe0-42f6-9f0f-b70d9f7520dc/r)." ] }, { @@ -771,12 +1115,14 @@ "Now that you understand the basics of how to create a chatbot in LangChain, some more advanced tutorials you may be interested in are:\n", "\n", "- [Conversational RAG](/docs/tutorials/qa_chat_history): Enable a chatbot experience over an external source of data\n", - "- [Agents](https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/): Build a chatbot that can take actions\n", + "- [Agents](https://langchain-ai.github.io/langgraphjs/tutorials/multi_agent/agent_supervisor/): Build a chatbot that can take actions\n", "\n", "If you want to dive deeper on specifics, some things worth checking out are:\n", "\n", "- [Streaming](/docs/how_to/streaming): streaming is *crucial* for chat applications\n", - "- [How to add message history](/docs/how_to/message_history): for a deeper dive into all things related to message history" + "- [How to add message history](/docs/how_to/message_history): for a deeper dive into all things related to message history\n", + "- [How to manage large message history](/docs/how_to/trim_messages/): more techniques for managing a large chat history\n", + "- [LangGraph main docs](https://langchain-ai.github.io/langgraph/): for more detail on building with LangGraph" ] } ], @@ -787,12 +1133,15 @@ "name": "deno" }, "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, "file_extension": ".ts", - "mimetype": "text/x.typescript", + "mimetype": "text/typescript", "name": "typescript", - "nb_converter": "script", - "pygments_lexer": "typescript", - "version": "5.3.3" + "version": "3.7.2" } }, "nbformat": 4, diff --git a/docs/core_docs/docs/tutorials/graph.ipynb b/docs/core_docs/docs/tutorials/graph.ipynb index b7fc6a76b6d6..0d8ab1ba18ad 100644 --- a/docs/core_docs/docs/tutorials/graph.ipynb +++ b/docs/core_docs/docs/tutorials/graph.ipynb @@ -181,8 +181,20 @@ "\n", "![graph_chain.webp](../../static/img/graph_chain.webp)\n", "\n", + "LangChain comes with a built-in chain for this workflow that is designed to work with Neo4j: `GraphCypherQAChain`.\n", "\n", - "LangChain comes with a built-in chain for this workflow that is designed to work with Neo4j: [GraphCypherQAChain](https://python.langchain.com/docs/use_cases/graph/graph_cypher_qa)" + "```{=mdx}\n", + ":::warning\n", + "\n", + "The `GraphCypherQAChain` used in this guide will execute Cypher statements against the provided database.\n", + "For production, make sure that the database connection uses credentials that are narrowly-scoped to only include necessary permissions.\n", + "\n", + "Failure to do so may result in data corruption or loss, since the calling code\n", + "may attempt commands that would result in deletion, mutation of data\n", + "if appropriately prompted or reading sensitive data if such data is present in the database.\n", + "\n", + ":::\n", + "```" ] }, { diff --git a/docs/core_docs/docs/tutorials/qa_chat_history.ipynb b/docs/core_docs/docs/tutorials/qa_chat_history.ipynb index b6bd8146c71a..b1694c4413ab 100644 --- a/docs/core_docs/docs/tutorials/qa_chat_history.ipynb +++ b/docs/core_docs/docs/tutorials/qa_chat_history.ipynb @@ -1,390 +1,1438 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Conversational RAG\n", - "\n", - ":::info Prerequisites\n", - "\n", - "This guide assumes familiarity with the following concepts:\n", - "\n", - "- [Chat history](/docs/concepts/#chat-history)\n", - "- [Chat models](/docs/concepts/#chat-models)\n", - "- [Embeddings](/docs/concepts/#embedding-models)\n", - "- [Vector stores](/docs/concepts/#vector-stores)\n", - "- [Retrieval-augmented generation](/docs/tutorials/rag/)\n", - "- [Tools](/docs/concepts/#tools)\n", - "- [Agents](/docs/concepts/#agents)\n", - "\n", - ":::\n", - "\n", - "In many Q&A applications we want to allow the user to have a back-and-forth conversation, meaning the application needs some sort of \"memory\" of past questions and answers, and some logic for incorporating those into its current thinking.\n", - "\n", - "In this guide we focus on **adding logic for incorporating historical messages.** Further details on chat history management is [covered here](/docs/how_to/message_history).\n", - "\n", - "We will cover two approaches:\n", - "\n", - "1. Chains, in which we always execute a retrieval step;\n", - "2. Agents, in which we give an LLM discretion over whether and how to execute a retrieval step (or multiple steps).\n", - "\n", - "For the external knowledge source, we will use the same [LLM Powered Autonomous Agents](https://lilianweng.github.io/posts/2023-06-23-agent/) blog post by Lilian Weng from the [RAG tutorial](/docs/tutorials/rag)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup\n", - "### Dependencies\n", - "\n", - "We’ll use an OpenAI chat model and embeddings and a Memory vector store in this walkthrough, but everything shown here works with any [ChatModel](/docs/concepts/#chat-models) or [LLM](/docs/concepts#llms), [Embeddings](/docs/concepts#embedding-models), and [VectorStore](/docs/concepts#vectorstores) or [Retriever](/docs/concepts#retrievers).\n", - "\n", - "We’ll use the following packages:\n", - "\n", - "```bash\n", - "npm install --save langchain @langchain/openai cheerio\n", - "```\n", - "\n", - "We need to set environment variable `OPENAI_API_KEY`:\n", - "\n", - "```bash\n", - "export OPENAI_API_KEY=YOUR_KEY\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### LangSmith\n", - "\n", - "Many of the applications you build with LangChain will contain multiple steps with multiple invocations of LLM calls. As these applications get more and more complex, it becomes crucial to be able to inspect what exactly is going on inside your chain or agent. The best way to do this is with [LangSmith](https://smith.langchain.com/).\n", - "\n", - "Note that LangSmith is not needed, but it is helpful. If you do want to use LangSmith, after you sign up at the link above, make sure to set your environment variables to start logging traces:\n", - "\n", - "\n", - "```bash\n", - "export LANGCHAIN_TRACING_V2=true\n", - "export LANGCHAIN_API_KEY=YOUR_KEY\n", - "\n", - "# Reduce tracing latency if you are not in a serverless environment\n", - "# export LANGCHAIN_CALLBACKS_BACKGROUND=true\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Initial setup" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "import \"cheerio\";\n", - "import { CheerioWebBaseLoader } from \"@langchain/community/document_loaders/web/cheerio\";\n", - "import { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n", - "import { MemoryVectorStore } from \"langchain/vectorstores/memory\"\n", - "import { OpenAIEmbeddings, ChatOpenAI } from \"@langchain/openai\";\n", - "import { pull } from \"langchain/hub\";\n", - "import { ChatPromptTemplate } from \"@langchain/core/prompts\";\n", - "import { RunnableSequence, RunnablePassthrough } from \"@langchain/core/runnables\";\n", - "import { StringOutputParser } from \"@langchain/core/output_parsers\";\n", - "\n", - "import { createStuffDocumentsChain } from \"langchain/chains/combine_documents\";\n", - "\n", - "const loader = new CheerioWebBaseLoader(\n", - " \"https://lilianweng.github.io/posts/2023-06-23-agent/\"\n", - ");\n", - "\n", - "const docs = await loader.load();\n", - "\n", - "const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000, chunkOverlap: 200 });\n", - "const splits = await textSplitter.splitDocuments(docs);\n", - "const vectorStore = await MemoryVectorStore.fromDocuments(splits, new OpenAIEmbeddings());\n", - "\n", - "// Retrieve and generate using the relevant snippets of the blog.\n", - "const retriever = vectorStore.asRetriever();\n", - "const prompt = await pull(\"rlm/rag-prompt\");\n", - "const llm = new ChatOpenAI({ model: \"gpt-3.5-turbo\", temperature: 0 });\n", - "const ragChain = await createStuffDocumentsChain({\n", - " llm,\n", - " prompt,\n", - " outputParser: new StringOutputParser(),\n", - "});" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's see what this prompt actually looks like:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\n", - "Question: {question} \n", - "Context: {context} \n", - "Answer:\n" - ] - } - ], - "source": [ - "console.log(prompt.promptMessages.map((msg) => msg.prompt.template).join(\"\\n\"));" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[32m\"Task decomposition is a technique used to break down complex tasks into smaller and simpler steps. I\"\u001b[39m... 208 more characters" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "await ragChain.invoke({\n", - " context: await retriever.invoke(\"What is Task Decomposition?\"),\n", - " question: \"What is Task Decomposition?\"\n", - "});" - ] - }, + "cells": [ + { + "cell_type": "raw", + "id": "023635f2-71cf-43f2-a2e2-a7b4ced30a74", + "metadata": {}, + "source": [ + "---\n", + "sidebar_position: 2\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "86fc5bb2-017f-434e-8cd6-53ab214a5604", + "metadata": {}, + "source": [ + "# Conversational RAG\n", + "\n", + ":::info Prerequisites\n", + "\n", + "This guide assumes familiarity with the following concepts:\n", + "\n", + "- [Chat history](/docs/concepts/#chat-history)\n", + "- [Chat models](/docs/concepts/#chat-models)\n", + "- [Embeddings](/docs/concepts/#embedding-models)\n", + "- [Vector stores](/docs/concepts/#vector-stores)\n", + "- [Retrieval-augmented generation](/docs/tutorials/rag/)\n", + "- [Tools](/docs/concepts/#tools)\n", + "- [Agents](/docs/concepts/#agents)\n", + "\n", + ":::\n", + "\n", + "In many Q&A applications we want to allow the user to have a back-and-forth conversation, meaning the application needs some sort of \"memory\" of past questions and answers, and some logic for incorporating those into its current thinking.\n", + "\n", + "In this guide we focus on **adding logic for incorporating historical messages.** Further details on chat history management is [covered here](/docs/how_to/message_history).\n", + "\n", + "We will cover two approaches:\n", + "\n", + "1. Chains, in which we always execute a retrieval step;\n", + "2. Agents, in which we give an LLM discretion over whether and how to execute a retrieval step (or multiple steps).\n", + "\n", + "For the external knowledge source, we will use the same [LLM Powered Autonomous Agents](https://lilianweng.github.io/posts/2023-06-23-agent/) blog post by Lilian Weng from the [RAG tutorial](/docs/tutorials/rag)." + ] + }, + { + "cell_type": "markdown", + "id": "487d8d79-5ee9-4aa4-9fdf-cd5f4303e099", + "metadata": {}, + "source": [ + "## Setup\n", + "### Dependencies\n", + "\n", + "We’ll use an OpenAI chat model and embeddings and a Memory vector store in this walkthrough, but everything shown here works with any [ChatModel](/docs/concepts/#chat-models) or [LLM](/docs/concepts#llms), [Embeddings](/docs/concepts#embedding-models), and [VectorStore](/docs/concepts#vectorstores) or [Retriever](/docs/concepts#retrievers).\n", + "\n", + "We’ll use the following packages:\n", + "\n", + "```bash\n", + "npm install --save langchain @langchain/openai langchain cheerio\n", + "```\n", + "\n", + "We need to set environment variable `OPENAI_API_KEY`:\n", + "\n", + "```bash\n", + "export OPENAI_API_KEY=YOUR_KEY\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "1665e740-ce01-4f09-b9ed-516db0bd326f", + "metadata": {}, + "source": [ + "### LangSmith\n", + "\n", + "Many of the applications you build with LangChain will contain multiple steps with multiple invocations of LLM calls. As these applications get more and more complex, it becomes crucial to be able to inspect what exactly is going on inside your chain or agent. The best way to do this is with [LangSmith](https://docs.smith.langchain.com).\n", + "\n", + "Note that LangSmith is not needed, but it is helpful. If you do want to use LangSmith, after you sign up at the link above, make sure to set your environment variables to start logging traces:\n", + "\n", + "\n", + "```bash\n", + "export LANGCHAIN_TRACING_V2=true\n", + "export LANGCHAIN_API_KEY=YOUR_KEY\n", + "\n", + "# Reduce tracing latency if you are not in a serverless environment\n", + "# export LANGCHAIN_CALLBACKS_BACKGROUND=true\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "fa6ba684-26cf-4860-904e-a4d51380c134", + "metadata": {}, + "source": [ + "## Chains {#chains}\n" + ] + }, + { + "cell_type": "markdown", + "id": "7d2cf4ef", + "metadata": {}, + "source": [ + "\n", + "Let's first revisit the Q&A app we built over the [LLM Powered Autonomous Agents](https://lilianweng.github.io/posts/2023-06-23-agent/) blog post by Lilian Weng in the [RAG tutorial](/docs/tutorials/rag)." + ] + }, + { + "cell_type": "markdown", + "id": "646840fb-5212-48ea-8bc7-ec7be5ec727e", + "metadata": {}, + "source": [ + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cb58f273-2111-4a9b-8932-9b64c95030c8", + "metadata": {}, + "outputs": [], + "source": [ + "// @lc-docs-hide-cell\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const llm = new ChatOpenAI({ model: \"gpt-4o\" });" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "820244ae-74b4-4593-b392-822979dd91b8", + "metadata": {}, + "outputs": [], + "source": [ + "import { CheerioWebBaseLoader } from \"@langchain/community/document_loaders/web/cheerio\";\n", + "import { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n", + "import { MemoryVectorStore } from \"langchain/vectorstores/memory\";\n", + "import { OpenAIEmbeddings } from \"@langchain/openai\";\n", + "import { ChatPromptTemplate } from \"@langchain/core/prompts\";\n", + "import { createRetrievalChain } from \"langchain/chains/retrieval\";\n", + "import { createStuffDocumentsChain } from \"langchain/chains/combine_documents\";\n", + "\n", + "// 1. Load, chunk and index the contents of the blog to create a retriever.\n", + "const loader = new CheerioWebBaseLoader(\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " {\n", + " selector: \".post-content, .post-title, .post-header\"\n", + " }\n", + ");\n", + "const docs = await loader.load();\n", + "\n", + "const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000, chunkOverlap: 200 });\n", + "const splits = await textSplitter.splitDocuments(docs);\n", + "const vectorstore = await MemoryVectorStore.fromDocuments(splits, new OpenAIEmbeddings());\n", + "const retriever = vectorstore.asRetriever();\n", + "\n", + "// 2. Incorporate the retriever into a question-answering chain.\n", + "const systemPrompt = \n", + " \"You are an assistant for question-answering tasks. \" +\n", + " \"Use the following pieces of retrieved context to answer \" +\n", + " \"the question. If you don't know the answer, say that you \" +\n", + " \"don't know. Use three sentences maximum and keep the \" +\n", + " \"answer concise.\" +\n", + " \"\\n\\n\" +\n", + " \"{context}\";\n", + "\n", + "const prompt = ChatPromptTemplate.fromMessages([\n", + " [\"system\", systemPrompt],\n", + " [\"human\", \"{input}\"],\n", + "]);\n", + "\n", + "const questionAnswerChain = await createStuffDocumentsChain({\n", + " llm,\n", + " prompt,\n", + "});\n", + "\n", + "const ragChain = await createRetrievalChain({\n", + " retriever,\n", + " combineDocsChain: questionAnswerChain,\n", + "});" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "bf55faaf-0d17-4b74-925d-c478b555f7b2", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Contextualizing the question\n", - "\n", - "First we'll need to define a sub-chain that takes historical messages and the latest user question, and reformulates the question if it makes reference to any information in the historical information.\n", - "\n", - "We'll use a prompt that includes a `MessagesPlaceholder` variable under the name \"chat_history\". This allows us to pass in a list of Messages to the prompt using the \"chat_history\" input key, and these messages will be inserted after the system message and before the human message containing the latest question." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Task decomposition involves breaking down large and complex tasks into smaller, more manageable subgoals or steps. This approach helps agents or models efficiently handle intricate tasks by simplifying them into easier components. Task decomposition can be achieved through techniques like Chain of Thought, Tree of Thoughts, or by using task-specific instructions and human input.\n" + ] + } + ], + "source": [ + "const response = await ragChain.invoke({ input: \"What is Task Decomposition?\" });\n", + "console.log(response.answer);" + ] + }, + { + "cell_type": "markdown", + "id": "187404c7-db47-49c5-be29-9ecb96dc9afa", + "metadata": {}, + "source": [ + "Note that we have used the built-in chain constructors `createStuffDocumentsChain` and `createRetrievalChain`, so that the basic ingredients to our solution are:\n", + "\n", + "1. retriever;\n", + "2. prompt;\n", + "3. LLM.\n", + "\n", + "This will simplify the process of incorporating chat history.\n", + "\n", + "### Adding chat history\n", + "\n", + "The chain we have built uses the input query directly to retrieve relevant context. But in a conversational setting, the user query might require conversational context to be understood. For example, consider this exchange:\n", + "\n", + "> Human: \"What is Task Decomposition?\"\n", + ">\n", + "> AI: \"Task decomposition involves breaking down complex tasks into smaller and simpler steps to make them more manageable for an agent or model.\"\n", + ">\n", + "> Human: \"What are common ways of doing it?\"\n", + "\n", + "In order to answer the second question, our system needs to understand that \"it\" refers to \"Task Decomposition.\"\n", + "\n", + "We'll need to update two things about our existing app:\n", + "\n", + "1. **Prompt**: Update our prompt to support historical messages as an input.\n", + "2. **Contextualizing questions**: Add a sub-chain that takes the latest user question and reformulates it in the context of the chat history. This can be thought of simply as building a new \"history aware\" retriever. Whereas before we had:\n", + " - `query` -> `retriever` \n", + " Now we will have:\n", + " - `(query, conversation history)` -> `LLM` -> `rephrased query` -> `retriever`" + ] + }, + { + "cell_type": "markdown", + "id": "776ae958-cbdc-4471-8669-c6087436f0b5", + "metadata": {}, + "source": [ + "#### Contextualizing the question\n", + "\n", + "First we'll need to define a sub-chain that takes historical messages and the latest user question, and reformulates the question if it makes reference to any information in the historical information.\n", + "\n", + "We'll use a prompt that includes a `MessagesPlaceholder` variable under the name \"chat_history\". This allows us to pass in a list of Messages to the prompt using the \"chat_history\" input key, and these messages will be inserted after the system message and before the human message containing the latest question.\n", + "\n", + "Note that we leverage a helper function [createHistoryAwareRetriever](https://api.js.langchain.com/functions/langchain.chains_history_aware_retriever.createHistoryAwareRetriever.html) for this step, which manages the case where `chat_history` is empty, and otherwise applies `prompt.pipe(llm).pipe(new StringOutputParser()).pipe(retriever)` in sequence.\n", + "\n", + "`createHistoryAwareRetriever` constructs a chain that accepts keys `input` and `chat_history` as input, and has the same output schema as a retriever." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2b685428-8b82-4af1-be4f-7232c5d55b73", + "metadata": {}, + "outputs": [], + "source": [ + "import { createHistoryAwareRetriever } from \"langchain/chains/history_aware_retriever\";\n", + "import { MessagesPlaceholder } from \"@langchain/core/prompts\";\n", + "\n", + "const contextualizeQSystemPrompt = \n", + " \"Given a chat history and the latest user question \" +\n", + " \"which might reference context in the chat history, \" +\n", + " \"formulate a standalone question which can be understood \" +\n", + " \"without the chat history. Do NOT answer the question, \" +\n", + " \"just reformulate it if needed and otherwise return it as is.\";\n", + "\n", + "const contextualizeQPrompt = ChatPromptTemplate.fromMessages([\n", + " [\"system\", contextualizeQSystemPrompt],\n", + " new MessagesPlaceholder(\"chat_history\"),\n", + " [\"human\", \"{input}\"],\n", + "]);\n", + "\n", + "const historyAwareRetriever = await createHistoryAwareRetriever({\n", + " llm,\n", + " retriever,\n", + " rephrasePrompt: contextualizeQPrompt,\n", + "});" + ] + }, + { + "cell_type": "markdown", + "id": "42a47168-4a1f-4e39-bd2d-d5b03609a243", + "metadata": {}, + "source": [ + "This chain prepends a rephrasing of the input query to our retriever, so that the retrieval incorporates the context of the conversation.\n", + "\n", + "Now we can build our full QA chain. This is as simple as updating the retriever to be our new `historyAwareRetriever`.\n", + "\n", + "Again, we will use [createStuffDocumentsChain](https://api.js.langchain.com/functions/langchain.chains_combine_documents.createStuffDocumentsChain.html) to generate a `questionAnswerChain2`, with input keys `context`, `chat_history`, and `input`-- it accepts the retrieved context alongside the conversation history and query to generate an answer. A more detailed explaination is over [here](/docs/tutorials/rag/#built-in-chains)\n", + "\n", + "We build our final `ragChain2` with [createRetrievalChain](https://api.js.langchain.com/functions/langchain.chains_retrieval.createRetrievalChain.html). This chain applies the `historyAwareRetriever` and `questionAnswerChain2` in sequence, retaining intermediate outputs such as the retrieved context for convenience. It has input keys `input` and `chat_history`, and includes `input`, `chat_history`, `context`, and `answer` in its output." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "66f275f3-ddef-4678-b90d-ee64576878f9", + "metadata": {}, + "outputs": [], + "source": [ + "const qaPrompt = ChatPromptTemplate.fromMessages([\n", + " [\"system\", systemPrompt],\n", + " new MessagesPlaceholder(\"chat_history\"),\n", + " [\"human\", \"{input}\"],\n", + "]);\n", + "\n", + "const questionAnswerChain2 = await createStuffDocumentsChain({\n", + " llm,\n", + " prompt: qaPrompt,\n", + "});\n", + "\n", + "const ragChain2 = await createRetrievalChain({\n", + " retriever: historyAwareRetriever,\n", + " combineDocsChain: questionAnswerChain2,\n", + "});" + ] + }, + { + "cell_type": "markdown", + "id": "1ba1ae56-7ecb-4563-b792-50a1a5042df3", + "metadata": {}, + "source": [ + "Let's try this. Below we ask a question and a follow-up question that requires contextualization to return a sensible response. Because our chain includes a `\"chat_history\"` input, the caller needs to manage the chat history. We can achieve this by appending input and output messages to a list:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0005810b-1b95-4666-a795-08d80e478b83", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\";\n", - "\n", - "const contextualizeQSystemPrompt = `Given a chat history and the latest user question\n", - "which might reference context in the chat history, formulate a standalone question\n", - "which can be understood without the chat history. Do NOT answer the question,\n", - "just reformulate it if needed and otherwise return it as is.`;\n", - "\n", - "const contextualizeQPrompt = ChatPromptTemplate.fromMessages([\n", - " [\"system\", contextualizeQSystemPrompt],\n", - " new MessagesPlaceholder(\"chat_history\"),\n", - " [\"human\", \"{question}\"]\n", - "]);\n", - "const contextualizeQChain = contextualizeQPrompt.pipe(llm).pipe(new StringOutputParser());" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Common ways of doing Task Decomposition include:\n", + "1. Using simple prompting with an LLM, such as asking it to outline steps or subgoals for a task.\n", + "2. Employing task-specific instructions, like \"Write a story outline\" for writing a novel.\n", + "3. Incorporating human inputs for guidance.\n", + "Additionally, advanced approaches like Chain of Thought (CoT) and Tree of Thoughts (ToT) can further refine the process, and using an external classical planner with PDDL (as in LLM+P) is another option.\n" + ] + } + ], + "source": [ + "import { BaseMessage, HumanMessage, AIMessage } from \"@langchain/core/messages\";\n", + "\n", + "let chatHistory: BaseMessage[] = [];\n", + "\n", + "const question = \"What is Task Decomposition?\";\n", + "const aiMsg1 = await ragChain2.invoke({ input: question, chat_history: chatHistory });\n", + "chatHistory = chatHistory.concat([\n", + " new HumanMessage(question),\n", + " new AIMessage(aiMsg1.answer),\n", + "]);\n", + "\n", + "const secondQuestion = \"What are common ways of doing it?\";\n", + "const aiMsg2 = await ragChain2.invoke({ input: secondQuestion, chat_history: chatHistory });\n", + "\n", + "console.log(aiMsg2.answer);" + ] + }, + { + "cell_type": "markdown", + "id": "53a662c2-f38b-45f9-95c4-66de15637614", + "metadata": {}, + "source": [ + "#### Stateful management of chat history\n", + "\n", + "Here we've gone over how to add application logic for incorporating historical outputs, but we're still manually updating the chat history and inserting it into each input. In a real Q&A application we'll want some way of persisting chat history and some way of automatically inserting and updating it.\n", + "\n", + "For this we can use:\n", + "\n", + "- [BaseChatMessageHistory](https://api.js.langchain.com/classes/_langchain_core.chat_history.BaseChatMessageHistory.html): Store chat history.\n", + "- [RunnableWithMessageHistory](/docs/how_to/message_history): Wrapper for an LCEL chain and a `BaseChatMessageHistory` that handles injecting chat history into inputs and updating it after each invocation.\n", + "\n", + "For a detailed walkthrough of how to use these classes together to create a stateful conversational chain, head to the [How to add message history (memory)](/docs/how_to/message_history) LCEL page.\n", + "\n", + "Instances of `RunnableWithMessageHistory` manage the chat history for you. They accept a config with a key (`\"sessionId\"` by default) that specifies what conversation history to fetch and prepend to the input, and append the output to the same conversation history. Below is an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "9c3fb176-8d6a-4dc7-8408-6a22c5f7cc72", + "metadata": {}, + "outputs": [], + "source": [ + "import { RunnableWithMessageHistory } from \"@langchain/core/runnables\";\n", + "import { ChatMessageHistory } from \"langchain/stores/message/in_memory\";\n", + "\n", + "const demoEphemeralChatMessageHistoryForChain = new ChatMessageHistory();\n", + "\n", + "const conversationalRagChain = new RunnableWithMessageHistory({\n", + " runnable: ragChain2,\n", + " getMessageHistory: (_sessionId) => demoEphemeralChatMessageHistoryForChain,\n", + " inputMessagesKey: \"input\",\n", + " historyMessagesKey: \"chat_history\",\n", + " outputMessagesKey: \"answer\",\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "1046c92f-21b3-4214-907d-92878d8cba23", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Using this chain we can ask follow-up questions that reference past messages and have them reformulated into standalone questions:" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Task Decomposition involves breaking down complicated tasks into smaller, more manageable subgoals. Techniques such as the Chain of Thought (CoT) and Tree of Thoughts extend this by decomposing problems into multiple thought steps and exploring multiple reasoning possibilities at each step. LLMs can perform task decomposition using simple prompts, task-specific instructions, or human inputs, and some approaches like LLM+P involve using external classical planners.\n" + ] + } + ], + "source": [ + "const result1 = await conversationalRagChain.invoke(\n", + " { input: \"What is Task Decomposition?\" },\n", + " { configurable: { sessionId: \"abc123\" } }\n", + ");\n", + "console.log(result1.answer);" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "0e89c75f-7ad7-4331-a2fe-57579eb8f840", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[32m'What is the definition of \"large\" in the context of a language model?'\u001b[39m" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import { AIMessage, HumanMessage } from \"@langchain/core/messages\";\n", - "\n", - "await contextualizeQChain.invoke({\n", - " chat_history: [\n", - " new HumanMessage(\"What does LLM stand for?\"),\n", - " new AIMessage(\"Large language model\") \n", - " ],\n", - " question: \"What is meant by large\",\n", - "})" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Common ways of doing task decomposition include:\n", + "\n", + "1. Using simple prompting with an LLM, such as \"Steps for XYZ.\\n1.\" or \"What are the subgoals for achieving XYZ?\"\n", + "2. Utilizing task-specific instructions, like \"Write a story outline.\" for writing a novel.\n", + "3. Incorporating human inputs to guide and refine the decomposition process. \n", + "\n", + "Additionally, the LLM+P approach utilizes an external classical planner, involving PDDL to describe and plan complex tasks.\n" + ] + } + ], + "source": [ + "const result2 = await conversationalRagChain.invoke(\n", + " { input: \"What are common ways of doing it?\" },\n", + " { configurable: { sessionId: \"abc123\" } }\n", + ");\n", + "console.log(result2.answer);" + ] + }, + { + "cell_type": "markdown", + "id": "0ab1ded4-76d9-453f-9b9b-db9a4560c737", + "metadata": {}, + "source": [ + "### Tying it together" + ] + }, + { + "cell_type": "markdown", + "id": "8a08a5ea-df5b-4547-93c6-2a3940dd5c3e", + "metadata": {}, + "source": [ + "![](../../static/img/conversational_retrieval_chain.png)\n", + "\n", + "For convenience, we tie together all of the necessary steps in a single code cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "71c32048-1a41-465f-a9e2-c4affc332fd9", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Chain with chat history\n", - "\n", - "And now we can build our full QA chain. \n", - "\n", - "Notice we add some routing functionality to only run the \"condense question chain\" when our chat history isn't empty. Here we're taking advantage of the fact that if a function in an LCEL chain returns another chain, that chain will itself be invoked." - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "{ input: 'What is Task Decomposition?' }\n", + "----\n", + "{ chat_history: [] }\n", + "----\n", + "{\n", + " context: [\n", + " Document {\n", + " pageContent: 'Fig. 1. Overview of a LLM-powered autonomous agent system.\\n' +\n", + " 'Component One: Planning#\\n' +\n", + " 'A complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\\n' +\n", + " 'Task Decomposition#\\n' +\n", + " 'Chain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\\n' +\n", + " 'Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.',\n", + " metadata: [Object],\n", + " id: undefined\n", + " },\n", + " Document {\n", + " pageContent: 'Task decomposition can be done (1) by LLM with simple prompting like \"Steps for XYZ.\\\\n1.\", \"What are the subgoals for achieving XYZ?\", (2) by using task-specific instructions; e.g. \"Write a story outline.\" for writing a novel, or (3) with human inputs.\\n' +\n", + " 'Another quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\\n' +\n", + " 'Self-Reflection#',\n", + " metadata: [Object],\n", + " id: undefined\n", + " },\n", + " Document {\n", + " pageContent: 'Planning\\n' +\n", + " '\\n' +\n", + " 'Subgoal and decomposition: The agent breaks down large tasks into smaller, manageable subgoals, enabling efficient handling of complex tasks.\\n' +\n", + " 'Reflection and refinement: The agent can do self-criticism and self-reflection over past actions, learn from mistakes and refine them for future steps, thereby improving the quality of final results.\\n' +\n", + " '\\n' +\n", + " '\\n' +\n", + " 'Memory\\n' +\n", + " '\\n' +\n", + " 'Short-term memory: I would consider all the in-context learning (See Prompt Engineering) as utilizing short-term memory of the model to learn.\\n' +\n", + " 'Long-term memory: This provides the agent with the capability to retain and recall (infinite) information over extended periods, often by leveraging an external vector store and fast retrieval.\\n' +\n", + " '\\n' +\n", + " '\\n' +\n", + " 'Tool use\\n' +\n", + " '\\n' +\n", + " 'The agent learns to call external APIs for extra information that is missing from the model weights (often hard to change after pre-training), including current information, code execution capability, access to proprietary information sources and more.',\n", + " metadata: [Object],\n", + " id: undefined\n", + " },\n", + " Document {\n", + " pageContent: 'Resources:\\n' +\n", + " '1. Internet access for searches and information gathering.\\n' +\n", + " '2. Long Term memory management.\\n' +\n", + " '3. GPT-3.5 powered Agents for delegation of simple tasks.\\n' +\n", + " '4. File output.\\n' +\n", + " '\\n' +\n", + " 'Performance Evaluation:\\n' +\n", + " '1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities.\\n' +\n", + " '2. Constructively self-criticize your big-picture behavior constantly.\\n' +\n", + " '3. Reflect on past decisions and strategies to refine your approach.\\n' +\n", + " '4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.',\n", + " metadata: [Object],\n", + " id: undefined\n", + " }\n", + " ]\n", + "}\n", + "----\n", + "{ answer: '' }\n", + "----\n", + "{ answer: 'Task' }\n", + "----\n", + "{ answer: ' decomposition' }\n", + "----\n", + "{ answer: ' involves' }\n", + "----\n", + "{ answer: ' breaking' }\n", + "----\n", + "{ answer: ' down' }\n", + "----\n", + "{ answer: ' a' }\n", + "----\n", + "{ answer: ' complex' }\n", + "----\n", + "{ answer: ' task' }\n", + "----\n", + "{ answer: ' into' }\n", + "----\n", + "{ answer: ' smaller' }\n", + "----\n", + "{ answer: ' and' }\n", + "----\n", + "{ answer: ' more' }\n", + "----\n", + "{ answer: ' manageable' }\n", + "----\n", + "{ answer: ' sub' }\n", + "----\n", + "{ answer: 'goals' }\n", + "----\n", + "{ answer: ' or' }\n", + "----\n", + "{ answer: ' steps' }\n", + "----\n", + "{ answer: '.' }\n", + "----\n", + "{ answer: ' This' }\n", + "----\n", + "{ answer: ' process' }\n", + "----\n", + "{ answer: ' allows' }\n", + "----\n", + "{ answer: ' an' }\n", + "----\n", + "{ answer: ' agent' }\n", + "----\n", + "{ answer: ' or' }\n", + "----\n", + "{ answer: ' model' }\n", + "----\n", + "{ answer: ' to' }\n", + "----\n", + "{ answer: ' efficiently' }\n", + "----\n", + "{ answer: ' handle' }\n", + "----\n", + "{ answer: ' intricate' }\n", + "----\n", + "{ answer: ' tasks' }\n", + "----\n", + "{ answer: ' by' }\n", + "----\n", + "{ answer: ' dividing' }\n", + "----\n", + "{ answer: ' them' }\n", + "----\n", + "{ answer: ' into' }\n", + "----\n", + "{ answer: ' simpler' }\n", + "----\n", + "{ answer: ' components' }\n", + "----\n", + "{ answer: '.' }\n", + "----\n", + "{ answer: ' Task' }\n", + "----\n", + "{ answer: ' decomposition' }\n", + "----\n", + "{ answer: ' can' }\n", + "----\n", + "{ answer: ' be' }\n", + "----\n", + "{ answer: ' achieved' }\n", + "----\n", + "{ answer: ' through' }\n", + "----\n", + "{ answer: ' techniques' }\n", + "----\n", + "{ answer: ' like' }\n", + "----\n", + "{ answer: ' Chain' }\n", + "----\n", + "{ answer: ' of' }\n", + "----\n", + "{ answer: ' Thought' }\n", + "----\n", + "{ answer: ',' }\n", + "----\n", + "{ answer: ' Tree' }\n", + "----\n", + "{ answer: ' of' }\n", + "----\n", + "{ answer: ' Thoughts' }\n", + "----\n", + "{ answer: ',' }\n", + "----\n", + "{ answer: ' or' }\n", + "----\n", + "{ answer: ' by' }\n", + "----\n", + "{ answer: ' using' }\n", + "----\n", + "{ answer: ' task' }\n", + "----\n", + "{ answer: '-specific' }\n", + "----\n", + "{ answer: ' instructions' }\n", + "----\n", + "{ answer: '.' }\n", + "----\n", + "{ answer: '' }\n", + "----\n", + "{ answer: '' }\n", + "----\n" + ] + } + ], + "source": [ + "import { CheerioWebBaseLoader } from \"@langchain/community/document_loaders/web/cheerio\";\n", + "import { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n", + "import { MemoryVectorStore } from \"langchain/vectorstores/memory\";\n", + "import { OpenAIEmbeddings, ChatOpenAI } from \"@langchain/openai\";\n", + "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\";\n", + "import { createHistoryAwareRetriever } from \"langchain/chains/history_aware_retriever\";\n", + "import { createStuffDocumentsChain } from \"langchain/chains/combine_documents\";\n", + "import { createRetrievalChain } from \"langchain/chains/retrieval\";\n", + "import { RunnableWithMessageHistory } from \"@langchain/core/runnables\";\n", + "import { ChatMessageHistory } from \"langchain/stores/message/in_memory\";\n", + "import { BaseChatMessageHistory } from \"@langchain/core/chat_history\";\n", + "\n", + "const llm2 = new ChatOpenAI({ model: \"gpt-3.5-turbo\", temperature: 0 });\n", + "\n", + "// Construct retriever\n", + "const loader2 = new CheerioWebBaseLoader(\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " {\n", + " selector: \".post-content, .post-title, .post-header\"\n", + " }\n", + ");\n", + "\n", + "const docs2 = await loader2.load();\n", + "\n", + "const textSplitter2 = new RecursiveCharacterTextSplitter({ chunkSize: 1000, chunkOverlap: 200 });\n", + "const splits2 = await textSplitter2.splitDocuments(docs2);\n", + "const vectorstore2 = await MemoryVectorStore.fromDocuments(splits2, new OpenAIEmbeddings());\n", + "const retriever2 = vectorstore2.asRetriever();\n", + "\n", + "// Contextualize question\n", + "const contextualizeQSystemPrompt2 = \n", + " \"Given a chat history and the latest user question \" +\n", + " \"which might reference context in the chat history, \" +\n", + " \"formulate a standalone question which can be understood \" +\n", + " \"without the chat history. Do NOT answer the question, \" +\n", + " \"just reformulate it if needed and otherwise return it as is.\";\n", + "\n", + "const contextualizeQPrompt2 = ChatPromptTemplate.fromMessages([\n", + " [\"system\", contextualizeQSystemPrompt2],\n", + " new MessagesPlaceholder(\"chat_history\"),\n", + " [\"human\", \"{input}\"],\n", + "]);\n", + "\n", + "const historyAwareRetriever2 = await createHistoryAwareRetriever({\n", + " llm: llm2,\n", + " retriever: retriever2,\n", + " rephrasePrompt: contextualizeQPrompt2\n", + "});\n", + "\n", + "// Answer question\n", + "const systemPrompt2 = \n", + " \"You are an assistant for question-answering tasks. \" +\n", + " \"Use the following pieces of retrieved context to answer \" +\n", + " \"the question. If you don't know the answer, say that you \" +\n", + " \"don't know. Use three sentences maximum and keep the \" +\n", + " \"answer concise.\" +\n", + " \"\\n\\n\" +\n", + " \"{context}\";\n", + "\n", + "const qaPrompt2 = ChatPromptTemplate.fromMessages([\n", + " [\"system\", systemPrompt2],\n", + " new MessagesPlaceholder(\"chat_history\"),\n", + " [\"human\", \"{input}\"],\n", + "]);\n", + "\n", + "const questionAnswerChain3 = await createStuffDocumentsChain({\n", + " llm,\n", + " prompt: qaPrompt2,\n", + "});\n", + "\n", + "const ragChain3 = await createRetrievalChain({\n", + " retriever: historyAwareRetriever2,\n", + " combineDocsChain: questionAnswerChain3,\n", + "});\n", + "\n", + "// Statefully manage chat history\n", + "const store2: Record = {};\n", + "\n", + "function getSessionHistory2(sessionId: string): BaseChatMessageHistory {\n", + " if (!(sessionId in store2)) {\n", + " store2[sessionId] = new ChatMessageHistory();\n", + " }\n", + " return store2[sessionId];\n", + "}\n", + "\n", + "const conversationalRagChain2 = new RunnableWithMessageHistory({\n", + " runnable: ragChain3,\n", + " getMessageHistory: getSessionHistory2,\n", + " inputMessagesKey: \"input\",\n", + " historyMessagesKey: \"chat_history\",\n", + " outputMessagesKey: \"answer\",\n", + "});\n", + "\n", + "// Example usage\n", + "const query2 = \"What is Task Decomposition?\";\n", + "\n", + "for await (const s of await conversationalRagChain2.stream(\n", + " { input: query2 },\n", + " { configurable: { sessionId: \"unique_session_id\" } }\n", + ")) {\n", + " console.log(s);\n", + " console.log(\"----\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "861da8ed-d890-4fdc-a3bf-30433db61e0d", + "metadata": {}, + "source": [ + "## Agents {#agents}\n", + "\n", + "Agents leverage the reasoning capabilities of LLMs to make decisions during execution. Using agents allow you to offload some discretion over the retrieval process. Although their behavior is less predictable than chains, they offer some advantages in this context:\n", + "\n", + "- Agents generate the input to the retriever directly, without necessarily needing us to explicitly build in contextualization, as we did above;\n", + "- Agents can execute multiple retrieval steps in service of a query, or refrain from executing a retrieval step altogether (e.g., in response to a generic greeting from a user).\n", + "\n", + "### Retrieval tool\n", + "\n", + "Agents can access \"tools\" and manage their execution. In this case, we will convert our retriever into a LangChain tool to be wielded by the agent:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "809cc747-2135-40a2-8e73-e4556343ee64", + "metadata": {}, + "outputs": [], + "source": [ + "import { createRetrieverTool } from \"langchain/tools/retriever\";\n", + "\n", + "const tool = createRetrieverTool(\n", + " retriever,\n", + " {\n", + " name: \"blog_post_retriever\",\n", + " description: \"Searches and returns excerpts from the Autonomous Agents blog post.\",\n", + " }\n", + ")\n", + "const tools = [tool]" + ] + }, + { + "cell_type": "markdown", + "id": "07dcb968-ed9a-458a-85e1-528cd28c6965", + "metadata": {}, + "source": [ + "Tools are LangChain [Runnables](/docs/concepts#langchain-expression-language-lcel), and implement the usual interface:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "931c4fe3-c603-4efb-9b37-5f7cbbb1cbbd", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "import { ChatPromptTemplate, MessagesPlaceholder } from \"@langchain/core/prompts\"\n", - "import { RunnablePassthrough, RunnableSequence } from \"@langchain/core/runnables\";\n", - "import { formatDocumentsAsString } from \"langchain/util/document\";\n", - "\n", - "const qaSystemPrompt = `You are an assistant for question-answering tasks.\n", - "Use the following pieces of retrieved context to answer the question.\n", - "If you don't know the answer, just say that you don't know.\n", - "Use three sentences maximum and keep the answer concise.\n", - "\n", - "{context}`\n", - "\n", - "const qaPrompt = ChatPromptTemplate.fromMessages([\n", - " [\"system\", qaSystemPrompt],\n", - " new MessagesPlaceholder(\"chat_history\"),\n", - " [\"human\", \"{question}\"]\n", - "]);\n", - "\n", - "const contextualizedQuestion = (input: Record) => {\n", - " if (\"chat_history\" in input) {\n", - " return contextualizeQChain;\n", - " }\n", - " return input.question;\n", - "};\n", - "\n", - "const ragChain = RunnableSequence.from([\n", - " RunnablePassthrough.assign({\n", - " context: (input: Record) => {\n", - " if (\"chat_history\" in input) {\n", - " const chain = contextualizedQuestion(input);\n", - " return chain.pipe(retriever).pipe(formatDocumentsAsString);\n", - " }\n", - " return \"\";\n", - " },\n", - " }),\n", - " qaPrompt,\n", - " llm\n", - "])" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Task decomposition can be done (1) by LLM with simple prompting like \"Steps for XYZ.\\n1.\", \"What are the subgoals for achieving XYZ?\", (2) by using task-specific instructions; e.g. \"Write a story outline.\" for writing a novel, or (3) with human inputs.\n", + "Another quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\n", + "Self-Reflection#\n", + "\n", + "Fig. 1. Overview of a LLM-powered autonomous agent system.\n", + "Component One: Planning#\n", + "A complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\n", + "Task Decomposition#\n", + "Chain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\n", + "Tree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\n", + "\n", + "(3) Task execution: Expert models execute on the specific tasks and log results.\n", + "Instruction:\n", + "\n", + "With the input and the inference results, the AI assistant needs to describe the process and results. The previous stages can be formed as - User Input: {{ User Input }}, Task Planning: {{ Tasks }}, Model Selection: {{ Model Assignment }}, Task Execution: {{ Predictions }}. You must first answer the user's request in a straightforward manner. Then describe the task process and show your analysis and model inference results to the user in the first person. If inference results contain a file path, must tell the user the complete file path.\n", + "\n", + "Resources:\n", + "1. Internet access for searches and information gathering.\n", + "2. Long Term memory management.\n", + "3. GPT-3.5 powered Agents for delegation of simple tasks.\n", + "4. File output.\n", + "\n", + "Performance Evaluation:\n", + "1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities.\n", + "2. Constructively self-criticize your big-picture behavior constantly.\n", + "3. Reflect on past decisions and strategies to refine your approach.\n", + "4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.\n" + ] + } + ], + "source": [ + "console.log(await tool.invoke({ query: \"task decomposition\" }))" + ] + }, + { + "cell_type": "markdown", + "id": "f77e0217-28be-4b8b-b4c4-9cc4ed5ec201", + "metadata": {}, + "source": [ + "### Agent constructor\n", + "\n", + "Now that we have defined the tools and the LLM, we can create the agent. We will be using [LangGraph](/docs/concepts/#langgraph) to construct the agent. \n", + "Currently we are using a high level interface to construct the agent, but the nice thing about LangGraph is that this high-level interface is backed by a low-level, highly controllable API in case you want to modify the agent logic." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "1726d151-4653-4c72-a187-a14840add526", + "metadata": {}, + "outputs": [], + "source": [ + "import { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n", + "\n", + "const agentExecutor = createReactAgent({ llm, tools });" + ] + }, + { + "cell_type": "markdown", + "id": "6d5152ca-1c3b-4f58-bb28-f31c0be7ba66", + "metadata": {}, + "source": [ + "We can now try it out. Note that so far it is not stateful (we still need to add in memory)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "170403a2-c914-41db-85d8-a2c381da112d", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AIMessage {\n", - " lc_serializable: true,\n", - " lc_kwargs: {\n", - " content: \"Task decomposition is a technique used to break down complex tasks into smaller and more manageable \"... 278 more characters,\n", - " additional_kwargs: { function_call: undefined, tool_calls: undefined }\n", - " },\n", - " lc_namespace: [ \"langchain_core\", \"messages\" ],\n", - " content: \"Task decomposition is a technique used to break down complex tasks into smaller and more manageable \"... 278 more characters,\n", - " name: undefined,\n", - " additional_kwargs: { function_call: undefined, tool_calls: undefined }\n", - "}\n" - ] - }, - { - "data": { - "text/plain": [ - "AIMessage {\n", - " lc_serializable: \u001b[33mtrue\u001b[39m,\n", - " lc_kwargs: {\n", - " content: \u001b[32m\"Common ways of task decomposition include using prompting techniques like Chain of Thought (CoT) or \"\u001b[39m... 332 more characters,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m }\n", - " },\n", - " lc_namespace: [ \u001b[32m\"langchain_core\"\u001b[39m, \u001b[32m\"messages\"\u001b[39m ],\n", - " content: \u001b[32m\"Common ways of task decomposition include using prompting techniques like Chain of Thought (CoT) or \"\u001b[39m... 332 more characters,\n", - " name: \u001b[90mundefined\u001b[39m,\n", - " additional_kwargs: { function_call: \u001b[90mundefined\u001b[39m, tool_calls: \u001b[90mundefined\u001b[39m }\n", - "}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "let chat_history = [];\n", - "\n", - "const question = \"What is task decomposition?\";\n", - "const aiMsg = await ragChain.invoke({ question, chat_history });\n", - "console.log(aiMsg)\n", - "chat_history = chat_history.concat(aiMsg);\n", - "\n", - "const secondQuestion = \"What are common ways of doing it?\";\n", - "await ragChain.invoke({ question: secondQuestion, chat_history });" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABABtUmgD1ZlOHZd0nD9TR8yb3mMe\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_dWxEY41mg9VSLamVYHltsUxL\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 19,\n", + " \"promptTokens\": 66,\n", + " \"totalTokens\": 85\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_3537616b13\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"blog_post_retriever\",\n", + " \"args\": {\n", + " \"query\": \"Task Decomposition\"\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_dWxEY41mg9VSLamVYHltsUxL\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 66,\n", + " \"output_tokens\": 19,\n", + " \"total_tokens\": 85\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " tools: {\n", + " messages: [\n", + " ToolMessage {\n", + " \"content\": \"Fig. 1. Overview of a LLM-powered autonomous agent system.\\nComponent One: Planning#\\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\\nTask Decomposition#\\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\\nTree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\\n\\nTask decomposition can be done (1) by LLM with simple prompting like \\\"Steps for XYZ.\\\\n1.\\\", \\\"What are the subgoals for achieving XYZ?\\\", (2) by using task-specific instructions; e.g. \\\"Write a story outline.\\\" for writing a novel, or (3) with human inputs.\\nAnother quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\\nSelf-Reflection#\\n\\n(3) Task execution: Expert models execute on the specific tasks and log results.\\nInstruction:\\n\\nWith the input and the inference results, the AI assistant needs to describe the process and results. The previous stages can be formed as - User Input: {{ User Input }}, Task Planning: {{ Tasks }}, Model Selection: {{ Model Assignment }}, Task Execution: {{ Predictions }}. You must first answer the user's request in a straightforward manner. Then describe the task process and show your analysis and model inference results to the user in the first person. If inference results contain a file path, must tell the user the complete file path.\\n\\nPlanning\\n\\nSubgoal and decomposition: The agent breaks down large tasks into smaller, manageable subgoals, enabling efficient handling of complex tasks.\\nReflection and refinement: The agent can do self-criticism and self-reflection over past actions, learn from mistakes and refine them for future steps, thereby improving the quality of final results.\\n\\n\\nMemory\\n\\nShort-term memory: I would consider all the in-context learning (See Prompt Engineering) as utilizing short-term memory of the model to learn.\\nLong-term memory: This provides the agent with the capability to retain and recall (infinite) information over extended periods, often by leveraging an external vector store and fast retrieval.\\n\\n\\nTool use\\n\\nThe agent learns to call external APIs for extra information that is missing from the model weights (often hard to change after pre-training), including current information, code execution capability, access to proprietary information sources and more.\",\n", + " \"name\": \"blog_post_retriever\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_dWxEY41mg9VSLamVYHltsUxL\"\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABABuSj5FHmHFdeR2Pv7Cxcmq5aQz\",\n", + " \"content\": \"Task Decomposition is a technique that allows an agent to break down a complex task into smaller, more manageable subtasks or steps. The primary goal is to simplify the task to ensure efficient execution and better understanding. \\n\\n### Methods in Task Decomposition:\\n1. **Chain of Thought (CoT)**:\\n - **Description**: This technique involves instructing the model to “think step by step” to decompose hard tasks into smaller ones. It transforms large tasks into multiple manageable tasks, enhancing the model's performance and providing insight into its thinking process. \\n - **Example**: When given a complex problem, the model outlines sequential steps to reach a solution.\\n\\n2. **Tree of Thoughts**:\\n - **Description**: This extends CoT by exploring multiple reasoning possibilities at each step. The problem is decomposed into multiple thought steps, with several thoughts generated per step, forming a sort of decision tree.\\n - **Example**: For a given task, the model might consider various alternative actions at each stage, evaluating each before proceeding.\\n\\n3. **LLM with Prompts**:\\n - **Description**: Basic task decomposition can be done via simple prompts like \\\"Steps for XYZ\\\" or \\\"What are the subgoals for achieving XYZ?\\\" This can also be guided by task-specific instructions or human inputs when necessary.\\n - **Example**: Asking the model to list the subgoals for writing a novel might produce an outline broken down into chapters, character development, and plot points.\\n\\n4. **LLM+P**:\\n - **Description**: This approach involves outsourcing long-term planning to an external classical planner using Planning Domain Definition Language (PDDL). The task is translated into a PDDL problem by the model, planned using classical planning tools, and then translated back into natural language.\\n - **Example**: In robotics, translating a task into PDDL and then using a domain-specific planner to generate a sequence of actions.\\n\\n### Applications:\\n- **Planning**: Helps an agent plan tasks by breaking them into clear, manageable steps.\\n- **Self-Reflection**: Allows agents to reflect and refine their actions, learning from past mistakes to improve future performance.\\n- **Memory**: Utilizes short-term memory for immediate context and long-term memory for retaining and recalling information over extended periods.\\n- **Tool Use**: Enables the agent to call external APIs for additional information or capabilities not inherent in the model.\\n\\nIn essence, task decomposition leverages various methodologies to simplify complex tasks, ensuring better performance, improved reasoning, and effective task execution.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 522,\n", + " \"promptTokens\": 821,\n", + " \"totalTokens\": 1343\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 821,\n", + " \"output_tokens\": 522,\n", + " \"total_tokens\": 1343\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" + ] + } + ], + "source": [ + "const query = \"What is Task Decomposition?\";\n", + "\n", + "for await (const s of await agentExecutor.stream(\n", + " { messages: [new HumanMessage(query)] }\n", + ")) {\n", + " console.log(s);\n", + " console.log(\"----\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "1df703b1-aad6-48fb-b6fa-703e32ea88b9", + "metadata": {}, + "source": [ + "LangGraph comes with built in persistence, so we don't need to use ChatMessageHistory! Rather, we can pass in a checkpointer to our LangGraph agent directly" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "04a3a664-3c3f-4cd1-9995-26662a52da7c", + "metadata": {}, + "outputs": [], + "source": [ + "import { MemorySaver } from \"@langchain/langgraph\";\n", + "\n", + "const memory = new MemorySaver();\n", + "\n", + "const agentExecutorWithMemory = createReactAgent({ llm, tools, checkpointSaver: memory });" + ] + }, + { + "cell_type": "markdown", + "id": "02026f78-338e-4d18-9f05-131e1dd59197", + "metadata": {}, + "source": [ + "This is all we need to construct a conversational RAG agent.\n", + "\n", + "Let's observe its behavior. Note that if we input a query that does not require a retrieval step, the agent does not execute one:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "d6d70833-b958-4cd7-9e27-29c1c08bb1b8", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "See the first [LastSmith trace here](https://smith.langchain.com/public/527981c6-5018-4b68-a11a-ebcde77843e7/r) and the [second trace here](https://smith.langchain.com/public/7b97994a-ab9f-4bf3-a2e4-abb609e5610a/r)" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABACGc1vDPUSHYN7YVkuUMwpKR20P\",\n", + " \"content\": \"Hello, Bob! How can I assist you today?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 12,\n", + " \"promptTokens\": 64,\n", + " \"totalTokens\": 76\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_e375328146\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 64,\n", + " \"output_tokens\": 12,\n", + " \"total_tokens\": 76\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" + ] + } + ], + "source": [ + "const config = { configurable: { thread_id: \"abc123\" } };\n", + "\n", + "for await (const s of await agentExecutorWithMemory.stream(\n", + " { messages: [new HumanMessage(\"Hi! I'm bob\")] },\n", + " config\n", + ")) {\n", + " console.log(s);\n", + " console.log(\"----\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "a7928865-3dd6-4d36-abc6-2a30de770d09", + "metadata": {}, + "source": [ + "Further, if we input a query that does require a retrieval step, the agent generates the input to the tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "e2c570ae-dd91-402c-8693-ae746de63b16", + "metadata": {}, + "outputs": [ { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here we've gone over how to add application logic for incorporating historical outputs, but we're still manually updating the chat history and inserting it into each input. In a real Q&A application we'll want some way of persisting chat history and some way of automatically inserting and updating it.\n", - "\n", - "For this we can use:\n", - "\n", - "- [BaseChatMessageHistory](https://api.js.langchain.com/classes/langchain_core.chat_history.BaseChatMessageHistory.html): Store chat history.\n", - "- [RunnableWithMessageHistory](/docs/how_to/message_history/): Wrapper for an LCEL chain and a `BaseChatMessageHistory` that handles injecting chat history into inputs and updating it after each invocation.\n", - "\n", - "For a detailed walkthrough of how to use these classes together to create a stateful conversational chain, head to the [How to add message history (memory)](/docs/how_to/message_history/) LCEL page." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABACI6WN7hkfJjFhIUBGt3TswtPOv\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_Lys2G4TbOMJ6RBuVvKnFSK4V\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 19,\n", + " \"promptTokens\": 89,\n", + " \"totalTokens\": 108\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_f82f5b050c\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"blog_post_retriever\",\n", + " \"args\": {\n", + " \"query\": \"Task Decomposition\"\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_Lys2G4TbOMJ6RBuVvKnFSK4V\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 89,\n", + " \"output_tokens\": 19,\n", + " \"total_tokens\": 108\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " tools: {\n", + " messages: [\n", + " ToolMessage {\n", + " \"content\": \"Fig. 1. Overview of a LLM-powered autonomous agent system.\\nComponent One: Planning#\\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\\nTask Decomposition#\\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\\nTree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\\n\\nTask decomposition can be done (1) by LLM with simple prompting like \\\"Steps for XYZ.\\\\n1.\\\", \\\"What are the subgoals for achieving XYZ?\\\", (2) by using task-specific instructions; e.g. \\\"Write a story outline.\\\" for writing a novel, or (3) with human inputs.\\nAnother quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\\nSelf-Reflection#\\n\\n(3) Task execution: Expert models execute on the specific tasks and log results.\\nInstruction:\\n\\nWith the input and the inference results, the AI assistant needs to describe the process and results. The previous stages can be formed as - User Input: {{ User Input }}, Task Planning: {{ Tasks }}, Model Selection: {{ Model Assignment }}, Task Execution: {{ Predictions }}. You must first answer the user's request in a straightforward manner. Then describe the task process and show your analysis and model inference results to the user in the first person. If inference results contain a file path, must tell the user the complete file path.\\n\\nPlanning\\n\\nSubgoal and decomposition: The agent breaks down large tasks into smaller, manageable subgoals, enabling efficient handling of complex tasks.\\nReflection and refinement: The agent can do self-criticism and self-reflection over past actions, learn from mistakes and refine them for future steps, thereby improving the quality of final results.\\n\\n\\nMemory\\n\\nShort-term memory: I would consider all the in-context learning (See Prompt Engineering) as utilizing short-term memory of the model to learn.\\nLong-term memory: This provides the agent with the capability to retain and recall (infinite) information over extended periods, often by leveraging an external vector store and fast retrieval.\\n\\n\\nTool use\\n\\nThe agent learns to call external APIs for extra information that is missing from the model weights (often hard to change after pre-training), including current information, code execution capability, access to proprietary information sources and more.\",\n", + " \"name\": \"blog_post_retriever\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_Lys2G4TbOMJ6RBuVvKnFSK4V\"\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABACJu56eYSAyyMNaV9UEUwHS8vRu\",\n", + " \"content\": \"Task Decomposition is a method used to break down complicated tasks into smaller, more manageable steps. This approach leverages the \\\"Chain of Thought\\\" (CoT) technique, which prompts models to \\\"think step by step\\\" to enhance performance on complex tasks. Here’s a summary of the key concepts related to Task Decomposition:\\n\\n1. **Chain of Thought (CoT):**\\n - A prompting technique that encourages models to decompose hard tasks into simpler steps, transforming big tasks into multiple manageable sub-tasks.\\n - CoT helps to provide insights into the model’s thinking process.\\n\\n2. **Tree of Thoughts:**\\n - An extension of CoT, this approach explores multiple reasoning paths at each step.\\n - It creates a tree structure by generating multiple thoughts per step, and uses search methods like breadth-first search (BFS) or depth-first search (DFS) to explore these thoughts.\\n - Each state is evaluated by a classifier or majority vote.\\n\\n3. **Methods for Task Decomposition:**\\n - Simple prompting such as instructing with phrases like \\\"Steps for XYZ: 1., 2., 3.\\\" or \\\"What are the subgoals for achieving XYZ?\\\".\\n - Using task-specific instructions like \\\"Write a story outline\\\" for specific tasks such as writing a novel.\\n - Incorporating human inputs for better granularity.\\n\\n4. **LLM+P (Long-horizon Planning):**\\n - A method that involves using an external classical planner for long-horizon planning.\\n - The process involves translating the problem into a Planning Domain Definition Language (PDDL) problem, using a classical planner to generate a PDDL plan, and then translating it back into natural language.\\n\\nTask Decomposition is essential in planning complex tasks, allowing for efficient handling by breaking them into sub-tasks and sub-goals. This process is integral to the functioning of autonomous agent systems and enhances their capability to execute intricate tasks effectively.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 396,\n", + " \"promptTokens\": 844,\n", + " \"totalTokens\": 1240\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_9f2bfdaa89\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 844,\n", + " \"output_tokens\": 396,\n", + " \"total_tokens\": 1240\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" + ] } - ], - "metadata": { - "kernelspec": { - "display_name": "Deno", - "language": "typescript", - "name": "deno" - }, - "language_info": { - "file_extension": ".ts", - "mimetype": "text/x.typescript", - "name": "typescript", - "nb_converter": "script", - "pygments_lexer": "typescript", - "version": "5.3.3" + ], + "source": [ + "for await (const s of await agentExecutorWithMemory.stream(\n", + " { messages: [new HumanMessage(query)] },\n", + " config\n", + ")) {\n", + " console.log(s);\n", + " console.log(\"----\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "26eaae33-3c4e-49fc-9fc6-db8967e25579", + "metadata": {}, + "source": [ + "Above, instead of inserting our query verbatim into the tool, the agent stripped unnecessary words like \"what\" and \"is\".\n", + "\n", + "This same principle allows the agent to use the context of the conversation when necessary:" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "570d8c68-136e-4ba5-969a-03ba195f6118", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABACPZzSugzrREQRO4mVQfI3cQOeL\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_5nSZb396Tcg73Pok6Bx1XV8b\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 22,\n", + " \"promptTokens\": 1263,\n", + " \"totalTokens\": 1285\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_9f2bfdaa89\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"blog_post_retriever\",\n", + " \"args\": {\n", + " \"query\": \"common ways of doing task decomposition\"\n", + " },\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_5nSZb396Tcg73Pok6Bx1XV8b\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 1263,\n", + " \"output_tokens\": 22,\n", + " \"total_tokens\": 1285\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " tools: {\n", + " messages: [\n", + " ToolMessage {\n", + " \"content\": \"Fig. 1. Overview of a LLM-powered autonomous agent system.\\nComponent One: Planning#\\nA complicated task usually involves many steps. An agent needs to know what they are and plan ahead.\\nTask Decomposition#\\nChain of thought (CoT; Wei et al. 2022) has become a standard prompting technique for enhancing model performance on complex tasks. The model is instructed to “think step by step” to utilize more test-time computation to decompose hard tasks into smaller and simpler steps. CoT transforms big tasks into multiple manageable tasks and shed lights into an interpretation of the model’s thinking process.\\nTree of Thoughts (Yao et al. 2023) extends CoT by exploring multiple reasoning possibilities at each step. It first decomposes the problem into multiple thought steps and generates multiple thoughts per step, creating a tree structure. The search process can be BFS (breadth-first search) or DFS (depth-first search) with each state evaluated by a classifier (via a prompt) or majority vote.\\n\\nTask decomposition can be done (1) by LLM with simple prompting like \\\"Steps for XYZ.\\\\n1.\\\", \\\"What are the subgoals for achieving XYZ?\\\", (2) by using task-specific instructions; e.g. \\\"Write a story outline.\\\" for writing a novel, or (3) with human inputs.\\nAnother quite distinct approach, LLM+P (Liu et al. 2023), involves relying on an external classical planner to do long-horizon planning. This approach utilizes the Planning Domain Definition Language (PDDL) as an intermediate interface to describe the planning problem. In this process, LLM (1) translates the problem into “Problem PDDL”, then (2) requests a classical planner to generate a PDDL plan based on an existing “Domain PDDL”, and finally (3) translates the PDDL plan back into natural language. Essentially, the planning step is outsourced to an external tool, assuming the availability of domain-specific PDDL and a suitable planner which is common in certain robotic setups but not in many other domains.\\nSelf-Reflection#\\n\\nPlanning\\n\\nSubgoal and decomposition: The agent breaks down large tasks into smaller, manageable subgoals, enabling efficient handling of complex tasks.\\nReflection and refinement: The agent can do self-criticism and self-reflection over past actions, learn from mistakes and refine them for future steps, thereby improving the quality of final results.\\n\\n\\nMemory\\n\\nShort-term memory: I would consider all the in-context learning (See Prompt Engineering) as utilizing short-term memory of the model to learn.\\nLong-term memory: This provides the agent with the capability to retain and recall (infinite) information over extended periods, often by leveraging an external vector store and fast retrieval.\\n\\n\\nTool use\\n\\nThe agent learns to call external APIs for extra information that is missing from the model weights (often hard to change after pre-training), including current information, code execution capability, access to proprietary information sources and more.\\n\\nResources:\\n1. Internet access for searches and information gathering.\\n2. Long Term memory management.\\n3. GPT-3.5 powered Agents for delegation of simple tasks.\\n4. File output.\\n\\nPerformance Evaluation:\\n1. Continuously review and analyze your actions to ensure you are performing to the best of your abilities.\\n2. Constructively self-criticize your big-picture behavior constantly.\\n3. Reflect on past decisions and strategies to refine your approach.\\n4. Every command has a cost, so be smart and efficient. Aim to complete tasks in the least number of steps.\",\n", + " \"name\": \"blog_post_retriever\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_call_id\": \"call_5nSZb396Tcg73Pok6Bx1XV8b\"\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n", + "{\n", + " agent: {\n", + " messages: [\n", + " AIMessage {\n", + " \"id\": \"chatcmpl-ABACQt9pT5dKCTaGQpVawcmCCWdET\",\n", + " \"content\": \"According to the blog post, common ways of performing Task Decomposition include:\\n\\n1. **Using Large Language Models (LLMs) with Simple Prompting:**\\n - Providing clear and structured prompts such as \\\"Steps for XYZ: 1., 2., 3.\\\" or asking \\\"What are the subgoals for achieving XYZ?\\\"\\n - This allows the model to break down the tasks step-by-step.\\n\\n2. **Task-Specific Instructions:**\\n - Employing specific instructions tailored to the task at hand, for example, \\\"Write a story outline\\\" for writing a novel.\\n - These instructions guide the model in decomposing the task appropriately.\\n\\n3. **Involving Human Inputs:**\\n - Integrating insights and directives from humans to aid in the decomposition process.\\n - This can ensure that the breakdown is comprehensive and accurately reflects the nuances of the task.\\n\\n4. **LLM+P Approach for Long-Horizon Planning:**\\n - Utilizing an external classical planner by translating the problem into Planning Domain Definition Language (PDDL).\\n - The process involves:\\n 1. Translating the problem into “Problem PDDL”.\\n 2. Requesting a classical planner to generate a PDDL plan based on an existing “Domain PDDL”.\\n 3. Translating the PDDL plan back into natural language.\\n\\nThese methods enable effective management and execution of complex tasks by transforming them into simpler, more manageable components.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 292,\n", + " \"promptTokens\": 2010,\n", + " \"totalTokens\": 2302\n", + " },\n", + " \"finish_reason\": \"stop\",\n", + " \"system_fingerprint\": \"fp_9f2bfdaa89\"\n", + " },\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 2010,\n", + " \"output_tokens\": 292,\n", + " \"total_tokens\": 2302\n", + " }\n", + " }\n", + " ]\n", + " }\n", + "}\n", + "----\n" + ] } + ], + "source": [ + "const query3 = \"What according to the blog post are common ways of doing it? redo the search\";\n", + "\n", + "for await (const s of await agentExecutorWithMemory.stream(\n", + " { messages: [new HumanMessage(query3)] },\n", + " config\n", + ")) {\n", + " console.log(s);\n", + " console.log(\"----\");\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "f2724616-c106-4e15-a61a-3077c535f692", + "metadata": {}, + "source": [ + "Note that the agent was able to infer that \"it\" in our query refers to \"task decomposition\", and generated a reasonable search query as a result-- in this case, \"common ways of task decomposition\"." + ] + }, + { + "cell_type": "markdown", + "id": "1cf87847-23bb-4672-b41c-12ad9cf81ed4", + "metadata": {}, + "source": [ + "### Tying it together\n", + "\n", + "For convenience, we tie together all of the necessary steps in a single code cell:" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "b1d2b4d4-e604-497d-873d-d345b808578e", + "metadata": {}, + "outputs": [], + "source": [ + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "import { MemorySaver } from \"@langchain/langgraph\";\n", + "import { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n", + "import { CheerioWebBaseLoader } from \"@langchain/community/document_loaders/web/cheerio\";\n", + "import { RecursiveCharacterTextSplitter } from \"langchain/text_splitter\";\n", + "import { MemoryVectorStore } from \"langchain/vectorstores/memory\";\n", + "import { createRetrieverTool } from \"langchain/tools/retriever\";\n", + "\n", + "const memory3 = new MemorySaver();\n", + "const llm3 = new ChatOpenAI({ model: \"gpt-4o\", temperature: 0 });\n", + "\n", + "// Construct retriever\n", + "const loader3 = new CheerioWebBaseLoader(\n", + " \"https://lilianweng.github.io/posts/2023-06-23-agent/\",\n", + " {\n", + " selector: \".post-content, .post-title, .post-header\"\n", + " }\n", + ");\n", + "\n", + "const docs3 = await loader3.load();\n", + "\n", + "const textSplitter3 = new RecursiveCharacterTextSplitter({ chunkSize: 1000, chunkOverlap: 200 });\n", + "const splits3 = await textSplitter3.splitDocuments(docs3);\n", + "const vectorstore3 = await MemoryVectorStore.fromDocuments(splits3, new OpenAIEmbeddings());\n", + "const retriever3 = vectorstore3.asRetriever();\n", + "\n", + "// Build retriever tool\n", + "const tool3 = createRetrieverTool(\n", + " retriever3,\n", + " {\n", + " name: \"blog_post_retriever\",\n", + " description: \"Searches and returns excerpts from the Autonomous Agents blog post.\",\n", + " }\n", + ");\n", + "const tools3 = [tool3];\n", + "\n", + "const agentExecutor3 = createReactAgent({ llm: llm3, tools: tools3, checkpointSaver: memory3 });" + ] + }, + { + "cell_type": "markdown", + "id": "cd6bf4f4-74f4-419d-9e26-f0ed83cf05fa", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "We've covered the steps to build a basic conversational Q&A application:\n", + "\n", + "- We used chains to build a predictable application that generates search queries for each user input;\n", + "- We used agents to build an application that \"decides\" when and how to generate search queries.\n", + "\n", + "To explore different types of retrievers and retrieval strategies, visit the [retrievers](/docs/how_to/#retrievers) section of the how-to guides.\n", + "\n", + "For a detailed walkthrough of LangChain's conversation memory abstractions, visit the [How to add message history (memory)](/docs/how_to/message_history) LCEL page." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "TypeScript", + "language": "typescript", + "name": "tslab" }, - "nbformat": 4, - "nbformat_minor": 2 + "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, + "file_extension": ".ts", + "mimetype": "text/typescript", + "name": "typescript", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/docs/core_docs/docs/tutorials/rag.ipynb b/docs/core_docs/docs/tutorials/rag.ipynb index c845c9149d72..5f19f579fb51 100644 --- a/docs/core_docs/docs/tutorials/rag.ipynb +++ b/docs/core_docs/docs/tutorials/rag.ipynb @@ -607,7 +607,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We’ll use the [LCEL Runnable](/docs/how_to/#langchain-expression-language-lcel) protocol to define the chain, allowing us to - pipe together components and functions in a transparent way - automatically trace our chain in LangSmith - get streaming, async, and batched calling out of the box" + "We’ll use [LangChain Expression Language (LCEL)](/docs/how_to/#langchain-expression-language-lcel) to define the chain, allowing us to - pipe together components and functions in a transparent way - automatically trace our chain in LangSmith - get streaming, async, and batched calling out of the box" ] }, { diff --git a/docs/core_docs/docs/versions/migrating_memory/chat_history.ipynb b/docs/core_docs/docs/versions/migrating_memory/chat_history.ipynb new file mode 100644 index 000000000000..927aec36ad0c --- /dev/null +++ b/docs/core_docs/docs/versions/migrating_memory/chat_history.ipynb @@ -0,0 +1,268 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c298a5c9-b9af-481d-9eba-cbd65f987a8a", + "metadata": {}, + "source": [ + "# How to use BaseChatMessageHistory with LangGraph\n", + "\n", + ":::info Prerequisites\n", + "\n", + "This guide assumes familiarity with the following concepts:\n", + "\n", + "- [Chat History](/docs/concepts/#chat-history)\n", + "- [RunnableWithMessageHistory](https://api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html)\n", + "- [LangGraph](https://langchain-ai.github.io/langgraphjs/concepts/high_level/)\n", + "- [Memory](https://langchain-ai.github.io/langgraphjs/concepts/agentic_concepts/#memory)\n", + "\n", + ":::\n", + "\n", + "We recommend that new LangChain applications take advantage of the [built-in LangGraph peristence](https://langchain-ai.github.io/langgraph/concepts/persistence/) to implement memory.\n", + "\n", + "In some situations, users may need to keep using an existing persistence solution for chat message history.\n", + "\n", + "Here, we will show how to use [LangChain chat message histories](/docs/integrations/memory/) (implementations of [BaseChatMessageHistory](https://api.js.langchain.com/classes/_langchain_core.chat_history.BaseChatMessageHistory.html)) with LangGraph." + ] + }, + { + "cell_type": "markdown", + "id": "548bc988-167b-43f1-860a-d247e28b2b42", + "metadata": {}, + "source": [ + "## Set up\n", + "\n", + "```typescript\n", + "process.env.ANTHROPIC_API_KEY = 'YOUR_API_KEY'\n", + "```\n", + "\n", + "```{=mdx}\n", + "import Npm2Yarn from \"@theme/Npm2Yarn\"\n", + "\n", + "\n", + " @langchain/core @langchain/langgraph @langchain/anthropic\n", + "\n", + "```" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "c5e08659-b68c-48f2-8b33-e79b0c6999e1", + "metadata": {}, + "source": [ + "## ChatMessageHistory\n", + "\n", + "A message history needs to be parameterized by a conversation ID or maybe by the 2-tuple of (user ID, conversation ID).\n", + "\n", + "Many of the [LangChain chat message histories](/docs/integrations/memory/) will have either a `sessionId` or some `namespace` to allow keeping track of different conversations. Please refer to the specific implementations to check how it is parameterized.\n", + "\n", + "The built-in `InMemoryChatMessageHistory` does not contains such a parameterization, so we'll create a dictionary to keep track of the message histories." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "28049308-2543-48e6-90d0-37a88951a637", + "metadata": {}, + "outputs": [], + "source": [ + "import { InMemoryChatMessageHistory } from \"@langchain/core/chat_history\";\n", + "\n", + "const chatsBySessionId: Record = {}\n", + "\n", + "const getChatHistory = (sessionId: string) => {\n", + " let chatHistory: InMemoryChatMessageHistory | undefined = chatsBySessionId[sessionId]\n", + " if (!chatHistory) {\n", + " chatHistory = new InMemoryChatMessageHistory()\n", + " chatsBySessionId[sessionId] = chatHistory\n", + " }\n", + " return chatHistory\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "94c53ce3-4212-41e6-8ad3-f0ab5df6130f", + "metadata": {}, + "source": [ + "## Use with LangGraph\n", + "\n", + "Next, we'll set up a basic chat bot using LangGraph. If you're not familiar with LangGraph, you should look at the following [Quick Start Tutorial](https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/).\n", + "\n", + "We'll create a [LangGraph node](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#nodes) for the chat model, and manually manage the conversation history, taking into account the conversation ID passed as part of the RunnableConfig.\n", + "\n", + "The conversation ID can be passed as either part of the RunnableConfig (as we'll do here), or as part of the [graph state](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#state)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d818e23f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hi! I'm bob\n", + "Hello Bob! It's nice to meet you. How can I assist you today?\n", + "what was my name?\n", + "You said your name is Bob.\n" + ] + } + ], + "source": [ + "import { v4 as uuidv4 } from \"uuid\";\n", + "import { ChatAnthropic } from \"@langchain/anthropic\";\n", + "import { StateGraph, MessagesAnnotation, END, START } from \"@langchain/langgraph\";\n", + "import { HumanMessage } from \"@langchain/core/messages\";\n", + "import { RunnableConfig } from \"@langchain/core/runnables\";\n", + "\n", + "// Define a chat model\n", + "const model = new ChatAnthropic({ modelName: \"claude-3-haiku-20240307\" });\n", + "\n", + "// Define the function that calls the model\n", + "const callModel = async (\n", + " state: typeof MessagesAnnotation.State,\n", + " config: RunnableConfig\n", + "): Promise> => {\n", + " if (!config.configurable?.sessionId) {\n", + " throw new Error(\n", + " \"Make sure that the config includes the following information: {'configurable': {'sessionId': 'some_value'}}\"\n", + " );\n", + " }\n", + "\n", + " const chatHistory = getChatHistory(config.configurable.sessionId as string);\n", + "\n", + " let messages = [...(await chatHistory.getMessages()), ...state.messages];\n", + "\n", + " if (state.messages.length === 1) {\n", + " // First message, ensure it's in the chat history\n", + " await chatHistory.addMessage(state.messages[0]);\n", + " }\n", + "\n", + " const aiMessage = await model.invoke(messages);\n", + "\n", + " // Update the chat history\n", + " await chatHistory.addMessage(aiMessage);\n", + "\n", + " return { messages: [aiMessage] };\n", + "};\n", + "\n", + "// Define a new graph\n", + "const workflow = new StateGraph(MessagesAnnotation)\n", + " .addNode(\"model\", callModel)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END);\n", + "\n", + "const app = workflow.compile();\n", + "\n", + "// Create a unique session ID to identify the conversation\n", + "const sessionId = uuidv4();\n", + "const config = { configurable: { sessionId }, streamMode: \"values\" as const };\n", + "\n", + "const inputMessage = new HumanMessage(\"hi! I'm bob\");\n", + "\n", + "for await (const event of await app.stream({ messages: [inputMessage] }, config)) {\n", + " const lastMessage = event.messages[event.messages.length - 1];\n", + " console.log(lastMessage.content);\n", + "}\n", + "\n", + "// Here, let's confirm that the AI remembers our name!\n", + "const followUpMessage = new HumanMessage(\"what was my name?\");\n", + "\n", + "for await (const event of await app.stream({ messages: [followUpMessage] }, config)) {\n", + " const lastMessage = event.messages[event.messages.length - 1];\n", + " console.log(lastMessage.content);\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "da0536dd-9a0b-49e3-b0b6-e8c7abf3b1f9", + "metadata": {}, + "source": [ + "## Using With RunnableWithMessageHistory\n", + "\n", + "This how-to guide used the `messages` and `addMessages` interface of `BaseChatMessageHistory` directly. \n", + "\n", + "Alternatively, you can use [RunnableWithMessageHistory](https://api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html), as [LCEL](/docs/concepts/#langchain-expression-language-lcel/) can be used inside any [LangGraph node](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#nodes).\n", + "\n", + "To do that replace the following code:\n", + "\n", + "```typescript\n", + "const callModel = async (\n", + " state: typeof MessagesAnnotation.State,\n", + " config: RunnableConfig\n", + "): Promise> => {\n", + " // highlight-start\n", + " if (!config.configurable?.sessionId) {\n", + " throw new Error(\n", + " \"Make sure that the config includes the following information: {'configurable': {'sessionId': 'some_value'}}\"\n", + " );\n", + " }\n", + "\n", + " const chatHistory = getChatHistory(config.configurable.sessionId as string);\n", + "\n", + " let messages = [...(await chatHistory.getMessages()), ...state.messages];\n", + "\n", + " if (state.messages.length === 1) {\n", + " // First message, ensure it's in the chat history\n", + " await chatHistory.addMessage(state.messages[0]);\n", + " }\n", + "\n", + " const aiMessage = await model.invoke(messages);\n", + "\n", + " // Update the chat history\n", + " await chatHistory.addMessage(aiMessage);\n", + " // highlight-end\n", + " return { messages: [aiMessage] };\n", + "};\n", + "```\n", + "\n", + "With the corresponding instance of `RunnableWithMessageHistory` defined in your current application.\n", + "\n", + "```typescript\n", + "const runnable = new RunnableWithMessageHistory({\n", + " // ... configuration from existing code\n", + "});\n", + "\n", + "const callModel = async (\n", + " state: typeof MessagesAnnotation.State,\n", + " config: RunnableConfig\n", + "): Promise> => {\n", + " // RunnableWithMessageHistory takes care of reading the message history\n", + " // and updating it with the new human message and AI response.\n", + " const aiMessage = await runnable.invoke(state.messages, config);\n", + " return {\n", + " messages: [aiMessage]\n", + " };\n", + "};\n", + "```" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "TypeScript", + "language": "typescript", + "name": "tslab" + }, + "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, + "file_extension": ".ts", + "mimetype": "text/typescript", + "name": "typescript", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/core_docs/docs/versions/migrating_memory/conversation_buffer_window_memory.ipynb b/docs/core_docs/docs/versions/migrating_memory/conversation_buffer_window_memory.ipynb new file mode 100644 index 000000000000..719cf9d1022e --- /dev/null +++ b/docs/core_docs/docs/versions/migrating_memory/conversation_buffer_window_memory.ipynb @@ -0,0 +1,643 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ce8457ed-c0b1-4a74-abbd-9d3d2211270f", + "metadata": {}, + "source": [ + "# Migrating off ConversationTokenBufferMemory\n", + "\n", + "Follow this guide if you're trying to migrate off one of the old memory classes listed below:\n", + "\n", + "\n", + "| Memory Type | Description |\n", + "|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n", + "| `ConversationTokenBufferMemory` | Keeps only the most recent messages in the conversation under the constraint that the total number of tokens in the conversation does not exceed a certain limit. |\n", + "\n", + "`ConversationTokenBufferMemory` applies additional processing on top of the raw conversation history to trim the conversation history to a size that fits inside the context window of a chat model. \n", + "\n", + "This processing functionality can be accomplished using LangChain's built-in [trimMessages](https://api.js.langchain.com/functions/_langchain_core.messages.trimMessages.html) function." + ] + }, + { + "cell_type": "markdown", + "id": "79935247-acc7-4a05-a387-5d72c9c8c8cb", + "metadata": {}, + "source": [ + "```{=mdx}\n", + ":::important\n", + "\n", + "We’ll begin by exploring a straightforward method that involves applying processing logic to the entire conversation history.\n", + "\n", + "While this approach is easy to implement, it has a downside: as the conversation grows, so does the latency, since the logic is re-applied to all previous exchanges in the conversation at each turn.\n", + "\n", + "More advanced strategies focus on incrementally updating the conversation history to avoid redundant processing.\n", + "\n", + "For instance, the LangGraph [how-to guide on summarization](https://langchain-ai.github.io/langgraphjs/how-tos/add-summary-conversation-history/) demonstrates\n", + "how to maintain a running summary of the conversation while discarding older messages, ensuring they aren't re-processed during later turns.\n", + ":::\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "d07f9459-9fb6-4942-99c9-64558aedd7d4", + "metadata": {}, + "source": [ + "## Set up\n", + "\n", + "### Dependencies\n", + "\n", + "```{=mdx}\n", + "import Npm2Yarn from \"@theme/Npm2Yarn\"\n", + "\n", + "\n", + " @langchain/openai @langchain/core zod\n", + "\n", + "```\n", + "\n", + "### Environment variables\n", + "\n", + "```typescript\n", + "process.env.OPENAI_API_KEY = \"YOUR_OPENAI_API_KEY\";\n", + "```\n", + "\n", + "```{=mdx}\n", + "
\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "7ce2d951", + "metadata": {}, + "source": [ + "## Reimplementing ConversationTokenBufferMemory logic\n", + "\n", + "Here, we'll use `trimMessages` to keeps the system message and the most recent messages in the conversation under the constraint that the total number of tokens in the conversation does not exceed a certain limit." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e1550bee", + "metadata": {}, + "outputs": [], + "source": [ + "import {\n", + " AIMessage,\n", + " HumanMessage,\n", + " SystemMessage,\n", + "} from \"@langchain/core/messages\";\n", + "\n", + "const messages = [\n", + " new SystemMessage(\"you're a good assistant, you always respond with a joke.\"),\n", + " new HumanMessage(\"i wonder why it's called langchain\"),\n", + " new AIMessage(\n", + " 'Well, I guess they thought \"WordRope\" and \"SentenceString\" just didn\\'t have the same ring to it!'\n", + " ),\n", + " new HumanMessage(\"and who is harrison chasing anyways\"),\n", + " new AIMessage(\n", + " \"Hmmm let me think.\\n\\nWhy, he's probably chasing after the last cup of coffee in the office!\"\n", + " ),\n", + " new HumanMessage(\"why is 42 always the answer?\"),\n", + " new AIMessage(\n", + " \"Because it's the only number that's constantly right, even when it doesn't add up!\"\n", + " ),\n", + " new HumanMessage(\"What did the cow say?\"),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6442f74b-2c36-48fd-a3d1-c7c5d18c050f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SystemMessage {\n", + " \"content\": \"you're a good assistant, you always respond with a joke.\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + "}\n", + "HumanMessage {\n", + " \"content\": \"and who is harrison chasing anyways\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + "}\n", + "AIMessage {\n", + " \"content\": \"Hmmm let me think.\\n\\nWhy, he's probably chasing after the last cup of coffee in the office!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + "}\n", + "HumanMessage {\n", + " \"content\": \"why is 42 always the answer?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + "}\n", + "AIMessage {\n", + " \"content\": \"Because it's the only number that's constantly right, even when it doesn't add up!\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {},\n", + " \"tool_calls\": [],\n", + " \"invalid_tool_calls\": []\n", + "}\n", + "HumanMessage {\n", + " \"content\": \"What did the cow say?\",\n", + " \"additional_kwargs\": {},\n", + " \"response_metadata\": {}\n", + "}\n" + ] + } + ], + "source": [ + "import { trimMessages } from \"@langchain/core/messages\";\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const selectedMessages = await trimMessages(\n", + " messages,\n", + " {\n", + " // Please see API reference for trimMessages for other ways to specify a token counter.\n", + " tokenCounter: new ChatOpenAI({ model: \"gpt-4o\" }),\n", + " maxTokens: 80, // <-- token limit\n", + " // The startOn is specified\n", + " // to make sure we do not generate a sequence where\n", + " // a ToolMessage that contains the result of a tool invocation\n", + " // appears before the AIMessage that requested a tool invocation\n", + " // as this will cause some chat models to raise an error.\n", + " startOn: \"human\",\n", + " strategy: \"last\",\n", + " includeSystem: true, // <-- Keep the system message\n", + " }\n", + ")\n", + "\n", + "for (const msg of selectedMessages) {\n", + " console.log(msg);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "0f05d272-2d22-44b7-9fa6-e617a48584b4", + "metadata": {}, + "source": [ + "```{=mdx}\n", + "
\n", + "```\n", + "\n", + "## Modern usage with LangGraph\n", + "\n", + "The example below shows how to use LangGraph to add simple conversation pre-processing logic.\n", + "\n", + "```{=mdx}\n", + ":::note\n", + "\n", + "If you want to avoid running the computation on the entire conversation history each time, you can follow\n", + "the [how-to guide on summarization](https://langchain-ai.github.io/langgraphjs/how-tos/add-summary-conversation-history/) that demonstrates\n", + "how to discard older messages, ensuring they aren't re-processed during later turns.\n", + "\n", + ":::\n", + "```\n", + "\n", + "```{=mdx}\n", + "
\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "05d360e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hi! I'm bob\n", + "Hello, Bob! How can I assist you today?\n", + "what was my name?\n", + "You mentioned that your name is Bob. How can I help you today?\n" + ] + } + ], + "source": [ + "import { v4 as uuidv4 } from 'uuid';\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "import { StateGraph, MessagesAnnotation, END, START, MemorySaver } from \"@langchain/langgraph\";\n", + "import { trimMessages } from \"@langchain/core/messages\";\n", + "\n", + "// Define a chat model\n", + "const model = new ChatOpenAI({ model: \"gpt-4o\" });\n", + "\n", + "// Define the function that calls the model\n", + "const callModel = async (state: typeof MessagesAnnotation.State): Promise> => {\n", + " // highlight-start\n", + " const selectedMessages = await trimMessages(\n", + " state.messages,\n", + " {\n", + " tokenCounter: (messages) => messages.length, // Simple message count instead of token count\n", + " maxTokens: 5, // Allow up to 5 messages\n", + " strategy: \"last\",\n", + " startOn: \"human\",\n", + " includeSystem: true,\n", + " allowPartial: false,\n", + " }\n", + " );\n", + " // highlight-end\n", + "\n", + " const response = await model.invoke(selectedMessages);\n", + "\n", + " // With LangGraph, we're able to return a single message, and LangGraph will concatenate\n", + " // it to the existing list\n", + " return { messages: [response] };\n", + "};\n", + "\n", + "\n", + "// Define a new graph\n", + "const workflow = new StateGraph(MessagesAnnotation)\n", + "// Define the two nodes we will cycle between\n", + " .addNode(\"model\", callModel)\n", + " .addEdge(START, \"model\")\n", + " .addEdge(\"model\", END)\n", + "\n", + "const app = workflow.compile({\n", + " // Adding memory is straightforward in LangGraph!\n", + " // Just pass a checkpointer to the compile method.\n", + " checkpointer: new MemorySaver()\n", + "});\n", + "\n", + "// The thread id is a unique key that identifies this particular conversation\n", + "// ---\n", + "// NOTE: this must be `thread_id` and not `threadId` as the LangGraph internals expect `thread_id`\n", + "// ---\n", + "const thread_id = uuidv4();\n", + "const config = { configurable: { thread_id }, streamMode: \"values\" as const };\n", + "\n", + "const inputMessage = {\n", + " role: \"user\",\n", + " content: \"hi! I'm bob\",\n", + "}\n", + "for await (const event of await app.stream({ messages: [inputMessage] }, config)) {\n", + " const lastMessage = event.messages[event.messages.length - 1];\n", + " console.log(lastMessage.content);\n", + "}\n", + "\n", + "// Here, let's confirm that the AI remembers our name!\n", + "const followUpMessage = {\n", + " role: \"user\",\n", + " content: \"what was my name?\",\n", + "}\n", + "\n", + "// ---\n", + "// NOTE: You must pass the same thread id to continue the conversation\n", + "// we do that here by passing the same `config` object to the `.stream` call.\n", + "// ---\n", + "for await (const event of await app.stream({ messages: [followUpMessage] }, config)) {\n", + " const lastMessage = event.messages[event.messages.length - 1];\n", + " console.log(lastMessage.content);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "84229e2e-a578-4b21-840a-814223406402", + "metadata": {}, + "source": [ + "```{=mdx}\n", + "
\n", + "```\n", + "\n", + "## Usage with a pre-built langgraph agent\n", + "\n", + "This example shows usage of an Agent Executor with a pre-built agent constructed using the [createReactAgent](https://langchain-ai.github.io/langgraphjs/reference/functions/langgraph_prebuilt.createReactAgent.html) function.\n", + "\n", + "If you are using one of the [old LangChain pre-built agents](https://js.langchain.com/v0.1/docs/modules/agents/agent_types/), you should be able\n", + "to replace that code with the new [LangGraph pre-built agent](https://langchain-ai.github.io/langgraphjs/how-tos/create-react-agent/) which leverages\n", + "native tool calling capabilities of chat models and will likely work better out of the box.\n", + "\n", + "```{=mdx}\n", + "
\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9e54ccdc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hi! I'm bob. What is my age?\n", + "\n", + "42 years old\n", + "Hi Bob! You are 42 years old.\n", + "do you remember my name?\n", + "Yes, your name is Bob! If there's anything else you'd like to know or discuss, feel free to ask.\n" + ] + } + ], + "source": [ + "import { z } from \"zod\";\n", + "import { v4 as uuidv4 } from 'uuid';\n", + "import { BaseMessage, trimMessages } from \"@langchain/core/messages\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "import { MemorySaver } from \"@langchain/langgraph\";\n", + "import { createReactAgent } from \"@langchain/langgraph/prebuilt\";\n", + "\n", + "const getUserAge = tool(\n", + " (name: string): string => {\n", + " // This is a placeholder for the actual implementation\n", + " if (name.toLowerCase().includes(\"bob\")) {\n", + " return \"42 years old\";\n", + " }\n", + " return \"41 years old\";\n", + " },\n", + " {\n", + " name: \"get_user_age\",\n", + " description: \"Use this tool to find the user's age.\",\n", + " schema: z.string().describe(\"the name of the user\"),\n", + " }\n", + ");\n", + "\n", + "const memory = new MemorySaver();\n", + "const model2 = new ChatOpenAI({ model: \"gpt-4o\" });\n", + "\n", + "// highlight-start\n", + "const stateModifier = async (messages: BaseMessage[]): Promise => {\n", + " // We're using the message processor defined above.\n", + " return trimMessages(\n", + " messages,\n", + " {\n", + " tokenCounter: (msgs) => msgs.length, // <-- .length will simply count the number of messages rather than tokens\n", + " maxTokens: 5, // <-- allow up to 5 messages.\n", + " strategy: \"last\",\n", + " // The startOn is specified\n", + " // to make sure we do not generate a sequence where\n", + " // a ToolMessage that contains the result of a tool invocation\n", + " // appears before the AIMessage that requested a tool invocation\n", + " // as this will cause some chat models to raise an error.\n", + " startOn: \"human\",\n", + " includeSystem: true, // <-- Keep the system message\n", + " allowPartial: false,\n", + " }\n", + " );\n", + "};\n", + "// highlight-end\n", + "\n", + "const app2 = createReactAgent({\n", + " llm: model2,\n", + " tools: [getUserAge],\n", + " checkpointSaver: memory,\n", + " // highlight-next-line\n", + " messageModifier: stateModifier,\n", + "});\n", + "\n", + "// The thread id is a unique key that identifies\n", + "// this particular conversation.\n", + "// We'll just generate a random uuid here.\n", + "const threadId2 = uuidv4();\n", + "const config2 = { configurable: { thread_id: threadId2 }, streamMode: \"values\" as const };\n", + "\n", + "// Tell the AI that our name is Bob, and ask it to use a tool to confirm\n", + "// that it's capable of working like an agent.\n", + "const inputMessage2 = {\n", + " role: \"user\",\n", + " content: \"hi! I'm bob. What is my age?\",\n", + "}\n", + "\n", + "for await (const event of await app2.stream({ messages: [inputMessage2] }, config2)) {\n", + " const lastMessage = event.messages[event.messages.length - 1];\n", + " console.log(lastMessage.content);\n", + "}\n", + "\n", + "// Confirm that the chat bot has access to previous conversation\n", + "// and can respond to the user saying that the user's name is Bob.\n", + "const followUpMessage2 = {\n", + " role: \"user\",\n", + " content: \"do you remember my name?\",\n", + "};\n", + "\n", + "for await (const event of await app2.stream({ messages: [followUpMessage2] }, config2)) {\n", + " const lastMessage = event.messages[event.messages.length - 1];\n", + " console.log(lastMessage.content);\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "f4d16e09-1d90-4153-8576-6d3996cb5a6c", + "metadata": {}, + "source": [ + "```{=mdx}\n", + "
\n", + "```\n", + "\n", + "## LCEL: Add a preprocessing step\n", + "\n", + "The simplest way to add complex conversation management is by introducing a pre-processing step in front of the chat model and pass the full conversation history to the pre-processing step.\n", + "\n", + "This approach is conceptually simple and will work in many situations; for example, if using a [RunnableWithMessageHistory](/docs/how_to/message_history/) instead of wrapping the chat model, wrap the chat model with the pre-processor.\n", + "\n", + "The obvious downside of this approach is that latency starts to increase as the conversation history grows because of two reasons:\n", + "\n", + "1. As the conversation gets longer, more data may need to be fetched from whatever store your'e using to store the conversation history (if not storing it in memory).\n", + "2. The pre-processing logic will end up doing a lot of redundant computation, repeating computation from previous steps of the conversation.\n", + "\n", + "```{=mdx}\n", + ":::caution\n", + "\n", + "If you want to use a chat model's tool calling capabilities, remember to bind the tools to the model before adding the history pre-processing step to it!\n", + "\n", + ":::\n", + "```\n", + "\n", + "```{=mdx}\n", + "
\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a1c8adf2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " \"id\": \"chatcmpl-AB6uzWscxviYlbADFeDlnwIH82Fzt\",\n", + " \"content\": \"\",\n", + " \"additional_kwargs\": {\n", + " \"tool_calls\": [\n", + " {\n", + " \"id\": \"call_TghBL9dzqXFMCt0zj0VYMjfp\",\n", + " \"type\": \"function\",\n", + " \"function\": \"[Object]\"\n", + " }\n", + " ]\n", + " },\n", + " \"response_metadata\": {\n", + " \"tokenUsage\": {\n", + " \"completionTokens\": 16,\n", + " \"promptTokens\": 95,\n", + " \"totalTokens\": 111\n", + " },\n", + " \"finish_reason\": \"tool_calls\",\n", + " \"system_fingerprint\": \"fp_a5d11b2ef2\"\n", + " },\n", + " \"tool_calls\": [\n", + " {\n", + " \"name\": \"what_did_the_cow_say\",\n", + " \"args\": {},\n", + " \"type\": \"tool_call\",\n", + " \"id\": \"call_TghBL9dzqXFMCt0zj0VYMjfp\"\n", + " }\n", + " ],\n", + " \"invalid_tool_calls\": [],\n", + " \"usage_metadata\": {\n", + " \"input_tokens\": 95,\n", + " \"output_tokens\": 16,\n", + " \"total_tokens\": 111\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "import { AIMessage, HumanMessage, SystemMessage, BaseMessage, trimMessages } from \"@langchain/core/messages\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "import { z } from \"zod\";\n", + "\n", + "const model3 = new ChatOpenAI({ model: \"gpt-4o\" });\n", + "\n", + "const whatDidTheCowSay = tool(\n", + " (): string => {\n", + " return \"foo\";\n", + " },\n", + " {\n", + " name: \"what_did_the_cow_say\",\n", + " description: \"Check to see what the cow said.\",\n", + " schema: z.object({}),\n", + " }\n", + ");\n", + "\n", + "// highlight-start\n", + "const messageProcessor = trimMessages(\n", + " {\n", + " tokenCounter: (msgs) => msgs.length, // <-- .length will simply count the number of messages rather than tokens\n", + " maxTokens: 5, // <-- allow up to 5 messages.\n", + " strategy: \"last\",\n", + " // The startOn is specified\n", + " // to make sure we do not generate a sequence where\n", + " // a ToolMessage that contains the result of a tool invocation\n", + " // appears before the AIMessage that requested a tool invocation\n", + " // as this will cause some chat models to raise an error.\n", + " startOn: \"human\",\n", + " includeSystem: true, // <-- Keep the system message\n", + " allowPartial: false,\n", + " }\n", + ");\n", + "// highlight-end\n", + "\n", + "// Note that we bind tools to the model first!\n", + "const modelWithTools = model3.bindTools([whatDidTheCowSay]);\n", + "\n", + "// highlight-next-line\n", + "const modelWithPreprocessor = messageProcessor.pipe(modelWithTools);\n", + "\n", + "const fullHistory = [\n", + " new SystemMessage(\"you're a good assistant, you always respond with a joke.\"),\n", + " new HumanMessage(\"i wonder why it's called langchain\"),\n", + " new AIMessage('Well, I guess they thought \"WordRope\" and \"SentenceString\" just didn\\'t have the same ring to it!'),\n", + " new HumanMessage(\"and who is harrison chasing anyways\"),\n", + " new AIMessage(\"Hmmm let me think.\\n\\nWhy, he's probably chasing after the last cup of coffee in the office!\"),\n", + " new HumanMessage(\"why is 42 always the answer?\"),\n", + " new AIMessage(\"Because it's the only number that's constantly right, even when it doesn't add up!\"),\n", + " new HumanMessage(\"What did the cow say?\"),\n", + "];\n", + "\n", + "// We pass it explicitly to the modelWithPreprocessor for illustrative purposes.\n", + "// If you're using `RunnableWithMessageHistory` the history will be automatically\n", + "// read from the source that you configure.\n", + "const result = await modelWithPreprocessor.invoke(fullHistory);\n", + "console.log(result);" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "id": "5da7225a-5e94-4f53-bb0d-86b6b528d150", + "metadata": {}, + "source": [ + "```{=mdx}\n", + "
\n", + "```\n", + "\n", + "If you need to implement more efficient logic and want to use `RunnableWithMessageHistory` for now the way to achieve this\n", + "is to subclass from [BaseChatMessageHistory](https://api.js.langchain.com/classes/_langchain_core.chat_history.BaseChatMessageHistory.html) and\n", + "define appropriate logic for `addMessages` (that doesn't simply append the history, but instead re-writes it).\n", + "\n", + "Unless you have a good reason to implement this solution, you should instead use LangGraph." + ] + }, + { + "cell_type": "markdown", + "id": "b2717810", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Explore persistence with LangGraph:\n", + "\n", + "* [LangGraph quickstart tutorial](https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/)\n", + "* [How to add persistence (\"memory\") to your graph](https://langchain-ai.github.io/langgraphjs/how-tos/persistence/)\n", + "* [How to manage conversation history](https://langchain-ai.github.io/langgraphjs/how-tos/manage-conversation-history/)\n", + "* [How to add summary of the conversation history](https://langchain-ai.github.io/langgraphjs/how-tos/add-summary-conversation-history/)\n", + "\n", + "Add persistence with simple LCEL (favor LangGraph for more complex use cases):\n", + "\n", + "* [How to add message history](/docs/how_to/message_history/)\n", + "\n", + "Working with message history:\n", + "\n", + "* [How to trim messages](/docs/how_to/trim_messages)\n", + "* [How to filter messages](/docs/how_to/filter_messages/)\n", + "* [How to merge message runs](/docs/how_to/merge_message_runs/)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "TypeScript", + "language": "typescript", + "name": "tslab" + }, + "language_info": { + "codemirror_mode": { + "mode": "typescript", + "name": "javascript", + "typescript": true + }, + "file_extension": ".ts", + "mimetype": "text/typescript", + "name": "typescript", + "version": "3.7.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/core_docs/docs/versions/migrating_memory/conversation_summary_memory.ipynb b/docs/core_docs/docs/versions/migrating_memory/conversation_summary_memory.ipynb new file mode 100644 index 000000000000..50b2fbea4f96 --- /dev/null +++ b/docs/core_docs/docs/versions/migrating_memory/conversation_summary_memory.ipynb @@ -0,0 +1,45 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ce8457ed-c0b1-4a74-abbd-9d3d2211270f", + "metadata": {}, + "source": [ + "# Migrating off ConversationSummaryMemory or ConversationSummaryBufferMemory\n", + "\n", + "Follow this guide if you're trying to migrate off one of the old memory classes listed below:\n", + "\n", + "\n", + "| Memory Type | Description |\n", + "|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|\n", + "| `ConversationSummaryMemory` | Continually summarizes the conversation history. The summary is updated after each conversation turn. The abstraction returns the summary of the conversation history. |\n", + "| `ConversationSummaryBufferMemory` | Provides a running summary of the conversation together with the most recent messages in the conversation under the constraint that the total number of tokens in the conversation does not exceed a certain limit. |\n", + "\n", + "Please follow the following [how-to guide on summarization](https://langchain-ai.github.io/langgraphjs/how-tos/add-summary-conversation-history/) in LangGraph. \n", + "\n", + "This guide shows how to maintain a running summary of the conversation while discarding older messages, ensuring they aren't re-processed during later turns." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/core_docs/docs/versions/migrating_memory/index.mdx b/docs/core_docs/docs/versions/migrating_memory/index.mdx new file mode 100644 index 000000000000..837e7f03c544 --- /dev/null +++ b/docs/core_docs/docs/versions/migrating_memory/index.mdx @@ -0,0 +1,139 @@ +--- +sidebar_position: 1 +--- + +# How to migrate to LangGraph memory + +As of the v0.3 release of LangChain, we recommend that LangChain users take advantage of LangGraph persistence to incorporate `memory` into their LangChain application. + +- Users that rely on `RunnableWithMessageHistory` or `BaseChatMessageHistory` do **not** need to make any changes, but are encouraged to consider using LangGraph for more complex use cases. +- Users that rely on deprecated memory abstractions from LangChain 0.0.x should follow this guide to upgrade to the new LangGraph persistence feature in LangChain 0.3.x. + +## Why use LangGraph for memory? + +The main advantages of persistence in LangGraph are: + +- Built-in support for multiple users and conversations, which is a typical requirement for real-world conversational AI applications. +- Ability to save and resume complex conversations at any point. This helps with: + - Error recovery + - Allowing human intervention in AI workflows + - Exploring different conversation paths ("time travel") +- Full compatibility with both traditional [language models](/docs/concepts/#llms) and modern [chat models](/docs/concepts/#chat-models). Early memory implementations in LangChain weren't designed for newer chat model APIs, causing issues with features like tool-calling. LangGraph memory can persist any custom state. +- Highly customizable, allowing you to fully control how memory works and use different storage backends. + +## Evolution of memory in LangChain + +The concept of memory has evolved significantly in LangChain since its initial release. + +### LangChain 0.0.x memory + +Broadly speaking, LangChain 0.0.x memory was used to handle three main use cases: + +| Use Case | Example | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | +| Managing conversation history | Keep only the last `n` turns of the conversation between the user and the AI. | +| Extraction of structured information | Extract structured information from the conversation history, such as a list of facts learned about the user. | +| Composite memory implementations | Combine multiple memory sources, e.g., a list of known facts about the user along with facts learned during a given conversation. | + +While the LangChain 0.0.x memory abstractions were useful, they were limited in their capabilities and not well suited for real-world conversational AI applications. These memory abstractions lacked built-in support for multi-user, multi-conversation scenarios, which are essential for practical conversational AI systems. + +Most of these implementations have been officially deprecated in LangChain 0.3.x in favor of LangGraph persistence. + +### RunnableWithMessageHistory and BaseChatMessageHistory + +:::note +Please see [How to use BaseChatMessageHistory with LangGraph](./chat_history), if you would like to use `BaseChatMessageHistory` (with or without `RunnableWithMessageHistory`) in LangGraph. +::: + +As of LangChain v0.1, we started recommending that users rely primarily on [BaseChatMessageHistory](https://api.js.langchain.com/classes/_langchain_core.chat_history.BaseChatMessageHistory.html). `BaseChatMessageHistory` serves +as a simple persistence for storing and retrieving messages in a conversation. + +At that time, the only option for orchestrating LangChain chains was via [LCEL](/docs/how_to/#langchain-expression-language-lcel). To incorporate memory with `LCEL`, users had to use the [RunnableWithMessageHistory](https://api.js.langchain.com/classes/_langchain_core.runnables.RunnableWithMessageHistory.html) interface. While sufficient for basic chat applications, many users found the API unintuitive and challenging to use. + +As of LangChain v0.3, we recommend that **new** code takes advantage of LangGraph for both orchestration and persistence: + +- Orchestration: In LangGraph, users define [graphs](https://langchain-ai.github.io/langgraphjs/concepts/low_level/) that specify the flow of the application. This allows users to keep using `LCEL` within individual nodes when `LCEL` is needed, while making it easy to define complex orchestration logic that is more readable and maintainable. +- Persistence: Users can rely on LangGraph's persistence to store and retrieve data. LangGraph persistence is extremely flexible and can support a much wider range of use cases than the `RunnableWithMessageHistory` interface. + +:::important +If you have been using `RunnableWithMessageHistory` or `BaseChatMessageHistory`, you do not need to make any changes. We do not plan on deprecating either functionality in the near future. This functionality is sufficient for simple chat applications and any code that uses `RunnableWithMessageHistory` will continue to work as expected. +::: + +## Migrations + +:::info Prerequisites + +These guides assume some familiarity with the following concepts: + +- [LangGraph](https://langchain-ai.github.io/langgraphjs/) +- [v0.0.x Memory](https://js.langchain.com/v0.1/docs/modules/memory/) +- [How to add persistence ("memory") to your graph](https://langchain-ai.github.io/langgraphjs/how-tos/persistence/) + ::: + +### 1. Managing conversation history + +The goal of managing conversation history is to store and retrieve the history in a way that is optimal for a chat model to use. + +Often this involves trimming and / or summarizing the conversation history to keep the most relevant parts of the conversation while having the conversation fit inside the context window of the chat model. + +Memory classes that fall into this category include: + +| Memory Type | How to Migrate | Description | +| --------------------------------- | :----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ConversationTokenBufferMemory` | [Link to Migration Guide](conversation_buffer_window_memory) | Keeps only the most recent messages in the conversation under the constraint that the total number of tokens in the conversation does not exceed a certain limit. | +| `ConversationSummaryMemory` | [Link to Migration Guide](conversation_summary_memory) | Continually summarizes the conversation history. The summary is updated after each conversation turn. The abstraction returns the summary of the conversation history. | +| `ConversationSummaryBufferMemory` | [Link to Migration Guide](conversation_summary_memory) | Provides a running summary of the conversation together with the most recent messages in the conversation under the constraint that the total number of tokens in the conversation does not exceed a certain limit. | + +### 2. Extraction of structured information from the conversation history + +Memory classes that fall into this category include: + +| Memory Type | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `BaseEntityStore` | An abstract interface that resembles a key-value store. It was used for storing structured information learned during the conversation. The information had to be represented as an object of key-value pairs. | + +And specific backend implementations of abstractions: + +| Memory Type | Description | +| --------------------- | -------------------------------------------------------------------------------------------------------- | +| `InMemoryEntityStore` | An implementation of `BaseEntityStore` that stores the information in the literal computer memory (RAM). | + +These abstractions have not received much development since their initial release. The reason +is that for these abstractions to be useful they typically require a lot of specialization for a particular application, so these +abstractions are not as widely used as the conversation history management abstractions. + +For this reason, there are no migration guides for these abstractions. If you're struggling to migrate an application +that relies on these abstractions, please pen an issue on the LangChain GitHub repository, explain your use case, and we'll try to provide more guidance on how to migrate these abstractions. + +The general strategy for extracting structured information from the conversation history is to use a chat model with tool calling capabilities to extract structured information from the conversation history. +The extracted information can then be saved into an appropriate data structure (e.g., an object), and information from it can be retrieved and added into the prompt as needed. + +### 3. Implementations that provide composite logic on top of one or more memory implementations + +Memory classes that fall into this category include: + +| Memory Type | Description | +| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `CombinedMemory` | This abstraction accepted a list of `BaseMemory` and fetched relevant memory information from each of them based on the input. | + +These implementations did not seem to be used widely or provide significant value. Users should be able +to re-implement these without too much difficulty in custom code. + +## Related Resources + +Explore persistence with LangGraph: + +- [LangGraph quickstart tutorial](https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/) +- [How to add persistence ("memory") to your graph](https://langchain-ai.github.io/langgraphjs/how-tos/persistence/) +- [How to manage conversation history](https://langchain-ai.github.io/langgraphjs/how-tos/manage-conversation-history/) +- [How to add summary of the conversation history](https://langchain-ai.github.io/langgraphjs/how-tos/add-summary-conversation-history/) + +Add persistence with simple LCEL (favor langgraph for more complex use cases): + +- [How to add message history](/docs/how_to/message_history/) + +Working with message history: + +- [How to trim messages](/docs/how_to/trim_messages) +- [How to filter messages](/docs/how_to/filter_messages/) +- [How to merge message runs](/docs/how_to/merge_message_runs/) diff --git a/docs/core_docs/docs/versions/overview.mdx b/docs/core_docs/docs/versions/overview.mdx deleted file mode 100644 index bc1f2dc086d9..000000000000 --- a/docs/core_docs/docs/versions/overview.mdx +++ /dev/null @@ -1,44 +0,0 @@ ---- -sidebar_position: 0 -sidebar_label: Overview ---- - -# LangChain Over Time - -Due to the rapidly evolving field, LangChain has also evolved rapidly. -This document serves to outline at a high level what has changed and why. - -## 0.1 - -The 0.1 release marked a few key changes for LangChain. -By this point, the LangChain ecosystem had become large both in the breadth of what it enabled as well as the community behind it. - -**Split of packages** - -LangChain was split up into several packages to increase modularity and decrease bloat. -First, `@langchain/core` is created as a lightweight core library containing the base abstractions, -some core implementations of those abstractions, and the generic runtime for creating chains. -Next, all third party integrations are split into `@langchain/community` or their own individual partner packages. -Higher level chains and agents remain in `langchain`. - -**`Runnables`** - -Having a specific class for each chain was proving not very scalable or flexible. -Although these classes were left alone (without deprecation warnings) for this release, -in the documentation much more space was given to generic runnables. - -## < 0.1 - -There are several key characteristics of LangChain pre-0.1. - -**Singular Package** - -LangChain was largely a singular package. -This meant that ALL integrations lived inside `langchain`. - -**Chains as classes** - -Most high level chains were largely their own classes. -There was a base `Chain` class from which all chains inherited. -This meant that in order to chain the logic inside a chain you basically had to modify the source code. -There were a few chains that were meant to be more generic (`SequentialChain`, `RouterChain`) diff --git a/docs/core_docs/docs/versions/packages.mdx b/docs/core_docs/docs/versions/packages.mdx deleted file mode 100644 index 516ca7ad10f0..000000000000 --- a/docs/core_docs/docs/versions/packages.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -sidebar_position: 3 -sidebar_label: Packages ---- - -# 📕 Package versioning - -As of now, LangChain has an ad hoc release process: releases are cut with high frequency by -a maintainer and published to [NPM](https://npm.org/). -The different packages are versioned slightly differently. - -## `@langchain/core` - -`@langchain/core` is currently on version `0.1.x`. - -As `@langchain/core` contains the base abstractions and runtime for the whole LangChain ecosystem, we will communicate any breaking changes with advance notice and version bumps. -The exception for this is anything marked as `beta` (you can see this in the API reference and will see warnings when using such functionality). -The reason for beta features is that given the rate of change of the field, being able to move quickly is still a priority. - -Minor version increases will occur for: - -- Breaking changes for any public interfaces marked as `beta`. - -Patch version increases will occur for: - -- Bug fixes -- New features -- Any changes to private interfaces -- Any changes to `beta` features - -## `langchain` - -`langchain` is currently on version `0.2.x` - -Minor version increases will occur for: - -- Breaking changes for any public interfaces NOT marked as `beta`. - -Patch version increases will occur for: - -- Bug fixes -- New features -- Any changes to private interfaces -- Any changes to `beta` features. - -## `@langchain/community` - -`@langchain/community` is currently on version `0.2.x` - -All changes will be accompanied by the same type of version increase as changes in `langchain`. - -## Partner Packages - -Partner packages are versioned independently. diff --git a/docs/core_docs/docs/versions/v0_2/migrating_astream_events.mdx b/docs/core_docs/docs/versions/v0_2/migrating_astream_events.mdx index 023c6ad234bd..8fde2bbfe3de 100644 --- a/docs/core_docs/docs/versions/v0_2/migrating_astream_events.mdx +++ b/docs/core_docs/docs/versions/v0_2/migrating_astream_events.mdx @@ -1,6 +1,6 @@ --- sidebar_position: 2 -sidebar_label: streamEvents v2 +sidebar_label: Migrating to streamEvents v2 --- # Migrating to streamEvents v2 diff --git a/docs/core_docs/docs/versions/v0_3/index.mdx b/docs/core_docs/docs/versions/v0_3/index.mdx new file mode 100644 index 000000000000..16f092bfe236 --- /dev/null +++ b/docs/core_docs/docs/versions/v0_3/index.mdx @@ -0,0 +1,141 @@ +--- +sidebar_position: 0 +sidebar_label: v0.3 +--- + +# LangChain v0.3 + +_Last updated: 09.14.24_ + +## What's changed + +- All LangChain packages now have `@langchain/core` as a peer dependency instead of a direct dependency to help avoid type errors around [core version conflicts](/docs/how_to/installation/#installing-integration-packages). + - You will now need to explicitly install `@langchain/core` rather than relying on an internally resolved version from other packages. +- Callbacks are now backgrounded and non-blocking by default rather than blocking. + - This means that if you are using e.g. LangSmith for tracing in a serverless environment, you will need to [await the callbacks to ensure they finish before your function ends](/docs/how_to/callbacks_serverless). +- Removed deprecated document loader and self-query entrypoints from `langchain` in favor of entrypoints in [`@langchain/community`](https://www.npmjs.com/package/@langchain/community) and integration packages. +- Removed deprecated Google PaLM entrypoints from community in favor of entrypoints in [`@langchain/google-vertexai`](https://www.npmjs.com/package/@langchain/google-vertexai) and [`@langchain/google-genai`](https://www.npmjs.com/package/@langchain/google-genai). +- Deprecated using objects with a `"type"` as a [`BaseMessageLike`](https://v03.api.js.langchain.com/types/_langchain_core.messages.BaseMessageLike.html) in favor of the more OpenAI-like [`MessageWithRole`](https://v03.api.js.langchain.com/types/_langchain_core.messages.MessageFieldWithRole.html) + +## What’s new + +The following features have been added during the development of 0.2.x: + +- Simplified tool definition and usage. Read more [here](https://blog.langchain.dev/improving-core-tool-interfaces-and-docs-in-langchain/). +- Added a [generalized chat model constructor](https://js.langchain.com/docs/how_to/chat_models_universal_init/). +- Added the ability to [dispatch custom events](https://js.langchain.com/docs/how_to/callbacks_custom_events/). +- Released LangGraph.js 0.2.0 and made it the [recommended way to create agents](https://js.langchain.com/docs/how_to/migrate_agent) with LangChain.js. +- Revamped integration docs and API reference. Read more [here](https://blog.langchain.dev/langchain-integration-docs-revamped/). + +## How to update your code + +If you're using `langchain` / `@langchain/community` / `@langchain/core` 0.0 or 0.1, we recommend that you first [upgrade to 0.2](https://js.langchain.com/v0.2/docs/versions/v0_2/). + +If you're using `@langchain/langgraph`, upgrade to `@langchain/langgraph>=0.2.3`. This will work with either 0.2 or 0.3 versions of all the base packages. + +Here is a complete list of all packages that have been released and what we recommend upgrading your version constraints to in your `package.json`. +Any package that now supports `@langchain/core` 0.3 had a minor version bump. + +### Base packages + +| Package | Latest | Recommended `package.json` constraint | +| ------------------------ | ------ | ------------------------------------- | +| langchain | 0.3.0 | >=0.3.0 <0.4.0 | +| @langchain/community | 0.3.0 | >=0.3.0 <0.4.0 | +| @langchain/textsplitters | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/core | 0.3.0 | >=0.3.0 <0.4.0 | + +### Downstream packages + +| Package | Latest | Recommended `package.json` constraint | +| -------------------- | ------ | ------------------------------------- | +| @langchain/langgraph | 0.2.3 | >=0.2.3 <0.3 | + +### Integration packages + +| Package | Latest | Recommended `package.json` constraint | +| --------------------------------- | ------ | ------------------------------------- | +| @langchain/anthropic | 0.3.0 | >=0.3.0 <0.4.0 | +| @langchain/aws | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/azure-cosmosdb | 0.2.0 | >=0.2.0 <0.3.0 | +| @langchain/azure-dynamic-sessions | 0.2.0 | >=0.2.0 <0.3.0 | +| @langchain/baidu-qianfan | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/cloudflare | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/cohere | 0.3.0 | >=0.3.0 <0.4.0 | +| @langchain/exa | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/google-genai | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/google-vertexai | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/google-vertexai-web | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/groq | 0.1.1 | >=0.1.1 <0.2.0 | +| @langchain/mistralai | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/mixedbread-ai | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/mongodb | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/nomic | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/ollama | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/openai | 0.3.0 | >=0.3.0 <0.4.0 | +| @langchain/pinecone | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/qdrant | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/redis | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/weaviate | 0.1.0 | >=0.1.0 <0.2.0 | +| @langchain/yandex | 0.1.0 | >=0.1.0 <0.2.0 | + +Once you've updated to recent versions of the packages, you will need to explicitly install `@langchain/core` if you haven't already: + +```bash npm2yarn +npm install @langchain/core +``` + +We also suggest checking your lockfile or running the [appropriate package manager command](/docs/how_to/installation/#installing-integration-packages) to make sure that your package manager only has one version of `@langchain/core` installed. + +If you are currently running your code in a serverless environment (e.g., a Cloudflare Worker, Edge function, or AWS Lambda function) +and you are using LangSmith tracing or other callbacks, you will need to [await callbacks to ensure they finish before your function ends](/docs/how_to/callbacks_serverless). +Here's a quick example: + +```ts +import { RunnableLambda } from "@langchain/core/runnables"; +import { awaitAllCallbacks } from "@langchain/core/callbacks/promises"; + +const runnable = RunnableLambda.from(() => "hello!"); + +const customHandler = { + handleChainEnd: async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + console.log("Call finished"); + }, +}; + +const startTime = new Date().getTime(); + +await runnable.invoke({ number: "2" }, { callbacks: [customHandler] }); + +console.log(`Elapsed time: ${new Date().getTime() - startTime}ms`); + +await awaitAllCallbacks(); + +console.log(`Final elapsed time: ${new Date().getTime() - startTime}ms`); +``` + +``` +Elapsed time: 1ms +Call finished +Final elapsed time: 2164ms +``` + +You can also set `LANGCHAIN_CALLBACKS_BACKGROUND` to `"false"` to make all callbacks blocking: + +```ts +process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "false"; + +const startTimeBlocking = new Date().getTime(); + +await runnable.invoke({ number: "2" }, { callbacks: [customHandler] }); + +console.log( + `Initial elapsed time: ${new Date().getTime() - startTimeBlocking}ms` +); +``` + +``` +Call finished +Initial elapsed time: 2002ms +``` diff --git a/docs/core_docs/docusaurus.config.js b/docs/core_docs/docusaurus.config.js index 70bb8eeb561d..cc1702a0b6d7 100644 --- a/docs/core_docs/docusaurus.config.js +++ b/docs/core_docs/docusaurus.config.js @@ -135,13 +135,6 @@ const config = { themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ - announcementBar: { - content: - 'Share your thoughts on AI agents. Take the 3-min survey.', - isCloseable: true, - backgroundColor: "rgba(53, 151, 147, 0.1)", - textColor: "rgb(53, 151, 147)", - }, prism: { theme: { ...baseLightCodeBlockTheme, @@ -172,7 +165,7 @@ const config = { label: "Integrations", }, { - href: "https://v02.api.js.langchain.com", + href: "https://v03.api.js.langchain.com", label: "API Reference", position: "left", }, @@ -189,9 +182,13 @@ const config = { to: "/docs/community", label: "Community", }, + { + to: "/docs/troubleshooting/errors", + label: "Error reference", + }, { to: "/docs/additional_resources/tutorials", - label: "Tutorials", + label: "External guides", }, { to: "/docs/contributing", @@ -308,7 +305,7 @@ const config = { // this is linked to erick@langchain.dev currently apiKey: "180851bbb9ba0ef6be9214849d6efeaf", - indexName: "js-langchain-0.2", + indexName: "js-langchain-latest", contextualSearch: false, }, diff --git a/docs/core_docs/package.json b/docs/core_docs/package.json index 59617c110f86..c89017791bb4 100644 --- a/docs/core_docs/package.json +++ b/docs/core_docs/package.json @@ -50,7 +50,7 @@ }, "devDependencies": { "@babel/eslint-parser": "^7.18.2", - "@langchain/langgraph": "0.0.28", + "@langchain/langgraph": "0.2.3", "@langchain/scripts": "workspace:*", "@microsoft/fetch-event-source": "^2.0.1", "@swc/core": "^1.3.62", diff --git a/docs/core_docs/sidebars.js b/docs/core_docs/sidebars.js index e4297b057712..50336c478fd8 100644 --- a/docs/core_docs/sidebars.js +++ b/docs/core_docs/sidebars.js @@ -73,9 +73,35 @@ module.exports = { collapsible: false, items: [ { - type: "autogenerated", - dirName: "versions", + type: "doc", + id: "versions/v0_3/index", + label: "v0.3", + }, + { + type: "category", + label: "v0.2", + items: [ + { + type: "autogenerated", + dirName: "versions/v0_2", + }, + ], + }, + { + type: "category", + label: "Migrating from v0.0 memory", + link: { type: "doc", id: "versions/migrating_memory/index" }, + collapsible: false, + collapsed: false, + items: [ + { + type: "autogenerated", + dirName: "versions/migrating_memory", + className: "hidden", + }, + ], }, + "versions/release_policy", ], }, "security", diff --git a/docs/core_docs/src/theme/NotFound.js b/docs/core_docs/src/theme/NotFound.js new file mode 100644 index 000000000000..d86f790e5755 --- /dev/null +++ b/docs/core_docs/src/theme/NotFound.js @@ -0,0 +1,1041 @@ +/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable no-nested-ternary */ +import React from "react"; +import { translate } from "@docusaurus/Translate"; +import { PageMetadata } from "@docusaurus/theme-common"; +import Layout from "@theme/Layout"; + +import { useLocation } from "react-router-dom"; + +function LegacyBadge() { + return LEGACY; +} + +const suggestedLinks = { + "/docs/additional_resources/tutorials/expression_language_cheatsheet/": { + canonical: "/docs/how_to/lcel_cheatsheet/", + alternative: [ + "/v0.1/docs/additional_resources/tutorials/expression_language_cheatsheet/", + ], + }, + "/docs/ecosystem/": { + canonical: "/docs/integrations/platforms/", + alternative: ["/v0.1/docs/ecosystem/"], + }, + "/docs/ecosystem/integrations/": { + canonical: "/docs/integrations/platforms/", + alternative: ["/v0.1/docs/ecosystem/integrations/"], + }, + "/docs/ecosystem/integrations/databerry/": { + canonical: "/docs/integrations/platforms/", + alternative: ["/v0.1/docs/ecosystem/integrations/databerry/"], + }, + "/docs/ecosystem/integrations/helicone/": { + canonical: "/docs/integrations/platforms/", + alternative: ["/v0.1/docs/ecosystem/integrations/helicone/"], + }, + "/docs/ecosystem/integrations/lunary/": { + canonical: "/docs/integrations/platforms/", + alternative: ["/v0.1/docs/ecosystem/integrations/lunary/"], + }, + "/docs/ecosystem/integrations/makersuite/": { + canonical: "/docs/integrations/platforms/", + alternative: ["/v0.1/docs/ecosystem/integrations/makersuite/"], + }, + "/docs/ecosystem/integrations/unstructured/": { + canonical: "/docs/integrations/document_loaders/file_loaders/unstructured/", + alternative: ["/v0.1/docs/ecosystem/integrations/unstructured/"], + }, + "/docs/ecosystem/langserve/": { + canonical: + "https://api.js.langchain.com/classes/_langchain_core.runnables_remote.RemoteRunnable.html", + alternative: ["/v0.1/docs/ecosystem/langserve/"], + }, + "/docs/expression_language/": { + canonical: "/docs/how_to/#langchain-expression-language-lcel", + alternative: ["/v0.1/docs/expression_language/"], + }, + "/docs/expression_language/cookbook/": { + canonical: "/docs/how_to/#langchain-expression-language-lcel", + alternative: ["/v0.1/docs/expression_language/cookbook/"], + }, + "/docs/expression_language/cookbook/adding_memory/": { + canonical: "/docs/how_to/message_history", + alternative: ["/v0.1/docs/expression_language/cookbook/adding_memory/"], + }, + "/docs/expression_language/cookbook/agents/": { + canonical: "/docs/how_to/agent_executor", + alternative: ["/v0.1/docs/expression_language/cookbook/agents/"], + }, + "/docs/expression_language/cookbook/multiple_chains/": { + canonical: "/docs/how_to/parallel", + alternative: ["/v0.1/docs/expression_language/cookbook/multiple_chains/"], + }, + "/docs/expression_language/cookbook/prompt_llm_parser/": { + canonical: "/docs/tutorials/llm_chain", + alternative: ["/v0.1/docs/expression_language/cookbook/prompt_llm_parser/"], + }, + "/docs/expression_language/cookbook/retrieval/": { + canonical: "/docs/tutorials/rag", + alternative: ["/v0.1/docs/expression_language/cookbook/retrieval/"], + }, + "/docs/expression_language/cookbook/sql_db/": { + canonical: "/docs/tutorials/sql_qa", + alternative: ["/v0.1/docs/expression_language/cookbook/sql_db/"], + }, + "/docs/expression_language/cookbook/tools/": { + canonical: "/docs/how_to/tool_calling/", + alternative: ["/v0.1/docs/expression_language/cookbook/tools/"], + }, + "/docs/expression_language/get_started/": { + canonical: "/docs/how_to/sequence", + alternative: ["/v0.1/docs/expression_language/get_started/"], + }, + "/docs/expression_language/how_to/map/": { + canonical: "/docs/how_to/cancel_execution/", + alternative: ["/v0.1/docs/expression_language/how_to/map/"], + }, + "/docs/expression_language/how_to/message_history/": { + canonical: "/docs/how_to/message_history", + alternative: ["/v0.1/docs/expression_language/how_to/message_history/"], + }, + "/docs/expression_language/how_to/routing/": { + canonical: "/docs/how_to/routing", + alternative: ["/v0.1/docs/expression_language/how_to/routing/"], + }, + "/docs/expression_language/how_to/with_history/": { + canonical: "/docs/how_to/message_history", + alternative: ["/v0.1/docs/expression_language/how_to/with_history/"], + }, + "/docs/expression_language/interface/": { + canonical: "/docs/how_to/lcel_cheatsheet", + alternative: ["/v0.1/docs/expression_language/interface/"], + }, + "/docs/expression_language/streaming/": { + canonical: "/docs/how_to/streaming", + alternative: ["/v0.1/docs/expression_language/streaming/"], + }, + "/docs/expression_language/why/": { + canonical: "/docs/concepts/#langchain-expression-language", + alternative: ["/v0.1/docs/expression_language/why/"], + }, + "/docs/get_started/": { + canonical: "/docs/introduction/", + alternative: ["/v0.1/docs/get_started/"], + }, + "/docs/get_started/installation/": { + canonical: "/docs/tutorials/", + alternative: ["/v0.1/docs/get_started/installation/"], + }, + "/docs/get_started/introduction/": { + canonical: "/docs/tutorials/", + alternative: ["/v0.1/docs/get_started/introduction/"], + }, + "/docs/get_started/quickstart/": { + canonical: "/docs/tutorials/", + alternative: ["/v0.1/docs/get_started/quickstart/"], + }, + "/docs/guides/": { + canonical: "/docs/how_to/", + alternative: ["/v0.1/docs/guides/"], + }, + "/docs/guides/debugging/": { + canonical: "/docs/how_to/debugging", + alternative: ["/v0.1/docs/guides/debugging/"], + }, + "/docs/guides/deployment/": { + canonical: "https://langchain-ai.github.io/langgraph/cloud/", + alternative: ["/v0.1/docs/guides/deployment/"], + }, + "/docs/guides/deployment/nextjs/": { + canonical: "https://github.com/langchain-ai/langchain-nextjs-template", + alternative: ["/v0.1/docs/guides/deployment/nextjs/"], + }, + "/docs/guides/deployment/sveltekit/": { + canonical: "https://github.com/langchain-ai/langchain-nextjs-template", + alternative: ["/v0.1/docs/guides/deployment/sveltekit/"], + }, + "/docs/guides/evaluation/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/"], + }, + "/docs/guides/evaluation/comparison/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/comparison/"], + }, + "/docs/guides/evaluation/comparison/pairwise_embedding_distance/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: [ + "/v0.1/docs/guides/evaluation/comparison/pairwise_embedding_distance/", + ], + }, + "/docs/guides/evaluation/comparison/pairwise_string/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/comparison/pairwise_string/"], + }, + "/docs/guides/evaluation/examples/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/examples/"], + }, + "/docs/guides/evaluation/examples/comparisons/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/examples/comparisons/"], + }, + "/docs/guides/evaluation/string/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/string/"], + }, + "/docs/guides/evaluation/string/criteria/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/string/criteria/"], + }, + "/docs/guides/evaluation/string/embedding_distance/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/string/embedding_distance/"], + }, + "/docs/guides/evaluation/trajectory/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/trajectory/"], + }, + "/docs/guides/evaluation/trajectory/trajectory_eval/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/evaluation/trajectory/trajectory_eval/"], + }, + "/docs/guides/extending_langchain/": { + canonical: "/docs/how_to/#custom", + alternative: ["/v0.1/docs/guides/extending_langchain/"], + }, + "/docs/guides/fallbacks/": { + canonical: "/docs/how_to/fallbacks", + alternative: ["/v0.1/docs/guides/fallbacks/"], + }, + "/docs/guides/langsmith_evaluation/": { + canonical: + "https://docs.smith.langchain.com/tutorials/Developers/evaluation", + alternative: ["/v0.1/docs/guides/langsmith_evaluation/"], + }, + "/docs/guides/migrating/": { + canonical: "https://js.langchain.com/v0.1/docs/guides/migrating/", + alternative: ["/v0.1/docs/guides/migrating/"], + }, + "/docs/integrations/chat_memory/": { + canonical: "/docs/integrations/memory", + alternative: ["/v0.1/docs/integrations/chat_memory/"], + }, + "/docs/integrations/chat_memory/astradb/": { + canonical: "/docs/integrations/memory/astradb", + alternative: ["/v0.1/docs/integrations/chat_memory/astradb/"], + }, + "/docs/integrations/chat_memory/cassandra/": { + canonical: "/docs/integrations/memory/cassandra", + alternative: ["/v0.1/docs/integrations/chat_memory/cassandra/"], + }, + "/docs/integrations/chat_memory/cloudflare_d1/": { + canonical: "/docs/integrations/memory/cloudflare_d1", + alternative: ["/v0.1/docs/integrations/chat_memory/cloudflare_d1/"], + }, + "/docs/integrations/chat_memory/convex/": { + canonical: "/docs/integrations/memory/convex", + alternative: ["/v0.1/docs/integrations/chat_memory/convex/"], + }, + "/docs/integrations/chat_memory/dynamodb/": { + canonical: "/docs/integrations/memory/dynamodb", + alternative: ["/v0.1/docs/integrations/chat_memory/dynamodb/"], + }, + "/docs/integrations/chat_memory/firestore/": { + canonical: "/docs/integrations/memory/firestore", + alternative: ["/v0.1/docs/integrations/chat_memory/firestore/"], + }, + "/docs/integrations/chat_memory/ipfs_datastore/": { + canonical: "/docs/integrations/memory/ipfs_datastore", + alternative: ["/v0.1/docs/integrations/chat_memory/ipfs_datastore/"], + }, + "/docs/integrations/chat_memory/momento/": { + canonical: "/docs/integrations/memory/momento", + alternative: ["/v0.1/docs/integrations/chat_memory/momento/"], + }, + "/docs/integrations/chat_memory/mongodb/": { + canonical: "/docs/integrations/memory/mongodb", + alternative: ["/v0.1/docs/integrations/chat_memory/mongodb/"], + }, + "/docs/integrations/chat_memory/motorhead_memory/": { + canonical: "/docs/integrations/memory/motorhead_memory", + alternative: ["/v0.1/docs/integrations/chat_memory/motorhead_memory/"], + }, + "/docs/integrations/chat_memory/planetscale/": { + canonical: "/docs/integrations/memory/planetscale", + alternative: ["/v0.1/docs/integrations/chat_memory/planetscale/"], + }, + "/docs/integrations/chat_memory/postgres/": { + canonical: "/docs/integrations/memory/postgres", + alternative: ["/v0.1/docs/integrations/chat_memory/postgres/"], + }, + "/docs/integrations/chat_memory/redis/": { + canonical: "/docs/integrations/memory/redis", + alternative: ["/v0.1/docs/integrations/chat_memory/redis/"], + }, + "/docs/integrations/chat_memory/upstash_redis/": { + canonical: "/docs/integrations/memory/upstash_redis", + alternative: ["/v0.1/docs/integrations/chat_memory/upstash_redis/"], + }, + "/docs/integrations/chat_memory/xata/": { + canonical: "/docs/integrations/memory/xata", + alternative: ["/v0.1/docs/integrations/chat_memory/xata/"], + }, + "/docs/integrations/chat_memory/zep_memory/": { + canonical: "/docs/integrations/memory/zep_memory", + alternative: ["/v0.1/docs/integrations/chat_memory/zep_memory/"], + }, + "/docs/integrations/document_compressors/": { + canonical: "/docs/integrations/document_transformers", + alternative: ["/v0.1/docs/integrations/document_compressors/"], + }, + "/docs/integrations/llms/togetherai/": { + canonical: "/docs/integrations/llms/together", + alternative: ["/v0.1/docs/integrations/llms/togetherai/"], + }, + "/docs/integrations/retrievers/vectorstore/": { + canonical: "/docs/how_to/vectorstore_retriever", + alternative: ["/v0.1/docs/integrations/retrievers/vectorstore/"], + }, + "/docs/integrations/vectorstores/azure_cosmosdb/": { + canonical: "/docs/integrations/vectorstores/azure_cosmosdb_mongodb", + alternative: ["/v0.1/docs/integrations/vectorstores/azure_cosmosdb/"], + }, + "/docs/langgraph/": { + canonical: "https://langchain-ai.github.io/langgraphjs/", + alternative: ["/v0.1/docs/langgraph/"], + }, + "/docs/modules/agents/agent_types/chat_conversation_agent/": { + canonical: "/docs/how_to/migrate_agent", + alternative: [ + "/v0.1/docs/modules/agents/agent_types/chat_conversation_agent/", + ], + }, + "/docs/modules/agents/agent_types/openai_assistant/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/openai_assistant/"], + }, + "/docs/modules/agents/agent_types/openai_functions_agent/": { + canonical: "/docs/how_to/migrate_agent", + alternative: [ + "/v0.1/docs/modules/agents/agent_types/openai_functions_agent/", + ], + }, + "/docs/modules/agents/agent_types/openai_tools_agent/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/openai_tools_agent/"], + }, + "/docs/modules/agents/agent_types/plan_and_execute/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/plan_and_execute/"], + }, + "/docs/modules/agents/agent_types/react/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/react/"], + }, + "/docs/modules/agents/agent_types/structured_chat/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/structured_chat/"], + }, + "/docs/modules/agents/agent_types/tool_calling/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/tool_calling/"], + }, + "/docs/modules/agents/agent_types/xml_legacy/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/xml_legacy/"], + }, + "/docs/modules/agents/agent_types/xml/": { + canonical: "/docs/how_to/migrate_agent", + alternative: ["/v0.1/docs/modules/agents/agent_types/xml/"], + }, + "/docs/modules/agents/how_to/callbacks/": { + canonical: "/docs/how_to/#callbacks", + alternative: ["/v0.1/docs/modules/agents/how_to/callbacks/"], + }, + "/docs/modules/agents/how_to/cancelling_requests/": { + canonical: "/docs/how_to/cancel_execution", + alternative: ["/v0.1/docs/modules/agents/how_to/cancelling_requests/"], + }, + "/docs/modules/agents/how_to/custom_agent/": { + canonical: + "https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/", + alternative: ["/v0.1/docs/modules/agents/how_to/custom_agent/"], + }, + "/docs/modules/agents/how_to/custom_llm_agent/": { + canonical: + "https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/", + alternative: ["/v0.1/docs/modules/agents/how_to/custom_llm_agent/"], + }, + "/docs/modules/agents/how_to/custom_llm_chat_agent/": { + canonical: + "https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/", + alternative: ["/v0.1/docs/modules/agents/how_to/custom_llm_chat_agent/"], + }, + "/docs/modules/agents/how_to/custom_mrkl_agent/": { + canonical: + "https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/", + alternative: ["/v0.1/docs/modules/agents/how_to/custom_mrkl_agent/"], + }, + "/docs/modules/agents/how_to/handle_parsing_errors/": { + canonical: + "https://langchain-ai.github.io/langgraphjs/how-tos/tool-calling-errors/", + alternative: ["/v0.1/docs/modules/agents/how_to/handle_parsing_errors/"], + }, + "/docs/modules/agents/how_to/intermediate_steps/": { + canonical: + "https://langchain-ai.github.io/langgraphjs/how-tos/stream-values/", + alternative: ["/v0.1/docs/modules/agents/how_to/intermediate_steps/"], + }, + "/docs/modules/agents/how_to/logging_and_tracing/": { + canonical: + "https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langgraph", + alternative: ["/v0.1/docs/modules/agents/how_to/logging_and_tracing/"], + }, + "/docs/modules/agents/how_to/timeouts/": { + canonical: "/docs/how_to/cancel_execution/", + alternative: ["/v0.1/docs/modules/agents/how_to/timeouts/"], + }, + "/docs/modules/agents/tools/": { + canonical: + "https://langchain-ai.github.io/langgraphjs/how-tos/tool-calling/", + alternative: ["/v0.1/docs/modules/agents/tools/"], + }, + "/docs/modules/agents/tools/dynamic/": { + canonical: "/docs/how_to/custom_tools/", + alternative: ["/v0.1/docs/modules/agents/tools/dynamic/"], + }, + "/docs/modules/agents/tools/how_to/agents_with_vectorstores/": { + canonical: "/docs/how_to/custom_tools", + alternative: [ + "/v0.1/docs/modules/agents/tools/how_to/agents_with_vectorstores/", + ], + }, + "/docs/modules/agents/tools/toolkits/": { + canonical: "/docs/how_to/tools_builtin", + alternative: ["/v0.1/docs/modules/agents/tools/toolkits/"], + }, + "/docs/modules/callbacks/how_to/background_callbacks/": { + canonical: "/docs/how_to/callbacks_backgrounding", + alternative: ["/v0.1/docs/modules/callbacks/how_to/background_callbacks/"], + }, + "/docs/modules/callbacks/how_to/create_handlers/": { + canonical: "/docs/how_to/custom_callbacks", + alternative: ["/v0.1/docs/modules/callbacks/how_to/create_handlers/"], + }, + "/docs/modules/callbacks/how_to/creating_subclasses/": { + canonical: "/docs/how_to/custom_callbacks", + alternative: ["/v0.1/docs/modules/callbacks/how_to/creating_subclasses/"], + }, + "/docs/modules/callbacks/how_to/tags/": { + canonical: "/docs/how_to/#callbacks", + alternative: ["/v0.1/docs/modules/callbacks/how_to/tags/"], + }, + "/docs/modules/callbacks/how_to/with_listeners/": { + canonical: "/docs/how_to/#callbacks", + alternative: ["/v0.1/docs/modules/callbacks/how_to/with_listeners/"], + }, + "/docs/modules/chains/additional/analyze_document/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/additional/analyze_document/", + alternative: ["/v0.1/docs/modules/chains/additional/analyze_document/"], + }, + "/docs/modules/chains/additional/constitutional_chain/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/additional/constitutional_chain/", + alternative: ["/v0.1/docs/modules/chains/additional/constitutional_chain/"], + }, + "/docs/modules/chains/additional/cypher_chain/": { + canonical: "/docs/tutorials/graph", + alternative: ["/v0.1/docs/modules/chains/additional/cypher_chain/"], + }, + "/docs/modules/chains/additional/moderation/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/additional/moderation/", + alternative: ["/v0.1/docs/modules/chains/additional/moderation/"], + }, + "/docs/modules/chains/additional/multi_prompt_router/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/additional/multi_prompt_router/", + alternative: ["/v0.1/docs/modules/chains/additional/multi_prompt_router/"], + }, + "/docs/modules/chains/additional/multi_retrieval_qa_router/": { + canonical: "/docs/how_to/multiple_queries", + alternative: [ + "/v0.1/docs/modules/chains/additional/multi_retrieval_qa_router/", + ], + }, + "/docs/modules/chains/additional/openai_functions/": { + canonical: "/docs/how_to/tool_calling", + alternative: ["/v0.1/docs/modules/chains/additional/openai_functions/"], + }, + "/docs/modules/chains/additional/openai_functions/extraction/": { + canonical: "/docs/tutorials/extraction", + alternative: [ + "/v0.1/docs/modules/chains/additional/openai_functions/extraction/", + ], + }, + "/docs/modules/chains/additional/openai_functions/openapi/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/additional/openai_functions/openapi/", + alternative: [ + "/v0.1/docs/modules/chains/additional/openai_functions/openapi/", + ], + }, + "/docs/modules/chains/additional/openai_functions/tagging/": { + canonical: "/docs/tutorials/extraction", + alternative: [ + "/v0.1/docs/modules/chains/additional/openai_functions/tagging/", + ], + }, + "/docs/modules/chains/document/": { + canonical: + "https://api.js.langchain.com/functions/langchain.chains_combine_documents.createStuffDocumentsChain.html", + alternative: ["/v0.1/docs/modules/chains/document/"], + }, + "/docs/modules/chains/document/map_reduce/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/document/map_reduce/", + alternative: ["/v0.1/docs/modules/chains/document/map_reduce/"], + }, + "/docs/modules/chains/document/refine/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/document/refine/", + alternative: ["/v0.1/docs/modules/chains/document/refine/"], + }, + "/docs/modules/chains/document/stuff/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/chains/document/stuff/", + alternative: ["/v0.1/docs/modules/chains/document/stuff/"], + }, + "/docs/modules/chains/foundational/llm_chain/": { + canonical: "/docs/tutorials/llm_chain", + alternative: ["/v0.1/docs/modules/chains/foundational/llm_chain/"], + }, + "/docs/modules/chains/how_to/debugging/": { + canonical: "/docs/how_to/debugging", + alternative: ["/v0.1/docs/modules/chains/how_to/debugging/"], + }, + "/docs/modules/chains/how_to/memory/": { + canonical: "/docs/how_to/qa_chat_history_how_to", + alternative: ["/v0.1/docs/modules/chains/how_to/memory/"], + }, + "/docs/modules/chains/popular/api/": { + canonical: "https://js.langchain.com/v0.1/docs/modules/chains/popular/api/", + alternative: ["/v0.1/docs/modules/chains/popular/api/"], + }, + "/docs/modules/chains/popular/chat_vector_db_legacy/": { + canonical: "/docs/tutorials/rag", + alternative: ["/v0.1/docs/modules/chains/popular/chat_vector_db_legacy/"], + }, + "/docs/modules/chains/popular/chat_vector_db/": { + canonical: "/docs/tutorials/rag", + alternative: ["/v0.1/docs/modules/chains/popular/chat_vector_db/"], + }, + "/docs/modules/chains/popular/sqlite_legacy/": { + canonical: "/docs/tutorials/sql_qa", + alternative: ["/v0.1/docs/modules/chains/popular/sqlite_legacy/"], + }, + "/docs/modules/chains/popular/sqlite/": { + canonical: "/docs/tutorials/sql_qa", + alternative: ["/v0.1/docs/modules/chains/popular/sqlite/"], + }, + "/docs/modules/chains/popular/structured_output/": { + canonical: "/docs/how_to/structured_output", + alternative: ["/v0.1/docs/modules/chains/popular/structured_output/"], + }, + "/docs/modules/chains/popular/summarize/": { + canonical: "/docs/tutorials/summarization", + alternative: ["/v0.1/docs/modules/chains/popular/summarize/"], + }, + "/docs/modules/chains/popular/vector_db_qa_legacy/": { + canonical: "/docs/tutorials/rag", + alternative: ["/v0.1/docs/modules/chains/popular/vector_db_qa_legacy/"], + }, + "/docs/modules/chains/popular/vector_db_qa/": { + canonical: "/docs/tutorials/rag", + alternative: ["/v0.1/docs/modules/chains/popular/vector_db_qa/"], + }, + "/docs/modules/data_connection/document_loaders/creating_documents/": { + canonical: "/docs/concepts#document", + alternative: [ + "/v0.1/docs/modules/data_connection/document_loaders/creating_documents/", + ], + }, + "/docs/modules/data_connection/document_transformers/contextual_chunk_headers/": + { + canonical: + "/docs/how_to/parent_document_retriever/#with-contextual-chunk-headers", + alternative: [ + "/v0.1/docs/modules/data_connection/document_transformers/contextual_chunk_headers/", + ], + }, + "/docs/modules/data_connection/document_transformers/custom_text_splitter/": { + canonical: "/docs/how_to/#text-splitters", + alternative: [ + "/v0.1/docs/modules/data_connection/document_transformers/custom_text_splitter/", + ], + }, + "/docs/modules/data_connection/document_transformers/token_splitter/": { + canonical: "/docs/how_to/split_by_token", + alternative: [ + "/v0.1/docs/modules/data_connection/document_transformers/token_splitter/", + ], + }, + "/docs/modules/data_connection/experimental/graph_databases/neo4j/": { + canonical: "/docs/tutorials/graph", + alternative: [ + "/v0.1/docs/modules/data_connection/experimental/graph_databases/neo4j/", + ], + }, + "/docs/modules/data_connection/experimental/multimodal_embeddings/google_vertex_ai/": + { + canonical: + "https://js.langchain.com/v0.1/docs/modules/data_connection/experimental/multimodal_embeddings/google_vertex_ai/", + alternative: [ + "/v0.1/docs/modules/data_connection/experimental/multimodal_embeddings/google_vertex_ai/", + ], + }, + "/docs/modules/data_connection/retrievers/custom/": { + canonical: "/docs/how_to/custom_retriever", + alternative: ["/v0.1/docs/modules/data_connection/retrievers/custom/"], + }, + "/docs/modules/data_connection/retrievers/matryoshka_retriever/": { + canonical: "/docs/how_to/reduce_retrieval_latency", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/matryoshka_retriever/", + ], + }, + "/docs/modules/data_connection/retrievers/multi-query-retriever/": { + canonical: "/docs/how_to/multiple_queries", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/multi-query-retriever/", + ], + }, + "/docs/modules/data_connection/retrievers/multi-vector-retriever/": { + canonical: "/docs/how_to/multi_vector", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/multi-vector-retriever/", + ], + }, + "/docs/modules/data_connection/retrievers/parent-document-retriever/": { + canonical: "/docs/how_to/parent_document_retriever", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/parent-document-retriever/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/chroma-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/chroma", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/chroma-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/hnswlib-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/hnswlib", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/hnswlib-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/memory-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/memory", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/memory-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/pinecone-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/pinecone", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/pinecone-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/qdrant-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/qdrant", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/qdrant-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/supabase-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/supabase", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/supabase-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/vectara-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/vectara", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/vectara-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/self_query/weaviate-self-query/": { + canonical: "/docs/integrations/retrievers/self_query/weaviate", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/self_query/weaviate-self-query/", + ], + }, + "/docs/modules/data_connection/retrievers/similarity-score-threshold-retriever/": + { + canonical: + "https://api.js.langchain.com/classes/langchain.retrievers_score_threshold.ScoreThresholdRetriever.html", + alternative: [ + "/v0.1/docs/modules/data_connection/retrievers/similarity-score-threshold-retriever/", + ], + }, + "/docs/modules/data_connection/text_embedding/api_errors/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/data_connection/text_embedding/api_errors/", + alternative: [ + "/v0.1/docs/modules/data_connection/text_embedding/api_errors/", + ], + }, + "/docs/modules/data_connection/text_embedding/caching_embeddings/": { + canonical: "/docs/how_to/caching_embeddings", + alternative: [ + "/v0.1/docs/modules/data_connection/text_embedding/caching_embeddings/", + ], + }, + "/docs/modules/data_connection/text_embedding/rate_limits/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/data_connection/text_embedding/rate_limits/", + alternative: [ + "/v0.1/docs/modules/data_connection/text_embedding/rate_limits/", + ], + }, + "/docs/modules/data_connection/text_embedding/timeouts/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/data_connection/text_embedding/timeouts/", + alternative: [ + "/v0.1/docs/modules/data_connection/text_embedding/timeouts/", + ], + }, + "/docs/modules/data_connection/vectorstores/custom/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/data_connection/vectorstores/custom/", + alternative: ["/v0.1/docs/modules/data_connection/vectorstores/custom/"], + }, + "/docs/modules/experimental/": { + canonical: "https://js.langchain.com/v0.1/docs/modules/experimental/", + alternative: ["/v0.1/docs/modules/experimental/"], + }, + "/docs/modules/experimental/mask/": { + canonical: + "https://api.js.langchain.com/modules/langchain.experimental_masking.html", + alternative: ["/v0.1/docs/modules/experimental/mask/"], + }, + "/docs/modules/experimental/prompts/custom_formats/": { + canonical: + "https://api.js.langchain.com/classes/langchain.experimental_prompts_handlebars.HandlebarsPromptTemplate.html", + alternative: ["/v0.1/docs/modules/experimental/prompts/custom_formats/"], + }, + "/docs/modules/memory/chat_messages/custom/": { + canonical: + "https://js.langchain.com/v0.1/docs/modules/memory/chat_messages/custom/", + alternative: ["/v0.1/docs/modules/memory/chat_messages/custom/"], + }, + "/docs/modules/memory/types/buffer_memory_chat/": { + canonical: + "https://api.js.langchain.com/classes/langchain.memory.BufferMemory.html", + alternative: ["/v0.1/docs/modules/memory/types/buffer_memory_chat/"], + }, + "/docs/modules/memory/types/buffer_window/": { + canonical: + "https://api.js.langchain.com/classes/langchain.memory.BufferWindowMemory.html", + alternative: ["/v0.1/docs/modules/memory/types/buffer_window/"], + }, + "/docs/modules/memory/types/entity_summary_memory/": { + canonical: + "https://api.js.langchain.com/classes/langchain.memory.EntityMemory.html", + alternative: ["/v0.1/docs/modules/memory/types/entity_summary_memory/"], + }, + "/docs/modules/memory/types/multiple_memory/": { + canonical: + "https://api.js.langchain.com/classes/langchain.memory.CombinedMemory.html", + alternative: ["/v0.1/docs/modules/memory/types/multiple_memory/"], + }, + "/docs/modules/memory/types/summary_buffer/": { + canonical: + "https://api.js.langchain.com/classes/langchain.memory.ConversationSummaryBufferMemory.html", + alternative: ["/v0.1/docs/modules/memory/types/summary_buffer/"], + }, + "/docs/modules/memory/types/summary/": { + canonical: + "https://api.js.langchain.com/classes/langchain.memory.ConversationSummaryMemory.html", + alternative: ["/v0.1/docs/modules/memory/types/summary/"], + }, + "/docs/modules/memory/types/vectorstore_retriever_memory/": { + canonical: + "https://api.js.langchain.com/classes/langchain.memory.VectorStoreRetrieverMemory.html", + alternative: [ + "/v0.1/docs/modules/memory/types/vectorstore_retriever_memory/", + ], + }, + "/docs/modules/model_io/chat/caching/": { + canonical: "/docs/how_to/chat_model_caching", + alternative: ["/v0.1/docs/modules/model_io/chat/caching/"], + }, + "/docs/modules/model_io/chat/cancelling_requests/": { + canonical: "/docs/how_to/cancel_execution", + alternative: ["/v0.1/docs/modules/model_io/chat/cancelling_requests/"], + }, + "/docs/modules/model_io/chat/custom_chat/": { + canonical: "/docs/how_to/custom_chat", + alternative: ["/v0.1/docs/modules/model_io/chat/custom_chat/"], + }, + "/docs/modules/model_io/chat/dealing_with_api_errors/": { + canonical: "/docs/how_to/fallbacks", + alternative: ["/v0.1/docs/modules/model_io/chat/dealing_with_api_errors/"], + }, + "/docs/modules/model_io/chat/dealing_with_rate_limits/": { + canonical: "/docs/how_to/fallbacks", + alternative: ["/v0.1/docs/modules/model_io/chat/dealing_with_rate_limits/"], + }, + "/docs/modules/model_io/chat/subscribing_events/": { + canonical: "/docs/how_to/custom_callbacks", + alternative: ["/v0.1/docs/modules/model_io/chat/subscribing_events/"], + }, + "/docs/modules/model_io/chat/timeouts/": { + canonical: "/docs/how_to/custom_callbacks", + alternative: ["/v0.1/docs/modules/model_io/chat/timeouts/"], + }, + "/docs/modules/model_io/llms/cancelling_requests/": { + canonical: "/docs/how_to/cancel_execution", + alternative: ["/v0.1/docs/modules/model_io/llms/cancelling_requests/"], + }, + "/docs/modules/model_io/llms/dealing_with_api_errors/": { + canonical: "/docs/how_to/fallbacks", + alternative: ["/v0.1/docs/modules/model_io/llms/dealing_with_api_errors/"], + }, + "/docs/modules/model_io/llms/dealing_with_rate_limits/": { + canonical: "/docs/how_to/fallbacks", + alternative: ["/v0.1/docs/modules/model_io/llms/dealing_with_rate_limits/"], + }, + "/docs/modules/model_io/llms/subscribing_events/": { + canonical: "/docs/how_to/custom_callbacks", + alternative: ["/v0.1/docs/modules/model_io/llms/subscribing_events/"], + }, + "/docs/modules/model_io/llms/timeouts/": { + canonical: "/docs/how_to/cancel_execution", + alternative: ["/v0.1/docs/modules/model_io/llms/timeouts/"], + }, + "/docs/modules/model_io/output_parsers/types/bytes/": { + canonical: + "https://api.js.langchain.com/modules/_langchain_core.output_parsers.html", + alternative: ["/v0.1/docs/modules/model_io/output_parsers/types/bytes/"], + }, + "/docs/modules/model_io/output_parsers/types/combining_output_parser/": { + canonical: + "https://api.js.langchain.com/classes/langchain.output_parsers.CombiningOutputParser.html", + alternative: [ + "/v0.1/docs/modules/model_io/output_parsers/types/combining_output_parser/", + ], + }, + "/docs/modules/model_io/output_parsers/types/csv/": { + canonical: + "https://api.js.langchain.com/classes/_langchain_core.output_parsers.CommaSeparatedListOutputParser.html", + alternative: ["/v0.1/docs/modules/model_io/output_parsers/types/csv/"], + }, + "/docs/modules/model_io/output_parsers/types/custom_list_parser/": { + canonical: + "https://api.js.langchain.com/classes/_langchain_core.output_parsers.CustomListOutputParser.html", + alternative: [ + "/v0.1/docs/modules/model_io/output_parsers/types/custom_list_parser/", + ], + }, + "/docs/modules/model_io/output_parsers/types/http_response/": { + canonical: + "https://api.js.langchain.com/classes/langchain.output_parsers.HttpResponseOutputParser.html", + alternative: [ + "/v0.1/docs/modules/model_io/output_parsers/types/http_response/", + ], + }, + "/docs/modules/model_io/output_parsers/types/json_functions/": { + canonical: + "https://api.js.langchain.com/classes/langchain.output_parsers.JsonOutputFunctionsParser.html", + alternative: [ + "/v0.1/docs/modules/model_io/output_parsers/types/json_functions/", + ], + }, + "/docs/modules/model_io/output_parsers/types/string/": { + canonical: + "https://api.js.langchain.com/classes/_langchain_core.output_parsers.StringOutputParser.html", + alternative: ["/v0.1/docs/modules/model_io/output_parsers/types/string/"], + }, + "/docs/modules/model_io/prompts/example_selector_types/": { + canonical: "/docs/how_to/#example-selectors", + alternative: [ + "/v0.1/docs/modules/model_io/prompts/example_selector_types/", + ], + }, + "/docs/modules/model_io/prompts/example_selector_types/length_based/": { + canonical: "/docs/how_to/example_selectors_length_based", + alternative: [ + "/v0.1/docs/modules/model_io/prompts/example_selector_types/length_based/", + ], + }, + "/docs/modules/model_io/prompts/example_selector_types/similarity/": { + canonical: "/docs/how_to/example_selectors_similarity", + alternative: [ + "/v0.1/docs/modules/model_io/prompts/example_selector_types/similarity/", + ], + }, + "/docs/modules/model_io/prompts/few_shot/": { + canonical: "/docs/how_to/few_shot_examples", + alternative: ["/v0.1/docs/modules/model_io/prompts/few_shot/"], + }, + "/docs/modules/model_io/prompts/pipeline/": { + canonical: "/docs/how_to/prompts_composition", + alternative: ["/v0.1/docs/modules/model_io/prompts/pipeline/"], + }, + "/docs/production/deployment/": { + canonical: "https://langchain-ai.github.io/langgraph/cloud/", + alternative: ["/v0.1/docs/production/deployment/"], + }, + "/docs/production/tracing/": { + canonical: + "https://docs.smith.langchain.com/how_to_guides/tracing/trace_with_langchain", + alternative: ["/v0.1/docs/production/tracing/"], + }, + "/docs/use_cases/agent_simulations/": { + canonical: + "https://js.langchain.com/v0.1/docs/use_cases/agent_simulations/", + alternative: ["/v0.1/docs/use_cases/agent_simulations/"], + }, + "/docs/use_cases/agent_simulations/generative_agents/": { + canonical: + "https://js.langchain.com/v0.1/docs/use_cases/agent_simulations/generative_agents/", + alternative: ["/v0.1/docs/use_cases/agent_simulations/generative_agents/"], + }, + "/docs/use_cases/agent_simulations/violation_of_expectations_chain/": { + canonical: + "https://js.langchain.com/v0.1/docs/use_cases/agent_simulations/violation_of_expectations_chain/", + alternative: [ + "/v0.1/docs/use_cases/agent_simulations/violation_of_expectations_chain/", + ], + }, + "/docs/use_cases/api/": { + canonical: "https://js.langchain.com/v0.1/docs/use_cases/api/", + alternative: ["/v0.1/docs/use_cases/api/"], + }, + "/docs/use_cases/autonomous_agents/": { + canonical: + "https://js.langchain.com/v0.1/docs/use_cases/autonomous_agents/", + alternative: ["/v0.1/docs/use_cases/autonomous_agents/"], + }, + "/docs/use_cases/autonomous_agents/auto_gpt/": { + canonical: + "https://js.langchain.com/v0.1/docs/use_cases/autonomous_agents/auto_gpt/", + alternative: ["/v0.1/docs/use_cases/autonomous_agents/auto_gpt/"], + }, + "/docs/use_cases/autonomous_agents/baby_agi/": { + canonical: + "https://js.langchain.com/v0.1/docs/use_cases/autonomous_agents/baby_agi/", + alternative: ["/v0.1/docs/use_cases/autonomous_agents/baby_agi/"], + }, + "/docs/use_cases/autonomous_agents/sales_gpt/": { + canonical: + "https://js.langchain.com/v0.1/docs/use_cases/autonomous_agents/sales_gpt/", + alternative: ["/v0.1/docs/use_cases/autonomous_agents/sales_gpt/"], + }, + "/docs/use_cases/graph/construction/": { + canonical: "/docs/tutorials/graph", + alternative: ["/v0.1/docs/use_cases/graph/construction/"], + }, + "/docs/use_cases/media/": { + canonical: "/docs/how_to/multimodal_prompts", + alternative: ["/v0.1/docs/use_cases/media/"], + }, + "/docs/use_cases/query_analysis/how_to/constructing_filters/": { + canonical: "/docs/tutorials/query_analysis", + alternative: [ + "/v0.1/docs/use_cases/query_analysis/how_to/constructing_filters/", + ], + }, + "/docs/use_cases/tabular/": { + canonical: "/docs/tutorials/sql_qa", + alternative: ["/v0.1/docs/use_cases/tabular/"], + }, +}; + +export default function NotFound() { + const location = useLocation(); + const pathname = location.pathname.endsWith("/") + ? location.pathname + : `${location.pathname}/`; // Ensure the path matches the keys in suggestedLinks + const { canonical, alternative } = suggestedLinks[pathname] || {}; + + return ( + <> + + +
+
+
+

+ {canonical + ? "Page Moved" + : alternative + ? "Page Removed" + : "Page Not Found"} +

+ {canonical ? ( +

+ You can find the new location here. +

+ ) : alternative ? ( +

The page you were looking for has been removed.

+ ) : ( +

We could not find what you were looking for.

+ )} + {alternative && ( +

+

+ Alternative pages +
    + {alternative.map((alt, index) => ( + // eslint-disable-next-line react/no-array-index-key +
  • + {alt} + {alt.startsWith("/v0.1/") && ( + <> + {" "} + + + )} +
  • + ))} +
+
+

+ )} +

+ Please contact the owner of the site that linked you to the + original URL and let them know their link{" "} + {canonical + ? "has moved." + : alternative + ? "has been removed." + : "is broken."} +

+
+
+
+
+ + ); +} diff --git a/docs/core_docs/static/img/conversational_retrieval_chain.png b/docs/core_docs/static/img/conversational_retrieval_chain.png new file mode 100644 index 000000000000..1130df556af2 Binary files /dev/null and b/docs/core_docs/static/img/conversational_retrieval_chain.png differ diff --git a/docs/core_docs/vercel.json b/docs/core_docs/vercel.json index 6f35f9b8ec32..d573c16d6e7b 100644 --- a/docs/core_docs/vercel.json +++ b/docs/core_docs/vercel.json @@ -64,6 +64,22 @@ { "source": "/docs/tutorials/agents(/?)", "destination": "https://langchain-ai.github.io/langgraphjs/tutorials/quickstart/" + }, + { + "source": "/docs/troubleshooting/errors/GRAPH_RECURSION_LIMIT(/?)", + "destination": "https://langchain-ai.github.io/langgraphjs/troubleshooting/errors/GRAPH_RECURSION_LIMIT/" + }, + { + "source": "/docs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE(/?)", + "destination": "https://langchain-ai.github.io/langgraphjs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE/" + }, + { + "source": "/docs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE(/?)", + "destination": "https://langchain-ai.github.io/langgraphjs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE/" + }, + { + "source": "/docs/troubleshooting/errors/MULTIPLE_SUBGRAPHS(/?)", + "destination": "https://langchain-ai.github.io/langgraphjs/troubleshooting/errors/MULTIPLE_SUBGRAPHS/" } ] } diff --git a/environment_tests/scripts/docker-ci-entrypoint.sh b/environment_tests/scripts/docker-ci-entrypoint.sh index eb813d5034d4..6c2a5472747c 100644 --- a/environment_tests/scripts/docker-ci-entrypoint.sh +++ b/environment_tests/scripts/docker-ci-entrypoint.sh @@ -17,12 +17,12 @@ mkdir -p ./libs/langchain-community/ mkdir -p ./libs/langchain-cohere/ mkdir -p ./libs/langchain/ -cp -r ../langchain-core ./libs/ -cp -r ../langchain-openai ./libs/ -cp -r ../langchain-anthropic ./libs/ -cp -r ../langchain-community ./libs/ -cp -r ../langchain-cohere ./libs/ -cp -r ../langchain ./libs/ +cp -r ../langchain-core/!(node_modules) ./libs/langchain-core +cp -r ../langchain-openai/!(node_modules) ./libs/langchain-openai +cp -r ../langchain-anthropic/!(node_modules) ./libs/langchain-anthropic +cp -r ../langchain-community/!(node_modules) ./libs/langchain-community +cp -r ../langchain-cohere/!(node_modules) ./libs/langchain-cohere +cp -r ../langchain/!(node_modules) ./libs/langchain # copy cache mkdir -p ./.yarn diff --git a/environment_tests/test-exports-cf/package.json b/environment_tests/test-exports-cf/package.json index 86264ea5ee3e..e4381ee76937 100644 --- a/environment_tests/test-exports-cf/package.json +++ b/environment_tests/test-exports-cf/package.json @@ -12,7 +12,6 @@ "@langchain/core": "workspace:*", "@langchain/openai": "workspace:*", "@tsconfig/recommended": "^1.0.2", - "d3-dsv": "2", "langchain": "workspace:*", "wrangler": "^3.19.0", "vitest": "0.34.3", diff --git a/environment_tests/test-exports-cf/src/index.ts b/environment_tests/test-exports-cf/src/index.ts index b89be67fcc5a..8bab0db82c3d 100644 --- a/environment_tests/test-exports-cf/src/index.ts +++ b/environment_tests/test-exports-cf/src/index.ts @@ -12,7 +12,6 @@ import "./entrypoints.js"; // Import a few things we'll use to test the exports -import { LLMChain } from "langchain/chains"; import { ChatOpenAI } from "@langchain/openai"; import { ChatPromptTemplate, @@ -20,6 +19,8 @@ import { } from "@langchain/core/prompts"; import { OpenAI } from "@langchain/openai"; import { OpenAIEmbeddings } from "@langchain/openai"; +import { StringOutputParser } from "@langchain/core/output_parsers"; +import { MemoryVectorStore } from "langchain/vectorstores/memory"; export interface Env { OPENAI_API_KEY?: string; @@ -51,14 +52,12 @@ export default { const emb = new OpenAIEmbeddings(constructorParameters); // Test a chain + prompt + model - const chain = new LLMChain({ - llm: new ChatOpenAI(constructorParameters), - prompt: ChatPromptTemplate.fromMessages([ - HumanMessagePromptTemplate.fromTemplate("{input}"), - ]), - }); - const res = await chain.run("hello"); - + const prompt = ChatPromptTemplate.fromMessages([ + HumanMessagePromptTemplate.fromTemplate("{input}"), + ]); + const llm = new ChatOpenAI(constructorParameters); + const chain = prompt.pipe(llm).pipe(new StringOutputParser()); + const res = await chain.invoke("hello"); return new Response( `Hello, from Cloudflare Worker at ${request.url}. Assistant says: ${res}` ); diff --git a/environment_tests/test-exports-cjs/package.json b/environment_tests/test-exports-cjs/package.json index 1c842dd768c9..869134a6856f 100644 --- a/environment_tests/test-exports-cjs/package.json +++ b/environment_tests/test-exports-cjs/package.json @@ -27,8 +27,6 @@ "@langchain/openai": "workspace:*", "@tsconfig/recommended": "^1.0.2", "@xenova/transformers": "^2.17.2", - "d3-dsv": "2", - "hnswlib-node": "^3.0.0", "langchain": "workspace:*", "typescript": "^5.0.0" }, diff --git a/environment_tests/test-exports-cjs/src/import.js b/environment_tests/test-exports-cjs/src/import.js index d0c116c774e5..752cfdea37ba 100644 --- a/environment_tests/test-exports-cjs/src/import.js +++ b/environment_tests/test-exports-cjs/src/import.js @@ -3,24 +3,17 @@ async function test() { const { OpenAI } = await import("@langchain/openai"); const { LLMChain } = await import("langchain/chains"); const { ChatPromptTemplate } = await import("@langchain/core/prompts"); - const { HNSWLib } = await import("@langchain/community/vectorstores/hnswlib"); const { HuggingFaceTransformersEmbeddings } = await import("@langchain/community/embeddings/hf_transformers"); const { Document } = await import("@langchain/core/documents"); + const { MemoryVectorStore } = await import("langchain/vectorstores/memory"); // Test exports assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); - - const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-cjs/src/index.mjs b/environment_tests/test-exports-cjs/src/index.mjs index 36ce9d72a71d..632b8081fbaa 100644 --- a/environment_tests/test-exports-cjs/src/index.mjs +++ b/environment_tests/test-exports-cjs/src/index.mjs @@ -1,7 +1,7 @@ import assert from "assert"; import { OpenAI } from "@langchain/openai"; import { LLMChain } from "langchain/chains"; -import { HNSWLib } from "@langchain/community/vectorstores/hnswlib"; +import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { ChatPromptTemplate } from "@langchain/core/prompts"; import { HuggingFaceTransformersEmbeddings } from "@langchain/community/embeddings/hf_transformers"; import { Document } from "@langchain/core/documents"; @@ -10,16 +10,9 @@ import { Document } from "@langchain/core/documents"; assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); -assert(typeof HNSWLib === "function"); +assert(typeof MemoryVectorStore === "function"); -// Test dynamic imports of peer dependencies -const { HierarchicalNSW } = await HNSWLib.imports(); - -const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), -}); +const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-cjs/src/index.ts b/environment_tests/test-exports-cjs/src/index.ts index 66a7b716b4d8..d2dcb9ebab0d 100644 --- a/environment_tests/test-exports-cjs/src/index.ts +++ b/environment_tests/test-exports-cjs/src/index.ts @@ -2,7 +2,7 @@ import assert from "assert"; import { OpenAI } from "@langchain/openai"; import { LLMChain } from "langchain/chains"; import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { HNSWLib } from "@langchain/community/vectorstores/hnswlib"; +import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { HuggingFaceTransformersEmbeddings } from "@langchain/community/embeddings/hf_transformers"; import { Document } from "@langchain/core/documents"; @@ -11,10 +11,9 @@ async function test(useAzure: boolean = false) { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); const openAIParameters = useAzure ? { azureOpenAIApiKey: "sk-XXXX", @@ -25,11 +24,8 @@ async function test(useAzure: boolean = false) { : { openAIApiKey: "sk-XXXX", }; - const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + + const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-cjs/src/require.js b/environment_tests/test-exports-cjs/src/require.js index 7f6fb549111d..1343f8587f35 100644 --- a/environment_tests/test-exports-cjs/src/require.js +++ b/environment_tests/test-exports-cjs/src/require.js @@ -2,7 +2,7 @@ const assert = require("assert"); const { OpenAI } = require("@langchain/openai"); const { LLMChain } = require("langchain/chains"); const { ChatPromptTemplate } = require("@langchain/core/prompts"); -const { HNSWLib } = require("@langchain/community/vectorstores/hnswlib"); +const { MemoryVectorStore } = require("langchain/vectorstores/memory"); const { HuggingFaceTransformersEmbeddings } = require("@langchain/community/embeddings/hf_transformers"); const { Document } = require("@langchain/core/documents"); @@ -11,16 +11,9 @@ async function test() { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); - - const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2" })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esbuild/package.json b/environment_tests/test-exports-esbuild/package.json index d4eb19c76517..03c66e9770b0 100644 --- a/environment_tests/test-exports-esbuild/package.json +++ b/environment_tests/test-exports-esbuild/package.json @@ -25,7 +25,6 @@ "@langchain/openai": "workspace:*", "@tsconfig/recommended": "^1.0.2", "esbuild": "^0.17.18", - "hnswlib-node": "^3.0.0", "langchain": "workspace:*", "typescript": "^5.0.0" }, diff --git a/environment_tests/test-exports-esbuild/src/import.cjs b/environment_tests/test-exports-esbuild/src/import.cjs index 5a63cc4eb32f..0d56038c6680 100644 --- a/environment_tests/test-exports-esbuild/src/import.cjs +++ b/environment_tests/test-exports-esbuild/src/import.cjs @@ -3,7 +3,7 @@ async function test() { const { OpenAI } = await import("@langchain/openai"); const { LLMChain } = await import("langchain/chains"); const { ChatPromptTemplate } = await import("@langchain/core/prompts"); - const { HNSWLib } = await import("@langchain/community/vectorstores/hnswlib"); + const { MemoryVectorStore } = await import("langchain/vectorstores/memory"); const { OpenAIEmbeddings } = await import("@langchain/openai"); const { Document } = await import("@langchain/core/documents"); @@ -11,16 +11,9 @@ async function test() { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); - - const vs = new HNSWLib(new OpenAIEmbeddings({ openAIApiKey: "sk-XXXX" }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new OpenAIEmbeddings({ openAIApiKey: "sk-XXXX" })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esbuild/src/index.js b/environment_tests/test-exports-esbuild/src/index.js index 167a8f92d612..dc1227f31169 100644 --- a/environment_tests/test-exports-esbuild/src/index.js +++ b/environment_tests/test-exports-esbuild/src/index.js @@ -2,7 +2,7 @@ import assert from "assert"; import { OpenAI } from "@langchain/openai"; import { LLMChain } from "langchain/chains"; import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { HNSWLib } from "@langchain/community/vectorstores/hnswlib"; +import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { OpenAIEmbeddings } from "@langchain/openai"; import { Document } from "@langchain/core/documents"; import { CallbackManager } from "@langchain/core/callbacks/manager"; @@ -11,18 +11,11 @@ import { CallbackManager } from "@langchain/core/callbacks/manager"; assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); -assert(typeof HNSWLib === "function"); +assert(typeof MemoryVectorStore === "function"); assert(typeof OpenAIEmbeddings === "function"); assert(typeof CallbackManager === "function"); -// Test dynamic imports of peer dependencies -const { HierarchicalNSW } = await HNSWLib.imports(); - -const vs = new HNSWLib(new OpenAIEmbeddings({ openAIApiKey: "sk-XXXX" }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), -}); +const vs = new MemoryVectorStore(new OpenAIEmbeddings({ openAIApiKey: "sk-XXXX" })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esbuild/src/require.cjs b/environment_tests/test-exports-esbuild/src/require.cjs index b0920e12d41e..7750e2def235 100644 --- a/environment_tests/test-exports-esbuild/src/require.cjs +++ b/environment_tests/test-exports-esbuild/src/require.cjs @@ -2,7 +2,7 @@ const assert = require("assert"); const { OpenAI } = require("@langchain/openai"); const { LLMChain } = require("langchain/chains"); const { ChatPromptTemplate } = require("@langchain/core/prompts"); -const { HNSWLib } = require("@langchain/community/vectorstores/hnswlib"); +const { MemoryVectorStore } = require("langchain/vectorstores/memory"); const { OpenAIEmbeddings } = require("@langchain/openai"); const { Document } = require("@langchain/core/documents"); @@ -11,16 +11,9 @@ async function test() { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); - - const vs = new HNSWLib(new OpenAIEmbeddings({ openAIApiKey: "sk-XXXX" }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new OpenAIEmbeddings({ openAIApiKey: "sk-XXXX" })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esbuild/src/typescript.ts b/environment_tests/test-exports-esbuild/src/typescript.ts index 2c3989e8100c..0fdc84f9bedd 100644 --- a/environment_tests/test-exports-esbuild/src/typescript.ts +++ b/environment_tests/test-exports-esbuild/src/typescript.ts @@ -2,7 +2,7 @@ import assert from "assert"; import { OpenAI } from "@langchain/openai"; import { LLMChain } from "langchain/chains"; import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { HNSWLib } from "@langchain/community/vectorstores/hnswlib"; +import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { OpenAIEmbeddings } from "@langchain/openai"; import { Document } from "@langchain/core/documents"; @@ -11,10 +11,8 @@ async function test(useAzure: boolean = false) { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); const openAIParameters = useAzure ? { azureOpenAIApiKey: "sk-XXXX", @@ -26,11 +24,7 @@ async function test(useAzure: boolean = false) { openAIApiKey: "sk-XXXX", }; - const vs = new HNSWLib(new OpenAIEmbeddings(openAIParameters), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new OpenAIEmbeddings(openAIParameters)); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esm/package.json b/environment_tests/test-exports-esm/package.json index 81e31aec7d05..df0bb2b40168 100644 --- a/environment_tests/test-exports-esm/package.json +++ b/environment_tests/test-exports-esm/package.json @@ -28,7 +28,6 @@ "@langchain/openai": "workspace:*", "@tsconfig/recommended": "^1.0.2", "@xenova/transformers": "^2.17.2", - "hnswlib-node": "^3.0.0", "langchain": "workspace:*", "typescript": "^5.0.0" }, diff --git a/environment_tests/test-exports-esm/src/import.cjs b/environment_tests/test-exports-esm/src/import.cjs index 31cfb59e0996..6837754c442c 100644 --- a/environment_tests/test-exports-esm/src/import.cjs +++ b/environment_tests/test-exports-esm/src/import.cjs @@ -3,7 +3,7 @@ async function test() { const { OpenAI } = await import("@langchain/openai"); const { LLMChain } = await import("langchain/chains"); const { ChatPromptTemplate } = await import("@langchain/core/prompts"); - const { HNSWLib } = await import("@langchain/community/vectorstores/hnswlib"); + const { MemoryVectorStore } = await import("langchain/vectorstores/memory"); const { HuggingFaceTransformersEmbeddings } = await import("@langchain/community/embeddings/hf_transformers"); const { Document } = await import("@langchain/core/documents"); @@ -11,16 +11,9 @@ async function test() { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); - - const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esm/src/index.js b/environment_tests/test-exports-esm/src/index.js index 950ff4934d24..2347699ee1dc 100644 --- a/environment_tests/test-exports-esm/src/index.js +++ b/environment_tests/test-exports-esm/src/index.js @@ -2,7 +2,7 @@ import assert from "assert"; import { OpenAI } from "@langchain/openai"; import { LLMChain } from "langchain/chains"; import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { HNSWLib } from "@langchain/community/vectorstores/hnswlib"; +import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { HuggingFaceTransformersEmbeddings } from "@langchain/community/embeddings/hf_transformers"; import { Document } from "@langchain/core/documents"; import { CallbackManager } from "@langchain/core/callbacks/manager"; @@ -11,18 +11,11 @@ import { CallbackManager } from "@langchain/core/callbacks/manager"; assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); -assert(typeof HNSWLib === "function"); +assert(typeof MemoryVectorStore === "function"); assert(typeof HuggingFaceTransformersEmbeddings === "function"); assert(typeof CallbackManager === "function"); -// Test dynamic imports of peer dependencies -const { HierarchicalNSW } = await HNSWLib.imports(); - -const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), -}); +const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esm/src/index.ts b/environment_tests/test-exports-esm/src/index.ts index f01a635b08ef..c29f419c07a8 100644 --- a/environment_tests/test-exports-esm/src/index.ts +++ b/environment_tests/test-exports-esm/src/index.ts @@ -2,7 +2,7 @@ import assert from "assert"; import { OpenAI } from "@langchain/openai"; import { LLMChain } from "langchain/chains"; import { ChatPromptTemplate } from "@langchain/core/prompts"; -import { HNSWLib } from "@langchain/community/vectorstores/hnswlib"; +import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { HuggingFaceTransformersEmbeddings } from "@langchain/community/embeddings/hf_transformers"; import { Document } from "@langchain/core/documents"; @@ -11,10 +11,8 @@ async function test(useAzure: boolean = false) { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); const openAIParameters = useAzure ? { azureOpenAIApiKey: "sk-XXXX", @@ -26,11 +24,7 @@ async function test(useAzure: boolean = false) { openAIApiKey: "sk-XXXX", }; - const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", })); await vs.addVectors( [ diff --git a/environment_tests/test-exports-esm/src/require.cjs b/environment_tests/test-exports-esm/src/require.cjs index b8dc4145ef4e..31461c4b6c23 100644 --- a/environment_tests/test-exports-esm/src/require.cjs +++ b/environment_tests/test-exports-esm/src/require.cjs @@ -2,7 +2,7 @@ const assert = require("assert"); const { OpenAI } = require("@langchain/openai"); const { LLMChain } = require("langchain/chains"); const { ChatPromptTemplate } = require("@langchain/core/prompts"); -const { HNSWLib } = require("@langchain/community/vectorstores/hnswlib"); +const { MemoryVectorStore } = require("langchain/vectorstores/memory"); const { HuggingFaceTransformersEmbeddings } = require("@langchain/community/embeddings/hf_transformers"); const { Document } = require("@langchain/core/documents"); @@ -11,16 +11,9 @@ async function test() { assert(typeof OpenAI === "function"); assert(typeof LLMChain === "function"); assert(typeof ChatPromptTemplate === "function"); - assert(typeof HNSWLib === "function"); + assert(typeof MemoryVectorStore === "function"); - // Test dynamic imports of peer dependencies - const { HierarchicalNSW } = await HNSWLib.imports(); - - const vs = new HNSWLib(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", }), { - space: "ip", - numDimensions: 3, - index: new HierarchicalNSW("ip", 3), - }); + const vs = new MemoryVectorStore(new HuggingFaceTransformersEmbeddings({ model: "Xenova/all-MiniLM-L6-v2", })); await vs.addVectors( [ diff --git a/examples/package.json b/examples/package.json index c3dab06a5b36..4d3048e5912b 100644 --- a/examples/package.json +++ b/examples/package.json @@ -49,7 +49,7 @@ "@langchain/google-vertexai": "workspace:*", "@langchain/google-vertexai-web": "workspace:*", "@langchain/groq": "workspace:*", - "@langchain/langgraph": "^0.0.28", + "@langchain/langgraph": "^0.2.3", "@langchain/mistralai": "workspace:*", "@langchain/mongodb": "workspace:*", "@langchain/nomic": "workspace:*", @@ -68,7 +68,6 @@ "@planetscale/database": "^1.8.0", "@prisma/client": "^4.11.0", "@qdrant/js-client-rest": "^1.9.0", - "@raycast/api": "^1.55.2", "@rockset/client": "^0.9.1", "@supabase/supabase-js": "^2.45.0", "@tensorflow/tfjs-backend-cpu": "^4.4.0", @@ -79,8 +78,8 @@ "@zilliz/milvus2-sdk-node": "^2.3.5", "axios": "^0.26.0", "chromadb": "^1.5.3", + "cohere-ai": "^7.14.0", "convex": "^1.3.1", - "couchbase": "^4.3.0", "date-fns": "^3.3.1", "duck-duck-scrape": "^2.2.5", "exa-js": "^1.0.12", @@ -91,7 +90,7 @@ "ioredis": "^5.3.2", "js-yaml": "^4.1.0", "langchain": "workspace:*", - "langsmith": "^0.1.43", + "langsmith": "^0.1.56", "mongodb": "^6.3.0", "pg": "^8.11.0", "pickleparser": "^0.2.1", @@ -102,7 +101,7 @@ "typeorm": "^0.3.20", "typesense": "^1.5.3", "uuid": "^10.0.0", - "vectordb": "^0.1.4", + "vectordb": "^0.9.0", "voy-search": "0.6.2", "weaviate-ts-client": "^2.0.0", "zod": "^3.22.4", diff --git a/examples/src/document_compressors/cohere_rerank_custom_client.ts b/examples/src/document_compressors/cohere_rerank_custom_client.ts new file mode 100644 index 000000000000..8970a97110ae --- /dev/null +++ b/examples/src/document_compressors/cohere_rerank_custom_client.ts @@ -0,0 +1,54 @@ +import { CohereRerank } from "@langchain/cohere"; +import { CohereClient } from "cohere-ai"; +import { Document } from "@langchain/core/documents"; + +const query = "What is the capital of the United States?"; +const docs = [ + new Document({ + pageContent: + "Carson City is the capital city of the American state of Nevada. At the 2010 United States Census, Carson City had a population of 55,274.", + }), + new Document({ + pageContent: + "The Commonwealth of the Northern Mariana Islands is a group of islands in the Pacific Ocean that are a political division controlled by the United States. Its capital is Saipan.", + }), + new Document({ + pageContent: + "Charlotte Amalie is the capital and largest city of the United States Virgin Islands. It has about 20,000 people. The city is on the island of Saint Thomas.", + }), + new Document({ + pageContent: + "Washington, D.C. (also known as simply Washington or D.C., and officially as the District of Columbia) is the capital of the United States. It is a federal district. The President of the USA and many major national government offices are in the territory. This makes it the political center of the United States of America.", + }), + new Document({ + pageContent: + "Capital punishment (the death penalty) has existed in the United States since before the United States was a country. As of 2017, capital punishment is legal in 30 of the 50 states. The federal government (including the United States military) also uses capital punishment.", + }), +]; + +const client = new CohereClient({ + token: process.env.COHERE_API_KEY, + environment: "", // optional + // other params +}); + +const cohereRerank = new CohereRerank({ + client, // apiKey will be ignored even if provided + model: "rerank-english-v2.0", +}); + +const rerankedDocuments = await cohereRerank.rerank(docs, query, { + topN: 5, +}); + +console.log(rerankedDocuments); + +/* + [ + { index: 3, relevanceScore: 0.9871293 }, + { index: 1, relevanceScore: 0.29961726 }, + { index: 4, relevanceScore: 0.27542195 }, + { index: 0, relevanceScore: 0.08977329 }, + { index: 2, relevanceScore: 0.041462272 } + ] +*/ diff --git a/examples/src/document_loaders/apify_dataset_existing.ts b/examples/src/document_loaders/apify_dataset_existing.ts index d9a647ad7403..827d6341575f 100644 --- a/examples/src/document_loaders/apify_dataset_existing.ts +++ b/examples/src/document_loaders/apify_dataset_existing.ts @@ -6,6 +6,9 @@ import { ChatPromptTemplate } from "@langchain/core/prompts"; import { createRetrievalChain } from "langchain/chains/retrieval"; import { createStuffDocumentsChain } from "langchain/chains/combine_documents"; +const APIFY_API_TOKEN = "YOUR-APIFY-API-TOKEN"; // or set as process.env.APIFY_API_TOKEN +const OPENAI_API_KEY = "YOUR-OPENAI-API-KEY"; // or set as process.env.OPENAI_API_KEY + /* * datasetMappingFunction is a function that maps your Apify dataset format to LangChain documents. * In the below example, the Apify dataset format looks like this: @@ -21,16 +24,20 @@ const loader = new ApifyDatasetLoader("your-dataset-id", { metadata: { source: item.url }, }), clientOptions: { - token: "your-apify-token", // Or set as process.env.APIFY_API_TOKEN + token: APIFY_API_TOKEN, }, }); const docs = await loader.load(); -const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings()); +const vectorStore = await HNSWLib.fromDocuments( + docs, + new OpenAIEmbeddings({ apiKey: OPENAI_API_KEY }) +); const model = new ChatOpenAI({ temperature: 0, + apiKey: OPENAI_API_KEY, }); const questionAnsweringPrompt = ChatPromptTemplate.fromMessages([ diff --git a/examples/src/document_loaders/apify_dataset_new.ts b/examples/src/document_loaders/apify_dataset_new.ts index e0c8718c481e..2e36875f60f6 100644 --- a/examples/src/document_loaders/apify_dataset_new.ts +++ b/examples/src/document_loaders/apify_dataset_new.ts @@ -6,6 +6,9 @@ import { ChatPromptTemplate } from "@langchain/core/prompts"; import { createStuffDocumentsChain } from "langchain/chains/combine_documents"; import { createRetrievalChain } from "langchain/chains/retrieval"; +const APIFY_API_TOKEN = "YOUR-APIFY-API-TOKEN"; // or set as process.env.APIFY_API_TOKEN +const OPENAI_API_KEY = "YOUR-OPENAI-API-KEY"; // or set as process.env.OPENAI_API_KEY + /* * datasetMappingFunction is a function that maps your Apify dataset format to LangChain documents. * In the below example, the Apify dataset format looks like this: @@ -17,6 +20,8 @@ import { createRetrievalChain } from "langchain/chains/retrieval"; const loader = await ApifyDatasetLoader.fromActorCall( "apify/website-content-crawler", { + maxCrawlPages: 10, + crawlerType: "cheerio", startUrls: [{ url: "https://js.langchain.com/docs/" }], }, { @@ -26,17 +31,21 @@ const loader = await ApifyDatasetLoader.fromActorCall( metadata: { source: item.url }, }), clientOptions: { - token: "your-apify-token", // Or set as process.env.APIFY_API_TOKEN + token: APIFY_API_TOKEN, }, } ); const docs = await loader.load(); -const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings()); +const vectorStore = await HNSWLib.fromDocuments( + docs, + new OpenAIEmbeddings({ apiKey: OPENAI_API_KEY }) +); const model = new ChatOpenAI({ temperature: 0, + apiKey: OPENAI_API_KEY, }); const questionAnsweringPrompt = ChatPromptTemplate.fromMessages([ diff --git a/examples/src/indexes/vector_stores/couchbase/similaritySearch.ts b/examples/src/indexes/vector_stores/couchbase/similaritySearch.ts deleted file mode 100644 index 0443c9f241eb..000000000000 --- a/examples/src/indexes/vector_stores/couchbase/similaritySearch.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { OpenAIEmbeddings } from "@langchain/openai"; -import { - CouchbaseVectorStoreArgs, - CouchbaseVectorStore, -} from "@langchain/community/vectorstores/couchbase"; -import { Cluster } from "couchbase"; -import { TextLoader } from "langchain/document_loaders/fs/text"; -import { CharacterTextSplitter } from "@langchain/textsplitters"; - -const connectionString = - process.env.COUCHBASE_DB_CONN_STR ?? "couchbase://localhost"; -const databaseUsername = process.env.COUCHBASE_DB_USERNAME ?? "Administrator"; -const databasePassword = process.env.COUCHBASE_DB_PASSWORD ?? "Password"; - -// Load documents from file -const loader = new TextLoader("./state_of_the_union.txt"); -const rawDocuments = await loader.load(); -const splitter = new CharacterTextSplitter({ - chunkSize: 500, - chunkOverlap: 0, -}); -const docs = await splitter.splitDocuments(rawDocuments); - -const couchbaseClient = await Cluster.connect(connectionString, { - username: databaseUsername, - password: databasePassword, - configProfile: "wanDevelopment", -}); - -// Open AI API Key is required to use OpenAIEmbeddings, some other embeddings may also be used -const embeddings = new OpenAIEmbeddings({ - apiKey: process.env.OPENAI_API_KEY, -}); - -const couchbaseConfig: CouchbaseVectorStoreArgs = { - cluster: couchbaseClient, - bucketName: "testing", - scopeName: "_default", - collectionName: "_default", - indexName: "vector-index", - textKey: "text", - embeddingKey: "embedding", -}; - -const store = await CouchbaseVectorStore.fromDocuments( - docs, - embeddings, - couchbaseConfig -); - -const query = "What did president say about Ketanji Brown Jackson"; - -const resultsSimilaritySearch = await store.similaritySearch(query); -console.log("resulting documents: ", resultsSimilaritySearch[0]); - -// Similarity Search With Score -const resultsSimilaritySearchWithScore = await store.similaritySearchWithScore( - query, - 1 -); -console.log("resulting documents: ", resultsSimilaritySearchWithScore[0][0]); -console.log("resulting scores: ", resultsSimilaritySearchWithScore[0][1]); - -const result = await store.similaritySearch(query, 1, { - fields: ["metadata.source"], -}); -console.log(result[0]); diff --git a/examples/src/indexes/vector_stores/lancedb/fromDocs.ts b/examples/src/indexes/vector_stores/lancedb/fromDocs.ts index 69715191321d..2907ccde6fc9 100644 --- a/examples/src/indexes/vector_stores/lancedb/fromDocs.ts +++ b/examples/src/indexes/vector_stores/lancedb/fromDocs.ts @@ -4,24 +4,29 @@ import { TextLoader } from "langchain/document_loaders/fs/text"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; -import { connect } from "vectordb"; // Create docs with a loader const loader = new TextLoader("src/document_loaders/example_data/example.txt"); const docs = await loader.load(); export const run = async () => { + const vectorStore = await LanceDB.fromDocuments(docs, new OpenAIEmbeddings()); + + const resultOne = await vectorStore.similaritySearch("hello world", 1); + console.log(resultOne); + + // [ + // Document { + // pageContent: 'Foo\nBar\nBaz\n\n', + // metadata: { source: 'src/document_loaders/example_data/example.txt' } + // } + // ] +}; + +export const run_with_existing_table = async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "lancedb-")); - const db = await connect(dir); - const table = await db.createTable("vectors", [ - { vector: Array(1536), text: "sample", source: "a" }, - ]); - - const vectorStore = await LanceDB.fromDocuments( - docs, - new OpenAIEmbeddings(), - { table } - ); + + const vectorStore = await LanceDB.fromDocuments(docs, new OpenAIEmbeddings()); const resultOne = await vectorStore.similaritySearch("hello world", 1); console.log(resultOne); diff --git a/examples/src/indexes/vector_stores/lancedb/fromTexts.ts b/examples/src/indexes/vector_stores/lancedb/fromTexts.ts index 2f70f340d5ad..4156d1a161d2 100644 --- a/examples/src/indexes/vector_stores/lancedb/fromTexts.ts +++ b/examples/src/indexes/vector_stores/lancedb/fromTexts.ts @@ -1,22 +1,27 @@ import { LanceDB } from "@langchain/community/vectorstores/lancedb"; import { OpenAIEmbeddings } from "@langchain/openai"; -import { connect } from "vectordb"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import os from "node:os"; export const run = async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "lancedb-")); - const db = await connect(dir); - const table = await db.createTable("vectors", [ - { vector: Array(1536), text: "sample", id: 1 }, - ]); + const vectorStore = await LanceDB.fromTexts( + ["Hello world", "Bye bye", "hello nice world"], + [{ id: 2 }, { id: 1 }, { id: 3 }], + new OpenAIEmbeddings() + ); + const resultOne = await vectorStore.similaritySearch("hello world", 1); + console.log(resultOne); + // [ Document { pageContent: 'hello nice world', metadata: { id: 3 } } ] +}; + +export const run_with_existing_table = async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "lancedb-")); const vectorStore = await LanceDB.fromTexts( ["Hello world", "Bye bye", "hello nice world"], [{ id: 2 }, { id: 1 }, { id: 3 }], - new OpenAIEmbeddings(), - { table } + new OpenAIEmbeddings() ); const resultOne = await vectorStore.similaritySearch("hello world", 1); diff --git a/examples/src/models/llm/raycast.ts b/examples/src/models/llm/raycast.ts deleted file mode 100644 index 94eaec66e98a..000000000000 --- a/examples/src/models/llm/raycast.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { RaycastAI } from "@langchain/community/llms/raycast"; - -import { showHUD } from "@raycast/api"; -import { initializeAgentExecutorWithOptions } from "langchain/agents"; -import { Tool } from "@langchain/core/tools"; - -const model = new RaycastAI({ - rateLimitPerMinute: 10, // It is 10 by default so you can omit this line - model: "gpt-3.5-turbo", - creativity: 0, // `creativity` is a term used by Raycast which is equivalent to `temperature` in some other LLMs -}); - -const tools: Tool[] = [ - // Add your tools here -]; - -export default async function main() { - // Initialize the agent executor with RaycastAI model - const executor = await initializeAgentExecutorWithOptions(tools, model, { - agentType: "chat-conversational-react-description", - }); - - const input = `Describe my today's schedule as Gabriel Garcia Marquez would describe it`; - - const answer = await executor.invoke({ input }); - - await showHUD(answer.output); -} diff --git a/langchain-core/.gitignore b/langchain-core/.gitignore index efc6a4dc88ba..6876afce9643 100644 --- a/langchain-core/.gitignore +++ b/langchain-core/.gitignore @@ -30,6 +30,10 @@ chat_history.cjs chat_history.js chat_history.d.ts chat_history.d.cts +context.cjs +context.js +context.d.ts +context.d.cts documents.cjs documents.js documents.d.ts diff --git a/langchain-core/langchain.config.js b/langchain-core/langchain.config.js index b7fd982357ee..d51e73e0b122 100644 --- a/langchain-core/langchain.config.js +++ b/langchain-core/langchain.config.js @@ -20,6 +20,7 @@ export const config = { "callbacks/manager": "callbacks/manager", "callbacks/promises": "callbacks/promises", chat_history: "chat_history", + context: "context", documents: "documents/index", "document_loaders/base": "document_loaders/base", "document_loaders/langsmith": "document_loaders/langsmith", diff --git a/langchain-core/package.json b/langchain-core/package.json index bf070eca71ff..80fa34025c24 100644 --- a/langchain-core/package.json +++ b/langchain-core/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/core", - "version": "0.3.0", + "version": "0.3.13", "description": "Core LangChain.js abstractions and schemas", "type": "module", "engines": { @@ -37,7 +37,7 @@ "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", - "langsmith": "^0.1.43", + "langsmith": "^0.1.65", "mustache": "^4.2.0", "p-queue": "^6.6.2", "p-retry": "4", @@ -68,7 +68,7 @@ "rimraf": "^5.0.1", "ts-jest": "^29.1.0", "typescript": "~5.1.6", - "web-streams-polyfill": "^3.3.3" + "web-streams-polyfill": "^4.0.0" }, "publishConfig": { "access": "public" @@ -160,6 +160,15 @@ "import": "./chat_history.js", "require": "./chat_history.cjs" }, + "./context": { + "types": { + "import": "./context.d.ts", + "require": "./context.d.cts", + "default": "./context.d.ts" + }, + "import": "./context.js", + "require": "./context.cjs" + }, "./documents": { "types": { "import": "./documents.d.ts", @@ -646,6 +655,10 @@ "chat_history.js", "chat_history.d.ts", "chat_history.d.cts", + "context.cjs", + "context.js", + "context.d.ts", + "context.d.cts", "documents.cjs", "documents.js", "documents.d.ts", diff --git a/langchain-core/src/callbacks/dispatch/index.ts b/langchain-core/src/callbacks/dispatch/index.ts index 9d0fe15a9bfa..762994d89cf8 100644 --- a/langchain-core/src/callbacks/dispatch/index.ts +++ b/langchain-core/src/callbacks/dispatch/index.ts @@ -1,10 +1,12 @@ +/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */ + import { AsyncLocalStorage } from "node:async_hooks"; import { dispatchCustomEvent as dispatchCustomEventWeb } from "./web.js"; import { type RunnableConfig, ensureConfig } from "../../runnables/config.js"; import { AsyncLocalStorageProviderSingleton } from "../../singletons/index.js"; -/* #__PURE__ */ AsyncLocalStorageProviderSingleton.initializeGlobalInstance( - /* #__PURE__ */ new AsyncLocalStorage() +AsyncLocalStorageProviderSingleton.initializeGlobalInstance( + new AsyncLocalStorage() ); /** diff --git a/langchain-core/src/context.ts b/langchain-core/src/context.ts new file mode 100644 index 000000000000..ff12a9eb4fc8 --- /dev/null +++ b/langchain-core/src/context.ts @@ -0,0 +1,131 @@ +/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */ +import { AsyncLocalStorage } from "node:async_hooks"; +import { RunTree } from "langsmith"; +import { isRunTree } from "langsmith/run_trees"; +import { + _CONTEXT_VARIABLES_KEY, + AsyncLocalStorageProviderSingleton, +} from "./singletons/index.js"; + +AsyncLocalStorageProviderSingleton.initializeGlobalInstance( + new AsyncLocalStorage() +); + +/** + * Set a context variable. Context variables are scoped to any + * child runnables called by the current runnable, or globally if set outside + * of any runnable. + * + * @remarks + * This function is only supported in environments that support AsyncLocalStorage, + * including Node.js, Deno, and Cloudflare Workers. + * + * @example + * ```ts + * import { RunnableLambda } from "@langchain/core/runnables"; + * import { + * getContextVariable, + * setContextVariable + * } from "@langchain/core/context"; + * + * const nested = RunnableLambda.from(() => { + * // "bar" because it was set by a parent + * console.log(getContextVariable("foo")); + * + * // Override to "baz", but only for child runnables + * setContextVariable("foo", "baz"); + * + * // Now "baz", but only for child runnables + * return getContextVariable("foo"); + * }); + * + * const runnable = RunnableLambda.from(async () => { + * // Set a context variable named "foo" + * setContextVariable("foo", "bar"); + * + * const res = await nested.invoke({}); + * + * // Still "bar" since child changes do not affect parents + * console.log(getContextVariable("foo")); + * + * return res; + * }); + * + * // undefined, because context variable has not been set yet + * console.log(getContextVariable("foo")); + * + * // Final return value is "baz" + * const result = await runnable.invoke({}); + * ``` + * + * @param name The name of the context variable. + * @param value The value to set. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function setContextVariable(name: PropertyKey, value: T): void { + const runTree = AsyncLocalStorageProviderSingleton.getInstance().getStore(); + const contextVars = { ...runTree?.[_CONTEXT_VARIABLES_KEY] }; + contextVars[name] = value; + let newValue = {}; + if (isRunTree(runTree)) { + newValue = new RunTree(runTree); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (newValue as any)[_CONTEXT_VARIABLES_KEY] = contextVars; + AsyncLocalStorageProviderSingleton.getInstance().enterWith(newValue); +} + +/** + * Get the value of a previously set context variable. Context variables + * are scoped to any child runnables called by the current runnable, + * or globally if set outside of any runnable. + * + * @remarks + * This function is only supported in environments that support AsyncLocalStorage, + * including Node.js, Deno, and Cloudflare Workers. + * + * @example + * ```ts + * import { RunnableLambda } from "@langchain/core/runnables"; + * import { + * getContextVariable, + * setContextVariable + * } from "@langchain/core/context"; + * + * const nested = RunnableLambda.from(() => { + * // "bar" because it was set by a parent + * console.log(getContextVariable("foo")); + * + * // Override to "baz", but only for child runnables + * setContextVariable("foo", "baz"); + * + * // Now "baz", but only for child runnables + * return getContextVariable("foo"); + * }); + * + * const runnable = RunnableLambda.from(async () => { + * // Set a context variable named "foo" + * setContextVariable("foo", "bar"); + * + * const res = await nested.invoke({}); + * + * // Still "bar" since child changes do not affect parents + * console.log(getContextVariable("foo")); + * + * return res; + * }); + * + * // undefined, because context variable has not been set yet + * console.log(getContextVariable("foo")); + * + * // Final return value is "baz" + * const result = await runnable.invoke({}); + * ``` + * + * @param name The name of the context variable. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getContextVariable(name: PropertyKey): T | undefined { + const runTree = AsyncLocalStorageProviderSingleton.getInstance().getStore(); + return runTree?.[_CONTEXT_VARIABLES_KEY]?.[name]; +} diff --git a/langchain-core/src/errors/index.ts b/langchain-core/src/errors/index.ts new file mode 100644 index 000000000000..2085c30ecf74 --- /dev/null +++ b/langchain-core/src/errors/index.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-param-reassign */ + +export type LangChainErrorCodes = + | "INVALID_PROMPT_INPUT" + | "INVALID_TOOL_RESULTS" + | "MESSAGE_COERCION_FAILURE" + | "MODEL_AUTHENTICATION" + | "MODEL_NOT_FOUND" + | "MODEL_RATE_LIMIT" + | "OUTPUT_PARSING_FAILURE"; + +export function addLangChainErrorFields( + error: any, + lc_error_code: LangChainErrorCodes +) { + (error as any).lc_error_code = lc_error_code; + error.message = `${error.message}\n\nTroubleshooting URL: https://js.langchain.com/docs/troubleshooting/errors/${lc_error_code}/\n`; + return error; +} diff --git a/langchain-core/src/language_models/base.ts b/langchain-core/src/language_models/base.ts index 80e1fa434969..4f1233426724 100644 --- a/langchain-core/src/language_models/base.ts +++ b/langchain-core/src/language_models/base.ts @@ -363,13 +363,14 @@ export abstract class BaseLanguageModel< callbackManager, ...params }: BaseLanguageModelParams) { + const { cache, ...rest } = params; super({ callbacks: callbacks ?? callbackManager, - ...params, + ...rest, }); - if (typeof params.cache === "object") { - this.cache = params.cache; - } else if (params.cache) { + if (typeof cache === "object") { + this.cache = cache; + } else if (cache) { this.cache = InMemoryCache.global(); } else { this.cache = undefined; diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index c7a5f84ed9fb..43d820ed338f 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -8,6 +8,7 @@ import { HumanMessage, coerceMessageLikeToMessage, AIMessageChunk, + isAIMessageChunk, } from "../messages/index.js"; import type { BasePromptValueInterface } from "../prompt_values.js"; import { @@ -141,7 +142,7 @@ export type BindToolsInput = export abstract class BaseChatModel< CallOptions extends BaseChatModelCallOptions = BaseChatModelCallOptions, // TODO: Fix the parameter order on the next minor version. - OutputMessageType extends BaseMessageChunk = BaseMessageChunk + OutputMessageType extends BaseMessageChunk = AIMessageChunk > extends BaseLanguageModel { // Backwards compatibility since fields have been moved to RunnableConfig declare ParsedCallOptions: Omit< @@ -258,6 +259,8 @@ export abstract class BaseChatModel< runnableConfig.runName ); let generationChunk: ChatGenerationChunk | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let llmOutput: Record | undefined; try { for await (const chunk of this._streamResponseChunks( messages, @@ -278,6 +281,18 @@ export abstract class BaseChatModel< } else { generationChunk = generationChunk.concat(chunk); } + if ( + isAIMessageChunk(chunk.message) && + chunk.message.usage_metadata !== undefined + ) { + llmOutput = { + tokenUsage: { + promptTokens: chunk.message.usage_metadata.input_tokens, + completionTokens: chunk.message.usage_metadata.output_tokens, + totalTokens: chunk.message.usage_metadata.total_tokens, + }, + }; + } } } catch (err) { await Promise.all( @@ -292,6 +307,7 @@ export abstract class BaseChatModel< runManager?.handleLLMEnd({ // TODO: Remove cast after figuring out inheritance generations: [[generationChunk as ChatGeneration]], + llmOutput, }) ) ); @@ -365,6 +381,8 @@ export abstract class BaseChatModel< runManagers?.[0] ); let aggregated; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let llmOutput: Record | undefined; for await (const chunk of stream) { if (chunk.message.id == null) { const runId = runManagers?.at(0)?.runId; @@ -375,6 +393,18 @@ export abstract class BaseChatModel< } else { aggregated = concat(aggregated, chunk); } + if ( + isAIMessageChunk(chunk.message) && + chunk.message.usage_metadata !== undefined + ) { + llmOutput = { + tokenUsage: { + promptTokens: chunk.message.usage_metadata.input_tokens, + completionTokens: chunk.message.usage_metadata.output_tokens, + totalTokens: chunk.message.usage_metadata.total_tokens, + }, + }; + } } if (aggregated === undefined) { throw new Error("Received empty response from chat model call."); @@ -382,7 +412,7 @@ export abstract class BaseChatModel< generations.push([aggregated]); await runManagers?.[0].handleLLMEnd({ generations, - llmOutput: {}, + llmOutput, }); } catch (e) { await runManagers?.[0].handleLLMError(e); diff --git a/langchain-core/src/language_models/tests/chat_models.test.ts b/langchain-core/src/language_models/tests/chat_models.test.ts index 70ff187243e8..f335d5edc40f 100644 --- a/langchain-core/src/language_models/tests/chat_models.test.ts +++ b/langchain-core/src/language_models/tests/chat_models.test.ts @@ -323,3 +323,15 @@ test("Test ChatModel can stream back a custom event", async () => { } expect(customEvent).toBeDefined(); }); + +test(`Test ChatModel should not serialize a passed "cache" parameter`, async () => { + const model = new FakeListChatModel({ + responses: ["hi"], + emitCustomEvent: true, + cache: true, + }); + console.log(JSON.stringify(model)); + expect(JSON.stringify(model)).toEqual( + `{"lc":1,"type":"constructor","id":["langchain","chat_models","fake-list","FakeListChatModel"],"kwargs":{"responses":["hi"],"emit_custom_event":true}}` + ); +}); diff --git a/langchain-core/src/messages/ai.ts b/langchain-core/src/messages/ai.ts index c8a5311f040c..b1b4c5261378 100644 --- a/langchain-core/src/messages/ai.ts +++ b/langchain-core/src/messages/ai.ts @@ -21,22 +21,83 @@ export type AIMessageFields = BaseMessageFields & { usage_metadata?: UsageMetadata; }; +/** + * Breakdown of input token counts. + * + * Does not *need* to sum to full input token count. Does *not* need to have all keys. + */ +export type InputTokenDetails = { + /** + * Audio input tokens. + */ + audio?: number; + + /** + * Input tokens that were cached and there was a cache hit. + * + * Since there was a cache hit, the tokens were read from the cache. + * More precisely, the model state given these tokens was read from the cache. + */ + cache_read?: number; + + /** + * Input tokens that were cached and there was a cache miss. + * + * Since there was a cache miss, the cache was created from these tokens. + */ + cache_creation?: number; +}; + +/** + * Breakdown of output token counts. + * + * Does *not* need to sum to full output token count. Does *not* need to have all keys. + */ +export type OutputTokenDetails = { + /** + * Audio output tokens + */ + audio?: number; + + /** + * Reasoning output tokens. + * + * Tokens generated by the model in a chain of thought process (i.e. by + * OpenAI's o1 models) that are not returned as part of model output. + */ + reasoning?: number; +}; + /** * Usage metadata for a message, such as token counts. */ export type UsageMetadata = { /** - * The count of input (or prompt) tokens. + * Count of input (or prompt) tokens. Sum of all input token types. */ input_tokens: number; /** - * The count of output (or completion) tokens + * Count of output (or completion) tokens. Sum of all output token types. */ output_tokens: number; /** - * The total token count + * Total token count. Sum of input_tokens + output_tokens. */ total_tokens: number; + + /** + * Breakdown of input token counts. + * + * Does *not* need to sum to full input token count. Does *not* need to have all keys. + */ + input_token_details?: InputTokenDetails; + + /** + * Breakdown of output token counts. + * + * Does *not* need to sum to full output token count. Does *not* need to have all keys. + */ + output_token_details?: OutputTokenDetails; }; /** @@ -185,6 +246,10 @@ export class AIMessageChunk extends BaseMessageChunk { tool_calls: fields.tool_calls ?? [], invalid_tool_calls: [], tool_call_chunks: [], + usage_metadata: + fields.usage_metadata !== undefined + ? fields.usage_metadata + : undefined, }; } else { const toolCalls: ToolCall[] = []; @@ -220,6 +285,10 @@ export class AIMessageChunk extends BaseMessageChunk { ...fields, tool_calls: toolCalls, invalid_tool_calls: invalidToolCalls, + usage_metadata: + fields.usage_metadata !== undefined + ? fields.usage_metadata + : undefined, }; } // Sadly, TypeScript only allows super() calls at root if the class has @@ -291,6 +360,48 @@ export class AIMessageChunk extends BaseMessageChunk { this.usage_metadata !== undefined || chunk.usage_metadata !== undefined ) { + const inputTokenDetails: InputTokenDetails = { + ...((this.usage_metadata?.input_token_details?.audio !== undefined || + chunk.usage_metadata?.input_token_details?.audio !== undefined) && { + audio: + (this.usage_metadata?.input_token_details?.audio ?? 0) + + (chunk.usage_metadata?.input_token_details?.audio ?? 0), + }), + ...((this.usage_metadata?.input_token_details?.cache_read !== + undefined || + chunk.usage_metadata?.input_token_details?.cache_read !== + undefined) && { + cache_read: + (this.usage_metadata?.input_token_details?.cache_read ?? 0) + + (chunk.usage_metadata?.input_token_details?.cache_read ?? 0), + }), + ...((this.usage_metadata?.input_token_details?.cache_creation !== + undefined || + chunk.usage_metadata?.input_token_details?.cache_creation !== + undefined) && { + cache_creation: + (this.usage_metadata?.input_token_details?.cache_creation ?? 0) + + (chunk.usage_metadata?.input_token_details?.cache_creation ?? 0), + }), + }; + + const outputTokenDetails: OutputTokenDetails = { + ...((this.usage_metadata?.output_token_details?.audio !== undefined || + chunk.usage_metadata?.output_token_details?.audio !== undefined) && { + audio: + (this.usage_metadata?.output_token_details?.audio ?? 0) + + (chunk.usage_metadata?.output_token_details?.audio ?? 0), + }), + ...((this.usage_metadata?.output_token_details?.reasoning !== + undefined || + chunk.usage_metadata?.output_token_details?.reasoning !== + undefined) && { + reasoning: + (this.usage_metadata?.output_token_details?.reasoning ?? 0) + + (chunk.usage_metadata?.output_token_details?.reasoning ?? 0), + }), + }; + const left: UsageMetadata = this.usage_metadata ?? { input_tokens: 0, output_tokens: 0, @@ -305,6 +416,14 @@ export class AIMessageChunk extends BaseMessageChunk { input_tokens: left.input_tokens + right.input_tokens, output_tokens: left.output_tokens + right.output_tokens, total_tokens: left.total_tokens + right.total_tokens, + // Do not include `input_token_details` / `output_token_details` keys in combined fields + // unless their values are defined. + ...(Object.keys(inputTokenDetails).length > 0 && { + input_token_details: inputTokenDetails, + }), + ...(Object.keys(outputTokenDetails).length > 0 && { + output_token_details: outputTokenDetails, + }), }; combinedFields.usage_metadata = usage_metadata; } diff --git a/langchain-core/src/messages/base.ts b/langchain-core/src/messages/base.ts index 0e327e334d57..790555f11ead 100644 --- a/langchain-core/src/messages/base.ts +++ b/langchain-core/src/messages/base.ts @@ -218,9 +218,24 @@ export abstract class BaseMessage */ id?: string; - /** The type of the message. */ + /** + * @deprecated Use .getType() instead or import the proper typeguard. + * For example: + * + * ```ts + * import { isAIMessage } from "@langchain/core/messages"; + * + * const message = new AIMessage("Hello!"); + * isAIMessage(message); // true + * ``` + */ abstract _getType(): MessageType; + /** The type of the message. */ + getType(): MessageType { + return this._getType(); + } + constructor( fields: string | BaseMessageFields, /** @deprecated */ @@ -480,7 +495,8 @@ export type BaseMessageLike = | ({ type: MessageType | "user" | "assistant" | "placeholder"; } & BaseMessageFields & - Record); + Record) + | SerializedConstructor; export function isBaseMessage( messageLike?: unknown diff --git a/langchain-core/src/messages/chat.ts b/langchain-core/src/messages/chat.ts index 7c3f80492389..376c05cceb84 100644 --- a/langchain-core/src/messages/chat.ts +++ b/langchain-core/src/messages/chat.ts @@ -108,3 +108,11 @@ export class ChatMessageChunk extends BaseMessageChunk { }; } } + +export function isChatMessage(x: BaseMessage): x is ChatMessage { + return x._getType() === "generic"; +} + +export function isChatMessageChunk(x: BaseMessageChunk): x is ChatMessageChunk { + return x._getType() === "generic"; +} diff --git a/langchain-core/src/messages/function.ts b/langchain-core/src/messages/function.ts index 12cd9c5d3693..7e3455b8ca51 100644 --- a/langchain-core/src/messages/function.ts +++ b/langchain-core/src/messages/function.ts @@ -73,3 +73,13 @@ export class FunctionMessageChunk extends BaseMessageChunk { }); } } + +export function isFunctionMessage(x: BaseMessage): x is FunctionMessage { + return x._getType() === "function"; +} + +export function isFunctionMessageChunk( + x: BaseMessageChunk +): x is FunctionMessageChunk { + return x._getType() === "function"; +} diff --git a/langchain-core/src/messages/human.ts b/langchain-core/src/messages/human.ts index a64ad32b7e2a..30b5354ee509 100644 --- a/langchain-core/src/messages/human.ts +++ b/langchain-core/src/messages/human.ts @@ -47,3 +47,13 @@ export class HumanMessageChunk extends BaseMessageChunk { }); } } + +export function isHumanMessage(x: BaseMessage): x is HumanMessage { + return x.getType() === "human"; +} + +export function isHumanMessageChunk( + x: BaseMessageChunk +): x is HumanMessageChunk { + return x.getType() === "human"; +} diff --git a/langchain-core/src/messages/index.ts b/langchain-core/src/messages/index.ts index aae54561f29c..d60fb4268ba3 100644 --- a/langchain-core/src/messages/index.ts +++ b/langchain-core/src/messages/index.ts @@ -14,4 +14,6 @@ export { ToolMessage, ToolMessageChunk, type InvalidToolCall, + isToolMessage, + isToolMessageChunk, } from "./tool.js"; diff --git a/langchain-core/src/messages/system.ts b/langchain-core/src/messages/system.ts index 5d88fdea5c8d..ae91a240e83f 100644 --- a/langchain-core/src/messages/system.ts +++ b/langchain-core/src/messages/system.ts @@ -47,3 +47,13 @@ export class SystemMessageChunk extends BaseMessageChunk { }); } } + +export function isSystemMessage(x: BaseMessage): x is SystemMessage { + return x._getType() === "system"; +} + +export function isSystemMessageChunk( + x: BaseMessageChunk +): x is SystemMessageChunk { + return x._getType() === "system"; +} diff --git a/langchain-core/src/messages/tests/base_message.test.ts b/langchain-core/src/messages/tests/base_message.test.ts index 0e6883c89dc0..b1c040f828f5 100644 --- a/langchain-core/src/messages/tests/base_message.test.ts +++ b/langchain-core/src/messages/tests/base_message.test.ts @@ -10,6 +10,7 @@ import { SystemMessage, } from "../index.js"; import { load } from "../../load/index.js"; +import { concat } from "../../utils/stream.js"; test("Test ChatPromptTemplate can format OpenAI content image messages", async () => { const message = new HumanMessage({ @@ -396,3 +397,60 @@ describe("Message like coercion", () => { ]); }); }); + +describe("usage_metadata serialized", () => { + test("usage_metadata is serialized when included in constructor", async () => { + const aiMsg = new AIMessage({ + content: "hello", + usage_metadata: { + input_tokens: 1, + output_tokens: 1, + total_tokens: 2, + }, + }); + const jsonAIMessage = JSON.stringify(aiMsg); + expect(jsonAIMessage).toContain("usage_metadata"); + expect(jsonAIMessage).toContain("input_tokens"); + expect(jsonAIMessage).toContain("output_tokens"); + expect(jsonAIMessage).toContain("total_tokens"); + }); + + test("usage_metadata is serialized when included in constructor", async () => { + const aiMsg = new AIMessageChunk({ + content: "hello", + usage_metadata: { + input_tokens: 1, + output_tokens: 1, + total_tokens: 2, + }, + }); + const jsonAIMessage = JSON.stringify(aiMsg); + expect(jsonAIMessage).toContain("usage_metadata"); + expect(jsonAIMessage).toContain("input_tokens"); + expect(jsonAIMessage).toContain("output_tokens"); + expect(jsonAIMessage).toContain("total_tokens"); + }); + + test("usage_metadata is serialized even when not included in constructor", async () => { + const aiMsg = new AIMessageChunk("hello"); + + const concatenatedAIMessageChunk = concat( + aiMsg, + new AIMessageChunk({ + content: "", + usage_metadata: { + input_tokens: 1, + output_tokens: 1, + total_tokens: 2, + }, + }) + ); + const jsonConcatenatedAIMessageChunk = JSON.stringify( + concatenatedAIMessageChunk + ); + expect(jsonConcatenatedAIMessageChunk).toContain("usage_metadata"); + expect(jsonConcatenatedAIMessageChunk).toContain("input_tokens"); + expect(jsonConcatenatedAIMessageChunk).toContain("output_tokens"); + expect(jsonConcatenatedAIMessageChunk).toContain("total_tokens"); + }); +}); diff --git a/langchain-core/src/messages/tool.ts b/langchain-core/src/messages/tool.ts index 443c3ecdc12a..d8683f6bfbc8 100644 --- a/langchain-core/src/messages/tool.ts +++ b/langchain-core/src/messages/tool.ts @@ -281,3 +281,11 @@ export function defaultToolCallParser( } return [toolCalls, invalidToolCalls]; } + +export function isToolMessage(x: BaseMessage): x is ToolMessage { + return x._getType() === "tool"; +} + +export function isToolMessageChunk(x: BaseMessageChunk): x is ToolMessageChunk { + return x._getType() === "tool"; +} diff --git a/langchain-core/src/messages/utils.ts b/langchain-core/src/messages/utils.ts index b9df034ee923..ae32060afb58 100644 --- a/langchain-core/src/messages/utils.ts +++ b/langchain-core/src/messages/utils.ts @@ -1,3 +1,5 @@ +import { addLangChainErrorFields } from "../errors/index.js"; +import { SerializedConstructor } from "../load/serializable.js"; import { _isToolCall } from "../tools/utils.js"; import { AIMessage, AIMessageChunk, AIMessageChunkFields } from "./ai.js"; import { @@ -55,10 +57,45 @@ function _coerceToolCall( } } +function isSerializedConstructor(x: unknown): x is SerializedConstructor { + return ( + typeof x === "object" && + x != null && + (x as SerializedConstructor).lc === 1 && + Array.isArray((x as SerializedConstructor).id) && + (x as SerializedConstructor).kwargs != null && + typeof (x as SerializedConstructor).kwargs === "object" + ); +} + function _constructMessageFromParams( - params: BaseMessageFields & { type: string } & Record + params: + | (BaseMessageFields & { type: string } & Record) + | SerializedConstructor ) { - const { type, ...rest } = params; + let type: string; + let rest: BaseMessageFields & Record; + // Support serialized messages + if (isSerializedConstructor(params)) { + const className = params.id.at(-1); + if (className === "HumanMessage" || className === "HumanMessageChunk") { + type = "user"; + } else if (className === "AIMessage" || className === "AIMessageChunk") { + type = "assistant"; + } else if ( + className === "SystemMessage" || + className === "SystemMessageChunk" + ) { + type = "system"; + } else { + type = "unknown"; + } + rest = params.kwargs as BaseMessageFields; + } else { + const { type: extractedType, ...otherParams } = params; + type = extractedType; + rest = otherParams; + } if (type === "human" || type === "user") { return new HumanMessage(rest); } else if (type === "ai" || type === "assistant") { @@ -78,9 +115,17 @@ function _constructMessageFromParams( name: rest.name, }); } else { - throw new Error( - `Unable to coerce message from array: only human, AI, or system message coercion is currently supported.` + const error = addLangChainErrorFields( + new Error( + `Unable to coerce message from array: only human, AI, system, or tool message coercion is currently supported.\n\nReceived: ${JSON.stringify( + params, + null, + 2 + )}` + ), + "MESSAGE_COERCION_FAILURE" ); + throw error; } } diff --git a/langchain-core/src/output_parsers/base.ts b/langchain-core/src/output_parsers/base.ts index 7d5e5eaaed57..3d0eaf0ef7cd 100644 --- a/langchain-core/src/output_parsers/base.ts +++ b/langchain-core/src/output_parsers/base.ts @@ -4,6 +4,7 @@ import type { BasePromptValueInterface } from "../prompt_values.js"; import type { BaseMessage, MessageContentComplex } from "../messages/index.js"; import type { Callbacks } from "../callbacks/manager.js"; import type { Generation, ChatGeneration } from "../outputs.js"; +import { addLangChainErrorFields } from "../errors/index.js"; /** * Options for formatting instructions. @@ -193,5 +194,7 @@ export class OutputParserException extends Error { ); } } + + addLangChainErrorFields(this, "OUTPUT_PARSING_FAILURE"); } } diff --git a/langchain-core/src/prompts/chat.ts b/langchain-core/src/prompts/chat.ts index 0d32610fbef0..406d9f6b6f17 100644 --- a/langchain-core/src/prompts/chat.ts +++ b/langchain-core/src/prompts/chat.ts @@ -39,6 +39,7 @@ import { parseFString, parseMustache, } from "./template.js"; +import { addLangChainErrorFields } from "../errors/index.js"; /** * Abstract class that serves as a base for creating message prompt @@ -168,6 +169,8 @@ export class MessagesPlaceholder< ].join("\n\n") ); error.name = "InputFormatError"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any).lc_error_code = e.lc_error_code; throw error; } @@ -996,9 +999,13 @@ export class ChatPromptTemplate< !(inputVariable in allValues) && !(isMessagesPlaceholder(promptMessage) && promptMessage.optional) ) { - throw new Error( - `Missing value for input variable \`${inputVariable.toString()}\`` + const error = addLangChainErrorFields( + new Error( + `Missing value for input variable \`${inputVariable.toString()}\`` + ), + "INVALID_PROMPT_INPUT" ); + throw error; } acc[inputVariable] = allValues[inputVariable]; return acc; diff --git a/langchain-core/src/prompts/prompt.ts b/langchain-core/src/prompts/prompt.ts index 998715895141..2ef8e9263ae9 100644 --- a/langchain-core/src/prompts/prompt.ts +++ b/langchain-core/src/prompts/prompt.ts @@ -84,7 +84,8 @@ type ExtractTemplateParamsRecursive< export type ParamsFromFString = { [Key in | ExtractTemplateParamsRecursive[number] - | (string & Record)]: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | (string & Record)]: any; }; export type ExtractedFStringParams< diff --git a/langchain-core/src/prompts/template.ts b/langchain-core/src/prompts/template.ts index 2188ba1f870f..b8de45f95fde 100644 --- a/langchain-core/src/prompts/template.ts +++ b/langchain-core/src/prompts/template.ts @@ -1,6 +1,7 @@ import mustache from "mustache"; import { MessageContent } from "../messages/index.js"; import type { InputValues } from "../utils/types/index.js"; +import { addLangChainErrorFields } from "../errors/index.js"; function configureMustache() { // Use unescaped HTML @@ -106,17 +107,22 @@ export const parseMustache = (template: string) => { return mustacheTemplateToNodes(parsed); }; -export const interpolateFString = (template: string, values: InputValues) => - parseFString(template).reduce((res, node) => { +export const interpolateFString = (template: string, values: InputValues) => { + return parseFString(template).reduce((res, node) => { if (node.type === "variable") { if (node.name in values) { - return res + values[node.name]; + const stringValue = + typeof values[node.name] === "string" + ? values[node.name] + : JSON.stringify(values[node.name]); + return res + stringValue; } throw new Error(`(f-string) Missing value for input ${node.name}`); } return res + node.text; }, ""); +}; export const interpolateMustache = (template: string, values: InputValues) => { configureMustache(); @@ -150,7 +156,14 @@ export const renderTemplate = ( template: string, templateFormat: TemplateFormat, inputValues: InputValues -) => DEFAULT_FORMATTER_MAPPING[templateFormat](template, inputValues); +) => { + try { + return DEFAULT_FORMATTER_MAPPING[templateFormat](template, inputValues); + } catch (e) { + const error = addLangChainErrorFields(e, "INVALID_PROMPT_INPUT"); + throw error; + } +}; export const parseTemplate = ( template: string, diff --git a/langchain-core/src/prompts/tests/chat.test.ts b/langchain-core/src/prompts/tests/chat.test.ts index b3fc38958ddc..62049ca55707 100644 --- a/langchain-core/src/prompts/tests/chat.test.ts +++ b/langchain-core/src/prompts/tests/chat.test.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { expect, test } from "@jest/globals"; import { AIMessagePromptTemplate, @@ -15,6 +17,7 @@ import { ChatMessage, FunctionMessage, } from "../../messages/index.js"; +import { Document } from "../../documents/document.js"; function createChatPromptTemplate() { const systemPrompt = new PromptTemplate({ @@ -72,13 +75,18 @@ test("Test format", async () => { test("Test format with invalid input values", async () => { const chatPrompt = createChatPromptTemplate(); - await expect( + let error: any | undefined; + try { // @ts-expect-error TS compiler should flag missing input variables - chatPrompt.formatPromptValue({ + await chatPrompt.formatPromptValue({ context: "This is a context", foo: "Foo", - }) - ).rejects.toThrow("Missing value for input variable `bar`"); + }); + } catch (e) { + error = e; + } + expect(error?.message).toContain("Missing value for input variable `bar`"); + expect(error?.lc_error_code).toEqual("INVALID_PROMPT_INPUT"); }); test("Test format with invalid input variables", async () => { @@ -129,6 +137,23 @@ test("Test fromTemplate", async () => { ]); }); +test("Test fromTemplate", async () => { + const chatPrompt = ChatPromptTemplate.fromTemplate("Hello {foo}, I'm {bar}"); + expect(chatPrompt.inputVariables).toEqual(["foo", "bar"]); + expect( + ( + await chatPrompt.invoke({ + foo: ["barbar"], + bar: [new Document({ pageContent: "bar" })], + }) + ).toChatMessages() + ).toEqual([ + new HumanMessage( + `Hello ["barbar"], I'm [{"pageContent":"bar","metadata":{}}]` + ), + ]); +}); + test("Test fromMessages", async () => { const systemPrompt = new PromptTemplate({ template: "Here's some context: {context}", @@ -155,6 +180,34 @@ test("Test fromMessages", async () => { ]); }); +test("Test fromMessages with non-string inputs", async () => { + const systemPrompt = new PromptTemplate({ + template: "Here's some context: {context}", + inputVariables: ["context"], + }); + const userPrompt = new PromptTemplate({ + template: "Hello {foo}, I'm {bar}", + inputVariables: ["foo", "bar"], + }); + // TODO: Fix autocomplete for the fromMessages method + const chatPrompt = ChatPromptTemplate.fromMessages([ + new SystemMessagePromptTemplate(systemPrompt), + new HumanMessagePromptTemplate(userPrompt), + ]); + expect(chatPrompt.inputVariables).toEqual(["context", "foo", "bar"]); + const messages = await chatPrompt.formatPromptValue({ + context: [new Document({ pageContent: "bar" })], + foo: "Foo", + bar: "Bar", + }); + expect(messages.toChatMessages()).toEqual([ + new SystemMessage( + `Here's some context: [{"pageContent":"bar","metadata":{}}]` + ), + new HumanMessage("Hello Foo, I'm Bar"), + ]); +}); + test("Test fromMessages with a variety of ways to declare prompt messages", async () => { const systemPrompt = new PromptTemplate({ template: "Here's some context: {context}", @@ -306,6 +359,24 @@ test("Test MessagesPlaceholder not optional", async () => { ); }); +test("Test MessagesPlaceholder not optional with invalid input should throw", async () => { + const prompt = new MessagesPlaceholder({ + variableName: "foo", + }); + const badInput = [new Document({ pageContent: "barbar", metadata: {} })]; + await expect( + prompt.formatMessages({ + foo: [new Document({ pageContent: "barbar", metadata: {} })], + }) + ).rejects.toThrow( + `Field "foo" in prompt uses a MessagesPlaceholder, which expects an array of BaseMessages or coerceable values as input.\n\nReceived value: ${JSON.stringify( + badInput, + null, + 2 + )}\n\nAdditional message: Unable to coerce message from array: only human, AI, system, or tool message coercion is currently supported.` + ); +}); + test("Test MessagesPlaceholder shorthand in a chat prompt template should throw for invalid syntax", async () => { expect(() => ChatPromptTemplate.fromMessages([["placeholder", "foo"]]) diff --git a/langchain-core/src/prompts/tests/prompt.test.ts b/langchain-core/src/prompts/tests/prompt.test.ts index 3d7e30e16436..e5902c447401 100644 --- a/langchain-core/src/prompts/tests/prompt.test.ts +++ b/langchain-core/src/prompts/tests/prompt.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "@jest/globals"; import { PromptTemplate } from "../prompt.js"; +import { Document } from "../../documents/document.js"; test("Test using partial", async () => { const prompt = new PromptTemplate({ @@ -26,6 +27,18 @@ test("Test fromTemplate", async () => { ).toBe("foobaz"); }); +test("Test fromTemplate with a non-string value", async () => { + const prompt = PromptTemplate.fromTemplate("{foo}{bar}"); + expect( + ( + await prompt.invoke({ + foo: ["barbar"], + bar: [new Document({ pageContent: "bar" })], + }) + ).value + ).toBe(`["barbar"][{"pageContent":"bar","metadata":{}}]`); +}); + test("Test fromTemplate with escaped strings", async () => { const prompt = PromptTemplate.fromTemplate("{{foo}}{{bar}}"); expect(await prompt.format({ unused: "eee" })).toBe("{foo}{bar}"); diff --git a/langchain-core/src/runnables/base.ts b/langchain-core/src/runnables/base.ts index 9caa530d699c..0890dc1463cb 100644 --- a/langchain-core/src/runnables/base.ts +++ b/langchain-core/src/runnables/base.ts @@ -58,14 +58,18 @@ import { ToolCall } from "../messages/tool.js"; export { type RunnableInterface, RunnableBatchOptions }; -export type RunnableFunc = ( +export type RunnableFunc< + RunInput, + RunOutput, + CallOptions extends RunnableConfig = RunnableConfig +> = ( input: RunInput, options: - | RunnableConfig + | CallOptions // eslint-disable-next-line @typescript-eslint/no-explicit-any | Record // eslint-disable-next-line @typescript-eslint/no-explicit-any - | (Record & RunnableConfig) + | (Record & CallOptions) ) => RunOutput | Promise; export type RunnableMapLike = { @@ -73,9 +77,15 @@ export type RunnableMapLike = { }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type RunnableLike = - | RunnableInterface - | RunnableFunc +export type RunnableLike< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunInput = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + RunOutput = any, + CallOptions extends RunnableConfig = RunnableConfig +> = + | RunnableInterface + | RunnableFunc | RunnableMapLike; export type RunnableRetryFailedAttemptHandler = ( @@ -174,7 +184,7 @@ export abstract class Runnable< */ withConfig( config: RunnableConfig - ): RunnableBinding { + ): Runnable { // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableBinding({ bound: this, @@ -471,7 +481,7 @@ export abstract class Runnable< runManager?: CallbackManagerForChainRun, options?: Partial ) => AsyncGenerator, - options?: CallOptions & { runType?: string } + options?: Partial & { runType?: string } ): AsyncGenerator { let finalInput: I | undefined; let finalInputSupported = true; @@ -695,7 +705,7 @@ export abstract class Runnable< config.callbacks = callbacks.concat([logStreamCallbackHandler]); } else { const copiedCallbacks = callbacks.copy(); - copiedCallbacks.inheritableHandlers.push(logStreamCallbackHandler); + copiedCallbacks.addHandler(logStreamCallbackHandler, true); // eslint-disable-next-line no-param-reassign config.callbacks = copiedCallbacks; } @@ -896,7 +906,7 @@ export abstract class Runnable< config.callbacks = callbacks.concat(eventStreamer); } else { const copiedCallbacks = callbacks.copy(); - copiedCallbacks.inheritableHandlers.push(eventStreamer); + copiedCallbacks.addHandler(eventStreamer, true); // eslint-disable-next-line no-param-reassign config.callbacks = copiedCallbacks; } @@ -1236,7 +1246,7 @@ export class RunnableBinding< withConfig( config: RunnableConfig - ): RunnableBinding { + ): Runnable { // eslint-disable-next-line @typescript-eslint/no-explicit-any return new (this.constructor as any)({ bound: this.bound, @@ -2234,15 +2244,28 @@ export class RunnableTraceable extends Runnable< } } -function assertNonTraceableFunction( +function assertNonTraceableFunction< + RunInput, + RunOutput, + CallOptions extends RunnableConfig = RunnableConfig +>( func: - | RunnableFunc> + | RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > | TraceableFunction< - RunnableFunc> + RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > > ): asserts func is RunnableFunc< RunInput, - RunOutput | Runnable + RunOutput | Runnable, + CallOptions > { if (isTraceableFunction(func)) { throw new Error( @@ -2254,10 +2277,11 @@ function assertNonTraceableFunction( /** * A runnable that runs a callable. */ -export class RunnableLambda extends Runnable< +export class RunnableLambda< RunInput, - RunOutput -> { + RunOutput, + CallOptions extends RunnableConfig = RunnableConfig +> extends Runnable { static lc_name() { return "RunnableLambda"; } @@ -2266,21 +2290,31 @@ export class RunnableLambda extends Runnable< protected func: RunnableFunc< RunInput, - RunOutput | Runnable + RunOutput | Runnable, + CallOptions >; constructor(fields: { func: - | RunnableFunc> + | RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > | TraceableFunction< - RunnableFunc> + RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > >; }) { if (isTraceableFunction(fields.func)) { // eslint-disable-next-line no-constructor-return return RunnableTraceable.from(fields.func) as unknown as RunnableLambda< RunInput, - RunOutput + RunOutput, + CallOptions >; } @@ -2290,23 +2324,51 @@ export class RunnableLambda extends Runnable< this.func = fields.func; } - static from( - func: RunnableFunc> - ): RunnableLambda; + static from< + RunInput, + RunOutput, + CallOptions extends RunnableConfig = RunnableConfig + >( + func: RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > + ): RunnableLambda; - static from( + static from< + RunInput, + RunOutput, + CallOptions extends RunnableConfig = RunnableConfig + >( func: TraceableFunction< - RunnableFunc> + RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > > - ): RunnableLambda; + ): RunnableLambda; - static from( + static from< + RunInput, + RunOutput, + CallOptions extends RunnableConfig = RunnableConfig + >( func: - | RunnableFunc> + | RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > | TraceableFunction< - RunnableFunc> + RunnableFunc< + RunInput, + RunOutput | Runnable, + CallOptions + > > - ): RunnableLambda { + ): RunnableLambda { return new RunnableLambda({ func, }); @@ -2314,7 +2376,7 @@ export class RunnableLambda extends Runnable< async _invoke( input: RunInput, - config?: Partial, + config?: Partial, runManager?: CallbackManagerForChainRun ) { return new Promise((resolve, reject) => { @@ -2328,7 +2390,6 @@ export class RunnableLambda extends Runnable< try { let output = await this.func(input, { ...childConfig, - config: childConfig, }); if (output && Runnable.isRunnable(output)) { if (config?.recursionLimit === 0) { @@ -2391,7 +2452,7 @@ export class RunnableLambda extends Runnable< async invoke( input: RunInput, - options?: Partial + options?: Partial ): Promise { return this._callWithConfig(this._invoke.bind(this), input, options); } @@ -2399,7 +2460,7 @@ export class RunnableLambda extends Runnable< async *_transform( generator: AsyncGenerator, runManager?: CallbackManagerForChainRun, - config?: Partial + config?: Partial ): AsyncGenerator { let finalChunk: RunInput | undefined; for await (const chunk of generator) { @@ -2465,7 +2526,7 @@ export class RunnableLambda extends Runnable< transform( generator: AsyncGenerator, - options?: Partial + options?: Partial ): AsyncGenerator { return this._transformStreamWithConfig( generator, @@ -2476,7 +2537,7 @@ export class RunnableLambda extends Runnable< async stream( input: RunInput, - options?: Partial + options?: Partial ): Promise> { async function* generator() { yield input; @@ -2721,16 +2782,25 @@ export class RunnableWithFallbacks extends Runnable< } // TODO: Figure out why the compiler needs help eliminating Error as a RunOutput type -export function _coerceToRunnable( - coerceable: RunnableLike -): Runnable> { +export function _coerceToRunnable< + RunInput, + RunOutput, + CallOptions extends RunnableConfig = RunnableConfig +>( + coerceable: RunnableLike +): Runnable, CallOptions> { if (typeof coerceable === "function") { return new RunnableLambda({ func: coerceable }) as Runnable< RunInput, - Exclude + Exclude, + CallOptions >; } else if (Runnable.isRunnable(coerceable)) { - return coerceable as Runnable>; + return coerceable as Runnable< + RunInput, + Exclude, + CallOptions + >; } else if (!Array.isArray(coerceable) && typeof coerceable === "object") { const runnables: Record> = {}; for (const [key, value] of Object.entries(coerceable)) { @@ -2738,7 +2808,7 @@ export function _coerceToRunnable( } return new RunnableMap({ steps: runnables, - }) as unknown as Runnable>; + }) as unknown as Runnable, CallOptions>; } else { throw new Error( `Expected a Runnable, function or object.\nInstead got an unsupported type.` diff --git a/langchain-core/src/runnables/graph.ts b/langchain-core/src/runnables/graph.ts index 1137b591da5c..9826af0afee9 100644 --- a/langchain-core/src/runnables/graph.ts +++ b/langchain-core/src/runnables/graph.ts @@ -9,31 +9,31 @@ import type { import { isRunnableInterface } from "./utils.js"; import { drawMermaid, drawMermaidPng } from "./graph_mermaid.js"; -const MAX_DATA_DISPLAY_NAME_LENGTH = 42; - export { Node, Edge }; -function nodeDataStr(node: Node): string { - if (!isUuid(node.id)) { - return node.id; - } else if (isRunnableInterface(node.data)) { +function nodeDataStr( + id: string | undefined, + data: RunnableInterface | RunnableIOSchema +): string { + if (id !== undefined && !isUuid(id)) { + return id; + } else if (isRunnableInterface(data)) { try { - let data = node.data.getName(); - data = data.startsWith("Runnable") ? data.slice("Runnable".length) : data; - if (data.length > MAX_DATA_DISPLAY_NAME_LENGTH) { - data = `${data.substring(0, MAX_DATA_DISPLAY_NAME_LENGTH)}...`; - } - return data; + let dataStr = data.getName(); + dataStr = dataStr.startsWith("Runnable") + ? dataStr.slice("Runnable".length) + : dataStr; + return dataStr; } catch (error) { - return node.data.getName(); + return data.getName(); } } else { - return node.data.name ?? "UnknownSchema"; + return data.name ?? "UnknownSchema"; } } function nodeDataJson(node: Node) { - // if node.data is implements Runnable + // if node.data implements Runnable if (isRunnableInterface(node.data)) { return { type: "runnable", @@ -55,6 +55,11 @@ export class Graph { edges: Edge[] = []; + constructor(params?: { nodes: Record; edges: Edge[] }) { + this.nodes = params?.nodes ?? this.nodes; + this.edges = params?.edges ?? this.edges; + } + // Convert the graph to a JSON-serializable format. // eslint-disable-next-line @typescript-eslint/no-explicit-any toJSON(): Record { @@ -86,12 +91,22 @@ export class Graph { }; } - addNode(data: RunnableInterface | RunnableIOSchema, id?: string): Node { + addNode( + data: RunnableInterface | RunnableIOSchema, + id?: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: Record + ): Node { if (id !== undefined && this.nodes[id] !== undefined) { throw new Error(`Node with id ${id} already exists`); } - const nodeId = id || uuidv4(); - const node: Node = { id: nodeId, data }; + const nodeId = id ?? uuidv4(); + const node: Node = { + id: nodeId, + data, + name: nodeDataStr(id, data), + metadata, + }; this.nodes[nodeId] = node; return node; } @@ -129,25 +144,11 @@ export class Graph { } firstNode(): Node | undefined { - const targets = new Set(this.edges.map((edge) => edge.target)); - const found: Node[] = []; - Object.values(this.nodes).forEach((node) => { - if (!targets.has(node.id)) { - found.push(node); - } - }); - return found[0]; + return _firstNode(this); } lastNode(): Node | undefined { - const sources = new Set(this.edges.map((edge) => edge.source)); - const found: Node[] = []; - Object.values(this.nodes).forEach((node) => { - if (!sources.has(node.id)) { - found.push(node); - } - }); - return found[0]; + return _lastNode(this); } /** @@ -188,28 +189,55 @@ export class Graph { trimFirstNode(): void { const firstNode = this.firstNode(); - if (firstNode) { - const outgoingEdges = this.edges.filter( - (edge) => edge.source === firstNode.id - ); - if (Object.keys(this.nodes).length === 1 || outgoingEdges.length === 1) { - this.removeNode(firstNode); - } + if (firstNode && _firstNode(this, [firstNode.id])) { + this.removeNode(firstNode); } } trimLastNode(): void { const lastNode = this.lastNode(); - if (lastNode) { - const incomingEdges = this.edges.filter( - (edge) => edge.target === lastNode.id - ); - if (Object.keys(this.nodes).length === 1 || incomingEdges.length === 1) { - this.removeNode(lastNode); - } + if (lastNode && _lastNode(this, [lastNode.id])) { + this.removeNode(lastNode); } } + /** + * Return a new graph with all nodes re-identified, + * using their unique, readable names where possible. + */ + reid(): Graph { + const nodeLabels: Record = Object.fromEntries( + Object.values(this.nodes).map((node) => [node.id, node.name]) + ); + const nodeLabelCounts = new Map(); + Object.values(nodeLabels).forEach((label) => { + nodeLabelCounts.set(label, (nodeLabelCounts.get(label) || 0) + 1); + }); + + const getNodeId = (nodeId: string): string => { + const label = nodeLabels[nodeId]; + if (isUuid(nodeId) && nodeLabelCounts.get(label) === 1) { + return label; + } else { + return nodeId; + } + }; + + return new Graph({ + nodes: Object.fromEntries( + Object.entries(this.nodes).map(([id, node]) => [ + getNodeId(id), + { ...node, id: getNodeId(id) }, + ]) + ), + edges: this.edges.map((edge) => ({ + ...edge, + source: getNodeId(edge.source), + target: getNodeId(edge.target), + })), + }); + } + drawMermaid(params?: { withStyles?: boolean; curveStyle?: string; @@ -219,23 +247,21 @@ export class Graph { const { withStyles, curveStyle, - nodeColors = { start: "#ffdfba", end: "#baffc9", other: "#fad7de" }, + nodeColors = { + default: "fill:#f2f0ff,line-height:1.2", + first: "fill-opacity:0", + last: "fill:#bfb6fc", + }, wrapLabelNWords, } = params ?? {}; - const nodes: Record = {}; - for (const node of Object.values(this.nodes)) { - nodes[node.id] = nodeDataStr(node); - } + const graph = this.reid(); + const firstNode = graph.firstNode(); - const firstNode = this.firstNode(); - const firstNodeLabel = firstNode ? nodeDataStr(firstNode) : undefined; - - const lastNode = this.lastNode(); - const lastNodeLabel = lastNode ? nodeDataStr(lastNode) : undefined; + const lastNode = graph.lastNode(); - return drawMermaid(nodes, this.edges, { - firstNodeLabel, - lastNodeLabel, + return drawMermaid(graph.nodes, graph.edges, { + firstNode: firstNode?.id, + lastNode: lastNode?.id, withStyles, curveStyle, nodeColors, @@ -256,3 +282,46 @@ export class Graph { }); } } +/** + * Find the single node that is not a target of any edge. + * Exclude nodes/sources with ids in the exclude list. + * If there is no such node, or there are multiple, return undefined. + * When drawing the graph, this node would be the origin. + */ +function _firstNode(graph: Graph, exclude: string[] = []): Node | undefined { + const targets = new Set( + graph.edges + .filter((edge) => !exclude.includes(edge.source)) + .map((edge) => edge.target) + ); + + const found: Node[] = []; + for (const node of Object.values(graph.nodes)) { + if (!exclude.includes(node.id) && !targets.has(node.id)) { + found.push(node); + } + } + return found.length === 1 ? found[0] : undefined; +} + +/** + * Find the single node that is not a source of any edge. + * Exclude nodes/targets with ids in the exclude list. + * If there is no such node, or there are multiple, return undefined. + * When drawing the graph, this node would be the destination. + */ +function _lastNode(graph: Graph, exclude: string[] = []): Node | undefined { + const sources = new Set( + graph.edges + .filter((edge) => !exclude.includes(edge.target)) + .map((edge) => edge.source) + ); + + const found: Node[] = []; + for (const node of Object.values(graph.nodes)) { + if (!exclude.includes(node.id) && !sources.has(node.id)) { + found.push(node); + } + } + return found.length === 1 ? found[0] : undefined; +} diff --git a/langchain-core/src/runnables/graph_mermaid.ts b/langchain-core/src/runnables/graph_mermaid.ts index 918fe07c9703..8bf296da29bc 100644 --- a/langchain-core/src/runnables/graph_mermaid.ts +++ b/langchain-core/src/runnables/graph_mermaid.ts @@ -1,23 +1,18 @@ -import { Edge } from "./types.js"; +import { Edge, Node } from "./types.js"; function _escapeNodeLabel(nodeLabel: string): string { // Escapes the node label for Mermaid syntax. return nodeLabel.replace(/[^a-zA-Z-_0-9]/g, "_"); } -// Adjusts Mermaid edge to map conditional nodes to pure nodes. -function _adjustMermaidEdge(edge: Edge, nodes: Record) { - const sourceNodeLabel = nodes[edge.source] ?? edge.source; - const targetNodeLabel = nodes[edge.target] ?? edge.target; - return [sourceNodeLabel, targetNodeLabel]; -} +const MARKDOWN_SPECIAL_CHARS = ["*", "_", "`"]; function _generateMermaidGraphStyles( nodeColors: Record ): string { let styles = ""; for (const [className, color] of Object.entries(nodeColors)) { - styles += `\tclassDef ${className}class fill:${color};\n`; + styles += `\tclassDef ${className} ${color};\n`; } return styles; } @@ -26,11 +21,11 @@ function _generateMermaidGraphStyles( * Draws a Mermaid graph using the provided graph data */ export function drawMermaid( - nodes: Record, + nodes: Record, edges: Edge[], config?: { - firstNodeLabel?: string; - lastNodeLabel?: string; + firstNode?: string; + lastNode?: string; curveStyle?: string; withStyles?: boolean; nodeColors?: Record; @@ -38,8 +33,8 @@ export function drawMermaid( } ): string { const { - firstNodeLabel, - lastNodeLabel, + firstNode, + lastNode, nodeColors, withStyles = true, curveStyle = "linear", @@ -53,92 +48,126 @@ export function drawMermaid( // Node formatting templates const defaultClassLabel = "default"; const formatDict: Record = { - [defaultClassLabel]: "{0}([{1}]):::otherclass", + [defaultClassLabel]: "{0}({1})", }; - if (firstNodeLabel !== undefined) { - formatDict[firstNodeLabel] = "{0}[{0}]:::startclass"; + if (firstNode !== undefined) { + formatDict[firstNode] = "{0}([{1}]):::first"; } - if (lastNodeLabel !== undefined) { - formatDict[lastNodeLabel] = "{0}[{0}]:::endclass"; + if (lastNode !== undefined) { + formatDict[lastNode] = "{0}([{1}]):::last"; } // Add nodes to the graph - for (const node of Object.values(nodes)) { - const nodeLabel = formatDict[node] ?? formatDict[defaultClassLabel]; - const escapedNodeLabel = _escapeNodeLabel(node); - const nodeParts = node.split(":"); - const nodeSplit = nodeParts[nodeParts.length - 1]; - mermaidGraph += `\t${nodeLabel - .replace(/\{0\}/g, escapedNodeLabel) - .replace(/\{1\}/g, nodeSplit)};\n`; + for (const [key, node] of Object.entries(nodes)) { + const nodeName = node.name.split(":").pop() ?? ""; + const label = MARKDOWN_SPECIAL_CHARS.some( + (char) => nodeName.startsWith(char) && nodeName.endsWith(char) + ) + ? `

${nodeName}

` + : nodeName; + + let finalLabel = label; + if (Object.keys(node.metadata ?? {}).length) { + finalLabel += `
${Object.entries(node.metadata ?? {}) + .map(([k, v]) => `${k} = ${v}`) + .join("\n")}`; + } + + const nodeLabel = (formatDict[key] ?? formatDict[defaultClassLabel]) + .replace("{0}", _escapeNodeLabel(key)) + .replace("{1}", finalLabel); + + mermaidGraph += `\t${nodeLabel}\n`; } } - let subgraph = ""; - // Add edges to the graph + + // Group edges by their common prefixes + const edgeGroups: Record = {}; for (const edge of edges) { - const sourcePrefix = edge.source.includes(":") - ? edge.source.split(":")[0] - : undefined; - const targetPrefix = edge.target.includes(":") - ? edge.target.split(":")[0] - : undefined; - // Exit subgraph if source or target is not in the same subgraph - if ( - subgraph !== "" && - (subgraph !== sourcePrefix || subgraph !== targetPrefix) - ) { - mermaidGraph += "\tend\n"; - subgraph = ""; + const srcParts = edge.source.split(":"); + const tgtParts = edge.target.split(":"); + const commonPrefix = srcParts + .filter((src, i) => src === tgtParts[i]) + .join(":"); + if (!edgeGroups[commonPrefix]) { + edgeGroups[commonPrefix] = []; } - // Enter subgraph if source and target are in the same subgraph - if ( - subgraph === "" && - sourcePrefix !== undefined && - sourcePrefix === targetPrefix - ) { - mermaidGraph = `\tsubgraph ${sourcePrefix}\n`; - subgraph = sourcePrefix; - } - const [source, target] = _adjustMermaidEdge(edge, nodes); - let edgeLabel = ""; - // Add BR every wrapLabelNWords words - if (edge.data !== undefined) { - let edgeData = edge.data; - const words = edgeData.split(" "); - // Group words into chunks of wrapLabelNWords size - if (words.length > wrapLabelNWords) { - edgeData = words - .reduce((acc: string[], word: string, i: number) => { - if (i % wrapLabelNWords === 0) acc.push(""); - acc[acc.length - 1] += ` ${word}`; - return acc; - }, []) - .join("
"); + edgeGroups[commonPrefix].push(edge); + } + + const seenSubgraphs = new Set(); + + function addSubgraph(edges: Edge[], prefix: string): void { + const selfLoop = edges.length === 1 && edges[0].source === edges[0].target; + if (prefix && !selfLoop) { + const subgraph = prefix.split(":").pop()!; + if (seenSubgraphs.has(subgraph)) { + throw new Error( + `Found duplicate subgraph '${subgraph}' -- this likely means that ` + + "you're reusing a subgraph node with the same name. " + + "Please adjust your graph to have subgraph nodes with unique names." + ); } - if (edge.conditional) { - edgeLabel = ` -. ${edgeData} .-> `; + + seenSubgraphs.add(subgraph); + mermaidGraph += `\tsubgraph ${subgraph}\n`; + } + + for (const edge of edges) { + const { source, target, data, conditional } = edge; + + let edgeLabel = ""; + if (data !== undefined) { + let edgeData = data; + const words = edgeData.split(" "); + if (words.length > wrapLabelNWords) { + edgeData = Array.from( + { length: Math.ceil(words.length / wrapLabelNWords) }, + (_, i) => + words + .slice(i * wrapLabelNWords, (i + 1) * wrapLabelNWords) + .join(" ") + ).join(" 
 "); + } + edgeLabel = conditional + ? ` -.  ${edgeData}  .-> ` + : ` --  ${edgeData}  --> `; } else { - edgeLabel = ` -- ${edgeData} --> `; + edgeLabel = conditional ? " -.-> " : " --> "; } - } else { - if (edge.conditional) { - edgeLabel = ` -.-> `; - } else { - edgeLabel = ` --> `; + + mermaidGraph += `\t${_escapeNodeLabel( + source + )}${edgeLabel}${_escapeNodeLabel(target)};\n`; + } + + // Recursively add nested subgraphs + for (const nestedPrefix in edgeGroups) { + if (nestedPrefix.startsWith(`${prefix}:`) && nestedPrefix !== prefix) { + addSubgraph(edgeGroups[nestedPrefix], nestedPrefix); } } - mermaidGraph += `\t${_escapeNodeLabel( - source - )}${edgeLabel}${_escapeNodeLabel(target)};\n`; + + if (prefix && !selfLoop) { + mermaidGraph += "\tend\n"; + } } - if (subgraph !== "") { - mermaidGraph += "end\n"; + + // Start with the top-level edges (no common prefix) + addSubgraph(edgeGroups[""] ?? [], ""); + + // Add remaining subgraphs + for (const prefix in edgeGroups) { + if (!prefix.includes(":") && prefix !== "") { + addSubgraph(edgeGroups[prefix], prefix); + } } // Add custom styles for nodes - if (withStyles && nodeColors !== undefined) { - mermaidGraph += _generateMermaidGraphStyles(nodeColors); + if (withStyles) { + mermaidGraph += _generateMermaidGraphStyles(nodeColors ?? {}); } + return mermaidGraph; } diff --git a/langchain-core/src/runnables/history.ts b/langchain-core/src/runnables/history.ts index 6be64077c89a..5d084b6ccf35 100644 --- a/langchain-core/src/runnables/history.ts +++ b/langchain-core/src/runnables/history.ts @@ -115,9 +115,9 @@ export class RunnableWithMessageHistory< getMessageHistory: GetSessionHistoryCallable; constructor(fields: RunnableWithMessageHistoryInputs) { - let historyChain: Runnable = new RunnableLambda({ - func: (input, options) => this._enterHistory(input, options ?? {}), - }).withConfig({ runName: "loadHistory" }); + let historyChain: Runnable = RunnableLambda.from((input, options) => + this._enterHistory(input, options ?? {}) + ).withConfig({ runName: "loadHistory" }); const messagesKey = fields.historyMessagesKey ?? fields.inputMessagesKey; if (messagesKey) { diff --git a/langchain-core/src/runnables/tests/data/mermaid.png b/langchain-core/src/runnables/tests/data/mermaid.png index c0db4875bff3..2926f7a13d16 100644 Binary files a/langchain-core/src/runnables/tests/data/mermaid.png and b/langchain-core/src/runnables/tests/data/mermaid.png differ diff --git a/langchain-core/src/runnables/tests/runnable.test.ts b/langchain-core/src/runnables/tests/runnable.test.ts index ca597ab35872..23438aa4b372 100644 --- a/langchain-core/src/runnables/tests/runnable.test.ts +++ b/langchain-core/src/runnables/tests/runnable.test.ts @@ -427,10 +427,14 @@ test("Create a runnable sequence with a static method with invalid output and ca } }; const runnable = RunnableSequence.from([promptTemplate, llm, parser]); - await expect(async () => { - const result = await runnable.invoke({ input: "Hello sequence!" }); - console.log(result); - }).rejects.toThrow(OutputParserException); + let error: any | undefined; + try { + await runnable.invoke({ input: "Hello sequence!" }); + } catch (e: any) { + error = e; + } + expect(error).toBeInstanceOf(OutputParserException); + expect(error?.lc_error_code).toEqual("OUTPUT_PARSING_FAILURE"); }); test("RunnableSequence can pass config to every step in batched request", async () => { diff --git a/langchain-core/src/runnables/tests/runnable_graph.test.ts b/langchain-core/src/runnables/tests/runnable_graph.test.ts index ead8eb472e30..877c9d1d5ed6 100644 --- a/langchain-core/src/runnables/tests/runnable_graph.test.ts +++ b/langchain-core/src/runnables/tests/runnable_graph.test.ts @@ -91,17 +91,17 @@ test("Test graph sequence", async () => { expect(graph.drawMermaid()) .toEqual(`%%{init: {'flowchart': {'curve': 'linear'}}}%% graph TD; -\tPromptTemplateInput[PromptTemplateInput]:::startclass; -\tPromptTemplate([PromptTemplate]):::otherclass; -\tFakeLLM([FakeLLM]):::otherclass; -\tCommaSeparatedListOutputParser([CommaSeparatedListOutputParser]):::otherclass; -\tCommaSeparatedListOutputParserOutput[CommaSeparatedListOutputParserOutput]:::endclass; +\tPromptTemplateInput([PromptTemplateInput]):::first +\tPromptTemplate(PromptTemplate) +\tFakeLLM(FakeLLM) +\tCommaSeparatedListOutputParser(CommaSeparatedListOutputParser) +\tCommaSeparatedListOutputParserOutput([CommaSeparatedListOutputParserOutput]):::last \tPromptTemplateInput --> PromptTemplate; \tPromptTemplate --> FakeLLM; \tCommaSeparatedListOutputParser --> CommaSeparatedListOutputParserOutput; \tFakeLLM --> CommaSeparatedListOutputParser; -\tclassDef startclass fill:#ffdfba; -\tclassDef endclass fill:#baffc9; -\tclassDef otherclass fill:#fad7de; +\tclassDef default fill:#f2f0ff,line-height:1.2; +\tclassDef first fill-opacity:0; +\tclassDef last fill:#bfb6fc; `); }); diff --git a/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts b/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts index 1702d226aa4b..2807b4935657 100644 --- a/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts +++ b/langchain-core/src/runnables/tests/runnable_stream_events_v2.test.ts @@ -4,6 +4,7 @@ import { test, expect, afterEach } from "@jest/globals"; import { z } from "zod"; +import { AsyncLocalStorage } from "node:async_hooks"; import { RunnableLambda, RunnableMap, @@ -28,8 +29,9 @@ import { DynamicStructuredTool, DynamicTool, tool } from "../../tools/index.js"; import { Document } from "../../documents/document.js"; import { PromptTemplate } from "../../prompts/prompt.js"; import { GenerationChunk } from "../../outputs.js"; -// Import from web to avoid side-effects from AsyncLocalStorage +// Import from web to avoid top-level side-effects from AsyncLocalStorage import { dispatchCustomEvent } from "../../callbacks/dispatch/web.js"; +import { AsyncLocalStorageProviderSingleton } from "../../singletons/index.js"; function reverse(s: string) { // Reverse a string. @@ -138,6 +140,73 @@ test("Runnable streamEvents method on a chat model", async () => { ]); }); +test("Runnable streamEvents call nested in another runnable + passed callbacks should still work", async () => { + AsyncLocalStorageProviderSingleton.initializeGlobalInstance( + new AsyncLocalStorage() + ); + + const model = new FakeListChatModel({ + responses: ["abc"], + }); + + const events: any[] = []; + const container = RunnableLambda.from(async (_) => { + const eventStream = model.streamEvents("hello", { version: "v2" }); + for await (const event of eventStream) { + events.push(event); + } + return events; + }); + + await container.invoke({}, { callbacks: [{ handleLLMStart: () => {} }] }); + + // used here to avoid casting every ID + const anyString = expect.any(String) as unknown as string; + + expect(events).toMatchObject([ + { + data: { input: "hello" }, + event: "on_chat_model_start", + name: "FakeListChatModel", + metadata: expect.any(Object), + run_id: expect.any(String), + tags: [], + }, + { + data: { chunk: new AIMessageChunk({ id: anyString, content: "a" }) }, + event: "on_chat_model_stream", + name: "FakeListChatModel", + metadata: expect.any(Object), + run_id: expect.any(String), + tags: [], + }, + { + data: { chunk: new AIMessageChunk({ id: anyString, content: "b" }) }, + event: "on_chat_model_stream", + name: "FakeListChatModel", + metadata: expect.any(Object), + run_id: expect.any(String), + tags: [], + }, + { + data: { chunk: new AIMessageChunk({ id: anyString, content: "c" }) }, + event: "on_chat_model_stream", + name: "FakeListChatModel", + metadata: expect.any(Object), + run_id: expect.any(String), + tags: [], + }, + { + data: { output: new AIMessageChunk({ id: anyString, content: "abc" }) }, + event: "on_chat_model_end", + name: "FakeListChatModel", + metadata: expect.any(Object), + run_id: expect.any(String), + tags: [], + }, + ]); +}); + test("Runnable streamEvents method with three runnables", async () => { const r = RunnableLambda.from(reverse); diff --git a/langchain-core/src/runnables/types.ts b/langchain-core/src/runnables/types.ts index e7ddfa8c3852..f40d80ee3831 100644 --- a/langchain-core/src/runnables/types.ts +++ b/langchain-core/src/runnables/types.ts @@ -71,16 +71,22 @@ export interface Edge { export interface Node { id: string; + name: string; data: RunnableIOSchema | RunnableInterface; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: Record; } -export interface RunnableConfig extends BaseCallbackConfig { +export interface RunnableConfig< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ConfigurableFieldType extends Record = Record +> extends BaseCallbackConfig { /** * Runtime values for attributes previously made configurable on this Runnable, * or sub-Runnables. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - configurable?: Record; + configurable?: ConfigurableFieldType; /** * Maximum number of times a call can recurse. If not provided, defaults to 25. diff --git a/langchain-core/src/singletons/index.ts b/langchain-core/src/singletons/index.ts index 3b4740591e50..e2c0119db4c3 100644 --- a/langchain-core/src/singletons/index.ts +++ b/langchain-core/src/singletons/index.ts @@ -7,6 +7,8 @@ export interface AsyncLocalStorageInterface { getStore: () => any | undefined; run: (store: any, callback: () => T) => T; + + enterWith: (store: any) => void; } export class MockAsyncLocalStorage implements AsyncLocalStorageInterface { @@ -17,6 +19,10 @@ export class MockAsyncLocalStorage implements AsyncLocalStorageInterface { run(_store: any, callback: () => T): T { return callback(); } + + enterWith(_store: any) { + return undefined; + } } const mockAsyncLocalStorage = new MockAsyncLocalStorage(); @@ -24,6 +30,8 @@ const mockAsyncLocalStorage = new MockAsyncLocalStorage(); const TRACING_ALS_KEY = Symbol.for("ls:tracing_async_local_storage"); const LC_CHILD_KEY = Symbol.for("lc:child_config"); +export const _CONTEXT_VARIABLES_KEY = Symbol.for("lc:context_variables"); + class AsyncLocalStorageProvider { getInstance(): AsyncLocalStorageInterface { return (globalThis as any)[TRACING_ALS_KEY] ?? mockAsyncLocalStorage; @@ -50,6 +58,7 @@ class AsyncLocalStorageProvider { config?.metadata ); const storage = this.getInstance(); + const previousValue = storage.getStore(); const parentRunId = callbackManager?.getParentRunId(); const langChainTracer = callbackManager?.handlers?.find( @@ -70,6 +79,14 @@ class AsyncLocalStorageProvider { runTree.extra = { ...runTree.extra, [LC_CHILD_KEY]: config }; } + if ( + previousValue !== undefined && + previousValue[_CONTEXT_VARIABLES_KEY] !== undefined + ) { + (runTree as any)[_CONTEXT_VARIABLES_KEY] = + previousValue[_CONTEXT_VARIABLES_KEY]; + } + return storage.run(runTree, callback); } diff --git a/langchain-core/src/tests/context.test.ts b/langchain-core/src/tests/context.test.ts new file mode 100644 index 000000000000..052c0d552583 --- /dev/null +++ b/langchain-core/src/tests/context.test.ts @@ -0,0 +1,26 @@ +import { test, expect } from "@jest/globals"; +import { RunnableLambda } from "../runnables/base.js"; +import { getContextVariable, setContextVariable } from "../context.js"; + +test("Getting and setting context variables within nested runnables", async () => { + const nested = RunnableLambda.from(() => { + expect(getContextVariable("foo")).toEqual("bar"); + expect(getContextVariable("toplevel")).toEqual(9); + setContextVariable("foo", "baz"); + return getContextVariable("foo"); + }); + const runnable = RunnableLambda.from(async () => { + setContextVariable("foo", "bar"); + expect(getContextVariable("foo")).toEqual("bar"); + expect(getContextVariable("toplevel")).toEqual(9); + const res = await nested.invoke({}); + expect(getContextVariable("foo")).toEqual("bar"); + return res; + }); + expect(getContextVariable("foo")).toEqual(undefined); + setContextVariable("toplevel", 9); + expect(getContextVariable("toplevel")).toEqual(9); + const result = await runnable.invoke({}); + expect(getContextVariable("toplevel")).toEqual(9); + expect(result).toEqual("baz"); +}); diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index 42ceff0813ff..844295e605f8 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -586,9 +586,25 @@ export function tool< fields.description ?? fields.schema?.description ?? `${fields.name} tool`, - // TS doesn't restrict the type here based on the guard above - // eslint-disable-next-line @typescript-eslint/no-explicit-any - func: func as any, + func: async (input, runManager, config) => { + return new Promise((resolve, reject) => { + const childConfig = patchConfig(config, { + callbacks: runManager?.getChild(), + }); + void AsyncLocalStorageProviderSingleton.runWithConfig( + childConfig, + async () => { + try { + // TS doesn't restrict the type here based on the guard above + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(func(input as any, childConfig)); + } catch (e) { + reject(e); + } + } + ); + }); + }, }); } @@ -606,7 +622,7 @@ export function tool< const childConfig = patchConfig(config, { callbacks: runManager?.getChild(), }); - void AsyncLocalStorageProviderSingleton.getInstance().run( + void AsyncLocalStorageProviderSingleton.runWithConfig( childConfig, async () => { try { diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index bdcc068a4d1a..d5587756ee2e 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -1,5 +1,6 @@ import { test, expect } from "@jest/globals"; import { z } from "zod"; + import { DynamicStructuredTool, tool } from "../index.js"; import { ToolMessage } from "../../messages/tool.js"; @@ -102,7 +103,8 @@ test("Returns tool message if responseFormat is content_and_artifact and returns test("Tool can accept single string input", async () => { const stringTool = tool( - (input: string): string => { + (input: string, config): string => { + expect(config).toMatchObject({ configurable: { foo: "bar" } }); return `${input}a`; }, { @@ -112,7 +114,7 @@ test("Tool can accept single string input", async () => { } ); - const result = await stringTool.invoke("b"); + const result = await stringTool.invoke("b", { configurable: { foo: "bar" } }); expect(result).toBe("ba"); }); diff --git a/langchain-core/src/tracers/event_stream.ts b/langchain-core/src/tracers/event_stream.ts index 015faa20e5c8..3972e7ce9b4b 100644 --- a/langchain-core/src/tracers/event_stream.ts +++ b/langchain-core/src/tracers/event_stream.ts @@ -364,7 +364,7 @@ export class EventStreamCallbackHandler extends BaseTracer { throw new Error(`onLLMNewToken: Run ID ${run.id} not found in run map.`); } // Top-level streaming events are covered by tapOutputIterable - if (run.parent_run_id === undefined) { + if (this.runInfoMap.size === 1) { return; } if (runInfo.runType === "chat_model") { diff --git a/langchain-core/src/tracers/tests/langsmith_interop.test.ts b/langchain-core/src/tracers/tests/langsmith_interop.test.ts index bfb7a0ab9387..9f684c9d75ef 100644 --- a/langchain-core/src/tracers/tests/langsmith_interop.test.ts +++ b/langchain-core/src/tracers/tests/langsmith_interop.test.ts @@ -178,6 +178,155 @@ test.each(["true", "false"])( } ); +test.each(["true", "false"])( + "traceables nested within runnables with a context var set and with background callbacks %s", + async (value) => { + const { setContextVariable, getContextVariable } = await import( + "../../context.js" + ); + process.env.LANGCHAIN_CALLBACKS_BACKGROUND = value; + + setContextVariable("foo", "bar"); + const aiGreet = traceable( + async (msg: BaseMessage, name = "world") => { + await new Promise((resolve) => setTimeout(resolve, 300)); + expect(getContextVariable("foo")).toEqual("baz"); + return msg.content + name; + }, + { name: "aiGreet", tracingEnabled: true } + ); + + const root = RunnableLambda.from(async (messages: BaseMessage[]) => { + const lastMsg = messages.at(-1) as HumanMessage; + expect(getContextVariable("foo")).toEqual("bar"); + setContextVariable("foo", "baz"); + const greetOne = await aiGreet(lastMsg, "David"); + + return [greetOne]; + }); + + await root.invoke([new HumanMessage({ content: "Hello!" })]); + + const relevantCalls = fetchMock.mock.calls.filter((call: any) => { + return call[0].startsWith("https://api.smith.langchain.com/runs"); + }); + + expect(relevantCalls.length).toEqual(4); + const firstCallParams = JSON.parse((relevantCalls[0][1] as any).body); + const secondCallParams = JSON.parse((relevantCalls[1][1] as any).body); + const thirdCallParams = JSON.parse((relevantCalls[2][1] as any).body); + const fourthCallParams = JSON.parse((relevantCalls[3][1] as any).body); + expect(firstCallParams).toMatchObject({ + id: firstCallParams.id, + name: "RunnableLambda", + start_time: expect.any(Number), + serialized: { + lc: 1, + type: "not_implemented", + id: ["langchain_core", "runnables", "RunnableLambda"], + }, + events: [{ name: "start", time: expect.any(String) }], + inputs: { + input: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + ], + }, + execution_order: 1, + child_execution_order: 1, + run_type: "chain", + extra: expect.any(Object), + tags: [], + trace_id: firstCallParams.id, + dotted_order: expect.any(String), + }); + expect(secondCallParams).toMatchObject({ + id: expect.any(String), + name: "aiGreet", + start_time: expect.any(Number), + run_type: "chain", + extra: expect.any(Object), + serialized: {}, + inputs: { + args: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + "David", + ], + }, + child_runs: [], + parent_run_id: firstCallParams.id, + trace_id: firstCallParams.id, + dotted_order: expect.stringContaining(`${firstCallParams.dotted_order}.`), + tags: [], + }); + expect(thirdCallParams).toMatchObject({ + end_time: expect.any(Number), + inputs: { + args: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + "David", + ], + }, + outputs: { outputs: "Hello!David" }, + parent_run_id: firstCallParams.id, + extra: expect.any(Object), + dotted_order: secondCallParams.dotted_order, + trace_id: firstCallParams.id, + tags: [], + }); + expect(fourthCallParams).toMatchObject({ + end_time: expect.any(Number), + outputs: { output: ["Hello!David"] }, + events: [ + { name: "start", time: expect.any(String) }, + { name: "end", time: expect.any(String) }, + ], + inputs: { + input: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + ], + }, + trace_id: firstCallParams.id, + dotted_order: firstCallParams.dotted_order, + }); + } +); + test.each(["true", "false"])( "streaming traceables nested within runnables with background callbacks %s", async (value) => { @@ -457,6 +606,153 @@ test.each(["true", "false"])( } ); +test.each(["true", "false"])( + "runnables nested within traceables and a context var set with background callbacks %s", + async (value) => { + const { setContextVariable, getContextVariable } = await import( + "../../context.js" + ); + process.env.LANGCHAIN_CALLBACKS_BACKGROUND = value; + setContextVariable("foo", "bar"); + + const nested = RunnableLambda.from(async (messages: BaseMessage[]) => { + const lastMsg = messages.at(-1) as HumanMessage; + await new Promise((resolve) => setTimeout(resolve, 300)); + expect(getContextVariable("foo")).toEqual("bar"); + return [lastMsg.content]; + }); + + const aiGreet = traceable( + async (msg: BaseMessage, name = "world") => { + const contents = await nested.invoke([msg]); + expect(getContextVariable("foo")).toEqual("bar"); + return contents[0] + name; + }, + { name: "aiGreet", tracingEnabled: true } + ); + + await aiGreet(new HumanMessage({ content: "Hello!" }), "mitochondria"); + + const relevantCalls = fetchMock.mock.calls.filter((call: any) => { + return call[0].startsWith("https://api.smith.langchain.com/runs"); + }); + + expect(relevantCalls.length).toEqual(4); + const firstCallParams = JSON.parse((relevantCalls[0][1] as any).body); + const secondCallParams = JSON.parse((relevantCalls[1][1] as any).body); + const thirdCallParams = JSON.parse((relevantCalls[2][1] as any).body); + const fourthCallParams = JSON.parse((relevantCalls[3][1] as any).body); + expect(firstCallParams).toMatchObject({ + id: firstCallParams.id, + name: "aiGreet", + start_time: expect.any(Number), + run_type: "chain", + extra: expect.any(Object), + serialized: {}, + inputs: { + args: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + "mitochondria", + ], + }, + child_runs: [], + trace_id: firstCallParams.id, + dotted_order: firstCallParams.dotted_order, + tags: [], + }); + expect(secondCallParams).toMatchObject({ + id: secondCallParams.id, + name: "RunnableLambda", + parent_run_id: firstCallParams.id, + start_time: expect.any(Number), + serialized: { + lc: 1, + type: "not_implemented", + id: ["langchain_core", "runnables", "RunnableLambda"], + }, + events: [{ name: "start", time: expect.any(String) }], + inputs: { + input: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + ], + }, + execution_order: 2, + child_execution_order: 2, + run_type: "chain", + extra: expect.any(Object), + tags: [], + trace_id: firstCallParams.id, + dotted_order: expect.stringContaining(`${firstCallParams.dotted_order}.`), + }); + expect(thirdCallParams).toMatchObject({ + end_time: expect.any(Number), + outputs: { output: ["Hello!"] }, + events: [ + { name: "start", time: expect.any(String) }, + { name: "end", time: expect.any(String) }, + ], + inputs: { + input: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + ], + }, + trace_id: firstCallParams.id, + dotted_order: expect.stringContaining(`${firstCallParams.dotted_order}.`), + parent_run_id: firstCallParams.id, + }); + expect(fourthCallParams).toMatchObject({ + end_time: expect.any(Number), + inputs: { + args: [ + { + lc: 1, + type: "constructor", + id: ["langchain_core", "messages", "HumanMessage"], + kwargs: { + content: "Hello!", + additional_kwargs: {}, + response_metadata: {}, + }, + }, + "mitochondria", + ], + }, + outputs: { outputs: "Hello!mitochondria" }, + extra: expect.any(Object), + dotted_order: firstCallParams.dotted_order, + trace_id: firstCallParams.id, + tags: [], + }); + } +); + test.each(["true", "false"])( "streaming runnables nested within traceables with background callbacks %s", async (value) => { diff --git a/langchain-core/src/utils/testing/index.ts b/langchain-core/src/utils/testing/index.ts index f14629794293..59f8c8437662 100644 --- a/langchain-core/src/utils/testing/index.ts +++ b/langchain-core/src/utils/testing/index.ts @@ -354,6 +354,8 @@ export class FakeListChatModel extends BaseChatModel=0.1.0 <0.2.0", "@swc/core": "^1.3.90", "@swc/jest": "^0.2.29", @@ -461,7 +461,6 @@ "@langchain/anthropic": "*", "@langchain/aws": "*", "@langchain/cohere": "*", - "@langchain/community": "*", "@langchain/core": ">=0.2.21 <0.4.0", "@langchain/google-genai": "*", "@langchain/google-vertexai": "*", @@ -484,9 +483,6 @@ "@langchain/cohere": { "optional": true }, - "@langchain/community": { - "optional": true - }, "@langchain/google-genai": { "optional": true }, @@ -524,7 +520,7 @@ "js-tiktoken": "^1.0.12", "js-yaml": "^4.1.0", "jsonpointer": "^5.0.1", - "langsmith": "~0.1.40", + "langsmith": "^0.1.56", "openapi-types": "^12.1.3", "p-retry": "4", "uuid": "^10.0.0", diff --git a/langchain/src/chains/graph_qa/cypher.ts b/langchain/src/chains/graph_qa/cypher.ts index 284c940798e6..c4f11a427b31 100644 --- a/langchain/src/chains/graph_qa/cypher.ts +++ b/langchain/src/chains/graph_qa/cypher.ts @@ -39,6 +39,8 @@ export interface FromLLMInput { } /** + * Chain for question-answering against a graph by generating Cypher statements. + * * @example * ```typescript * const chain = new GraphCypherQAChain({ @@ -47,6 +49,18 @@ export interface FromLLMInput { * }); * const res = await chain.invoke("Who played in Pulp Fiction?"); * ``` + * + * @security + * This chain will execute Cypher statements against the provided database. + * Make sure that the database connection uses credentials + * that are narrowly-scoped to only include necessary permissions. + * Failure to do so may result in data corruption or loss, since the calling code + * may attempt commands that would result in deletion, mutation of data + * if appropriately prompted or reading sensitive data if such data is present in the database. + * The best way to guard against such negative outcomes is to (as appropriate) limit the + * permissions granted to the credentials used with this tool. + * + * See https://js.langchain.com/docs/security for more information. */ export class GraphCypherQAChain extends BaseChain { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/langchain/src/chat_models/universal.ts b/langchain/src/chat_models/universal.ts index eaa1ea821d31..2682f470cb6c 100644 --- a/langchain/src/chat_models/universal.ts +++ b/langchain/src/chat_models/universal.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { BaseLanguageModelInput, ToolDefinition, diff --git a/langchain/src/output_parsers/openai_tools.ts b/langchain/src/output_parsers/openai_tools.ts index 4a9324fb1864..492bee21e310 100644 --- a/langchain/src/output_parsers/openai_tools.ts +++ b/langchain/src/output_parsers/openai_tools.ts @@ -106,6 +106,7 @@ export type JsonOutputKeyToolsParserParams = { /** * @deprecated Import from "@langchain/core/output_parsers/openai_tools" */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export class JsonOutputKeyToolsParser extends BaseLLMOutputParser { static lc_name() { return "JsonOutputKeyToolsParser"; diff --git a/libs/langchain-anthropic/package.json b/libs/langchain-anthropic/package.json index 1d8a25bb9bce..58e94edb84c4 100644 --- a/libs/langchain-anthropic/package.json +++ b/libs/langchain-anthropic/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/anthropic", - "version": "0.3.0", + "version": "0.3.5", "description": "Anthropic integrations for LangChain.js", "type": "module", "engines": { @@ -35,7 +35,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.25.2", + "@anthropic-ai/sdk": "^0.27.3", "fast-xml-parser": "^4.4.1", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.4" diff --git a/libs/langchain-anthropic/src/chat_models.ts b/libs/langchain-anthropic/src/chat_models.ts index 1e8853ff5b8a..3627aed6272c 100644 --- a/libs/langchain-anthropic/src/chat_models.ts +++ b/libs/langchain-anthropic/src/chat_models.ts @@ -33,7 +33,7 @@ import type { import { isLangChainTool } from "@langchain/core/utils/function_calling"; import { AnthropicToolsOutputParser } from "./output_parsers.js"; import { extractToolCallChunk, handleToolChoice } from "./utils/tools.js"; -import { _formatMessagesForAnthropic } from "./utils/message_inputs.js"; +import { _convertMessagesToAnthropicPayload } from "./utils/message_inputs.js"; import { _makeMessageChunkFromAnthropicEvent, anthropicResponseToChatMessages, @@ -46,6 +46,7 @@ import { AnthropicToolChoice, ChatAnthropicToolType, } from "./types.js"; +import { wrapAnthropicClientError } from "./utils/errors.js"; export interface ChatAnthropicCallOptions extends BaseChatModelCallOptions, @@ -782,7 +783,7 @@ export class ChatAnthropicMessages< runManager?: CallbackManagerForLLMRun ): AsyncGenerator { const params = this.invocationParams(options); - const formattedMessages = _formatMessagesForAnthropic(messages); + const formattedMessages = _convertMessagesToAnthropicPayload(messages); const coerceContentToString = !_toolsInParams({ ...params, ...formattedMessages, @@ -818,7 +819,7 @@ export class ChatAnthropicMessages< // Extract the text content token for text field and runManager. const token = extractToken(chunk); - yield new ChatGenerationChunk({ + const generationChunk = new ChatGenerationChunk({ message: new AIMessageChunk({ // Just yield chunk as it is and tool_use will be concat by BaseChatModel._generateUncached(). content: chunk.content, @@ -830,10 +831,16 @@ export class ChatAnthropicMessages< }), text: token ?? "", }); - - if (token) { - await runManager?.handleLLMNewToken(token); - } + yield generationChunk; + + await runManager?.handleLLMNewToken( + token ?? "", + undefined, + undefined, + undefined, + undefined, + { chunk: generationChunk } + ); } } @@ -852,7 +859,7 @@ export class ChatAnthropicMessages< { ...params, stream: false, - ..._formatMessagesForAnthropic(messages), + ..._convertMessagesToAnthropicPayload(messages), }, requestOptions ); @@ -923,6 +930,7 @@ export class ChatAnthropicMessages< if (!this.streamingClient) { const options_ = this.apiUrl ? { baseURL: this.apiUrl } : undefined; this.streamingClient = this.createClient({ + dangerouslyAllowBrowser: true, ...this.clientOptions, ...options_, apiKey: this.apiKey, @@ -930,15 +938,21 @@ export class ChatAnthropicMessages< maxRetries: 0, }); } - const makeCompletionRequest = async () => - this.streamingClient.messages.create( - { - ...request, - ...this.invocationKwargs, - stream: true, - } as AnthropicStreamingMessageCreateParams, - options - ); + const makeCompletionRequest = async () => { + try { + return await this.streamingClient.messages.create( + { + ...request, + ...this.invocationKwargs, + stream: true, + } as AnthropicStreamingMessageCreateParams, + options + ); + } catch (e) { + const error = wrapAnthropicClientError(e); + throw error; + } + }; return this.caller.call(makeCompletionRequest); } @@ -950,20 +964,27 @@ export class ChatAnthropicMessages< if (!this.batchClient) { const options = this.apiUrl ? { baseURL: this.apiUrl } : undefined; this.batchClient = this.createClient({ + dangerouslyAllowBrowser: true, ...this.clientOptions, ...options, apiKey: this.apiKey, maxRetries: 0, }); } - const makeCompletionRequest = async () => - this.batchClient.messages.create( - { - ...request, - ...this.invocationKwargs, - } as AnthropicMessageCreateParams, - options - ); + const makeCompletionRequest = async () => { + try { + return await this.batchClient.messages.create( + { + ...request, + ...this.invocationKwargs, + } as AnthropicMessageCreateParams, + options + ); + } catch (e) { + const error = wrapAnthropicClientError(e); + throw error; + } + }; return this.caller.callWithOptions( { signal: options.signal ?? undefined }, makeCompletionRequest diff --git a/libs/langchain-anthropic/src/index.ts b/libs/langchain-anthropic/src/index.ts index 38c7cea7f478..f6dcb46020ce 100644 --- a/libs/langchain-anthropic/src/index.ts +++ b/libs/langchain-anthropic/src/index.ts @@ -1 +1,2 @@ export * from "./chat_models.js"; +export { convertPromptToAnthropic } from "./utils/prompts.js"; diff --git a/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts b/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts index 645f9304b07e..8deb3c8626e1 100644 --- a/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts +++ b/libs/langchain-anthropic/src/tests/chat_models-tools.int.test.ts @@ -1,4 +1,5 @@ /* eslint-disable no-process-env */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { expect, test } from "@jest/globals"; import { @@ -11,6 +12,7 @@ import { StructuredTool, tool } from "@langchain/core/tools"; import { concat } from "@langchain/core/utils/stream"; import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; +import { RunnableLambda } from "@langchain/core/runnables"; import { ChatAnthropic } from "../chat_models.js"; import { AnthropicToolResponse } from "../types.js"; @@ -86,6 +88,36 @@ test("Few shotting with tool calls", async () => { expect(res.content).toContain("24"); }); +test("Invalid tool calls should throw an appropriate error", async () => { + const chat = model.bindTools([new WeatherTool()]); + let error; + try { + await chat.invoke([ + new HumanMessage("What is the weather in SF?"), + new AIMessage({ + content: "Let me look up the current weather.", + tool_calls: [ + { + id: "toolu_feiwjf9u98r389u498", + name: "get_weather", + args: { + location: "SF", + }, + }, + ], + }), + new ToolMessage({ + tool_call_id: "badbadbad", + content: "It is currently 24 degrees with hail in San Francisco.", + }), + ]); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect((error as any).lc_error_code).toEqual("INVALID_TOOL_RESULTS"); +}); + test("Can bind & invoke StructuredTools", async () => { const tools = [new WeatherTool()]; @@ -202,6 +234,73 @@ test("Can bind & stream AnthropicTools", async () => { expect(args.location).toBeTruthy(); }); +test("stream events with no tool calls has string message content", async () => { + const wrapper = RunnableLambda.from(async (_, config) => { + const res = await model.invoke( + "What is the weather in London today?", + config + ); + return res; + }); + const eventStream = await wrapper.streamEvents( + "What is the weather in London today?", + { + version: "v2", + } + ); + + const chatModelStreamEvents = []; + for await (const event of eventStream) { + if (event.event === "on_chat_model_stream") { + chatModelStreamEvents.push(event); + } + } + expect(chatModelStreamEvents.length).toBeGreaterThan(0); + expect( + chatModelStreamEvents.every( + (event) => typeof event.data.chunk.content === "string" + ) + ).toBe(true); +}); + +test("stream events with tool calls has raw message content", async () => { + const modelWithTools = model.bind({ + tools: [anthropicTool], + tool_choice: { + type: "tool", + name: "get_weather", + }, + }); + + const wrapper = RunnableLambda.from(async (_, config) => { + const res = await modelWithTools.invoke( + "What is the weather in London today?", + config + ); + return res; + }); + const eventStream = await wrapper.streamEvents( + "What is the weather in London today?", + { + version: "v2", + } + ); + + const chatModelStreamEvents = []; + for await (const event of eventStream) { + if (event.event === "on_chat_model_stream") { + console.log(event); + chatModelStreamEvents.push(event); + } + } + expect(chatModelStreamEvents.length).toBeGreaterThan(0); + expect( + chatModelStreamEvents.every((event) => + Array.isArray(event.data.chunk.content) + ) + ).toBe(true); +}); + test("withStructuredOutput with zod schema", async () => { const modelWithTools = model.withStructuredOutput<{ location: string }>( zodSchema, diff --git a/libs/langchain-anthropic/src/tests/chat_models.int.test.ts b/libs/langchain-anthropic/src/tests/chat_models.int.test.ts index 7d823727fc17..d9240f2f03aa 100644 --- a/libs/langchain-anthropic/src/tests/chat_models.int.test.ts +++ b/libs/langchain-anthropic/src/tests/chat_models.int.test.ts @@ -1,4 +1,5 @@ /* eslint-disable no-process-env */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { expect, test } from "@jest/globals"; import { @@ -30,6 +31,39 @@ test("Test ChatAnthropic", async () => { expect(res.response_metadata.usage).toBeDefined(); }); +test("Test ChatAnthropic with a bad API key throws appropriate error", async () => { + const chat = new ChatAnthropic({ + modelName: "claude-3-sonnet-20240229", + maxRetries: 0, + apiKey: "bad", + }); + let error; + try { + const message = new HumanMessage("Hello!"); + await chat.invoke([message]); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect((error as any).lc_error_code).toEqual("MODEL_AUTHENTICATION"); +}); + +test("Test ChatAnthropic with unknown model throws appropriate error", async () => { + const chat = new ChatAnthropic({ + modelName: "badbad", + maxRetries: 0, + }); + let error; + try { + const message = new HumanMessage("Hello!"); + await chat.invoke([message]); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect((error as any).lc_error_code).toEqual("MODEL_NOT_FOUND"); +}); + test("Test ChatAnthropic Generate", async () => { const chat = new ChatAnthropic({ modelName: "claude-3-sonnet-20240229", diff --git a/libs/langchain-anthropic/src/tests/chat_models.test.ts b/libs/langchain-anthropic/src/tests/chat_models.test.ts index bc6308c75770..e605337c2d8e 100644 --- a/libs/langchain-anthropic/src/tests/chat_models.test.ts +++ b/libs/langchain-anthropic/src/tests/chat_models.test.ts @@ -3,7 +3,7 @@ import { AIMessage, HumanMessage, ToolMessage } from "@langchain/core/messages"; import { z } from "zod"; import { OutputParserException } from "@langchain/core/output_parsers"; import { ChatAnthropic } from "../chat_models.js"; -import { _formatMessagesForAnthropic } from "../utils/message_inputs.js"; +import { _convertMessagesToAnthropicPayload } from "../utils/message_inputs.js"; test("withStructuredOutput with output validation", async () => { const model = new ChatAnthropic({ @@ -143,7 +143,7 @@ test("Can properly format anthropic messages when given two tool results", async }), ]; - const formattedMessages = _formatMessagesForAnthropic(messageHistory); + const formattedMessages = _convertMessagesToAnthropicPayload(messageHistory); expect(formattedMessages).toEqual({ messages: [ diff --git a/libs/langchain-anthropic/src/tests/prompts.int.test.ts b/libs/langchain-anthropic/src/tests/prompts.int.test.ts new file mode 100644 index 000000000000..14b662b9c420 --- /dev/null +++ b/libs/langchain-anthropic/src/tests/prompts.int.test.ts @@ -0,0 +1,28 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { ChatPromptTemplate } from "@langchain/core/prompts"; + +import { convertPromptToAnthropic } from "../utils/prompts.js"; + +test("Convert hub prompt to Anthropic payload and invoke", async () => { + const prompt = ChatPromptTemplate.fromMessages([ + ["system", "You are a world class comedian"], + ["human", "Tell me a joke about {topic}"], + ]); + const formattedPrompt = await prompt.invoke({ + topic: "cats", + }); + + const { system, messages } = convertPromptToAnthropic(formattedPrompt); + + const anthropicClient = new Anthropic(); + + const anthropicResponse = await anthropicClient.messages.create({ + model: "claude-3-haiku-20240307", + system, + messages, + max_tokens: 1024, + stream: false, + }); + + expect(anthropicResponse.content).toBeDefined(); +}); diff --git a/libs/langchain-anthropic/src/utils/errors.ts b/libs/langchain-anthropic/src/utils/errors.ts new file mode 100644 index 000000000000..b57c2c088573 --- /dev/null +++ b/libs/langchain-anthropic/src/utils/errors.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-param-reassign */ + +// Duplicate of core +// TODO: Remove once we stop supporting 0.2.x core versions +export type LangChainErrorCodes = + | "INVALID_PROMPT_INPUT" + | "INVALID_TOOL_RESULTS" + | "MESSAGE_COERCION_FAILURE" + | "MODEL_AUTHENTICATION" + | "MODEL_NOT_FOUND" + | "MODEL_RATE_LIMIT" + | "OUTPUT_PARSING_FAILURE"; + +export function addLangChainErrorFields( + error: any, + lc_error_code: LangChainErrorCodes +) { + (error as any).lc_error_code = lc_error_code; + error.message = `${error.message}\n\nTroubleshooting URL: https://js.langchain.com/docs/troubleshooting/errors/${lc_error_code}/\n`; + return error; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wrapAnthropicClientError(e: any) { + let error; + if (e.status === 400 && e.message.includes("tool")) { + error = addLangChainErrorFields(e, "INVALID_TOOL_RESULTS"); + } else if (e.status === 401) { + error = addLangChainErrorFields(e, "MODEL_AUTHENTICATION"); + } else if (e.status === 404) { + error = addLangChainErrorFields(e, "MODEL_NOT_FOUND"); + } else if (e.status === 429) { + error = addLangChainErrorFields(e, "MODEL_RATE_LIMIT"); + } else { + error = e; + } + return error; +} diff --git a/libs/langchain-anthropic/src/utils/message_inputs.ts b/libs/langchain-anthropic/src/utils/message_inputs.ts index b32786e43f02..8f810b231f97 100644 --- a/libs/langchain-anthropic/src/utils/message_inputs.ts +++ b/libs/langchain-anthropic/src/utils/message_inputs.ts @@ -185,10 +185,11 @@ function _formatContent(content: MessageContent) { /** * Formats messages as a prompt for the model. + * Used in LangSmith, export is important here. * @param messages The base messages to format as a prompt. * @returns The formatted prompt. */ -export function _formatMessagesForAnthropic( +export function _convertMessagesToAnthropicPayload( messages: BaseMessage[] ): AnthropicMessageCreateParams { const mergedMessages = _mergeMessages(messages); diff --git a/libs/langchain-anthropic/src/utils/prompts.ts b/libs/langchain-anthropic/src/utils/prompts.ts new file mode 100644 index 000000000000..7fb32b5903c2 --- /dev/null +++ b/libs/langchain-anthropic/src/utils/prompts.ts @@ -0,0 +1,51 @@ +import type { BasePromptValue } from "@langchain/core/prompt_values"; +import Anthropic from "@anthropic-ai/sdk"; + +import { _convertMessagesToAnthropicPayload } from "./message_inputs.js"; + +/** + * Convert a formatted LangChain prompt (e.g. pulled from the hub) into + * a format expected by Anthropic's JS SDK. + * + * Requires the "@langchain/anthropic" package to be installed in addition + * to the Anthropic SDK. + * + * @example + * ```ts + * import { convertPromptToAnthropic } from "langsmith/utils/hub/anthropic"; + * import { pull } from "langchain/hub"; + * + * import Anthropic from '@anthropic-ai/sdk'; + * + * const prompt = await pull("jacob/joke-generator"); + * const formattedPrompt = await prompt.invoke({ + * topic: "cats", + * }); + * + * const { system, messages } = convertPromptToAnthropic(formattedPrompt); + * + * const anthropicClient = new Anthropic({ + * apiKey: 'your_api_key', + * }); + * + * const anthropicResponse = await anthropicClient.messages.create({ + * model: "claude-3-5-sonnet-20240620", + * max_tokens: 1024, + * stream: false, + * system, + * messages, + * }); + * ``` + * @param formattedPrompt + * @returns A partial Anthropic payload. + */ +export function convertPromptToAnthropic( + formattedPrompt: BasePromptValue +): Anthropic.Messages.MessageCreateParams { + const messages = formattedPrompt.toChatMessages(); + const anthropicBody = _convertMessagesToAnthropicPayload(messages); + if (anthropicBody.messages === undefined) { + anthropicBody.messages = []; + } + return anthropicBody; +} diff --git a/libs/langchain-aws/package.json b/libs/langchain-aws/package.json index 6d550a9f49d8..ce8979edcc2d 100644 --- a/libs/langchain-aws/package.json +++ b/libs/langchain-aws/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/aws", - "version": "0.0.10", + "version": "0.1.1", "description": "LangChain AWS integration", "type": "module", "engines": { diff --git a/libs/langchain-aws/src/common.ts b/libs/langchain-aws/src/common.ts index fbb3356e90e4..8b0d8e0d2e18 100644 --- a/libs/langchain-aws/src/common.ts +++ b/libs/langchain-aws/src/common.ts @@ -144,6 +144,17 @@ export function convertToConverseMessages(messages: BaseMessage[]): { return { text: block.text, }; + } else if ( + block.type === "document" && + block.document !== undefined + ) { + return { + document: block.document, + }; + } else if (block.type === "image" && block.image !== undefined) { + return { + image: block.image, + }; } else { throw new Error(`Unsupported content block type: ${block.type}`); } diff --git a/libs/langchain-azure-cosmosdb/package.json b/libs/langchain-azure-cosmosdb/package.json index 01b940714bc4..8b3e38f30e4c 100644 --- a/libs/langchain-azure-cosmosdb/package.json +++ b/libs/langchain-azure-cosmosdb/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/azure-cosmosdb", - "version": "0.1.0", + "version": "0.2.0", "description": "Azure CosmosDB integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-azure-dynamic-sessions/package.json b/libs/langchain-azure-dynamic-sessions/package.json index a328f3c31650..e753162110a1 100644 --- a/libs/langchain-azure-dynamic-sessions/package.json +++ b/libs/langchain-azure-dynamic-sessions/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/azure-dynamic-sessions", - "version": "0.1.0", + "version": "0.2.0", "description": "Sample integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-baidu-qianfan/package.json b/libs/langchain-baidu-qianfan/package.json index 651def22f09d..86569d744929 100644 --- a/libs/langchain-baidu-qianfan/package.json +++ b/libs/langchain-baidu-qianfan/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/baidu-qianfan", - "version": "0.0.4", + "version": "0.1.0", "description": "Baidu Qianfan integration for LangChain.js", "type": "module", "engines": { @@ -32,7 +32,7 @@ "license": "MIT", "dependencies": { "@baiducloud/qianfan": "^0.1.6", - "@langchain/openai": "~0.1.0", + "@langchain/openai": "~0.3.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.5" }, @@ -42,7 +42,7 @@ "devDependencies": { "@jest/globals": "^29.5.0", "@langchain/core": "workspace:*", - "@langchain/openai": "~0.1.0", + "@langchain/openai": "~0.3.0", "@langchain/scripts": ">=0.1.0 <0.2.0", "@swc/core": "^1.3.90", "@swc/jest": "^0.2.29", diff --git a/libs/langchain-cloudflare/.gitignore b/libs/langchain-cloudflare/.gitignore index 3443681d0418..c10034e2f1be 100644 --- a/libs/langchain-cloudflare/.gitignore +++ b/libs/langchain-cloudflare/.gitignore @@ -2,10 +2,6 @@ index.cjs index.js index.d.ts index.d.cts -langgraph/checkpointers.cjs -langgraph/checkpointers.js -langgraph/checkpointers.d.ts -langgraph/checkpointers.d.cts node_modules dist .yarn diff --git a/libs/langchain-cloudflare/langchain.config.js b/libs/langchain-cloudflare/langchain.config.js index e112dcf90c6c..68c69b2d0f44 100644 --- a/libs/langchain-cloudflare/langchain.config.js +++ b/libs/langchain-cloudflare/langchain.config.js @@ -13,8 +13,7 @@ function abs(relativePath) { export const config = { internals: [/node\:/, /@langchain\/core\//, /@langchain\/langgraph\/web/], entrypoints: { - index: "index", - "langgraph/checkpointers": "langgraph/checkpointers", + index: "index" }, tsConfigPath: resolve("./tsconfig.json"), cjsSource: "./dist-cjs", diff --git a/libs/langchain-cloudflare/package.json b/libs/langchain-cloudflare/package.json index a2b52bdc5b1c..07ba32df8481 100644 --- a/libs/langchain-cloudflare/package.json +++ b/libs/langchain-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/cloudflare", - "version": "0.0.7", + "version": "0.1.0", "description": "Cloudflare integration for LangChain.js", "type": "module", "engines": { @@ -42,7 +42,6 @@ "@cloudflare/workers-types": "^4.20240909.0", "@jest/globals": "^29.5.0", "@langchain/core": "workspace:*", - "@langchain/langgraph": "~0.0.31", "@langchain/scripts": ">=0.1.0 <0.2.0", "@langchain/standard-tests": "0.0.0", "@swc/core": "^1.3.90", @@ -68,13 +67,7 @@ "typescript": "<5.2.0" }, "peerDependencies": { - "@langchain/core": ">=0.2.21 <0.4.0", - "@langchain/langgraph": "*" - }, - "peerDependenciesMeta": { - "@langchain/langgraph": { - "optional": true - } + "@langchain/core": ">=0.2.21 <0.4.0" }, "publishConfig": { "access": "public" @@ -89,15 +82,6 @@ "import": "./index.js", "require": "./index.cjs" }, - "./langgraph/checkpointers": { - "types": { - "import": "./langgraph/checkpointers.d.ts", - "require": "./langgraph/checkpointers.d.cts", - "default": "./langgraph/checkpointers.d.ts" - }, - "import": "./langgraph/checkpointers.js", - "require": "./langgraph/checkpointers.cjs" - }, "./package.json": "./package.json" }, "files": [ @@ -105,10 +89,6 @@ "index.cjs", "index.js", "index.d.ts", - "index.d.cts", - "langgraph/checkpointers.cjs", - "langgraph/checkpointers.js", - "langgraph/checkpointers.d.ts", - "langgraph/checkpointers.d.cts" + "index.d.cts" ] } diff --git a/libs/langchain-cloudflare/src/langgraph/checkpointers.ts b/libs/langchain-cloudflare/src/langgraph/checkpointers.ts deleted file mode 100644 index 1d3ad8dfb194..000000000000 --- a/libs/langchain-cloudflare/src/langgraph/checkpointers.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { D1Database } from "@cloudflare/workers-types"; - -import { RunnableConfig } from "@langchain/core/runnables"; -import { - BaseCheckpointSaver, - Checkpoint, - CheckpointMetadata, - CheckpointTuple, - SerializerProtocol, -} from "@langchain/langgraph/web"; - -// snake_case is used to match Python implementation -interface Row { - checkpoint: string; - metadata: string; - parent_id?: string; - thread_id: string; - checkpoint_id: string; -} - -export type CloudflareD1SaverFields = { - db: D1Database; -}; - -export class CloudflareD1Saver extends BaseCheckpointSaver { - db: D1Database; - - protected isSetup: boolean; - - constructor( - fields: CloudflareD1SaverFields, - serde?: SerializerProtocol - ) { - super(serde); - this.db = fields.db; - this.isSetup = false; - } - - private async setup() { - if (this.isSetup) { - return; - } - - try { - await this.db.exec(` -CREATE TABLE IF NOT EXISTS checkpoints (thread_id TEXT NOT NULL, checkpoint_id TEXT NOT NULL, parent_id TEXT, checkpoint BLOB, metadata BLOB, PRIMARY KEY (thread_id, checkpoint_id));`); - } catch (error) { - console.log("Error creating checkpoints table", error); - throw error; - } - - this.isSetup = true; - } - - async getTuple(config: RunnableConfig): Promise { - await this.setup(); - const thread_id = config.configurable?.thread_id; - const checkpoint_id = config.configurable?.checkpoint_id; - - if (checkpoint_id) { - try { - const row: Row | null = await this.db - .prepare( - `SELECT checkpoint, parent_id, metadata FROM checkpoints WHERE thread_id = ? AND checkpoint_id = ?` - ) - .bind(thread_id, checkpoint_id) - .first(); - - if (row) { - return { - config, - checkpoint: (await this.serde.parse(row.checkpoint)) as Checkpoint, - metadata: (await this.serde.parse( - row.metadata - )) as CheckpointMetadata, - parentConfig: row.parent_id - ? { - configurable: { - thread_id, - checkpoint_id: row.parent_id, - }, - } - : undefined, - }; - } - } catch (error) { - console.log("Error retrieving checkpoint", error); - throw error; - } - } else { - const row: Row | null = await this.db - .prepare( - `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM checkpoints WHERE thread_id = ? ORDER BY checkpoint_id DESC LIMIT 1` - ) - .bind(thread_id) - .first(); - - if (row) { - return { - config: { - configurable: { - thread_id: row.thread_id, - checkpoint_id: row.checkpoint_id, - }, - }, - checkpoint: (await this.serde.parse(row.checkpoint)) as Checkpoint, - metadata: (await this.serde.parse( - row.metadata - )) as CheckpointMetadata, - parentConfig: row.parent_id - ? { - configurable: { - thread_id: row.thread_id, - checkpoint_id: row.parent_id, - }, - } - : undefined, - }; - } - } - - return undefined; - } - - async *list( - config: RunnableConfig, - limit?: number, - before?: RunnableConfig - ): AsyncGenerator { - await this.setup(); - const thread_id = config.configurable?.thread_id; - let sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM checkpoints WHERE thread_id = ? ${ - before ? "AND checkpoint_id < ?" : "" - } ORDER BY checkpoint_id DESC`; - if (limit) { - sql += ` LIMIT ${limit}`; - } - const args = [thread_id, before?.configurable?.checkpoint_id].filter( - Boolean - ); - - try { - const { results: rows }: { results: Row[] } = await this.db - .prepare(sql) - .bind(...args) - .all(); - - if (rows) { - for (const row of rows) { - yield { - config: { - configurable: { - thread_id: row.thread_id, - checkpoint_id: row.checkpoint_id, - }, - }, - checkpoint: (await this.serde.parse(row.checkpoint)) as Checkpoint, - metadata: (await this.serde.parse( - row.metadata - )) as CheckpointMetadata, - parentConfig: row.parent_id - ? { - configurable: { - thread_id: row.thread_id, - checkpoint_id: row.parent_id, - }, - } - : undefined, - }; - } - } - } catch (error) { - console.log("Error listing checkpoints", error); - throw error; - } - } - - async put( - config: RunnableConfig, - checkpoint: Checkpoint, - metadata: CheckpointMetadata - ): Promise { - await this.setup(); - - try { - const row = [ - config.configurable?.thread_id ?? null, - checkpoint.id, - config.configurable?.checkpoint_id ?? null, - this.serde.stringify(checkpoint), - this.serde.stringify(metadata), - ]; - - await this.db - .prepare( - `INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_id, parent_id, checkpoint, metadata) VALUES (?, ?, ?, ?, ?)` - ) - .bind(...row) - .run(); - } catch (error) { - console.log("Error saving checkpoint", error); - throw error; - } - - return { - configurable: { - thread_id: config.configurable?.thread_id, - checkpoint_id: checkpoint.id, - }, - }; - } -} diff --git a/libs/langchain-cohere/package.json b/libs/langchain-cohere/package.json index 02552f52b0e6..2f9eedec14c7 100644 --- a/libs/langchain-cohere/package.json +++ b/libs/langchain-cohere/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/cohere", - "version": "0.2.2", + "version": "0.3.1", "description": "Cohere integration for LangChain.js", "type": "module", "engines": { @@ -35,7 +35,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "cohere-ai": "^7.10.5", + "cohere-ai": "^7.14.0", "uuid": "^10.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.1" diff --git a/libs/langchain-cohere/src/chat_models.ts b/libs/langchain-cohere/src/chat_models.ts index ec5ed43b6b43..50695e1f5fcd 100644 --- a/libs/langchain-cohere/src/chat_models.ts +++ b/libs/langchain-cohere/src/chat_models.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { CohereClient, Cohere } from "cohere-ai"; +import { Cohere, CohereClient } from "cohere-ai"; import { ToolResult } from "cohere-ai/api/index.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import { - MessageType, - type BaseMessage, - MessageContent, AIMessage, + type BaseMessage, isAIMessage, + MessageContent, + MessageType, } from "@langchain/core/messages"; import { BaseLanguageModelInput, @@ -17,11 +17,11 @@ import { import { isLangChainTool } from "@langchain/core/utils/function_calling"; import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager"; import { - type BaseChatModelParams, BaseChatModel, - LangSmithParams, BaseChatModelCallOptions, + type BaseChatModelParams, BindToolsInput, + LangSmithParams, } from "@langchain/core/language_models/chat_models"; import { ChatGeneration, @@ -29,22 +29,22 @@ import { ChatResult, } from "@langchain/core/outputs"; import { AIMessageChunk } from "@langchain/core/messages"; -import { getEnvironmentVariable } from "@langchain/core/utils/env"; import { NewTokenIndices } from "@langchain/core/callbacks/base"; import { - ToolMessage, ToolCall, ToolCallChunk, + ToolMessage, } from "@langchain/core/messages/tool"; import * as uuid from "uuid"; import { Runnable } from "@langchain/core/runnables"; +import { CohereClientOptions, getCohereClient } from "./client.js"; type ChatCohereToolType = BindToolsInput | Cohere.Tool; /** * Input interface for ChatCohere */ -export interface ChatCohereInput extends BaseChatModelParams { +export interface BaseChatCohereInput extends BaseChatModelParams { /** * The API key to use. * @default {process.env.COHERE_API_KEY} @@ -78,6 +78,8 @@ export interface ChatCohereInput extends BaseChatModelParams { streamUsage?: boolean; } +export type ChatCohereInput = BaseChatCohereInput & CohereClientOptions; + interface TokenUsage { completionTokens?: number; promptTokens?: number; @@ -732,14 +734,8 @@ export class ChatCohere< constructor(fields?: ChatCohereInput) { super(fields ?? {}); - const token = fields?.apiKey ?? getEnvironmentVariable("COHERE_API_KEY"); - if (!token) { - throw new Error("No API key provided for ChatCohere."); - } + this.client = getCohereClient(fields); - this.client = new CohereClient({ - token, - }); this.model = fields?.model ?? this.model; this.temperature = fields?.temperature ?? this.temperature; this.streaming = fields?.streaming ?? this.streaming; diff --git a/libs/langchain-cohere/src/client.ts b/libs/langchain-cohere/src/client.ts new file mode 100644 index 000000000000..65d1a9093ff8 --- /dev/null +++ b/libs/langchain-cohere/src/client.ts @@ -0,0 +1,28 @@ +import { CohereClient } from "cohere-ai"; +import { getEnvironmentVariable } from "@langchain/core/utils/env"; + +export type CohereClientOptions = { + /** + * The API key to use. Ignored if `client` is provided + * @default {process.env.COHERE_API_KEY} + */ + apiKey?: string; + + /** + * The CohereClient instance to use. Superseeds `apiKey` + */ + client?: CohereClient; +}; + +export function getCohereClient(fields?: CohereClientOptions): CohereClient { + if (fields?.client) { + return fields.client; + } + + const apiKey = fields?.apiKey ?? getEnvironmentVariable("COHERE_API_KEY"); + + if (!apiKey) { + throw new Error("COHERE_API_KEY must be set"); + } + return new CohereClient({ token: apiKey }); +} diff --git a/libs/langchain-cohere/src/embeddings.ts b/libs/langchain-cohere/src/embeddings.ts index f04ee1815887..396a6ee5049c 100644 --- a/libs/langchain-cohere/src/embeddings.ts +++ b/libs/langchain-cohere/src/embeddings.ts @@ -1,8 +1,8 @@ import { CohereClient } from "cohere-ai"; -import { getEnvironmentVariable } from "@langchain/core/utils/env"; import { Embeddings, EmbeddingsParams } from "@langchain/core/embeddings"; import { chunkArray } from "@langchain/core/utils/chunk_array"; +import { CohereClientOptions, getCohereClient } from "./client.js"; /** * Interface that extends EmbeddingsParams and defines additional @@ -57,23 +57,13 @@ export class CohereEmbeddings constructor( fields?: Partial & { verbose?: boolean; - apiKey?: string; - } + } & CohereClientOptions ) { const fieldsWithDefaults = { maxConcurrency: 2, ...fields }; super(fieldsWithDefaults); - const apiKey = - fieldsWithDefaults?.apiKey || getEnvironmentVariable("COHERE_API_KEY"); - - if (!apiKey) { - throw new Error("Cohere API key not found"); - } - - this.client = new CohereClient({ - token: apiKey, - }); + this.client = getCohereClient(fieldsWithDefaults); this.model = fieldsWithDefaults?.model ?? this.model; if (!this.model) { diff --git a/libs/langchain-cohere/src/llms.ts b/libs/langchain-cohere/src/llms.ts index 1021643ac6ef..a9b57cb6b579 100644 --- a/libs/langchain-cohere/src/llms.ts +++ b/libs/langchain-cohere/src/llms.ts @@ -1,14 +1,14 @@ import { CohereClient, Cohere as CohereTypes } from "cohere-ai"; -import { getEnvironmentVariable } from "@langchain/core/utils/env"; import { LLM, type BaseLLMParams } from "@langchain/core/language_models/llms"; import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager"; import type { BaseLanguageModelCallOptions } from "@langchain/core/language_models/base"; +import { CohereClientOptions, getCohereClient } from "./client.js"; /** * Interface for the input parameters specific to the Cohere model. */ -export interface CohereInput extends BaseLLMParams { +export interface BaseCohereInput extends BaseLLMParams { /** Sampling temperature to use */ temperature?: number; @@ -19,10 +19,10 @@ export interface CohereInput extends BaseLLMParams { /** Model to use */ model?: string; - - apiKey?: string; } +export type CohereInput = BaseCohereInput & CohereClientOptions; + interface CohereCallOptions extends BaseLanguageModelCallOptions, Partial> {} @@ -78,17 +78,7 @@ export class Cohere extends LLM implements CohereInput { constructor(fields?: CohereInput) { super(fields ?? {}); - const apiKey = fields?.apiKey ?? getEnvironmentVariable("COHERE_API_KEY"); - - if (!apiKey) { - throw new Error( - "Please set the COHERE_API_KEY environment variable or pass it to the constructor as the apiKey field." - ); - } - - this.client = new CohereClient({ - token: apiKey, - }); + this.client = getCohereClient(fields); this.maxTokens = fields?.maxTokens ?? this.maxTokens; this.temperature = fields?.temperature ?? this.temperature; this.model = fields?.model ?? this.model; diff --git a/libs/langchain-cohere/src/rerank.ts b/libs/langchain-cohere/src/rerank.ts index 21e188cc70ff..78a13efa2f83 100644 --- a/libs/langchain-cohere/src/rerank.ts +++ b/libs/langchain-cohere/src/rerank.ts @@ -1,14 +1,9 @@ import { DocumentInterface } from "@langchain/core/documents"; import { BaseDocumentCompressor } from "@langchain/core/retrievers/document_compressors"; -import { getEnvironmentVariable } from "@langchain/core/utils/env"; import { CohereClient } from "cohere-ai"; +import { CohereClientOptions, getCohereClient } from "./client.js"; -export interface CohereRerankArgs { - /** - * The API key to use. - * @default {process.env.COHERE_API_KEY} - */ - apiKey?: string; +export interface BaseCohereRerankArgs { /** * The name of the model to use. * @default {"rerank-english-v2.0"} @@ -25,6 +20,7 @@ export interface CohereRerankArgs { maxChunksPerDoc?: number; } +type CohereRerankArgs = BaseCohereRerankArgs & CohereClientOptions; /** * Document compressor that uses `Cohere Rerank API`. */ @@ -39,14 +35,9 @@ export class CohereRerank extends BaseDocumentCompressor { constructor(fields?: CohereRerankArgs) { super(); - const token = fields?.apiKey ?? getEnvironmentVariable("COHERE_API_KEY"); - if (!token) { - throw new Error("No API key provided for CohereRerank."); - } - this.client = new CohereClient({ - token, - }); + this.client = getCohereClient(fields); + this.model = fields?.model ?? this.model; if (!this.model) { throw new Error( diff --git a/libs/langchain-community/.gitignore b/libs/langchain-community/.gitignore index f479e8233d4d..676b316638e8 100644 --- a/libs/langchain-community/.gitignore +++ b/libs/langchain-community/.gitignore @@ -370,6 +370,10 @@ vectorstores/lancedb.cjs vectorstores/lancedb.js vectorstores/lancedb.d.ts vectorstores/lancedb.d.cts +vectorstores/libsql.cjs +vectorstores/libsql.js +vectorstores/libsql.d.ts +vectorstores/libsql.d.cts vectorstores/milvus.cjs vectorstores/milvus.js vectorstores/milvus.d.ts @@ -1026,10 +1030,6 @@ chains/graph_qa/cypher.cjs chains/graph_qa/cypher.js chains/graph_qa/cypher.d.ts chains/graph_qa/cypher.d.cts -langgraph/checkpointers/vercel_kv.cjs -langgraph/checkpointers/vercel_kv.js -langgraph/checkpointers/vercel_kv.d.ts -langgraph/checkpointers/vercel_kv.d.cts node_modules dist .yarn diff --git a/libs/langchain-community/langchain.config.js b/libs/langchain-community/langchain.config.js index 9e70e27a8f1c..78118b4d1d5e 100644 --- a/libs/langchain-community/langchain.config.js +++ b/libs/langchain-community/langchain.config.js @@ -129,6 +129,7 @@ export const config = { "vectorstores/hnswlib": "vectorstores/hnswlib", "vectorstores/hanavector": "vectorstores/hanavector", "vectorstores/lancedb": "vectorstores/lancedb", + "vectorstores/libsql": "vectorstores/libsql", "vectorstores/milvus": "vectorstores/milvus", "vectorstores/momento_vector_index": "vectorstores/momento_vector_index", "vectorstores/mongodb_atlas": "vectorstores/mongodb_atlas", @@ -316,9 +317,7 @@ export const config = { "experimental/llms/chrome_ai": "experimental/llms/chrome_ai", "experimental/tools/pyinterpreter": "experimental/tools/pyinterpreter", // chains - "chains/graph_qa/cypher": "chains/graph_qa/cypher", - // langgraph checkpointers - "langgraph/checkpointers/vercel_kv": "langgraph/checkpointers/vercel_kv" + "chains/graph_qa/cypher": "chains/graph_qa/cypher" }, requiresOptionalDependency: [ "tools/aws_sfn", @@ -377,6 +376,7 @@ export const config = { "vectorstores/hnswlib", "vectorstores/hanavector", "vectorstores/lancedb", + "vectorstores/libsql", "vectorstores/milvus", "vectorstores/momento_vector_index", "vectorstores/mongodb_atlas", diff --git a/libs/langchain-community/package.json b/libs/langchain-community/package.json index d25ffcfeebf5..158b27368022 100644 --- a/libs/langchain-community/package.json +++ b/libs/langchain-community/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/community", - "version": "0.3.0-rc.0", + "version": "0.3.7", "description": "Third-party integrations for LangChain.js", "type": "module", "engines": { @@ -41,7 +41,7 @@ "flat": "^5.0.2", "js-yaml": "^4.1.0", "langchain": ">=0.2.3 <0.4.0", - "langsmith": "~0.1.30", + "langsmith": "~0.1.56", "uuid": "^10.0.0", "zod": "^3.22.3", "zod-to-json-schema": "^3.22.5" @@ -79,10 +79,10 @@ "@huggingface/inference": "^2.6.4", "@jest/globals": "^29.5.0", "@langchain/core": "workspace:*", - "@langchain/langgraph": "~0.0.26", "@langchain/scripts": ">=0.1.0 <0.2.0", "@langchain/standard-tests": "0.0.0", "@layerup/layerup-security": "^1.5.12", + "@libsql/client": "^0.14.0", "@mendable/firecrawl-js": "^0.0.36", "@mlc-ai/web-llm": ">=0.2.62 <0.3.0", "@mozilla/readability": "^0.4.4", @@ -93,7 +93,7 @@ "@planetscale/database": "^1.8.0", "@premai/prem-sdk": "^0.3.25", "@qdrant/js-client-rest": "^1.8.2", - "@raycast/api": "^1.55.2", + "@raycast/api": "^1.83.1", "@rockset/client": "^0.9.1", "@smithy/eventstream-codec": "^2.0.5", "@smithy/protocol-http": "^3.0.6", @@ -124,7 +124,7 @@ "@types/ws": "^8", "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", - "@upstash/ratelimit": "^1.2.1", + "@upstash/ratelimit": "^2.0.3", "@upstash/redis": "^1.32.0", "@upstash/vector": "^1.1.1", "@vercel/kv": "^0.2.3", @@ -185,7 +185,7 @@ "mongodb": "^5.2.0", "mysql2": "^3.9.8", "neo4j-driver": "^5.17.0", - "node-llama-cpp": "2.7.3", + "node-llama-cpp": "^2", "notion-to-md": "^3.1.0", "officeparser": "^4.0.4", "pdf-parse": "1.1.1", @@ -208,7 +208,7 @@ "typescript": "~5.1.6", "typesense": "^1.5.3", "usearch": "^1.1.1", - "vectordb": "^0.1.4", + "vectordb": "^0.9.0", "voy-search": "0.6.2", "weaviate-ts-client": "^1.4.0", "web-auth-library": "^1.0.3", @@ -244,8 +244,8 @@ "@gradientai/nodejs-sdk": "^1.2.0", "@huggingface/inference": "^2.6.4", "@langchain/core": ">=0.2.21 <0.4.0", - "@langchain/langgraph": "*", "@layerup/layerup-security": "^1.5.12", + "@libsql/client": "^0.14.0", "@mendable/firecrawl-js": "^0.0.13", "@mlc-ai/web-llm": "*", "@mozilla/readability": "*", @@ -267,7 +267,7 @@ "@tensorflow-models/universal-sentence-encoder": "*", "@tensorflow/tfjs-converter": "*", "@tensorflow/tfjs-core": "*", - "@upstash/ratelimit": "^1.1.3", + "@upstash/ratelimit": "^1.1.3 || ^2.0.3", "@upstash/redis": "^1.20.6", "@upstash/vector": "^1.1.1", "@vercel/kv": "^0.2.3", @@ -288,7 +288,6 @@ "closevector-web": "0.1.6", "cohere-ai": "*", "convex": "^1.3.1", - "couchbase": "^4.3.0", "crypto-js": "^4.2.0", "d3-dsv": "^2.0.0", "discord.js": "^14.14.1", @@ -314,7 +313,6 @@ "mongodb": ">=5.2.0", "mysql2": "^3.9.8", "neo4j-driver": "*", - "node-llama-cpp": "*", "notion-to-md": "^3.1.0", "officeparser": "^4.0.4", "pdf-parse": "1.1.1", @@ -422,10 +420,10 @@ "@huggingface/inference": { "optional": true }, - "@langchain/langgraph": { + "@layerup/layerup-security": { "optional": true }, - "@layerup/layerup-security": { + "@libsql/client": { "optional": true }, "@mendable/firecrawl-js": { @@ -554,9 +552,6 @@ "convex": { "optional": true }, - "couchbase": { - "optional": true - }, "crypto-js": { "optional": true }, @@ -632,9 +627,6 @@ "neo4j-driver": { "optional": true }, - "node-llama-cpp": { - "optional": true - }, "notion-to-md": { "optional": true }, @@ -1549,6 +1541,15 @@ "import": "./vectorstores/lancedb.js", "require": "./vectorstores/lancedb.cjs" }, + "./vectorstores/libsql": { + "types": { + "import": "./vectorstores/libsql.d.ts", + "require": "./vectorstores/libsql.d.cts", + "default": "./vectorstores/libsql.d.ts" + }, + "import": "./vectorstores/libsql.js", + "require": "./vectorstores/libsql.cjs" + }, "./vectorstores/milvus": { "types": { "import": "./vectorstores/milvus.d.ts", @@ -3025,15 +3026,6 @@ "import": "./chains/graph_qa/cypher.js", "require": "./chains/graph_qa/cypher.cjs" }, - "./langgraph/checkpointers/vercel_kv": { - "types": { - "import": "./langgraph/checkpointers/vercel_kv.d.ts", - "require": "./langgraph/checkpointers/vercel_kv.d.cts", - "default": "./langgraph/checkpointers/vercel_kv.d.ts" - }, - "import": "./langgraph/checkpointers/vercel_kv.js", - "require": "./langgraph/checkpointers/vercel_kv.cjs" - }, "./package.json": "./package.json" }, "files": [ @@ -3410,6 +3402,10 @@ "vectorstores/lancedb.js", "vectorstores/lancedb.d.ts", "vectorstores/lancedb.d.cts", + "vectorstores/libsql.cjs", + "vectorstores/libsql.js", + "vectorstores/libsql.d.ts", + "vectorstores/libsql.d.cts", "vectorstores/milvus.cjs", "vectorstores/milvus.js", "vectorstores/milvus.d.ts", @@ -4065,10 +4061,6 @@ "chains/graph_qa/cypher.cjs", "chains/graph_qa/cypher.js", "chains/graph_qa/cypher.d.ts", - "chains/graph_qa/cypher.d.cts", - "langgraph/checkpointers/vercel_kv.cjs", - "langgraph/checkpointers/vercel_kv.js", - "langgraph/checkpointers/vercel_kv.d.ts", - "langgraph/checkpointers/vercel_kv.d.cts" + "chains/graph_qa/cypher.d.cts" ] } diff --git a/libs/langchain-community/src/callbacks/handlers/upstash_ratelimit.ts b/libs/langchain-community/src/callbacks/handlers/upstash_ratelimit.ts index aefc4b380181..255c474adbd0 100644 --- a/libs/langchain-community/src/callbacks/handlers/upstash_ratelimit.ts +++ b/libs/langchain-community/src/callbacks/handlers/upstash_ratelimit.ts @@ -148,7 +148,12 @@ class UpstashRatelimitHandler extends BaseCallbackHandler { _name?: string ): Promise { if (this.tokenRatelimit) { - const remaining = await this.tokenRatelimit.getRemaining(this.identifier); + const result = await this.tokenRatelimit.getRemaining(this.identifier); + + // result of getRemaining was changed from a number to an object in v2.0.0. + // we check to make sure that it works with versions before & after: + const remaining = typeof result === "number" ? result : result.remaining; + if (remaining <= 0) { throw new UpstashRatelimitError("Token limit reached!", "token"); } diff --git a/libs/langchain-community/src/chains/graph_qa/cypher.ts b/libs/langchain-community/src/chains/graph_qa/cypher.ts index a36110c1e062..424905cddc46 100644 --- a/libs/langchain-community/src/chains/graph_qa/cypher.ts +++ b/libs/langchain-community/src/chains/graph_qa/cypher.ts @@ -31,6 +31,8 @@ export interface FromLLMInput { } /** + * Chain for question-answering against a graph by generating Cypher statements. + * * @example * ```typescript * const chain = new GraphCypherQAChain({ @@ -39,6 +41,18 @@ export interface FromLLMInput { * }); * const res = await chain.invoke("Who played in Pulp Fiction?"); * ``` + * + * @security + * This chain will execute Cypher statements against the provided database. + * Make sure that the database connection uses credentials + * that are narrowly-scoped to only include necessary permissions. + * Failure to do so may result in data corruption or loss, since the calling code + * may attempt commands that would result in deletion, mutation of data + * if appropriately prompted or reading sensitive data if such data is present in the database. + * The best way to guard against such negative outcomes is to (as appropriate) limit the + * permissions granted to the credentials used with this tool. + * + * See https://js.langchain.com/docs/security for more information. */ export class GraphCypherQAChain extends BaseChain { private graph: Neo4jGraph; diff --git a/libs/langchain-community/src/chat_models/bedrock/index.ts b/libs/langchain-community/src/chat_models/bedrock/index.ts index 9802beffe4cb..9cf533a4c3f5 100644 --- a/libs/langchain-community/src/chat_models/bedrock/index.ts +++ b/libs/langchain-community/src/chat_models/bedrock/index.ts @@ -21,9 +21,9 @@ export interface BedrockChatFields * * ```bash * npm install @langchain/openai - * export BEDROCK_AWS_REGION="your-aws-region" - * export BEDROCK_AWS_SECRET_ACCESS_KEY="your-aws-secret-access-key" - * export BEDROCK_AWS_ACCESS_KEY_ID="your-aws-access-key-id" + * export AWS_REGION="your-aws-region" + * export AWS_SECRET_ACCESS_KEY="your-aws-secret-access-key" + * export AWS_ACCESS_KEY_ID="your-aws-access-key-id" * ``` * * ## [Constructor args](/classes/langchain_community_chat_models_bedrock.BedrockChat.html#constructor) @@ -60,15 +60,21 @@ export interface BedrockChatFields * const llm = new BedrockChat({ * region: process.env.BEDROCK_AWS_REGION, * maxRetries: 0, - * credentials: { - * secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!, - * accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!, - * }, * model: "anthropic.claude-3-5-sonnet-20240620-v1:0", * temperature: 0, * maxTokens: undefined, * // other params... * }); + * + * // You can also pass credentials in explicitly: + * const llmWithCredentials = new BedrockChat({ + * region: process.env.BEDROCK_AWS_REGION, + * model: "anthropic.claude-3-5-sonnet-20240620-v1:0", + * credentials: { + * secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!, + * accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!, + * }, + * }); * ``` * * diff --git a/libs/langchain-community/src/chat_models/bedrock/web.ts b/libs/langchain-community/src/chat_models/bedrock/web.ts index 69a0bb42eb8b..8e227a81ec2d 100644 --- a/libs/langchain-community/src/chat_models/bedrock/web.ts +++ b/libs/langchain-community/src/chat_models/bedrock/web.ts @@ -183,9 +183,9 @@ export interface BedrockChatFields * * ```bash * npm install @langchain/openai - * export BEDROCK_AWS_REGION="your-aws-region" - * export BEDROCK_AWS_SECRET_ACCESS_KEY="your-aws-secret-access-key" - * export BEDROCK_AWS_ACCESS_KEY_ID="your-aws-access-key-id" + * export AWS_REGION="your-aws-region" + * export AWS_SECRET_ACCESS_KEY="your-aws-secret-access-key" + * export AWS_ACCESS_KEY_ID="your-aws-access-key-id" * ``` * * ## [Constructor args](/classes/langchain_community_chat_models_bedrock.BedrockChat.html#constructor) @@ -217,20 +217,26 @@ export interface BedrockChatFields * Instantiate * * ```typescript - * import { BedrockChat } from '@langchain/community/chat_models/bedrock'; + * import { BedrockChat } from '@langchain/community/chat_models/bedrock/web'; * * const llm = new BedrockChat({ - * region: process.env.BEDROCK_AWS_REGION, + * region: process.env.AWS_REGION, * maxRetries: 0, - * credentials: { - * secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!, - * accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!, - * }, * model: "anthropic.claude-3-5-sonnet-20240620-v1:0", * temperature: 0, * maxTokens: undefined, * // other params... * }); + * + * // You can also pass credentials in explicitly: + * const llmWithCredentials = new BedrockChat({ + * region: process.env.BEDROCK_AWS_REGION, + * model: "anthropic.claude-3-5-sonnet-20240620-v1:0", + * credentials: { + * secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!, + * accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!, + * }, + * }); * ``` * * @@ -542,8 +548,12 @@ export class BedrockChat get lc_secrets(): { [key: string]: string } | undefined { return { - "credentials.accessKeyId": "BEDROCK_AWS_ACCESS_KEY_ID", - "credentials.secretAccessKey": "BEDROCK_AWS_SECRET_ACCESS_KEY", + "credentials.accessKeyId": "AWS_ACCESS_KEY_ID", + "credentials.secretAccessKey": "AWS_SECRET_ACCESS_KEY", + "credentials.sessionToken": "AWS_SECRET_ACCESS_KEY", + awsAccessKeyId: "AWS_ACCESS_KEY_ID", + awsSecretAccessKey: "AWS_SECRET_ACCESS_KEY", + awsSessionToken: "AWS_SESSION_TOKEN", }; } @@ -566,7 +576,32 @@ export class BedrockChat } constructor(fields?: BedrockChatFields) { - super(fields ?? {}); + const awsAccessKeyId = + fields?.awsAccessKeyId ?? getEnvironmentVariable("AWS_ACCESS_KEY_ID"); + const awsSecretAccessKey = + fields?.awsSecretAccessKey ?? + getEnvironmentVariable("AWS_SECRET_ACCESS_KEY"); + const awsSessionToken = + fields?.awsSessionToken ?? getEnvironmentVariable("AWS_SESSION_TOKEN"); + + let credentials = fields?.credentials; + if (credentials === undefined) { + if (awsAccessKeyId === undefined || awsSecretAccessKey === undefined) { + throw new Error( + "Please set your AWS credentials in the 'credentials' field or set env vars AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, and optionally AWS_SESSION_TOKEN." + ); + } + credentials = { + accessKeyId: awsAccessKeyId, + secretAccessKey: awsSecretAccessKey, + sessionToken: awsSessionToken, + }; + } + + // eslint-disable-next-line no-param-reassign + fields = { ...fields, awsAccessKeyId, awsSecretAccessKey, awsSessionToken }; + + super(fields); this.model = fields?.model ?? this.model; this.modelProvider = getModelProvider(this.model); @@ -585,12 +620,6 @@ export class BedrockChat } this.region = region; - const credentials = fields?.credentials; - if (!credentials) { - throw new Error( - "Please set the AWS credentials in the 'credentials' field." - ); - } this.credentials = credentials; this.temperature = fields?.temperature ?? this.temperature; diff --git a/libs/langchain-community/src/chat_models/llama_cpp.ts b/libs/langchain-community/src/chat_models/llama_cpp.ts index 0c1c25f20445..94685dc087df 100644 --- a/libs/langchain-community/src/chat_models/llama_cpp.ts +++ b/libs/langchain-community/src/chat_models/llama_cpp.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { LlamaModel, LlamaContext, diff --git a/libs/langchain-community/src/chat_models/tests/chatbedrock.test.ts b/libs/langchain-community/src/chat_models/tests/chatbedrock.test.ts index 4caba4ee3a5c..666daca6fc68 100644 --- a/libs/langchain-community/src/chat_models/tests/chatbedrock.test.ts +++ b/libs/langchain-community/src/chat_models/tests/chatbedrock.test.ts @@ -26,3 +26,34 @@ test("Test Bedrock identifying params", async () => { model, }); }); + +test("Test Bedrock serialization", async () => { + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + const bedrock = new BedrockChat({ + region: "us-west-2", + model: "anthropic.claude-3-sonnet-20240229-v1:0", + credentials: { + accessKeyId: "unused", + secretAccessKey: "unused", + sessionToken: "unused", + }, + }); + + expect(JSON.stringify(bedrock)).toEqual( + `{"lc":1,"type":"constructor","id":["langchain","chat_models","bedrock","BedrockChat"],"kwargs":{"region_name":"us-west-2","model_id":"anthropic.claude-3-sonnet-20240229-v1:0","credentials":{"accessKeyId":{"lc":1,"type":"secret","id":["AWS_ACCESS_KEY_ID"]},"secretAccessKey":{"lc":1,"type":"secret","id":["AWS_SECRET_ACCESS_KEY"]},"sessionToken":{"lc":1,"type":"secret","id":["AWS_SECRET_ACCESS_KEY"]}}}}` + ); +}); + +test("Test Bedrock serialization from environment variables", async () => { + process.env.AWS_ACCESS_KEY_ID = "foo"; + process.env.AWS_SECRET_ACCESS_KEY = "bar"; + const bedrock = new BedrockChat({ + region: "us-west-2", + model: "anthropic.claude-3-sonnet-20240229-v1:0", + }); + + expect(JSON.stringify(bedrock)).toEqual( + `{"lc":1,"type":"constructor","id":["langchain","chat_models","bedrock","BedrockChat"],"kwargs":{"region_name":"us-west-2","model_id":"anthropic.claude-3-sonnet-20240229-v1:0","aws_access_key_id":{"lc":1,"type":"secret","id":["AWS_ACCESS_KEY_ID"]},"aws_secret_access_key":{"lc":1,"type":"secret","id":["AWS_SECRET_ACCESS_KEY"]},"credentials":{"accessKeyId":{"lc":1,"type":"secret","id":["AWS_ACCESS_KEY_ID"]},"secretAccessKey":{"lc":1,"type":"secret","id":["AWS_SECRET_ACCESS_KEY"]}}}}` + ); +}); diff --git a/libs/langchain-community/src/document_loaders/web/apify_dataset.ts b/libs/langchain-community/src/document_loaders/web/apify_dataset.ts index 0f5b1dceaed5..eda81f5affd4 100644 --- a/libs/langchain-community/src/document_loaders/web/apify_dataset.ts +++ b/libs/langchain-community/src/document_loaders/web/apify_dataset.ts @@ -81,17 +81,50 @@ export class ApifyDatasetLoader> super(); const { clientOptions, datasetMappingFunction, ...asyncCallerParams } = config; - const token = ApifyDatasetLoader._getApifyApiToken(clientOptions); - this.apifyClient = new ApifyClient({ ...clientOptions, token }); + this.apifyClient = ApifyDatasetLoader._getApifyClient(clientOptions); this.datasetId = datasetId; this.datasetMappingFunction = datasetMappingFunction; this.caller = new AsyncCaller(asyncCallerParams); } + /** + * Creates an instance of the ApifyClient class with the provided clientOptions. + * Adds a User-Agent header to the request config for langchainjs attribution. + * @param clientOptions + * @private + */ + private static _getApifyClient( + clientOptions?: ApifyClientOptions + ): ApifyClient { + const token = ApifyDatasetLoader._getApifyApiToken(clientOptions); + const updatedClientOptions = { + ...clientOptions, + token, + requestInterceptors: [ + ...(clientOptions?.requestInterceptors ?? []), + ApifyDatasetLoader._addUserAgent, + ], + }; + return new ApifyClient({ ...updatedClientOptions, token }); + } + private static _getApifyApiToken(config?: { token?: string }) { return config?.token ?? getEnvironmentVariable("APIFY_API_TOKEN"); } + /** + * Adds a User-Agent header to the request config. + * @param config + * @private + */ + private static _addUserAgent(config: any): any { + const updatedConfig = { ...config }; + updatedConfig.headers ??= {}; + updatedConfig.headers["User-Agent"] = + (updatedConfig.headers["User-Agent"] ?? "") + "; Origin/langchainjs"; + return updatedConfig; + } + /** * Retrieves the dataset items from the Apify platform and applies the * datasetMappingFunction to each item to create an array of Document @@ -116,8 +149,8 @@ export class ApifyDatasetLoader> * Create an ApifyDatasetLoader by calling an Actor on the Apify platform and waiting for its results to be ready. * @param actorId The ID or name of the Actor on the Apify platform. * @param input The input object of the Actor that you're trying to run. - * @param options Options specifying settings for the Actor run. - * @param options.datasetMappingFunction A function that takes a single object (an Apify dataset item) and converts it to an instance of the Document class. + * @param config Options specifying settings for the Actor run. + * @param config.datasetMappingFunction A function that takes a single object (an Apify dataset item) and converts it to an instance of the Document class. * @returns An instance of `ApifyDatasetLoader` with the results from the Actor run. */ static async fromActorCall>( @@ -132,8 +165,9 @@ export class ApifyDatasetLoader> const apifyApiToken = ApifyDatasetLoader._getApifyApiToken( config.clientOptions ); - const apifyClient = new ApifyClient({ token: apifyApiToken }); - + const apifyClient = ApifyDatasetLoader._getApifyClient( + config.clientOptions + ); const actorCall = await apifyClient .actor(actorId) .call(input, config.callOptions ?? {}); @@ -148,8 +182,10 @@ export class ApifyDatasetLoader> * Create an ApifyDatasetLoader by calling a saved Actor task on the Apify platform and waiting for its results to be ready. * @param taskId The ID or name of the task on the Apify platform. * @param input The input object of the task that you're trying to run. Overrides the task's saved input. - * @param options Options specifying settings for the task run. - * @param options.datasetMappingFunction A function that takes a single object (an Apify dataset item) and converts it to an instance of the Document class. + * @param config Options specifying settings for the task run. + * @param config.callOptions Options specifying settings for the task run. + * @param config.clientOptions Options specifying settings for the Apify client. + * @param config.datasetMappingFunction A function that takes a single object (an Apify dataset item) and converts it to an instance of the Document class. * @returns An instance of `ApifyDatasetLoader` with the results from the task's run. */ static async fromActorTaskCall>( @@ -164,8 +200,9 @@ export class ApifyDatasetLoader> const apifyApiToken = ApifyDatasetLoader._getApifyApiToken( config.clientOptions ); - const apifyClient = new ApifyClient({ token: apifyApiToken }); - + const apifyClient = ApifyDatasetLoader._getApifyClient( + config.clientOptions + ); const taskCall = await apifyClient .task(taskId) .call(input, config.callOptions ?? {}); diff --git a/libs/langchain-community/src/document_loaders/web/couchbase.ts b/libs/langchain-community/src/document_loaders/web/couchbase.ts index 5b611a6af196..ba5e75985e15 100644 --- a/libs/langchain-community/src/document_loaders/web/couchbase.ts +++ b/libs/langchain-community/src/document_loaders/web/couchbase.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { Cluster, QueryResult } from "couchbase"; import { Document } from "@langchain/core/documents"; import { diff --git a/libs/langchain-community/src/embeddings/llama_cpp.ts b/libs/langchain-community/src/embeddings/llama_cpp.ts index aad4163ac499..ab0a60cff60b 100644 --- a/libs/langchain-community/src/embeddings/llama_cpp.ts +++ b/libs/langchain-community/src/embeddings/llama_cpp.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { LlamaModel, LlamaContext } from "node-llama-cpp"; import { Embeddings, type EmbeddingsParams } from "@langchain/core/embeddings"; import { diff --git a/libs/langchain-community/src/experimental/llms/chrome_ai.ts b/libs/langchain-community/src/experimental/llms/chrome_ai.ts index dc543fe7c043..c41ba13121a6 100644 --- a/libs/langchain-community/src/experimental/llms/chrome_ai.ts +++ b/libs/langchain-community/src/experimental/llms/chrome_ai.ts @@ -5,29 +5,99 @@ import { GenerationChunk } from "@langchain/core/outputs"; import { IterableReadableStream } from "@langchain/core/utils/stream"; import { BaseLLMParams, LLM } from "@langchain/core/language_models/llms"; -export interface AI { - canCreateTextSession(): Promise; - createTextSession(options?: AITextSessionOptions): Promise; - defaultTextSessionOptions(): Promise; +export interface AILanguageModelFactory { + create(options?: AILanguageModelCreateOptions): Promise; + capabilities(): Promise; } -export interface AITextSession { - prompt(input: string): Promise; - promptStreaming(input: string): ReadableStream; +export interface AILanguageModel extends EventTarget { + prompt( + input: AILanguageModelPromptInput, + options?: AILanguageModelPromptOptions + ): Promise; + promptStreaming( + input: AILanguageModelPromptInput, + options?: AILanguageModelPromptOptions + ): ReadableStream; + + countPromptTokens( + input: AILanguageModelPromptInput, + options?: AILanguageModelPromptOptions + ): Promise; + + get maxTokens(): number; + get tokensSoFar(): number; + get tokensLeft(): number; + + get topK(): number; + get temperature(): number; + + oncontextoverflow: (event: Event) => void; + + clone(options?: AILanguageModelCloneOptions): Promise; destroy(): void; - clone(): AITextSession; } -export interface AITextSessionOptions { +interface AILanguageModelCapabilities { + readonly available: AICapabilityAvailability; + languageAvailable(languageTag: string): AICapabilityAvailability; + + get defaultTopK(): number | undefined; + get maxTopK(): number | undefined; + get defaultTemperature(): number | undefined; + get maxTemperature(): number | undefined; +} + +interface AILanguageModelCreateOptions { + signal?: AbortSignal; + monitor?: AICreateMonitorCallback; + systemPrompt?: string; + initialPrompts?: AILanguageModelInitialPrompt[]; topK: number; temperature: number; } -export type AIModelAvailability = "readily" | "after-download" | "no"; +export interface AILanguageModelInitialPrompt { + role: AILanguageModelInitialPromptRole; + content: string; +} + +export interface AILanguageModelPrompt { + role: AILanguageModelPromptRole; + content: string; +} + +export interface AILanguageModelPromptOptions { + signal?: AbortSignal; +} + +export interface AILanguageModelCloneOptions { + signal?: AbortSignal; +} + +export type AILanguageModelPromptInput = + | string + | AILanguageModelPrompt + | AILanguageModelPrompt[]; + +enum AILanguageModelInitialPromptRole { + "system", + "user", + "assistant", +} + +enum AILanguageModelPromptRole { + "user", + "assistant", +} + +export type AICapabilityAvailability = "yes" | "no"; +export type AICreateMonitorCallback = () => void; export interface ChromeAIInputs extends BaseLLMParams { topK?: number; temperature?: number; + systemPrompt?: string; } export interface ChromeAICallOptions extends BaseLanguageModelCallOptions {} @@ -52,11 +122,11 @@ export interface ChromeAICallOptions extends BaseLanguageModelCallOptions {} * ``` */ export class ChromeAI extends LLM { - session?: AITextSession; + temperature?: number; - temperature = 0.5; + topK?: number; - topK = 40; + systemPrompt?: string; static lc_name() { return "ChromeAI"; @@ -68,6 +138,7 @@ export class ChromeAI extends LLM { }); this.temperature = inputs?.temperature ?? this.temperature; this.topK = inputs?.topK ?? this.topK; + this.systemPrompt = inputs?.systemPrompt; } _llmType() { @@ -78,50 +149,33 @@ export class ChromeAI extends LLM { * Initialize the model. This method may be called before invoking the model * to set up a chat session in advance. */ - async initialize() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let ai: any; + protected async createSession() { // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (typeof window !== "undefined" && (window as any).ai !== undefined) { - // Browser context + let aiInstance: any; + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore Experimental browser-only global + aiInstance = ai; // eslint-disable-next-line @typescript-eslint/no-explicit-any - ai = (window as any).ai; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } else if (typeof self !== undefined && (self as any).ai !== undefined) { - // Worker context - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ai = (self as any).ai; - } else { + } catch (e: any) { throw new Error( - "Could not initialize ChromeAI instance. Make sure you are running a version of Chrome with the proper experimental flags enabled." + `Could not initialize ChromeAI instance. Make sure you are running a version of Chrome with the proper experimental flags enabled.\n\nError message: ${e.message}` ); } - const canCreateTextSession: AIModelAvailability = - await ai.canCreateTextSession(); - if (canCreateTextSession === "no") { + const { available } = await aiInstance.languageModel.capabilities(); + if (available === "no") { throw new Error("The AI model is not available."); - } else if (canCreateTextSession === "after-download") { + } else if (available === "after-download") { throw new Error("The AI model is not yet downloaded."); } - this.session = await ai.createTextSession({ + const session = await aiInstance.languageModel.create({ + systemPrompt: this.systemPrompt, topK: this.topK, temperature: this.temperature, }); - } - /** - * Call `.destroy()` to free resources if you no longer need a session. - * When a session is destroyed, it can no longer be used, and any ongoing - * execution will be aborted. You may want to keep the session around if - * you intend to prompt the model often since creating a session can take - * some time. - */ - destroy() { - if (!this.session) { - return console.log("No session found. Returning."); - } - this.session.destroy(); + return session; } async *_streamResponseChunks( @@ -129,22 +183,26 @@ export class ChromeAI extends LLM { _options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun ): AsyncGenerator { - if (!this.session) { - await this.initialize(); - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const stream = this.session!.promptStreaming(prompt); - const iterableStream = IterableReadableStream.fromReadableStream(stream); - - let previousContent = ""; - for await (const chunk of iterableStream) { - const newContent = chunk.slice(previousContent.length); - previousContent += newContent; - yield new GenerationChunk({ - text: newContent, - }); - await runManager?.handleLLMNewToken(newContent); + let session; + try { + session = await this.createSession(); + + const stream = session.promptStreaming(prompt); + const iterableStream = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + IterableReadableStream.fromReadableStream(stream); + + let previousContent = ""; + for await (const chunk of iterableStream) { + const newContent = chunk.slice(previousContent.length); + previousContent += newContent; + yield new GenerationChunk({ + text: newContent, + }); + await runManager?.handleLLMNewToken(newContent); + } + } finally { + session?.destroy(); } } diff --git a/libs/langchain-community/src/langgraph/checkpointers/tests/checkpointer.int.test.ts b/libs/langchain-community/src/langgraph/checkpointers/tests/checkpointer.int.test.ts deleted file mode 100644 index 59596040080a..000000000000 --- a/libs/langchain-community/src/langgraph/checkpointers/tests/checkpointer.int.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* eslint-disable no-process-env */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ - -import { describe, test, expect } from "@jest/globals"; -import { Checkpoint, CheckpointTuple } from "@langchain/langgraph"; -import { VercelKVSaver } from "../vercel_kv.js"; - -const checkpoint1: Checkpoint = { - v: 1, - id: "1ef390c8-3ed9-6132-ffff-12d236274621", - ts: "2024-04-19T17:19:07.952Z", - channel_values: { - someKey1: "someValue1", - }, - channel_versions: { - someKey2: 1, - }, - versions_seen: { - someKey3: { - someKey4: 1, - }, - }, - pending_sends: [], -}; - -const checkpoint2: Checkpoint = { - v: 1, - id: "1ef390c8-3ed9-6133-8001-419c612dad04", - ts: "2024-04-20T17:19:07.952Z", - channel_values: { - someKey1: "someValue2", - }, - channel_versions: { - someKey2: 2, - }, - versions_seen: { - someKey3: { - someKey4: 2, - }, - }, - pending_sends: [], -}; - -describe("VercelKVSaver", () => { - const vercelSaver = new VercelKVSaver({ - url: process.env.VERCEL_KV_API_URL!, - token: process.env.VERCEL_KV_API_TOKEN!, - }); - - test("should save and retrieve checkpoints correctly", async () => { - // save checkpoint - const runnableConfig = await vercelSaver.put( - { configurable: { thread_id: "1" } }, - checkpoint1, - { source: "update", step: -1, writes: null } - ); - expect(runnableConfig).toEqual({ - configurable: { - thread_id: "1", - checkpoint_id: checkpoint1.id, - }, - }); - - // get checkpoint tuple - const checkpointTuple = await vercelSaver.getTuple({ - configurable: { thread_id: "1" }, - }); - expect(checkpointTuple?.config).toEqual({ - configurable: { - thread_id: "1", - checkpoint_id: checkpoint1.id, - }, - }); - expect(checkpointTuple?.checkpoint).toEqual(checkpoint1); - - // save another checkpoint - await vercelSaver.put( - { - configurable: { - thread_id: "1", - }, - }, - checkpoint2, - { source: "update", step: -1, writes: null } - ); - // list checkpoints - const checkpointTupleGenerator = vercelSaver.list({ - configurable: { thread_id: "1" }, - }); - - const checkpointTuples: CheckpointTuple[] = []; - - for await (const checkpoint of checkpointTupleGenerator) { - checkpointTuples.push(checkpoint); - } - expect(checkpointTuples.length).toBe(2); - - const checkpointTuple1 = checkpointTuples[0]; - const checkpointTuple2 = checkpointTuples[1]; - - expect(checkpointTuple1.checkpoint.ts).toBe("2024-04-20T17:19:07.952Z"); - expect(checkpointTuple2.checkpoint.ts).toBe("2024-04-19T17:19:07.952Z"); - }); -}); diff --git a/libs/langchain-community/src/langgraph/checkpointers/vercel_kv.ts b/libs/langchain-community/src/langgraph/checkpointers/vercel_kv.ts deleted file mode 100644 index d9f68eebe7ff..000000000000 --- a/libs/langchain-community/src/langgraph/checkpointers/vercel_kv.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { VercelKV, createClient } from "@vercel/kv"; - -import { RunnableConfig } from "@langchain/core/runnables"; -import { - BaseCheckpointSaver, - Checkpoint, - CheckpointMetadata, - CheckpointTuple, - SerializerProtocol, -} from "@langchain/langgraph/web"; - -// snake_case is used to match Python implementation -interface KVRow { - checkpoint: string; - metadata: string; -} - -interface KVConfig { - url: string; - token: string; -} - -export class VercelKVSaver extends BaseCheckpointSaver { - private kv: VercelKV; - - constructor(config: KVConfig, serde?: SerializerProtocol) { - super(serde); - this.kv = createClient(config); - } - - async getTuple(config: RunnableConfig): Promise { - const thread_id = config.configurable?.thread_id; - const checkpoint_id = config.configurable?.checkpoint_id; - - if (!thread_id) { - return undefined; - } - - const key = checkpoint_id - ? `${thread_id}:${checkpoint_id}` - : `${thread_id}:last`; - - const row: KVRow | null = await this.kv.get(key); - - if (!row) { - return undefined; - } - - const [checkpoint, metadata] = await Promise.all([ - this.serde.parse(row.checkpoint), - this.serde.parse(row.metadata), - ]); - - return { - checkpoint: checkpoint as Checkpoint, - metadata: metadata as CheckpointMetadata, - config: checkpoint_id - ? config - : { - configurable: { - thread_id, - checkpoint_id: (checkpoint as Checkpoint).id, - }, - }, - }; - } - - async *list( - config: RunnableConfig, - limit?: number, - before?: RunnableConfig - ): AsyncGenerator { - const thread_id: string = config.configurable?.thread_id; - - // LUA script to get keys excluding those starting with "last" - const luaScript = ` - local prefix = ARGV[1] - local cursor = '0' - local result = {} - repeat - local scanResult = redis.call('SCAN', cursor, 'MATCH', prefix .. '*', 'COUNT', 1000) - cursor = scanResult[1] - local keys = scanResult[2] - for _, key in ipairs(keys) do - if key:sub(-5) ~= ':last' then - table.insert(result, key) - end - end - until cursor == '0' - return result - `; - - // Execute the LUA script with the thread_id as an argument - const keys: string[] = await this.kv.eval(luaScript, [], [thread_id]); - - const filteredKeys = keys.filter((key: string) => { - const [, checkpoint_id] = key.split(":"); - - return !before || checkpoint_id < before?.configurable?.checkpoint_id; - }); - - const sortedKeys = filteredKeys - .sort((a: string, b: string) => b.localeCompare(a)) - .slice(0, limit); - - const rows: (KVRow | null)[] = await this.kv.mget(...sortedKeys); - for (const row of rows) { - if (row) { - const [checkpoint, metadata] = await Promise.all([ - this.serde.parse(row.checkpoint), - this.serde.parse(row.metadata), - ]); - - yield { - config: { - configurable: { - thread_id, - checkpoint_id: (checkpoint as Checkpoint).id, - }, - }, - checkpoint: checkpoint as Checkpoint, - metadata: metadata as CheckpointMetadata, - }; - } - } - } - - async put( - config: RunnableConfig, - checkpoint: Checkpoint, - metadata: CheckpointMetadata - ): Promise { - const thread_id = config.configurable?.thread_id; - - if (!thread_id || !checkpoint.id) { - throw new Error("Thread ID and Checkpoint ID must be defined"); - } - - const row: KVRow = { - checkpoint: this.serde.stringify(checkpoint), - metadata: this.serde.stringify(metadata), - }; - - // LUA script to set checkpoint data atomically" - const luaScript = ` - local thread_id = ARGV[1] - local checkpoint_id = ARGV[2] - local row = ARGV[3] - - redis.call('SET', thread_id .. ':' .. checkpoint_id, row) - redis.call('SET', thread_id .. ':last', row) - `; - - // Save the checkpoint and the last checkpoint - await this.kv.eval(luaScript, [], [thread_id, checkpoint.id, row]); - - return { - configurable: { - thread_id, - checkpoint_id: checkpoint.id, - }, - }; - } -} diff --git a/libs/langchain-community/src/llms/llama_cpp.ts b/libs/langchain-community/src/llms/llama_cpp.ts index 249fb0071a7a..e39831299829 100644 --- a/libs/langchain-community/src/llms/llama_cpp.ts +++ b/libs/langchain-community/src/llms/llama_cpp.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { LlamaModel, LlamaContext, diff --git a/libs/langchain-community/src/llms/raycast.ts b/libs/langchain-community/src/llms/raycast.ts index 233a02f00776..4befeb0fbb0d 100644 --- a/libs/langchain-community/src/llms/raycast.ts +++ b/libs/langchain-community/src/llms/raycast.ts @@ -53,7 +53,11 @@ export class RaycastAI extends LLM implements RaycastAIInput { throw new Error("Raycast AI environment is not accessible."); } - this.model = fields.model ?? "text-davinci-003"; + if (fields.model === undefined) { + throw new Error(`You must provide a "model" field in your params.`); + } + + this.model = fields.model; this.creativity = fields.creativity ?? 0.5; this.rateLimitPerMinute = fields.rateLimitPerMinute ?? 10; } diff --git a/libs/langchain-community/src/load/import_constants.ts b/libs/langchain-community/src/load/import_constants.ts index 238b9cc9b707..963f64864a04 100644 --- a/libs/langchain-community/src/load/import_constants.ts +++ b/libs/langchain-community/src/load/import_constants.ts @@ -53,6 +53,7 @@ export const optionalImportEntrypoints: string[] = [ "langchain_community/vectorstores/hnswlib", "langchain_community/vectorstores/hanavector", "langchain_community/vectorstores/lancedb", + "langchain_community/vectorstores/libsql", "langchain_community/vectorstores/milvus", "langchain_community/vectorstores/momento_vector_index", "langchain_community/vectorstores/mongodb_atlas", @@ -178,5 +179,4 @@ export const optionalImportEntrypoints: string[] = [ "langchain_community/experimental/hubs/makersuite/googlemakersuitehub", "langchain_community/experimental/tools/pyinterpreter", "langchain_community/chains/graph_qa/cypher", - "langchain_community/langgraph/checkpointers/vercel_kv", ]; diff --git a/libs/langchain-community/src/structured_query/supabase.ts b/libs/langchain-community/src/structured_query/supabase.ts index 7f09b522d009..602f46bfebfa 100644 --- a/libs/langchain-community/src/structured_query/supabase.ts +++ b/libs/langchain-community/src/structured_query/supabase.ts @@ -224,7 +224,7 @@ export class SupabaseTranslator< attribute, value, false - )}.${comparator}.${value}}`; + )}.${comparator}.${value}`; } /** diff --git a/libs/langchain-community/src/utils/bedrock/index.ts b/libs/langchain-community/src/utils/bedrock/index.ts index 551787dbe6d5..9b3203e8ce76 100644 --- a/libs/langchain-community/src/utils/bedrock/index.ts +++ b/libs/langchain-community/src/utils/bedrock/index.ts @@ -168,6 +168,12 @@ export interface BaseBedrockInput { tagSuffix: string; streamProcessingMode: "SYNCHRONOUS" | "ASYNCHRONOUS"; }; + + awsAccessKeyId?: string; + + awsSecretAccessKey?: string; + + awsSessionToken?: string; } type Dict = { [key: string]: unknown }; diff --git a/libs/langchain-community/src/utils/llama_cpp.ts b/libs/langchain-community/src/utils/llama_cpp.ts index 980921d6dcef..3bc37c6861d8 100644 --- a/libs/langchain-community/src/utils/llama_cpp.ts +++ b/libs/langchain-community/src/utils/llama_cpp.ts @@ -1,3 +1,4 @@ +/* eslint-disable import/no-extraneous-dependencies */ import { LlamaModel, LlamaContext, diff --git a/libs/langchain-community/src/vectorstores/couchbase.ts b/libs/langchain-community/src/vectorstores/couchbase.ts index dad489225f29..fdceedae22cf 100644 --- a/libs/langchain-community/src/vectorstores/couchbase.ts +++ b/libs/langchain-community/src/vectorstores/couchbase.ts @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ /* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable import/no-extraneous-dependencies */ import { EmbeddingsInterface } from "@langchain/core/embeddings"; import { VectorStore } from "@langchain/core/vectorstores"; import { diff --git a/libs/langchain-community/src/vectorstores/hanavector.ts b/libs/langchain-community/src/vectorstores/hanavector.ts index 7650eeb40bdd..8f55568adb27 100644 --- a/libs/langchain-community/src/vectorstores/hanavector.ts +++ b/libs/langchain-community/src/vectorstores/hanavector.ts @@ -269,8 +269,11 @@ export class HanaDB extends VectorStore { throw new Error(`Column ${columnName} has the wrong type: ${dataType}`); } + // Length can either be -1 (QRC01+02-24) or 0 (QRC03-24 onwards) + // to indicate no length constraint being present. + // Check length, if parameter was provided - if (columnLength !== undefined && length !== columnLength) { + if (columnLength !== undefined && length !== columnLength && length > 0) { throw new Error(`Column ${columnName} has the wrong length: ${length}`); } } diff --git a/libs/langchain-community/src/vectorstores/lancedb.ts b/libs/langchain-community/src/vectorstores/lancedb.ts index def5a6d61cb4..7df73aac93e7 100644 --- a/libs/langchain-community/src/vectorstores/lancedb.ts +++ b/libs/langchain-community/src/vectorstores/lancedb.ts @@ -1,4 +1,4 @@ -import { Table } from "vectordb"; +import { connect, Table, Connection, WriteMode } from "vectordb"; import type { EmbeddingsInterface } from "@langchain/core/embeddings"; import { VectorStore } from "@langchain/core/vectorstores"; import { Document } from "@langchain/core/documents"; @@ -8,8 +8,11 @@ import { Document } from "@langchain/core/documents"; * table and an optional textKey. */ export type LanceDBArgs = { - table: Table; + table?: Table; textKey?: string; + uri?: string; + tableName?: string; + mode?: WriteMode; }; /** @@ -18,15 +21,24 @@ export type LanceDBArgs = { * embeddings. */ export class LanceDB extends VectorStore { - private table: Table; + private table?: Table; private textKey: string; - constructor(embeddings: EmbeddingsInterface, args: LanceDBArgs) { - super(embeddings, args); - this.table = args.table; + private uri: string; + + private tableName: string; + + private mode?: WriteMode; + + constructor(embeddings: EmbeddingsInterface, args?: LanceDBArgs) { + super(embeddings, args || {}); + this.table = args?.table; this.embeddings = embeddings; - this.textKey = args.textKey || "text"; + this.textKey = args?.textKey || "text"; + this.uri = args?.uri || "~/lancedb"; + this.tableName = args?.tableName || "langchain"; + this.mode = args?.mode || WriteMode.Overwrite; } /** @@ -71,6 +83,14 @@ export class LanceDB extends VectorStore { }); data.push(record); } + if (!this.table) { + const db: Connection = await connect(this.uri); + this.table = await db.createTable(this.tableName, data, { + writeMode: this.mode, + }); + + return; + } await this.table.add(data); } @@ -85,6 +105,11 @@ export class LanceDB extends VectorStore { query: number[], k: number ): Promise<[Document, number][]> { + if (!this.table) { + throw new Error( + "Table not found. Please add vectors to the table first." + ); + } const results = await this.table.search(query).limit(k).execute(); const docsAndScore: [Document, number][] = []; @@ -119,7 +144,7 @@ export class LanceDB extends VectorStore { texts: string[], metadatas: object[] | object, embeddings: EmbeddingsInterface, - dbConfig: LanceDBArgs + dbConfig?: LanceDBArgs ): Promise { const docs: Document[] = []; for (let i = 0; i < texts.length; i += 1) { @@ -143,7 +168,7 @@ export class LanceDB extends VectorStore { static async fromDocuments( docs: Document[], embeddings: EmbeddingsInterface, - dbConfig: LanceDBArgs + dbConfig?: LanceDBArgs ): Promise { const instance = new this(embeddings, dbConfig); await instance.addDocuments(docs); diff --git a/libs/langchain-community/src/vectorstores/libsql.ts b/libs/langchain-community/src/vectorstores/libsql.ts new file mode 100644 index 000000000000..3740c62d99db --- /dev/null +++ b/libs/langchain-community/src/vectorstores/libsql.ts @@ -0,0 +1,209 @@ +import type { Client } from "@libsql/client"; +import { VectorStore } from "@langchain/core/vectorstores"; +import type { EmbeddingsInterface } from "@langchain/core/embeddings"; +import { Document } from "@langchain/core/documents"; + +/** + * Interface for LibSQLVectorStore configuration options. + */ +export interface LibSQLVectorStoreArgs { + db: Client; + /** Name of the table to store vectors. Defaults to "vectors". */ + table?: string; + /** Name of the column to store embeddings. Defaults to "embedding". */ + column?: string; + // TODO: Support adding additional columns to the table for metadata. +} + +/** + * A vector store using LibSQL/Turso for storage and retrieval. + */ +export class LibSQLVectorStore extends VectorStore { + declare FilterType: (doc: Document) => boolean; + + private db; + + private readonly table: string; + + private readonly column: string; + + /** + * Returns the type of vector store. + * @returns {string} The string "libsql". + */ + _vectorstoreType(): string { + return "libsql"; + } + + /** + * Initializes a new instance of the LibSQLVectorStore. + * @param {EmbeddingsInterface} embeddings - The embeddings interface to use. + * @param {Client} db - The LibSQL client instance. + * @param {LibSQLVectorStoreArgs} options - Configuration options for the vector store. + */ + constructor(embeddings: EmbeddingsInterface, options: LibSQLVectorStoreArgs) { + super(embeddings, options); + + this.db = options.db; + this.table = options.table || "vectors"; + this.column = options.column || "embedding"; + } + + /** + * Adds documents to the vector store. + * @param {Document[]} documents - The documents to add. + * @returns {Promise} The IDs of the added documents. + */ + async addDocuments(documents: Document[]): Promise { + const texts = documents.map(({ pageContent }) => pageContent); + const embeddings = await this.embeddings.embedDocuments(texts); + + return this.addVectors(embeddings, documents); + } + + /** + * Adds vectors to the vector store. + * @param {number[][]} vectors - The vectors to add. + * @param {Document[]} documents - The documents associated with the vectors. + * @returns {Promise} The IDs of the added vectors. + */ + async addVectors( + vectors: number[][], + documents: Document[] + ): Promise { + const rows = vectors.map((embedding, idx) => ({ + content: documents[idx].pageContent, + embedding: `[${embedding.join(",")}]`, + metadata: JSON.stringify(documents[idx].metadata), + })); + + const batchSize = 100; + const ids: string[] = []; + + for (let i = 0; i < rows.length; i += batchSize) { + const chunk = rows.slice(i, i + batchSize); + const insertQueries = chunk.map( + (row) => + `INSERT INTO ${this.table} (content, metadata, ${this.column}) VALUES (${row.content}, ${row.metadata}, vector(${row.embedding})) RETURNING id` + ); + + const results = await this.db.batch(insertQueries); + + for (const result of results) { + if ( + result && + result.rows && + result.rows.length > 0 && + result.rows[0].id != null + ) { + ids.push(result.rows[0].id.toString()); + } + } + } + + return ids; + } + + /** + * Performs a similarity search using a vector query and returns documents with their scores. + * @param {number[]} query - The query vector. + * @param {number} k - The number of results to return. + * @returns {Promise<[Document, number][]>} An array of tuples containing the similar documents and their scores. + */ + async similaritySearchVectorWithScore( + query: number[], + k: number + // filter is currently unused + // filter?: this["FilterType"] + ): Promise<[Document, number][]> { + // Potential SQL injection risk if query vector is not properly sanitized. + if (!query.every((num) => typeof num === "number" && !Number.isNaN(num))) { + throw new Error("Invalid query vector: all elements must be numbers"); + } + + const queryVector = `[${query.join(",")}]`; + + const sql = ` + SELECT content, metadata, vector_distance_cos(${this.column}, vector(${queryVector})) AS distance + FROM vector_top_k('${this.table}_idx', vector(${queryVector}), ${k}) + JOIN ${this.table} ON ${this.table}.rowid = id + `; + + const results = await this.db.execute(sql); + + return results.rows.map((row: any) => { + const metadata = JSON.parse(row.metadata); + + const doc = new Document({ + metadata, + pageContent: row.content, + }); + + return [doc, row.distance]; + }); + } + + /** + * Deletes vectors from the store. + * @param {Object} params - Delete parameters. + * @param {string[] | number[]} [params.ids] - The ids of the vectors to delete. + * @returns {Promise} + */ + async delete(params: { ids?: string[] | number[] }): Promise { + if (!params.ids) { + await this.db.execute(`DELETE FROM ${this.table}`); + return; + } + + const idsToDelete = params.ids.join(", "); + + await this.db.execute({ + sql: `DELETE FROM ${this.table} WHERE id IN (?)`, + args: [idsToDelete], + }); + } + + /** + * Creates a new LibSQLVectorStore instance from texts. + * @param {string[]} texts - The texts to add to the store. + * @param {object[] | object} metadatas - The metadata for the texts. + * @param {EmbeddingsInterface} embeddings - The embeddings interface to use. + * @param {Client} dbClient - The LibSQL client instance. + * @param {LibSQLVectorStoreArgs} [options] - Configuration options for the vector store. + * @returns {Promise} A new LibSQLVectorStore instance. + */ + static async fromTexts( + texts: string[], + metadatas: object[] | object, + embeddings: EmbeddingsInterface, + options: LibSQLVectorStoreArgs + ): Promise { + const docs = texts.map((text, i) => { + const metadata = Array.isArray(metadatas) ? metadatas[i] : metadatas; + + return new Document({ pageContent: text, metadata }); + }); + + return LibSQLVectorStore.fromDocuments(docs, embeddings, options); + } + + /** + * Creates a new LibSQLVectorStore instance from documents. + * @param {Document[]} docs - The documents to add to the store. + * @param {EmbeddingsInterface} embeddings - The embeddings interface to use. + * @param {Client} dbClient - The LibSQL client instance. + * @param {LibSQLVectorStoreArgs} [options] - Configuration options for the vector store. + * @returns {Promise} A new LibSQLVectorStore instance. + */ + static async fromDocuments( + docs: Document[], + embeddings: EmbeddingsInterface, + options: LibSQLVectorStoreArgs + ): Promise { + const instance = new this(embeddings, options); + + await instance.addDocuments(docs); + + return instance; + } +} diff --git a/libs/langchain-community/src/vectorstores/pgvector.ts b/libs/langchain-community/src/vectorstores/pgvector.ts index ded20864b9f4..4b5e39b95827 100644 --- a/libs/langchain-community/src/vectorstores/pgvector.ts +++ b/libs/langchain-community/src/vectorstores/pgvector.ts @@ -686,6 +686,7 @@ export class PGVectorStore extends VectorStore { const document = new Document({ pageContent: doc[this.contentColumnName], metadata: doc[this.metadataColumnName], + id: doc[this.idColumnName], }); results.push([document, doc._distance]); } diff --git a/libs/langchain-community/src/vectorstores/prisma.ts b/libs/langchain-community/src/vectorstores/prisma.ts index 54fbd3603bcd..669fb51a5262 100644 --- a/libs/langchain-community/src/vectorstores/prisma.ts +++ b/libs/langchain-community/src/vectorstores/prisma.ts @@ -1,6 +1,6 @@ +import { Document } from "@langchain/core/documents"; import type { EmbeddingsInterface } from "@langchain/core/embeddings"; import { VectorStore } from "@langchain/core/vectorstores"; -import { Document } from "@langchain/core/documents"; const IdColumnSymbol = Symbol("id"); const ContentColumnSymbol = Symbol("content"); @@ -306,12 +306,13 @@ export class PrismaVectorStore< const vectorColumnRaw = this.Prisma.raw(`"${this.vectorColumnName}"`); await this.db.$transaction( - vectors.map( - (vector, idx) => this.db.$executeRaw` - UPDATE ${tableNameRaw} - SET ${vectorColumnRaw} = ${`[${vector.join(",")}]`}::vector - WHERE ${idColumnRaw} = ${documents[idx].metadata[this.idColumn]} - ` + vectors.map((vector, idx) => + this.db.$executeRaw( + this.Prisma.sql`UPDATE ${tableNameRaw} + SET ${vectorColumnRaw} = ${`[${vector.join(",")}]`}::vector + WHERE ${idColumnRaw} = ${documents[idx].metadata[this.idColumn]} + ` + ) ) ); } diff --git a/libs/langchain-community/src/vectorstores/tests/faiss.int.test.data/requirements.txt b/libs/langchain-community/src/vectorstores/tests/faiss.int.test.data/requirements.txt index 2fc60ecb31ed..be1f7b791532 100644 --- a/libs/langchain-community/src/vectorstores/tests/faiss.int.test.data/requirements.txt +++ b/libs/langchain-community/src/vectorstores/tests/faiss.int.test.data/requirements.txt @@ -1,2 +1,2 @@ -langchain==0.1.0 -langchain-community==0.0.12 \ No newline at end of file +langchain==0.2.10 +langchain-community==0.2.9 \ No newline at end of file diff --git a/libs/langchain-community/src/vectorstores/tests/lancedb.int.test.ts b/libs/langchain-community/src/vectorstores/tests/lancedb.int.test.ts index ec9bb2bb566e..3d561c903440 100644 --- a/libs/langchain-community/src/vectorstores/tests/lancedb.int.test.ts +++ b/libs/langchain-community/src/vectorstores/tests/lancedb.int.test.ts @@ -45,3 +45,27 @@ describe("LanceDB", () => { expect(resultsTwo.length).toBe(5); }); }); + +describe("LanceDB empty schema", () => { + test("Test fromTexts + addDocuments", async () => { + const embeddings = new OpenAIEmbeddings(); + const vectorStore = await LanceDB.fromTexts( + ["hello bye", "hello world", "bye bye"], + [{ id: 1 }, { id: 2 }, { id: 3 }], + embeddings + ); + + const results = await vectorStore.similaritySearch("hello bye", 10); + expect(results.length).toBe(3); + + await vectorStore.addDocuments([ + new Document({ + pageContent: "a new world", + metadata: { id: 4 }, + }), + ]); + + const resultsTwo = await vectorStore.similaritySearch("hello bye", 10); + expect(resultsTwo.length).toBe(4); + }); +}); diff --git a/libs/langchain-exa/package.json b/libs/langchain-exa/package.json index a0998bbc4a0c..4e94dd1a643c 100644 --- a/libs/langchain-exa/package.json +++ b/libs/langchain-exa/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/exa", - "version": "0.0.5", + "version": "0.1.0", "description": "Exa integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-google-common/package.json b/libs/langchain-google-common/package.json index ee3bc6877756..4209d3c70f66 100644 --- a/libs/langchain-google-common/package.json +++ b/libs/langchain-google-common/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-common", - "version": "0.0.27", + "version": "0.1.1", "description": "Core types and classes for Google services.", "type": "module", "engines": { diff --git a/libs/langchain-google-common/src/chat_models.ts b/libs/langchain-google-common/src/chat_models.ts index 4ee4e0f6ed05..d770c25b026f 100644 --- a/libs/langchain-google-common/src/chat_models.ts +++ b/libs/langchain-google-common/src/chat_models.ts @@ -54,7 +54,7 @@ import type { } from "./types.js"; import { zodToGeminiParameters } from "./utils/zod_to_gemini_parameters.js"; -class ChatConnection extends AbstractGoogleLLMConnection< +export class ChatConnection extends AbstractGoogleLLMConnection< BaseMessage[], AuthOptions > { diff --git a/libs/langchain-google-common/src/utils/gemini.ts b/libs/langchain-google-common/src/utils/gemini.ts index aab0699cec56..472f4c5725d8 100644 --- a/libs/langchain-google-common/src/utils/gemini.ts +++ b/libs/langchain-google-common/src/utils/gemini.ts @@ -624,6 +624,11 @@ export function getGeminiAPI(config?: GeminiAPIConfig) { response: GoogleLLMResponse ): ChatGeneration[] { const parts = responseToParts(response); + + if (parts.length === 0) { + return []; + } + let ret = parts.map((part) => partToChatGeneration(part)); if (ret.every((item) => typeof item.message.content === "string")) { const combinedContent = ret.map((item) => item.message.content).join(""); diff --git a/libs/langchain-google-gauth/package.json b/libs/langchain-google-gauth/package.json index 8dcc881897b5..2614af2df617 100644 --- a/libs/langchain-google-gauth/package.json +++ b/libs/langchain-google-gauth/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-gauth", - "version": "0.0.27", + "version": "0.1.0", "description": "Google auth based authentication support for Google services", "type": "module", "engines": { @@ -35,7 +35,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-common": "~0.0.27", + "@langchain/google-common": "~0.1.0", "google-auth-library": "^8.9.0" }, "peerDependencies": { diff --git a/libs/langchain-google-genai/package.json b/libs/langchain-google-genai/package.json index 1d7bc83524d5..b97f308d368f 100644 --- a/libs/langchain-google-genai/package.json +++ b/libs/langchain-google-genai/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-genai", - "version": "0.0.26", + "version": "0.1.0", "description": "Google Generative AI integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-google-vertexai-web/package.json b/libs/langchain-google-vertexai-web/package.json index 86dc96d8cd1b..5fdcb89f3314 100644 --- a/libs/langchain-google-vertexai-web/package.json +++ b/libs/langchain-google-vertexai-web/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-vertexai-web", - "version": "0.0.27", + "version": "0.1.0", "description": "LangChain.js support for Google Vertex AI Web", "type": "module", "engines": { @@ -32,7 +32,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-webauth": "~0.0.27" + "@langchain/google-webauth": "~0.1.0" }, "peerDependencies": { "@langchain/core": ">=0.2.21 <0.4.0" @@ -40,7 +40,7 @@ "devDependencies": { "@jest/globals": "^29.5.0", "@langchain/core": "workspace:*", - "@langchain/google-common": "^0.0.27", + "@langchain/google-common": "^0.1.0", "@langchain/scripts": ">=0.1.0 <0.2.0", "@langchain/standard-tests": "0.0.0", "@swc/core": "^1.3.90", diff --git a/libs/langchain-google-vertexai/package.json b/libs/langchain-google-vertexai/package.json index 7792d8cdefeb..7274f14f58ae 100644 --- a/libs/langchain-google-vertexai/package.json +++ b/libs/langchain-google-vertexai/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-vertexai", - "version": "0.0.27", + "version": "0.1.0", "description": "LangChain.js support for Google Vertex AI", "type": "module", "engines": { @@ -32,7 +32,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-gauth": "~0.0.27" + "@langchain/google-gauth": "~0.1.0" }, "peerDependencies": { "@langchain/core": ">=0.2.21 <0.4.0" @@ -40,7 +40,7 @@ "devDependencies": { "@jest/globals": "^29.5.0", "@langchain/core": "workspace:*", - "@langchain/google-common": "^0.0.27", + "@langchain/google-common": "^0.1.0", "@langchain/scripts": ">=0.1.0 <0.2.0", "@langchain/standard-tests": "0.0.0", "@swc/core": "^1.3.90", diff --git a/libs/langchain-google-webauth/package.json b/libs/langchain-google-webauth/package.json index ec5192795866..149054defe14 100644 --- a/libs/langchain-google-webauth/package.json +++ b/libs/langchain-google-webauth/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/google-webauth", - "version": "0.0.27", + "version": "0.1.0", "description": "Web-based authentication support for Google services", "type": "module", "engines": { @@ -32,7 +32,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/google-common": "~0.0.27", + "@langchain/google-common": "~0.1.0", "web-auth-library": "^1.0.3" }, "peerDependencies": { diff --git a/libs/langchain-groq/package.json b/libs/langchain-groq/package.json index f3b62befb487..551e3770686e 100644 --- a/libs/langchain-groq/package.json +++ b/libs/langchain-groq/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/groq", - "version": "0.0.17", + "version": "0.1.2", "description": "Groq integration for LangChain.js", "type": "module", "engines": { @@ -35,7 +35,7 @@ "author": "LangChain", "license": "MIT", "dependencies": { - "@langchain/openai": "~0.2.6", + "@langchain/openai": "~0.3.0", "groq-sdk": "^0.5.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.5" diff --git a/libs/langchain-groq/src/chat_models.ts b/libs/langchain-groq/src/chat_models.ts index c16b58752520..49bba333c6de 100644 --- a/libs/langchain-groq/src/chat_models.ts +++ b/libs/langchain-groq/src/chat_models.ts @@ -42,6 +42,7 @@ import { ChatCompletionCreateParams, ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming, + CompletionCreateParams, } from "groq-sdk/resources/chat/completions"; import { Runnable, @@ -73,7 +74,7 @@ export interface ChatGroqCallOptions extends BaseChatModelCallOptions { headers?: Record; tools?: ChatGroqToolType[]; tool_choice?: OpenAIClient.ChatCompletionToolChoiceOption | "any" | string; - response_format?: { type: "json_object" }; + response_format?: CompletionCreateParams.ResponseFormat; } export interface ChatGroqInput extends BaseChatModelParams { @@ -659,6 +660,8 @@ export class ChatGroq extends BaseChatModel< streaming = false; + apiKey?: string; + static lc_name() { return "ChatGroq"; } @@ -689,6 +692,7 @@ export class ChatGroq extends BaseChatModel< apiKey, dangerouslyAllowBrowser: true, }); + this.apiKey = apiKey; this.temperature = fields?.temperature ?? this.temperature; this.modelName = fields?.model ?? fields?.modelName ?? this.model; this.model = this.modelName; diff --git a/libs/langchain-groq/src/tests/chat_models.test.ts b/libs/langchain-groq/src/tests/chat_models.test.ts index b1ed1988d647..8f6b06f13edb 100644 --- a/libs/langchain-groq/src/tests/chat_models.test.ts +++ b/libs/langchain-groq/src/tests/chat_models.test.ts @@ -10,3 +10,11 @@ test("Serialization", () => { `{"lc":1,"type":"constructor","id":["langchain","chat_models","groq","ChatGroq"],"kwargs":{"api_key":{"lc":1,"type":"secret","id":["GROQ_API_KEY"]}}}` ); }); + +test("Serialization with no params", () => { + process.env.GROQ_API_KEY = "foo"; + const model = new ChatGroq(); + expect(JSON.stringify(model)).toEqual( + `{"lc":1,"type":"constructor","id":["langchain","chat_models","groq","ChatGroq"],"kwargs":{"api_key":{"lc":1,"type":"secret","id":["GROQ_API_KEY"]}}}` + ); +}); diff --git a/libs/langchain-mistralai/package.json b/libs/langchain-mistralai/package.json index 0cf1d4c40abd..35d65923078f 100644 --- a/libs/langchain-mistralai/package.json +++ b/libs/langchain-mistralai/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/mistralai", - "version": "0.1.0", + "version": "0.1.1", "description": "MistralAI integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-mistralai/src/chat_models.ts b/libs/langchain-mistralai/src/chat_models.ts index 8984ec0d43f6..4d9e1a447fd9 100644 --- a/libs/langchain-mistralai/src/chat_models.ts +++ b/libs/langchain-mistralai/src/chat_models.ts @@ -87,7 +87,7 @@ export interface ChatMistralAICallOptions response_format?: { type: "text" | "json_object"; }; - tools: ChatMistralAIToolType[]; + tools?: ChatMistralAIToolType[]; tool_choice?: MistralAIToolChoice; /** * Whether or not to include token usage in the stream. diff --git a/libs/langchain-mixedbread-ai/package.json b/libs/langchain-mixedbread-ai/package.json index e5f5c0e854b7..ac19774366cf 100644 --- a/libs/langchain-mixedbread-ai/package.json +++ b/libs/langchain-mixedbread-ai/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/mixedbread-ai", - "version": "0.1.0", + "version": "0.2.0", "description": "Mixedbread AI integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-mongodb/package.json b/libs/langchain-mongodb/package.json index be01ef09c9e4..a80cdc0e888a 100644 --- a/libs/langchain-mongodb/package.json +++ b/libs/langchain-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/mongodb", - "version": "0.0.6", + "version": "0.1.0", "description": "Sample integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-nomic/package.json b/libs/langchain-nomic/package.json index 9581683bf289..f2245cd1c152 100644 --- a/libs/langchain-nomic/package.json +++ b/libs/langchain-nomic/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/nomic", - "version": "0.0.6", + "version": "0.1.0", "description": "Nomic integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-ollama/package.json b/libs/langchain-ollama/package.json index caaee41e4b57..5df996b61191 100644 --- a/libs/langchain-ollama/package.json +++ b/libs/langchain-ollama/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/ollama", - "version": "0.0.4", + "version": "0.1.0", "description": "Ollama integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-ollama/src/chat_models.ts b/libs/langchain-ollama/src/chat_models.ts index 8c8ea9fc66d1..f016f80d3b16 100644 --- a/libs/langchain-ollama/src/chat_models.ts +++ b/libs/langchain-ollama/src/chat_models.ts @@ -109,7 +109,7 @@ export interface ChatOllamaInput * const llmWithTools = llm.bindTools( * [...], * { - * tool_choice: "auto", + * stop: ["\n"], * } * ); * ``` diff --git a/libs/langchain-ollama/src/embeddings.ts b/libs/langchain-ollama/src/embeddings.ts index a7ff0199b44e..afdba456c108 100644 --- a/libs/langchain-ollama/src/embeddings.ts +++ b/libs/langchain-ollama/src/embeddings.ts @@ -23,7 +23,7 @@ interface OllamaEmbeddingsParams extends EmbeddingsParams { /** * Defaults to "5m" */ - keepAlive?: string; + keepAlive?: string | number; /** * Whether or not to truncate the input text to fit inside the model's @@ -45,7 +45,7 @@ export class OllamaEmbeddings extends Embeddings { baseUrl = "http://localhost:11434"; - keepAlive = "5m"; + keepAlive: string | number = "5m"; requestOptions?: Partial; diff --git a/libs/langchain-openai/audio.json b/libs/langchain-openai/audio.json new file mode 100644 index 000000000000..300d41f31b73 --- /dev/null +++ b/libs/langchain-openai/audio.json @@ -0,0 +1,6 @@ +{ + "id": "audio_67118ff3274c81909cd2868f46786059", + "data": "UklGRmZVAwBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguMjkuMTAwAGRhdGEgVQMADQAJAAkACgAHAAsACAAJAAoACgAKAAUABgAEAAYABQAFAAIAAwAEAAEABAAAAAIA/v8CAP3//v/8/wAA/P/7/////P/8//j/+//4//v/+P/5//f/9v/5//X/9v/x//n/8v/3//P/9f/0//X/9P/v//X/8//1/+//9P/t//D/6v/v/+z/7f/w/+n/7f/m/+7/5v/t/+n/6P/o/+r/6//m/+v/5//p/+P/5P/j/+D/4//i/+X/4//i/+X/4v/n/+P/5v/h/+P/6P/k/+T/4f/m/+H/6//n/+n/6P/p/+v/6v/s/+r/7f/p/+v/6//v/+3/7f/v/+//7//w//D/8v/y//H/8v/z//D/8v/x/+//8P/w/+//8v/w//D/8f/x//H/8v/z//P/8//1//H/8P/v//D/7v/w//D/8f/y//P/8v/x//D/8f/v/+//8P/u/+z/7f/u/+z/8P/y//T/+P/0//b/9f/3//j/9f/z//b/8//1/+//8f/s/+//8f/v//D/6//x/+r/7v/o/+v/5f/m/+j/5//v/+b/8//o//T/8//8//P/+P/5//n////6//z/9//9//T//f/4/wMABAAJAA0ACQAPAAwAEgALAAoACAAGAAIAAwAEAAQABwAKABEAEgAVABcAFQAWABwAHAAXAB8AHQAeABcAGQAYABsAHAAbABwAIAAgACEAJAAhACQAIgAkAB4AHQAVABMACQAHAAQA/f/7//X/9v/s/+//8f/y//D/9P/z//b/8f/2//H/8v/u//L/8//t/+r/3v/e/9P/z//H/8f/w//C/8T/v//H/8n/2P/b/+D/7f/s//P/7f/y/+z/8v/z//v/BAALABoAHgAtACsANgA2ADgAOgAuACcAGAAYABsAFgAkACgAOAA4AEkAUgBQAEsAQQBIADwATAA8AEIANwA9AEgAWgCBAJoAzQDYAPwA8QD8APQA5gDjANMAzgC7AK0AlgB9AGoAWwBNAEYAPgA2ACYAGwAMAAwAFwArAEsAYQB5AHcAdgBhAE8AQwA2AD8AOwBLAFAAXQBqAIAAkACmALsAxgDCAKAAfQBEABoA7v/P/8H/vf/I/8v/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xf+//8H/xP/D/8f/wP/J/8P/xf/H/8b/yP/E/8T/xv/D/8f/wP/F/8T/w/+//8H/wP/A/8H/vv+//7v/uP+5/7L/tf+y/7D/sP+r/67/qf+q/6X/qP+j/6L/pf+h/6T/nv+l/5n/oP+b/53/n/+e/5//mv+d/5n/nP+a/53/mf+a/5v/mv+d/5n/oP+Z/6H/nP+i/6P/p/+o/6f/rv+k/7D/qv+1/7L/uv+6/7v/vv++/8j/wf/M/8r/0f/V/9X/2//b/+P/4P/q/+n/8v/w//X/+v/6/wQAAQANAAYAFQASABsAGwAlACEAKgAwAC8ANwAyADwAOwBDAEMARwBIAEoAUQBQAFQAVQBbAFwAXQBhAGUAZABmAGsAbABxAHEAdQB4AHgAewB7AH8AgQCFAIMAhgCIAIgAiwCIAI0AjACNAI8AjwCUAJAAlACQAJUAkwCUAJUAkgCXAJEAlACMAJUAjgCUAI4AkwCPAIsAjgCIAIsAhgCLAIEAhQB+AIIAewB9AHoAdQB4AHMAdABsAG0AaABnAGQAYgBgAFgAWQBQAFMATQBLAEcARgBBADwAOgA2ADMALAAtACcAJAAiABwAHwAaABcAEwAPAA4ACQAJAAIAAwD8/wIA+P/5//f/8P/x/+z/7P/k/+b/4v/g/97/2f/X/9b/0f/M/9L/zv/Q/8//yv/K/8j/x//D/8D/wP+8/7v/vf+8/7z/uv+6/7j/uP+0/7T/t/+z/7b/sP+1/7b/uf+2/7r/uP+5/7r/vP+//7v/vv+8/7v/u/+6/7v/vf+9/7z/u/+8/7//vP++/73/wf++/8T/wP/F/8X/w//E/8r/y//L/83/zv/Q/9D/0v/T/9b/2v/b/93/2v/b/9j/3v/f/93/3v/b/97/2//g/9v/4P/h/+H/4f/f/+T/4P/j/+P/5f/l/9//4P/e/+P/3//i/9//4P/h/9//4v/g/+L/4v/j/+P/4v/j/+b/5v/o/+v/6f/o/+v/6//t/+z/7v/r/+z/6//t/+3/7v/t/+3/7f/t//D/7f/s/+z/6//o/+n/6v/o/+f/6P/n/+b/4v/k/+X/5//n/+f/7P/p/+v/6v/r/+z/7P/s/+z/6v/q/+j/6v/p/+X/5v/j/+f/5v/k/+P/5P/j/+j/5f/l/+b/4f/g/9//3f/Y/9X/1P/P/8//0f/P/8v/yf/H/8r/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NwBeAIYArADOAPEADAEwAVMBcAGMAaABuwHNAeAB8AH/ARECHAIqAjICOwJFAk4CVQJhAm4CdAKCAokCmwKkAq0CuQLAAs4C1QLhAuUC8gLyAvgC9wL4AvUC8QLzAugC7ALiAuIC2ALZAtICywLGAsYCwwK+Ar4CsQKvAp4ClgKIAnUCagJWAkgCLwIlAg4CAALtAd4B0AG/AbIBnwGaAYkBggF0AWcBUwFHATkBJgEZAQMB8gDcAMcAsQCcAIcAcwBhAE4APwAvACAAFwAFAAAA7v/k/9j/yP/D/7D/qf+Z/4//fv91/23/Yf9f/1T/Wv9P/1L/Sf9K/0n/TP9Q/0//U/9S/1f/Wf9X/1z/W/9f/2D/Zv9t/2r/cP9s/3z/ff+I/4L/i/+P/4//mv+Y/5z/m/+j/57/oP+j/6X/pv+l/6v/rP+s/67/tP+1/7z/vv/F/8T/yf/I/9D/zv/R/9L/z//V/8z/1f/S/9P/0v/R/9D/z//Q/8z/0f/L/8r/zP/F/8X/wP+//7r/tf+3/6z/sP+l/6f/o/+i/5r/lv+Y/5H/lv+Q/5b/jv+V/5X/mf+c/5n/n/+b/6X/oP+q/6f/r/+v/7P/u/+5/8P/wv/Q/8//1//a/+H/6P/r//X/8f/8/wIABgAQABAAFAAWACEAIAAjACkALQAwADQAOgA8AEMARABJAEwAVABUAF4AXQBlAGQAawBwAG4AeABxAHsAeAB/AIEAgQCJAIYAjwCNAJQAlwCaAJ4AngCjAKIAqACmAK0AqgCuAK4AsQC1ALIAuAC1ALoAuQDAALsAvgDBAMAAxQDBAMoAwwDHAMYAywDOAMoAzQDKANAAzgDRAM8A0QDTANMA1QDPANcAzQDYAMoA1QDRANIAzwDMAM8AygDRAMcAzgDAAMkAwADIAMAAwgDCAL4AvwC4ALsAswC4ALUAtQCwAK0ArQCjAKUAoQCjAJwAnACTAJEAjgCKAIUAfgB7AHUAcQBvAGcAYwBeAFgAVQBMAEgAPwA9ADQALwAmACUAGgAUABEABAADAPP/+v/q/+r/3//Z/9n/xv/P/73/w/+v/7j/rP+n/6r/mP+h/5T/l/+P/4//iP+E/4T/fv9+/3f/d/9x/3D/bf9s/2f/bf9i/2v/Wv9k/17/YP9b/2H/Xf9a/17/V/9g/1n/Yf9g/1//X/9b/1//X/9j/2H/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AwAFAAAA/f/6//z/BwAMAAIA/f/+/wYACwAPAAcABAAHAAgADgANAAkACgAMABAAFQASABEADwAPABQAGwAeABsADQAQABUAGwAeABcAFQAWACAAIgAeABsAHAAbACEAKAAoACAAGAAcACkAMQArAB4AIQAlACsALAAqACcAKwAsACoAKAAjACwAMAAwADIALAAuADIANQArACkALAAzADkANQAxACcALAAyADMALQAoACoAMwA9ADoANAAlACoAPABIADwANQAqADEAQQBIADoAMgAxADoAPwA4ADcANAA5AD0AQAA+AD4AMgAzAD4ARAA9ADIAMAA3ADwAPgA5ADMANABDAEUANAAwADoANAAyADIAPAA4ADEALAAyADMAKwAvADYALAAhACUALgApAB8AIwAiABkAFAAWABwAFAATABQADwAOABIADQD//wAACQAKAPv/+//4//j/9P/z//X/7//v//L/8v/q/+//5//p/+n/6//o/+b/4f/c/+D/2//b/9j/2f/a/9f/1//P/9H/0f/W/9v/1P/Q/9X/2v/T/9H/1f/W/9D/z//Y/9b/1f/P/9j/2P/a/+D/3//l/+f/5v/g/+X/6v/n/+X/6v/r//X////9//v/+f/8//n/AQAAAAcABwAFAAwAAwAOAAoAEgARABAAGAAaABcAFAAaABwAJwAmACQAJwAlACgAJAAjACUAIAAdABsAIQAgACMAHQAcACYAKgAlACMAFwATABIACQAPAA4ACwANAAcAEgAJAAUA///3/wIA+P/6//P/5P/h/9r/4v/j/9//2//V/97/5P/f/+H/3P/b/9r/1f/b/97/3P/S/9z/7P/h/+T/2v/Y/+b/5P/v//T/9//5//v/AgADAAQA+f/v//X/8//v/+3/3v/W/9H/0v/T/9D/xP+9/7X/tP+y/6X/lv+A/33/cP9m/17/UP9F/0D/Qv9E/z//Nv8y/zT/L/8t/yz/H/8c/xX/GP8e/yH/I/8j/xz/Gv8b/xz/Gf8Y/x3/F/8U/w7/B//+/vz+BP8D//v+8/7z/vL+5v7x/gD/C/8a/xz/J/9J/0b/Rf88/0P/Uf9E/z7/Uv9B/z3/Rv9v/5r/tP/Q/8P/1P/c/9n/yf+6/7D/pP/M/87/0//K/6v/uP+3/7T/wv+v/47/c/9Z/1H/Y/+B/4L/iP+a/4r/kP+j/7//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+//6//j/+//4//j/+f/6//z//f/+//7//v///wEAAQAEAAYACAAHAAoACgAMABAAEgATABIAFAAUABUAFgAWABUAFwAXABgAGQAdABwAIAAfACEAIwAlACQAJwAoACwALAAvAC8AMQAwADEANAA0ADQANAA2ADcANgA3ADcAOwA6ADsAPAA9AD4APgBAAEIARABDAEQARABFAEYARQBGAEUARwBFAEcARwBHAEcASQBKAEoATABKAEsATABLAEwATABNAE4ATwBNAE4ATgBNAEwATgBNAE4ATgBOAE8ATgBOAE8AUABPAE8ATwBNAE4ATgBPAE0AUABNAE0ATgBOAEwATgBOAE0ATgBNAEsASwBKAEoARQBHAEYASABHAEUARABCAEMAQQBBAD8APQA7ADoAOQA2ADUANAAyADIAMAAvAC4ALQApACoAJgAmACYAIwAfACAAHQAcABsAGAAWABYAFgATABMAEAANAAsACgAHAAYABAADAAEA///+//3/+v/4//f/9f/y//D/7//t/+v/6f/p/+f/5//l/+X/4v/g/9//3f/c/9n/2f/Y/9X/1v/U/9T/0f/R/9D/zv/O/8z/zP/I/8j/yf/H/8b/xv/I/8X/w//E/8b/w//C/8L/wf/B/8D/wf/A/7//wP+//7//vv+//7//wP/A/8D/v/++/7//wf+//8D/v/+//77/vv+9/77/vP+9/7z/vf+9/73/vv+7/7v/u/+7/7z/u/+8/7z/uv+6/7j/uP+5/7j/uf+6/7f/uf+2/7b/tf+0/7X/tv+1/7L/sv+x/7H/sP+s/6v/qv+q/6n/qf+o/6j/pv+l/6b/pv+l/6P/o/+i/6H/oP+e/5//nv+e/5z/mv+Z/5n/mf+a/5b/l/+U/5P/lf+U/5T/lP+T/5P/kP+V/5L/lf+S/5P/lv+V/5T/l/+W/5X/lv+W/5j/l/+Y/5f/mv+a/5z/nP+e/6D/oP+j/6X/qP+p/63/r/+y/7L/tP+z/7L/tv+2/7f/uf+6/7v/uv+7/7z/u/+//73/vv++/8T/w//H/8j/xv/H/8r/y//K/87/0P/S/9H/0f/S/9X/1//Y/9j/2//b/9v/3P/f/9//4v/l/+X/5//o/+v/7f/w//P/8P/y//P/8//0//T/9P/1//T/9P/y//L/8f/1//P/9f/0//X/+P/5//r/+//9////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FgAvAEQAVgBgAGEATgAxAAkA8f/y/wsAMQBSAF4AWwBJADcALQAiABYAHwAsAFEAdwCQAIMAWQAgAPH/2P/g//D/AQAKABIAGgAhACIADgDy/9L/vf+5/8T/0v/g/+j/8v/7/wYABAD///L/3//a/9b/3P/l//H/9v/8//z/8//s/+r/8P/0//3/9v/0//X/BgAYACwAMgAsAB8AEAAIAPj/5P/J/7j/uf/M/+r/AAAJAP//9//v//D/9/8HAB8ANgBSAGMAYABLAC4AFwASABEAHAAvAEUAWwByAHsAXAAsAPX/0v/W//f/FgAuADUAMAAoABoA///e/8D/vf/d/xUARgBbAFAAOgAlAB4AIQAgABwAFwAdADAAOwA+ADQAHwAVABwAJgAwADUAOgA+AEkATwBKADcAHAANABEAFAAUAAgA9P/q/+v/AQATABYABQDl/83/x//P/9b/1v/d/+z/BgAmADgALwARAO//1//c//L/EQAmADYAQwBLAFAATgBAACoAFwALAA0AIQAyAEEARwBCADgAJAAEAOX/1P/R/93/8f8IABQAGgAeACQAKwAsACQAGgAeADEATgBmAGwAVwA1ABUABwABAP7//v8BAAwAIQA3AEEANgAfAAcA+v/1//f/7//s/+//7f/v/+v/3v/K/7b/rP+l/6D/l/+W/5z/sv/K/9j/3f/U/87/zP/X/+T/9/8DAAoADQAQABIAHQAhACEAFwAJAAAA+v/+/wUACQAIAAEA9//s/9n/x/+5/7r/u//D/8n/yv+8/6r/m/+O/4T/hv+S/6n/u//L/9D/zP/C/7b/r/+s/6z/qP+s/7b/xv/f/+//5//K/6P/f/9w/4H/qf/V//b//f/0/9X/tP+g/57/qf+1/7n/sf+t/6z/tv/E/8//1P/O/8X/wP/H/9T/4//x//X/7f/d/8//yf/P/9z/8P/7//r/8P/k/9b/0//P/9H/0v/M/8D/vP+9/83/6P8KACgANAAtABoABgACAA0AHQAvAD0ARQBJAEUARQBHAEYAPwA6AC8AKQAoACgAMQBBAE0AVgBSAEMAKwAaABMAFQAjAC4AMQAnABYA+v/v//H/AQAZACQAJQAWAAQA9//2////FAAnADUARABGAEsARQBEAD8AOgA5ADcAPgBMAFoAZABdAE4AOQAhABoAGQAuAD0AVABXAE0APwAnABEACgALABUAHwArAC4ALwAmABgABgDy/+v/8f8FABoALgA2ADcAJwAYAA8ACgAIAAoAAwAAAP//AQASACgAPABIAD0AJAACAOb/3//m//n/CAAQAAoA+v/q/+L/5v/o//D/6//p/97/1v/a/+b/6v/q/+H/zv/C/7//y//j////FgAeABEAAQDr/9b/0P/Y/+7/BAAWACAAKAAjABQABQD1/+j/5f/x/wkAIgA5AEAALgAaAAAA6//k/+j/9v8JABYAGwAXAAQA6//Q/8D/uP+6/8j/1f/i/+L/2P/F/7D/nv+X/5X/ov+t/7b/wP++/7r/sv+p/6H/mP+X/5n/of+s/7r/w//F/7v/r/+g/57/of+y/8X/1P/Y/8//xf+4/63/rP+u/7b/wv/Q/97/4v/j/9f/xv+8/7j/wv/S/+b/9f/9/wIA+P/0/+j/5P/t/wAAEAAfACsAKwAqACgAIwAbAAcA9f/m/+n/8/8IACIANgBKAFIATQA5ACIAEgANABgAMQBLAGIAcABwAGIASQAuABQACQAUAC8AUgBrAH4AfgBwAFwAQwAtABwAHQAuAE8AdACSAKIAoQCPAHQATwAvABYAEwAiAEEAWgBlAGUAVQA/ACIACwD4/+v/9/8VAEEAXwBuAGEASAArABAAAwADABEAJABBAFcAZQBhAFIAPgApAB4AGwAfACEAKwA2AEYASwBGADQAHgASAAkADwAbACkAMgA2ADQAKwAdABIACwAFAAMA9//z//D/9P/9////9v/r/97/1P/X/97/5f/u/+3/5//g/9r/0P/L/8r/yf/V/9v/4f/i/9//1P/M/8T/tP+r/6H/mv+Z/5r/nv+i/6D/mv+P/4r/iv+P/5z/p/+0/7v/vv+7/7T/rP+e/5T/k/+e/7P/yv/d/+T/3//W/8v/vf+y/6v/r/+4/8j/2v/l/+r/8f/w/+3/7P/y//L/9////wkADgAUABUAEQAPAAsACAAHAAwAGgAjACYAIgAcABIADQAJAAsAEgATABYAFQAcACIAKgAtACkAIwAcABYAEgATABEAEwASABYAGwAaABAABgD8//X/7v/w/+7/8v/+/wwAIQAoACsAIgAXAAwABwAVACkAPABFAEsARgBDADcANAAzADoASQBaAGsAcwBsAGAATAA7ACwAKAAuADkARwBQAFEARQA1ACQAEwAKAAsABgAHAAQA/v/z/+j/3f/S/8//z//Q/9P/1//d/9//4//n/+f/4//g/+L/5P/u//D/9P/5//v/AQAFAAwAEAAJAPr/5v/T/8T/v//H/8//3P/f/93/zP+6/6j/nf+d/6v/uP/F/8z/zP/G/8L/vv+9/7z/v//L/9H/3P/h/+T/5f/j/+T/5P/r//X/AQAJAA8AEAARABEAFAAZACEAJwAxADcAPQBEAEcAQwBDAEIAQwBDAEQANwAxACoAIgAhAB4AHwAjACUAJwAkAB4AEwAMAAkADgASAA8ACAD9//P/7//1//7/CgAYAB8AJgAiABkACwD6//P/6//x//H//P8AAAEA///3//f/7//0//P/+P8EABEAGgAaABUACQADAPz//////wQABwAHAAYA/P/x/+L/2f/U/9T/2f/d/97/4P/f/+X/7f/5/wQADgAQAA8ADgAOAA4ADwAVABIADAAAAO//2//Q/83/0P/d/+f/7P/s/+z/6P/q/+v/6//r/+b/5P/l/+v/8v/7/wIAAgD7/+v/2//S/9X/5P/6/wwAGQAbABkAFQAUABoAIgAqAC8AMAAtACwALwAnAB0AEgABAPD/4v/h/+P/7P/z//X/7v/i/9L/yv/I/87/2v/j/+r/7v/y//H/8//w//D/8v/1//7/CQAVAB4AIQAiACEAIAAjAB8AJwAiACQAHAAWAAsAAwD7//H/6//i/9v/0f/P/9T/2f/j/+H/5P/h/+P/6v/v//n//f/8//r/9v/0/+//9P/3//n//P/y/+r/6f/r//D/9f/5//T/7v/o/+b/5//u//P/+////wIABwAIAAcABAD9//z/9v/8//j//f/7//j/9v/r/+D/3//b/9v/3P/j/+b/6f/t/+3/8P/t/+//7P/s//D/9P/+/wsAEAAPAAEA7P/Z/8z/xv/P/+D/8f/9/wUA/v/5//X/9v/8/wYAEgAlADgARgBPAE8ARQA0ACcAGwAYABsAJQAsADEALwAkABMACgAGAAgAFgAmADYAQgBJAEkAQgBDADsAPQA7AEIATABOAEYAPQA1AC0ALAAtAC8AMgAxACgAHAAVAAsADwAbAC0APgBDAEEANwAqACIAHwAoADMAQwBMAEsARgA0ACQAFwASABQAGAAlAC8ANwA6ADYALwAlACEAHgAoADUAQgBTAF4AXgBWAEoAPgA1ADAAMQA2ADwAPQA9ADUALAAgABsAGQAeACYALQA1ADgANAAsACkAIAAeAB8AHgAdABwAFQAUAA8AEAARABEADwANAAcAAQD//wEACQASAB4AIQAkABwAFgAOAAMA/v/0//P/7//w/+v/5P/b/9j/zP/F/8X/x//U/9//6v/v//H/6f/k/97/3//m//T/BwASABkAFgAMAP3/9P/s/+7/8f/7/wAAAAD8//j/8f/s/+r/6v/p/+n/6f/m/+X/4//j/+H/2f/T/87/zP/L/9L/2f/h/+n/5f/j/9j/zP/G/8b/y//R/9r/3//j/9//1v/N/8L/u/+0/6z/qP+p/6f/rP+x/7n/vf+2/63/n/+U/5L/l/+p/7v/yv/L/8b/tv+m/5n/nP+q/7r/zv/Y/97/1//K/8H/t/+3/8H/0f/h/+v/8f/v/+r/4//e/9f/2f/c/+b/7P/1//r/AAAEAAgACwAQABQAFwAkAC0ANAA6ADkANAAtACMAIgAfAB8AIAAcABIABQD4/+3/6f/o/+r/7//4//z/AgD9//r/8P/u//P/9f/9/wMABQADAPj/6//c/8//y//R/9b/3f/l/+j/5//g/9z/3P/e/+b/8P/4//3/+v/3/+z/4//b/9P/zP/C/7r/r/+r/6b/n/+e/5z/m/+W/5T/lP+Y/6H/qf+4/8j/1v/e/+D/4v/h/+D/4f/k/+7/9v/+//7/+//0/+j/3P/T/9D/0v/a/+H/5v/m/+b/4//f/9j/2f/Y/9f/3//j//D/9/8EAAMAAQD9//v/+//+/wcAEgAgAC4AOwBEAEcASQBLAFIAWQBjAGsAbwB3AIAAfgB+AHcAbQBlAGMAYQBkAGUAZABlAF0AVABMAEcASwBQAFMAWgBdAGEAXgBdAFkAVwBVAFgAWwBcAFsAWQBaAFEASwBCADkAMgAsACsAJwAqACkAKQArACgAJQAiACEAJQApAC4ANgA7ADsANwAxACkAIAAeAB8AIwAjACMAHwASAAYA+v/z/+7/6v/t/+3/6//o/+T/4P/e/9v/3P/c/97/3f/c/9n/1//T/8//yf/A/77/uv++/8f/zv/V/9z/2//Z/9b/1f/e/+n/+P8FAA0AEwAVABEAEgATABYAGwAeACEAIgAgAB0AGgAVAA0AAwD7//b/8v/z//T/9P/y/+7/6v/h/97/3v/g/+P/5//q/+3/7//y//b/+f8AAAQACwATABoAIQAmAC0ALwAvAC8ALQAvACsALwAwADQANwA2ADAAKQAgABYAEAANAA0ACwAPAA8ADgAMAAoACAAJAAoADQAUABoAHgAdABoAGAAOAAUA///8//r/+v/7//z/+P/0//D/6v/m/+H/5P/p//D/+/8DAAcACQAJAAgABwAFAAsADQAOAAsABgADAP3/9f/r/+H/3P/V/9b/2f/h/+X/6f/q/+r/6P/m/+n/7//1//z/AgABAP//9//t/+H/1//R/8z/yP/G/77/u/+0/67/rP+r/6z/sf+7/8f/0//i/+z/9f/5//3//f8AAAIACAANAA8ADwAKAAEA+P/r/+D/2P/X/9P/0//U/9X/1//T/83/x//G/8L/w//N/9T/3v/i/+b/5P/g/9n/2P/X/93/5P/r/+7/8P/x/+7/7P/q/+n/7P/v//H/9f/3//n/+P/3//X/8P/v/+v/8P/x//T/9//4//j/9f/1/+//8f/w//j/+f/6//z/9f/0//X/8f/w//H/8f/y//H/8v/x//H/8//1//P/9v/4////CQASABcAGwAfABwAHAAfAB4AIgAiACQAIQAfAB0AFQAQAAoACAAFAAcACQAJAAgABAACAAMABAAGAAkADwAVABsAIAAmACcAKQAqAC0ALwAxADUAOgA3ADgANgA2ADUALwAwADAALQAsACoAJwAlACAAHgAbABcAFgATABUAFQAXABoAGgAdACEAIwAmACcAKwAtADMAMwA1ADIALQApAC0AKQAmACYAJAAhACEAHwAdABwAHQAdACAAJAApAC4ANAA6AD0AQABCAD8APAA6ADcANAAwAC8AKQAjABkAEQAKAAQAAAD+//z//v/6//3///8EAAUADAAMABEAFQAaAB4AIgAmACcAKgAnACMAHAAZABYAFAASABAADAAMAAkAAgABAP//AQADAAcACgANAA8AEgATABEADwAKAAkAAgD///z/9v/1//L/6//r/+b/4v/h/+L/4//l/+f/7P/z//j//f8FAAkADAAMAAsADQAPABEAEwASABUAEQARAAwACAAHAAUAAgABAPz/+//5//3//////////P////7/AQAEAAYACQAKAAoACwAMAA4ACwANAAwADAAJAAcACAAHAAgACgAJAAcACgALAAoACQAJAAsADAANAAsACwALAAkACAAHAAUAAAD///7/+//4//P/8P/u/+v/6f/q/+f/6P/j/+T/4//j/97/4P/f/+D/3v/e/97/3f/d/93/3P/a/9n/2P/Y/9T/0v/Q/8//yv/M/8n/xf/C/8b/w//B/8H/wf/C/8L/xP/D/8P/w//D/8P/xf/D/8X/w//D/8P/xP/B/8L/w//D/8P/xP/E/8T/x//H/8r/yP/J/8n/yP/J/8r/yv/K/8r/x//K/8z/zf/Q/9D/0f/Q/9H/0v/U/9T/1f/W/9b/1//Z/9z/3f/i/+H/5f/j/+f/7f/x//H/+P/7//z/AAACAAQABgAJAAoACgAMAA4ADwAQABEAFAATABUAFQAVABcAGQAZABgAGwAdAB8AIgAjACIAJQAnACkAKgAtAC4AMQAyADQANgA3ADcANwA6ADkAOQA4ADsAOwA7ADoAOAA8ADoAPAA7ADsAOQA5ADoAOQA6ADoAOgA6ADsAOgA5ADgAOAA4ADkAOAA5ADgANQA3ADgAOAA4ADoAOwA7ADsAPQA9AD0APQA8ADwAPAA/AD8APgA8AD4APQA6ADgANwA2ADgANwA3ADYAMwAyADQAMQAwADAALQAuAC4ALAAqACoAKwApACcAJgAkACIAIgAgAB8AHAAaABkAFgAVABMADgAMAA0ADQAMAAcABAACAAAA//////n/9//z//H/7v/s/+b/5P/j/+D/3f/b/9r/1//W/9P/0//R/9H/zf/K/8n/xf/E/8P/wf/B/7//vP+6/7r/tv+2/7L/r/+t/6//q/+q/6j/pf+h/6H/nv+e/5z/mf+Y/5n/lv+T/5H/j/+O/4v/h/+G/4b/g/+E/4H/f/98/3v/ev96/3n/d/92/3T/cf9w/27/a/9q/2f/Z/9l/2X/ZP9l/2P/X/9h/2H/X/9f/1//Yv9g/13/Xf9d/1//Xv9h/2L/Yv9i/2H/ZP9k/2X/Z/9o/2r/a/9t/3D/cP90/3f/eP94/33/fP9+/4L/hP+I/4n/jP+O/5L/k/+X/5r/nP+g/6T/pv+p/6r/rv+x/7T/t/+8/7//xP/H/8j/zP/O/9L/0//Z/9z/3P/i/+f/6f/s//D/8//3//v//v8DAAcACwANABEAEwAYABsAIAAlACsALgAxADYAOQA9AEEAQwBIAE0AUQBWAFkAXwBhAGcAbABwAHIAdwB7AH0AhACGAIkAjwCPAJMAlQCYAJsAngCgAKQApgCpAKkArACsAK0ArwCvALEAsgCyALIAtQC2ALMAtQC1ALYAuQC4ALgAuAC3ALcAtQC2ALQAtgCzALEAsACxALAAsQCwALAAsQCvAK8ArgCtAKsAqwCsAKwAqgCpAKkApgCmAKcApgCkAKUApACkAKMAoAChAKEAnwCfAJ4AngCfAJ8AngCcAJwAmwCaAJsAnACbAJgAmQCZAJgAlgCVAJQAlACSAJAAkACNAI8AjgCMAIwAiwCKAIoAiACFAIMAgwCCAIEAfgB7AHwAfQB9AHoAeAB2AHUAdwB2AHEAcABtAGwAawBqAGcAZgBnAGcAZABiAGEAXwBcAFoAVgBVAFUAVABRAE8ASQBHAEYARwBFAEEAQABAAD8APAA5ADgANwA0ADIALQArACkAJgAiACEAHAAZABgAFQATABAACgAHAAUAAgAAAPv/+P/z//H/7v/r/+f/5P/h/93/2P/X/9b/0f/Q/8v/w//A/7//uf+5/7b/sv+v/63/qf+l/6H/n/+c/5r/lP+T/4//jf+M/4j/hf+D/4H/f/99/3r/d/91/3T/c/90/3D/b/9s/2r/bf9q/2n/Zv9n/2L/Xv9e/1z/WP9X/1T/UP9Q/03/Tv9O/0r/Sv9J/0n/R/9J/0n/SP9I/0n/R/9H/0b/Sf9J/0v/SP9J/0r/TP9N/03/S/9M/0j/S/9L/0z/TP9M/0v/Sv9L/0z/S/9N/07/Tv9P/0//T/9T/1T/Vv9a/1r/W/9d/1z/Xf9e/2D/Yf9h/1//Xf9e/2H/Y/9i/2H/Y/9l/2T/ZP9o/2n/aP9p/23/bf9v/3D/cf90/3f/d/94/37/gf+F/4b/h/+K/4v/iv+M/4v/jP+N/4//kP+T/5T/k/+S/5P/lP+X/5j/mv+c/5//pv+n/6j/q/+t/7D/s/+3/7n/u/+//8D/w//F/8b/yP/M/83/zP/Q/9L/1P/X/9r/2f/a/9v/2//a/9v/3f/d/9//4f/i/+T/6f/p/+v/7f/y//P/9v/2//z//f8BAAIAAQAEAAgACQALAA4AEQARABIAFAAVABoAGwAeACAAJAAlACcAKAAnACYAJwApACkAKgAsAC8AMQAyADMALgArACwAKAArACgAKQAnACoAJQAlACEAHQAdAB8AHwAfAB8AIAAiACgAJwAtADAA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/f/+//3//P/9/wAAAgADAAUABwAKAAwADwASABMAEgAUABYAFwAYABkAGQAcABoAFwAZABkAGQAXABYAFwAVABMAEgASABIAEQAPAA4ADAAMAA0ACwAMAAwACwALAAsADQALAAoACwAJAAoADAAOAAwACgALAAwACwAKAAoABwAHAAYABAAFAAIAAQACAAEA///9//3//P/8//j/+f/0//j/9v/6//f/9P/0//j/9f/x//P/8f/x//D/7v/u/+3/7v/t/+7/6v/q/+7/7P/u//L/8v/0//f/9//2//b/9//6//3/AAACAAUABwAIAAgACgALAA4AEAATABcAGQAaABwAGwAdACAAHQAgACIAIgAgAB8AIQAfABsAGgAcABkAGwAZABwAHQAfAB0AGgAbAB8AHQAhACUAJgAoACwAKQAqACsAKAAtADMANAAzAC8AMgAsACsAKQAqACcAIgAhACAAGwAUABEADQAHAAMA/v/9//n/9P/v/+n/5P/i/+D/3f/d/+H/3f/a/9z/3f/e/9v/2P/c/+D/4v/m/+b/6P/n/+b/4//k/+b/5//u//H/8P/w//L/7P/m/+X/4f/h/+P/5P/l/+P/5v/a/8//zv/N/8z/0f/S/9X/1v/Y/9v/2v/b/+L/5P/o/+3/8P/y//P/9P/2//j/9v/8/wIABwAIAAQACQAEAAQABQAFAAcACgALAAoAAgD6//T/8P/x//P/7//v/+3/6v/p/+X/5P/i/+b/6P/y////BgAIAAoACgAGAAAABAAOABoAJwA0ADkAOgA6ADEAKwAmACwANABFAFwAXQBiAF0ATgBJADsANgA3ADcASwBMAFIAXwBSAFUAQwBJAD8ANQA3ACEAKwAcAC0AQwBJAGAATgBAACAAAQDx/9n/3f/i//j/CAAPABUABQDv/9L/xP+1/6f/tP+3/8z/4P/p/+f/2P/E/6v/of+o/8b/7P8TADgAQgA/AC8AEwD+//n/AQAeAD0AVwBGACsA///K/5r/f/99/4L/jv+J/3r/Zv9H/yb/Ev8Z/zb/V/99/47/jP+O/5X/q//P/wAAJgBGAFMAVABMAE0AZwB9AJcArQCsAJsAZAAmAPv/7f8GACcASABHACEAzf96/y7/7/7Y/uj+Ef9I/4X/qf/J/9f/xv+6/7L/y//e//f/LwBRAIgAkACDAFAA8//T/7b/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kQCJAIEAeABvAGcAXQBVAE0ARAA8ADMAKgAiABsAEwANAAMA+v/v/+j/3//Y/87/wf+3/67/pP+a/5H/i/+D/3z/df9u/2n/Y/9g/1v/Wv9W/1H/Tv9K/0n/Rf9D/0D/O/86/zf/Nf8x/y//Lv8t/yf/J/8m/yX/J/8n/yv/LP8u/zP/Nf86/zn/Qv9G/0r/Vf9Z/1//aP91/3v/gf+L/5b/oP+q/7T/vv/J/9P/4P/r//P//P8HABIAGwAmACwAMwA6AEMASQBPAFgAWwBiAGcAbQB0AHcAewB/AIQAiQCNAJIAmACeAKEApwCrAK4AtQC3ALoAvwDCAMUAyADNAMoA0ADRANIA1wDVANQA1ADRANAAzgDPAM4AzQDNAMsAywDHAMgAxwDHAMcAxQDGAMQAxADBAL8AwAC9AL0AuQC6ALgAuAC5ALgAtQC2ALUAtgCzALQAswC1ALAArgCtAK4AqwCrAKsAqACpAKMAowCgAJ4AngCaAJcAkwCPAI0AiACHAIEAewB2AHMAcABqAGUAYgBgAFoAVQBPAE4ASQBGAEQAPwA+ADYAMwAqACUAJAAdABoAFQAVAAsABwABAP3/9//0/+3/5//i/9r/0//O/8v/xP/A/7r/s/+s/6f/pf+g/5n/l/+S/4z/iP+F/4D/gP9+/3z/eP90/3f/dP9z/3L/aP9q/2r/Z/9o/2j/ZP9h/2T/Yv9j/2P/ZP9j/2X/Zv9k/2L/aP9m/2f/Y/9o/2r/af9t/3H/cf9y/3T/dP95/3n/ff9//4D/gf+A/4X/iP+N/43/kP+T/5f/mf+f/5//of+j/6r/p/+t/67/rv+u/7b/s/+3/7v/u/+8/77/wP/A/8L/xv/D/8P/wP/E/8L/w//F/8H/wv/D/8L/w//E/8L/xP/E/8b/w//D/8T/wv/A/8P/w//F/8L/xf/C/8b/xv/H/8X/xf/G/8P/xP/D/8T/xf/F/8X/x//G/8b/xf/F/8X/xf/E/8L/wP/A/8D/vf+9/7z/u/+8/7r/uv+5/7f/tv+2/7X/sv+y/7L/sP+w/67/sf+y/67/r/+v/67/rf+t/63/rf+u/6z/qP+p/6n/qv+q/6r/qf+p/6n/qP+q/6v/q/+s/6f/qP+p/6r/rP+s/6v/rv+x/7T/tP+3/7j/u/+7/77/wP/D/8T/yf/L/87/0v/a/9n/3v/g/+X/7P/w//L/9//9/wAABQAJAAsAEwATABsAIAAnACgALQAuADMAOgBAAEAARwBKAE8AVgBXAFwAXwBiAGYAbABvAHEAcAB1AHgAeQB9AH4AgACAAIQAhgCIAIYAiQCLAI0AjQCPAI4AjACLAI4AjQCOAIYAiACJAIcAhwCHAIUAhgCEAIMAggB/AIAAfQB6AHoAdgB1AHAAbwBsAGoAZgBmAGQAYABeAFgAVQBUAFEASgBJAEcARgBHAEIAPwA5ADkAPAA/ADgANQAwADIANQA0AC0AKgAnACcAKQAoACQAHgAfACAAIAAcABgAEQAVABcAFAARAA4ACwANABAAEAALAAcABgALAAwACgAHAAUABAAFAAYABwAEAP7/AQACAP////8AAP/////8//v/+f/5//n/+P/1//L/8f/0//T/8//w/+7/8P/y//T/8v/v/+z/7v/w/+//8P/s/+v/7v/x//P/7//s/+7/+f////n/9P/3//z//v8CAAEAAgAAAAUADAAOAAgACAAKAA4AEgAPAA8ADwATABkAGQAZABgAFwAZAB0AIAAhAB4AIgAmACUAJgAkACYAJwApACoAKwAqADAALwAwADMAMwA1ADcAOQA1ADQANwA5ADgANQA3ADYAOQA+AD0AOgA1ADcANwA8ADgANAAwAC8ANAA2AC8AKgAoACYALAAqACoAJQAlACQAJQAjACQAHQAaABkAFwAYABMADQAJAAkACgAHAAEAAAD///3//P/+//n/+f/2//P/8v/x/+z/6P/p/+j/5v/m/+L/3//e/9//2//W/9b/1P/T/9L/0//P/9D/0P/R/9H/zf/M/8v/x//E/8P/xP/C/8H/wf/C/8L/wf/E/8f/xv/E/8P/w//C/8T/v/++/77/wf/C/8L/vv+8/7v/wf/K/8X/xv/A/8X/yP/G/7z/wP++/7v/w/+//73/vf+7/7z/xf/I/77/wv/P/9L/0P/T/9H/z//R/9z/2//P/9T/2v/c/9b/2v/f/9//3P/q//H/5//n//D/8f/u/+3/7f/o/+z/9//5//b//P8EAAoABwAIAAYACQASABMADAAKABAAFwATAAsADgARABcAHgAgACQAJgApACkALAAuADYAMQAzADoAPAA9ADgAMgAuAC0AKgAqACkAKwApACoALwAyADMANQA4AEIARABFAEMAQgA5ADkA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", + "expires_at": 1729207811, + "transcript": "I'm sorry, but I can't create an audio clip of yelling. Is there anything else I can help you with?" +} \ No newline at end of file diff --git a/libs/langchain-openai/package.json b/libs/langchain-openai/package.json index eee9e586474e..82012d7d9e4f 100644 --- a/libs/langchain-openai/package.json +++ b/libs/langchain-openai/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/openai", - "version": "0.3.0", + "version": "0.3.11", "description": "OpenAI integrations for LangChain.js", "type": "module", "engines": { @@ -36,7 +36,7 @@ "license": "MIT", "dependencies": { "js-tiktoken": "^1.0.12", - "openai": "^4.57.3", + "openai": "^4.68.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.22.3" }, diff --git a/libs/langchain-openai/src/azure/chat_models.ts b/libs/langchain-openai/src/azure/chat_models.ts index e9a460e0f9f6..e8d78a204a6a 100644 --- a/libs/langchain-openai/src/azure/chat_models.ts +++ b/libs/langchain-openai/src/azure/chat_models.ts @@ -434,6 +434,11 @@ export class AzureChatOpenAI extends ChatOpenAI { openAIApiKey: "openai_api_key", openAIApiVersion: "openai_api_version", openAIBasePath: "openai_api_base", + deploymentName: "deployment_name", + azureOpenAIEndpoint: "azure_endpoint", + azureOpenAIApiVersion: "openai_api_version", + azureOpenAIBasePath: "openai_api_base", + azureOpenAIApiDeploymentName: "deployment_name", }; } @@ -477,6 +482,7 @@ export class AzureChatOpenAI extends ChatOpenAI { azureOpenAIBasePath: this.azureOpenAIBasePath, azureADTokenProvider: this.azureADTokenProvider, baseURL: this.clientConfig.baseURL, + azureOpenAIEndpoint: this.azureOpenAIEndpoint, }; const endpoint = getEndpoint(openAIEndpointConfig); @@ -541,6 +547,44 @@ export class AzureChatOpenAI extends ChatOpenAI { delete json.kwargs.azure_openai_api_key; delete json.kwargs.azure_openai_api_version; delete json.kwargs.azure_open_ai_base_path; + + if (!json.kwargs.azure_endpoint && this.azureOpenAIEndpoint) { + json.kwargs.azure_endpoint = this.azureOpenAIEndpoint; + } + if (!json.kwargs.azure_endpoint && this.azureOpenAIBasePath) { + const parts = this.azureOpenAIBasePath.split("/openai/deployments/"); + if (parts.length === 2 && parts[0].startsWith("http")) { + const [endpoint] = parts; + json.kwargs.azure_endpoint = endpoint; + } + } + if (!json.kwargs.azure_endpoint && this.azureOpenAIApiInstanceName) { + json.kwargs.azure_endpoint = `https://${this.azureOpenAIApiInstanceName}.openai.azure.com/`; + } + if (!json.kwargs.deployment_name && this.azureOpenAIApiDeploymentName) { + json.kwargs.deployment_name = this.azureOpenAIApiDeploymentName; + } + if (!json.kwargs.deployment_name && this.azureOpenAIBasePath) { + const parts = this.azureOpenAIBasePath.split("/openai/deployments/"); + if (parts.length === 2) { + const [, deployment] = parts; + json.kwargs.deployment_name = deployment; + } + } + + if ( + json.kwargs.azure_endpoint && + json.kwargs.deployment_name && + json.kwargs.openai_api_base + ) { + delete json.kwargs.openai_api_base; + } + if ( + json.kwargs.azure_openai_api_instance_name && + json.kwargs.azure_endpoint + ) { + delete json.kwargs.azure_openai_api_instance_name; + } } return json; diff --git a/libs/langchain-openai/src/chat_models.ts b/libs/langchain-openai/src/chat_models.ts index 3a0cccf13d04..4a0d438f0ba5 100644 --- a/libs/langchain-openai/src/chat_models.ts +++ b/libs/langchain-openai/src/chat_models.ts @@ -15,6 +15,7 @@ import { OpenAIToolCall, isAIMessage, convertToChunk, + UsageMetadata, } from "@langchain/core/messages"; import { type ChatGeneration, @@ -169,9 +170,15 @@ function openAIResponseToChatMessage( let response_metadata: Record | undefined; if (rawResponse.system_fingerprint) { response_metadata = { + usage: { ...rawResponse.usage }, system_fingerprint: rawResponse.system_fingerprint, }; } + + if (message.audio) { + additional_kwargs.audio = message.audio; + } + return new AIMessage({ content: message.content || "", tool_calls: toolCalls, @@ -210,8 +217,17 @@ function _convertDeltaToMessageChunk( if (includeRawResponse) { additional_kwargs.__raw_response = rawResponse; } + + if (delta.audio) { + additional_kwargs.audio = { + ...delta.audio, + index: rawResponse.choices[0].index, + }; + } + + const response_metadata = { usage: { ...rawResponse.usage } }; if (role === "user") { - return new HumanMessageChunk({ content }); + return new HumanMessageChunk({ content, response_metadata }); } else if (role === "assistant") { const toolCallChunks: ToolCallChunk[] = []; if (Array.isArray(delta.tool_calls)) { @@ -230,29 +246,35 @@ function _convertDeltaToMessageChunk( tool_call_chunks: toolCallChunks, additional_kwargs, id: rawResponse.id, + response_metadata, }); } else if (role === "system") { - return new SystemMessageChunk({ content }); + return new SystemMessageChunk({ content, response_metadata }); } else if (role === "function") { return new FunctionMessageChunk({ content, additional_kwargs, name: delta.name, + response_metadata, }); } else if (role === "tool") { return new ToolMessageChunk({ content, additional_kwargs, tool_call_id: delta.tool_call_id, + response_metadata, }); } else { - return new ChatMessageChunk({ content, role }); + return new ChatMessageChunk({ content, role, response_metadata }); } } -function convertMessagesToOpenAIParams(messages: BaseMessage[]) { +// Used in LangSmith, export is important here +export function _convertMessagesToOpenAIParams( + messages: BaseMessage[] +): OpenAICompletionParam[] { // TODO: Function messages do not support array content, fix cast - return messages.map((message) => { + return messages.flatMap((message) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const completionParam: Record = { role: messageToOpenAIRole(message), @@ -278,6 +300,21 @@ function convertMessagesToOpenAIParams(messages: BaseMessage[]) { completionParam.tool_call_id = (message as ToolMessage).tool_call_id; } } + + if ( + message.additional_kwargs.audio && + typeof message.additional_kwargs.audio === "object" && + "id" in message.additional_kwargs.audio + ) { + const audioMessage = { + role: "assistant", + audio: { + id: message.additional_kwargs.audio.id, + }, + }; + return [completionParam, audioMessage] as OpenAICompletionParam[]; + } + return completionParam as OpenAICompletionParam; }); } @@ -365,6 +402,27 @@ export interface ChatOpenAICallOptions * @version 0.2.6 */ strict?: boolean; + + /** + * Output types that you would like the model to generate for this request. Most + * models are capable of generating text, which is the default: + * + * `["text"]` + * + * The `gpt-4o-audio-preview` model can also be used to + * [generate audio](https://platform.openai.com/docs/guides/audio). To request that + * this model generate both text and audio responses, you can use: + * + * `["text", "audio"]` + */ + modalities?: Array; + + /** + * Parameters for audio output. Required when audio output is requested with + * `modalities: ["audio"]`. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ + audio?: OpenAIClient.Chat.ChatCompletionAudioParam; } export interface ChatOpenAIFields @@ -629,7 +687,10 @@ export interface ChatOpenAIFields * rating: z.number().optional().describe("How funny the joke is, from 1 to 10") * }).describe('Joke to tell user.'); * - * const structuredLlm = llm.withStructuredOutput(Joke, { name: "Joke" }); + * const structuredLlm = llm.withStructuredOutput(Joke, { + * name: "Joke", + * strict: true, // Optionally enable OpenAI structured outputs + * }); * const jokeResult = await structuredLlm.invoke("Tell me a joke about cats"); * console.log(jokeResult); * ``` @@ -832,6 +893,80 @@ export interface ChatOpenAIFields * * *
+ * + *
+ * Audio Outputs + * + * ```typescript + * import { ChatOpenAI } from "@langchain/openai"; + * + * const modelWithAudioOutput = new ChatOpenAI({ + * model: "gpt-4o-audio-preview", + * // You may also pass these fields to `.bind` as a call argument. + * modalities: ["text", "audio"], // Specifies that the model should output audio. + * audio: { + * voice: "alloy", + * format: "wav", + * }, + * }); + * + * const audioOutputResult = await modelWithAudioOutput.invoke("Tell me a joke about cats."); + * const castMessageContent = audioOutputResult.content[0] as Record; + * + * console.log({ + * ...castMessageContent, + * data: castMessageContent.data.slice(0, 100) // Sliced for brevity + * }) + * ``` + * + * ```txt + * { + * id: 'audio_67117718c6008190a3afad3e3054b9b6', + * data: 'UklGRqYwBgBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguMjkuMTAwAGRhdGFg', + * expires_at: 1729201448, + * transcript: 'Sure! Why did the cat sit on the computer? Because it wanted to keep an eye on the mouse!' + * } + * ``` + *
+ * + *
+ * + *
+ * Audio Outputs + * + * ```typescript + * import { ChatOpenAI } from "@langchain/openai"; + * + * const modelWithAudioOutput = new ChatOpenAI({ + * model: "gpt-4o-audio-preview", + * // You may also pass these fields to `.bind` as a call argument. + * modalities: ["text", "audio"], // Specifies that the model should output audio. + * audio: { + * voice: "alloy", + * format: "wav", + * }, + * }); + * + * const audioOutputResult = await modelWithAudioOutput.invoke("Tell me a joke about cats."); + * const castAudioContent = audioOutputResult.additional_kwargs.audio as Record; + * + * console.log({ + * ...castAudioContent, + * data: castAudioContent.data.slice(0, 100) // Sliced for brevity + * }) + * ``` + * + * ```txt + * { + * id: 'audio_67117718c6008190a3afad3e3054b9b6', + * data: 'UklGRqYwBgBXQVZFZm10IBAAAAABAAEAwF0AAIC7AAACABAATElTVBoAAABJTkZPSVNGVA4AAABMYXZmNTguMjkuMTAwAGRhdGFg', + * expires_at: 1729201448, + * transcript: 'Sure! Why did the cat sit on the computer? Because it wanted to keep an eye on the mouse!' + * } + * ``` + *
+ * + *
*/ export class ChatOpenAI< CallOptions extends ChatOpenAICallOptions = ChatOpenAICallOptions @@ -932,6 +1067,8 @@ export class ChatOpenAI< azureOpenAIBasePath?: string; + azureOpenAIEndpoint?: string; + organization?: string; __includeRawResponse?: boolean; @@ -946,6 +1083,10 @@ export class ChatOpenAI< */ supportsStrictToolCalling?: boolean; + audio?: OpenAIClient.Chat.ChatCompletionAudioParam; + + modalities?: Array; + constructor( fields?: ChatOpenAIFields, /** @deprecated */ @@ -992,6 +1133,10 @@ export class ChatOpenAI< fields?.configuration?.organization ?? getEnvironmentVariable("OPENAI_ORGANIZATION"); + this.azureOpenAIEndpoint = + fields?.azureOpenAIEndpoint ?? + getEnvironmentVariable("AZURE_OPENAI_ENDPOINT"); + this.modelName = fields?.model ?? fields?.modelName ?? this.model; this.model = this.modelName; this.modelKwargs = fields?.modelKwargs ?? {}; @@ -1010,11 +1155,25 @@ export class ChatOpenAI< this.stopSequences = this?.stop; this.user = fields?.user; this.__includeRawResponse = fields?.__includeRawResponse; + this.audio = fields?.audio; + this.modalities = fields?.modalities; if (this.azureOpenAIApiKey || this.azureADTokenProvider) { - if (!this.azureOpenAIApiInstanceName && !this.azureOpenAIBasePath) { + if ( + !this.azureOpenAIApiInstanceName && + !this.azureOpenAIBasePath && + !this.azureOpenAIEndpoint + ) { throw new Error("Azure OpenAI API instance name not found"); } + + if (!this.azureOpenAIApiDeploymentName && this.azureOpenAIBasePath) { + const parts = this.azureOpenAIBasePath.split("/openai/deployments/"); + if (parts.length === 2) { + const [, deployment] = parts; + this.azureOpenAIApiDeploymentName = deployment; + } + } if (!this.azureOpenAIApiDeploymentName) { throw new Error("Azure OpenAI API deployment name not found"); } @@ -1162,6 +1321,12 @@ export class ChatOpenAI< seed: options?.seed, ...streamOptionsConfig, parallel_tool_calls: options?.parallel_tool_calls, + ...(this.audio || options?.audio + ? { audio: this.audio || options?.audio } + : {}), + ...(this.modalities || options?.modalities + ? { modalities: this.modalities || options?.modalities } + : {}), ...this.modelKwargs, }; return params; @@ -1200,7 +1365,7 @@ export class ChatOpenAI< return; } const messagesMapped: OpenAICompletionParam[] = - convertMessagesToOpenAIParams(messages); + _convertMessagesToOpenAIParams(messages); const params = { ...this.invocationParams(options, { streaming: true, @@ -1209,36 +1374,11 @@ export class ChatOpenAI< stream: true as const, }; let defaultRole: OpenAIRoleEnum | undefined; - if ( - params.response_format && - params.response_format.type === "json_schema" - ) { - console.warn( - `OpenAI does not yet support streaming with "response_format" set to "json_schema". Falling back to non-streaming mode.` - ); - const res = await this._generate(messages, options, runManager); - const chunk = new ChatGenerationChunk({ - message: new AIMessageChunk({ - ...res.generations[0].message, - }), - text: res.generations[0].text, - generationInfo: res.generations[0].generationInfo, - }); - yield chunk; - return runManager?.handleLLMNewToken( - res.generations[0].text ?? "", - undefined, - undefined, - undefined, - undefined, - { chunk } - ); - } const streamIterable = await this.completionWithRetry(params, options); let usage: OpenAIClient.Completions.CompletionUsage | undefined; for await (const data of streamIterable) { - const choice = data?.choices[0]; + const choice = data?.choices?.[0]; if (data.usage) { usage = data.usage; } @@ -1294,13 +1434,38 @@ export class ChatOpenAI< ); } if (usage) { + const inputTokenDetails = { + ...(usage.prompt_tokens_details?.audio_tokens !== null && { + audio: usage.prompt_tokens_details?.audio_tokens, + }), + ...(usage.prompt_tokens_details?.cached_tokens !== null && { + cache_read: usage.prompt_tokens_details?.cached_tokens, + }), + }; + const outputTokenDetails = { + ...(usage.completion_tokens_details?.audio_tokens !== null && { + audio: usage.completion_tokens_details?.audio_tokens, + }), + ...(usage.completion_tokens_details?.reasoning_tokens !== null && { + reasoning: usage.completion_tokens_details?.reasoning_tokens, + }), + }; const generationChunk = new ChatGenerationChunk({ message: new AIMessageChunk({ content: "", + response_metadata: { + usage: { ...usage }, + }, usage_metadata: { input_tokens: usage.prompt_tokens, output_tokens: usage.completion_tokens, total_tokens: usage.total_tokens, + ...(Object.keys(inputTokenDetails).length > 0 && { + input_token_details: inputTokenDetails, + }), + ...(Object.keys(outputTokenDetails).length > 0 && { + output_token_details: outputTokenDetails, + }), }, }), text: "", @@ -1326,10 +1491,10 @@ export class ChatOpenAI< options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun ): Promise { - const tokenUsage: TokenUsage = {}; + const usageMetadata = {} as UsageMetadata; const params = this.invocationParams(options); const messagesMapped: OpenAICompletionParam[] = - convertMessagesToOpenAIParams(messages); + _convertMessagesToOpenAIParams(messages); if (params.stream) { const stream = this._streamResponseChunks(messages, options, runManager); @@ -1365,10 +1530,19 @@ export class ChatOpenAI< generations ); - tokenUsage.promptTokens = promptTokenUsage; - tokenUsage.completionTokens = completionTokenUsage; - tokenUsage.totalTokens = promptTokenUsage + completionTokenUsage; - return { generations, llmOutput: { estimatedTokenUsage: tokenUsage } }; + usageMetadata.input_tokens = promptTokenUsage; + usageMetadata.output_tokens = completionTokenUsage; + usageMetadata.total_tokens = promptTokenUsage + completionTokenUsage; + return { + generations, + llmOutput: { + estimatedTokenUsage: { + promptTokens: usageMetadata.input_tokens, + completionTokens: usageMetadata.output_tokens, + totalTokens: usageMetadata.total_tokens, + }, + }, + }; } else { let data; if ( @@ -1404,19 +1578,51 @@ export class ChatOpenAI< completion_tokens: completionTokens, prompt_tokens: promptTokens, total_tokens: totalTokens, + prompt_tokens_details: promptTokensDetails, + completion_tokens_details: completionTokensDetails, } = data?.usage ?? {}; if (completionTokens) { - tokenUsage.completionTokens = - (tokenUsage.completionTokens ?? 0) + completionTokens; + usageMetadata.output_tokens = + (usageMetadata.output_tokens ?? 0) + completionTokens; } if (promptTokens) { - tokenUsage.promptTokens = (tokenUsage.promptTokens ?? 0) + promptTokens; + usageMetadata.input_tokens = + (usageMetadata.input_tokens ?? 0) + promptTokens; } if (totalTokens) { - tokenUsage.totalTokens = (tokenUsage.totalTokens ?? 0) + totalTokens; + usageMetadata.total_tokens = + (usageMetadata.total_tokens ?? 0) + totalTokens; + } + + if ( + promptTokensDetails?.audio_tokens !== null || + promptTokensDetails?.cached_tokens !== null + ) { + usageMetadata.input_token_details = { + ...(promptTokensDetails?.audio_tokens !== null && { + audio: promptTokensDetails?.audio_tokens, + }), + ...(promptTokensDetails?.cached_tokens !== null && { + cache_read: promptTokensDetails?.cached_tokens, + }), + }; + } + + if ( + completionTokensDetails?.audio_tokens !== null || + completionTokensDetails?.reasoning_tokens !== null + ) { + usageMetadata.output_token_details = { + ...(completionTokensDetails?.audio_tokens !== null && { + audio: completionTokensDetails?.audio_tokens, + }), + ...(completionTokensDetails?.reasoning_tokens !== null && { + reasoning: completionTokensDetails?.reasoning_tokens, + }), + }; } const generations: ChatGeneration[] = []; @@ -1435,17 +1641,24 @@ export class ChatOpenAI< ...(part.logprobs ? { logprobs: part.logprobs } : {}), }; if (isAIMessage(generation.message)) { - generation.message.usage_metadata = { - input_tokens: tokenUsage.promptTokens ?? 0, - output_tokens: tokenUsage.completionTokens ?? 0, - total_tokens: tokenUsage.totalTokens ?? 0, - }; + generation.message.usage_metadata = usageMetadata; } + // Fields are not serialized unless passed to the constructor + // Doing this ensures all fields on the message are serialized + generation.message = new AIMessage({ + ...generation.message, + }); generations.push(generation); } return { generations, - llmOutput: { tokenUsage }, + llmOutput: { + tokenUsage: { + promptTokens: usageMetadata.input_tokens, + completionTokens: usageMetadata.output_tokens, + totalTokens: usageMetadata.total_tokens, + }, + }, }; } } @@ -1653,6 +1866,7 @@ export class ChatOpenAI< azureOpenAIApiKey: this.azureOpenAIApiKey, azureOpenAIBasePath: this.azureOpenAIBasePath, baseURL: this.clientConfig.baseURL, + azureOpenAIEndpoint: this.azureOpenAIEndpoint, }; const endpoint = getEndpoint(openAIEndpointConfig); diff --git a/libs/langchain-openai/src/index.ts b/libs/langchain-openai/src/index.ts index 76e8df482138..c18e60f26a0a 100644 --- a/libs/langchain-openai/src/index.ts +++ b/libs/langchain-openai/src/index.ts @@ -9,3 +9,4 @@ export * from "./types.js"; export * from "./utils/openai.js"; export * from "./utils/azure.js"; export * from "./tools/index.js"; +export { convertPromptToOpenAI } from "./utils/prompts.js"; diff --git a/libs/langchain-openai/src/tests/azure/chat_models.test.ts b/libs/langchain-openai/src/tests/azure/chat_models.test.ts new file mode 100644 index 000000000000..84bfd726dd76 --- /dev/null +++ b/libs/langchain-openai/src/tests/azure/chat_models.test.ts @@ -0,0 +1,37 @@ +import { AzureChatOpenAI } from "../../azure/chat_models.js"; + +test("Test Azure OpenAI serialization from azure endpoint", async () => { + const chat = new AzureChatOpenAI({ + azureOpenAIEndpoint: "https://foobar.openai.azure.com/", + azureOpenAIApiDeploymentName: "gpt-4o", + azureOpenAIApiVersion: "2024-08-01-preview", + azureOpenAIApiKey: "foo", + }); + expect(JSON.stringify(chat)).toEqual( + `{"lc":1,"type":"constructor","id":["langchain","chat_models","azure_openai","AzureChatOpenAI"],"kwargs":{"azure_endpoint":"https://foobar.openai.azure.com/","openai_api_key":{"lc":1,"type":"secret","id":["OPENAI_API_KEY"]},"deployment_name":"gpt-4o"}}` + ); +}); + +test("Test Azure OpenAI serialization from base path", async () => { + const chat = new AzureChatOpenAI({ + azureOpenAIBasePath: + "https://foobar.openai.azure.com/openai/deployments/gpt-4o", + azureOpenAIApiVersion: "2024-08-01-preview", + azureOpenAIApiKey: "foo", + }); + expect(JSON.stringify(chat)).toEqual( + `{"lc":1,"type":"constructor","id":["langchain","chat_models","azure_openai","AzureChatOpenAI"],"kwargs":{"openai_api_key":{"lc":1,"type":"secret","id":["OPENAI_API_KEY"]},"azure_endpoint":"https://foobar.openai.azure.com","deployment_name":"gpt-4o"}}` + ); +}); + +test("Test Azure OpenAI serialization from instance name", async () => { + const chat = new AzureChatOpenAI({ + azureOpenAIApiInstanceName: "foobar", + azureOpenAIApiDeploymentName: "gpt-4o", + azureOpenAIApiVersion: "2024-08-01-preview", + azureOpenAIApiKey: "foo", + }); + expect(JSON.stringify(chat)).toEqual( + `{"lc":1,"type":"constructor","id":["langchain","chat_models","azure_openai","AzureChatOpenAI"],"kwargs":{"openai_api_key":{"lc":1,"type":"secret","id":["OPENAI_API_KEY"]},"azure_endpoint":"https://foobar.openai.azure.com/","deployment_name":"gpt-4o"}}` + ); +}); diff --git a/libs/langchain-openai/src/tests/chat_models-extended.int.test.ts b/libs/langchain-openai/src/tests/chat_models-extended.int.test.ts index 99c8dc9c7c00..970400634d4f 100644 --- a/libs/langchain-openai/src/tests/chat_models-extended.int.test.ts +++ b/libs/langchain-openai/src/tests/chat_models-extended.int.test.ts @@ -1,5 +1,8 @@ +/* eslint-disable no-promise-executor-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { test, expect, jest } from "@jest/globals"; import { AIMessage, HumanMessage, ToolMessage } from "@langchain/core/messages"; +import { concat } from "@langchain/core/utils/stream"; import { InMemoryCache } from "@langchain/core/caches"; import { ChatOpenAI } from "../chat_models.js"; @@ -164,6 +167,17 @@ test("Test ChatOpenAI tool calling with ToolMessages", async () => { ), }) ); + let toolError; + try { + await chat.invoke([ + ["human", "What's the weather like in San Francisco, Tokyo, and Paris?"], + res, + ]); + } catch (e) { + toolError = e; + } + expect(toolError).toBeDefined(); + expect((toolError as any)?.lc_error_code).toEqual("INVALID_TOOL_RESULTS"); // @eslint-disable-next-line/@typescript-eslint/ban-ts-comment // @ts-expect-error unused var const finalResponse = await chat.invoke([ @@ -328,3 +342,297 @@ test("Test ChatOpenAI with raw response", async () => { ).toBeDefined(); } }); + +const CACHED_TEXT = `## Components + +LangChain provides standard, extendable interfaces and external integrations for various components useful for building with LLMs. +Some components LangChain implements, some components we rely on third-party integrations for, and others are a mix. + +### Chat models + + + +Language models that use a sequence of messages as inputs and return chat messages as outputs (as opposed to using plain text). +These are generally newer models (older models are generally \`LLMs\`, see below). +Chat models support the assignment of distinct roles to conversation messages, helping to distinguish messages from the AI, users, and instructions such as system messages. + +Although the underlying models are messages in, message out, the LangChain wrappers also allow these models to take a string as input. +This gives them the same interface as LLMs (and simpler to use). +When a string is passed in as input, it will be converted to a \`HumanMessage\` under the hood before being passed to the underlying model. + +LangChain does not host any Chat Models, rather we rely on third party integrations. + +We have some standardized parameters when constructing ChatModels: + +- \`model\`: the name of the model + +Chat Models also accept other parameters that are specific to that integration. + +:::important +Some chat models have been fine-tuned for **tool calling** and provide a dedicated API for it. +Generally, such models are better at tool calling than non-fine-tuned models, and are recommended for use cases that require tool calling. +Please see the [tool calling section](/docs/concepts/#functiontool-calling) for more information. +::: + +For specifics on how to use chat models, see the [relevant how-to guides here](/docs/how_to/#chat-models). + +#### Multimodality + +Some chat models are multimodal, accepting images, audio and even video as inputs. +These are still less common, meaning model providers haven't standardized on the "best" way to define the API. +Multimodal outputs are even less common. As such, we've kept our multimodal abstractions fairly light weight +and plan to further solidify the multimodal APIs and interaction patterns as the field matures. + +In LangChain, most chat models that support multimodal inputs also accept those values in OpenAI's content blocks format. +So far this is restricted to image inputs. For models like Gemini which support video and other bytes input, the APIs also support the native, model-specific representations. + +For specifics on how to use multimodal models, see the [relevant how-to guides here](/docs/how_to/#multimodal). + +### LLMs + + + +:::caution +Pure text-in/text-out LLMs tend to be older or lower-level. Many popular models are best used as [chat completion models](/docs/concepts/#chat-models), +even for non-chat use cases. + +You are probably looking for [the section above instead](/docs/concepts/#chat-models). +::: + +Language models that takes a string as input and returns a string. +These are traditionally older models (newer models generally are [Chat Models](/docs/concepts/#chat-models), see above). + +Although the underlying models are string in, string out, the LangChain wrappers also allow these models to take messages as input. +This gives them the same interface as [Chat Models](/docs/concepts/#chat-models). +When messages are passed in as input, they will be formatted into a string under the hood before being passed to the underlying model. + +LangChain does not host any LLMs, rather we rely on third party integrations. + +For specifics on how to use LLMs, see the [relevant how-to guides here](/docs/how_to/#llms). + +### Message types + +Some language models take an array of messages as input and return a message. +There are a few different types of messages. +All messages have a \`role\`, \`content\`, and \`response_metadata\` property. + +The \`role\` describes WHO is saying the message. +LangChain has different message classes for different roles. + +The \`content\` property describes the content of the message. +This can be a few different things: + +- A string (most models deal this type of content) +- A List of objects (this is used for multi-modal input, where the object contains information about that input type and that input location) + +#### HumanMessage + +This represents a message from the user. + +#### AIMessage + +This represents a message from the model. In addition to the \`content\` property, these messages also have: + +**\`response_metadata\`** + +The \`response_metadata\` property contains additional metadata about the response. The data here is often specific to each model provider. +This is where information like log-probs and token usage may be stored. + +**\`tool_calls\`** + +These represent a decision from an language model to call a tool. They are included as part of an \`AIMessage\` output. +They can be accessed from there with the \`.tool_calls\` property. + +This property returns a list of \`ToolCall\`s. A \`ToolCall\` is an object with the following arguments: + +- \`name\`: The name of the tool that should be called. +- \`args\`: The arguments to that tool. +- \`id\`: The id of that tool call. + +#### SystemMessage + +This represents a system message, which tells the model how to behave. Not every model provider supports this. + +#### ToolMessage + +This represents the result of a tool call. In addition to \`role\` and \`content\`, this message has: + +- a \`tool_call_id\` field which conveys the id of the call to the tool that was called to produce this result. +- an \`artifact\` field which can be used to pass along arbitrary artifacts of the tool execution which are useful to track but which should not be sent to the model. + +#### (Legacy) FunctionMessage + +This is a legacy message type, corresponding to OpenAI's legacy function-calling API. \`ToolMessage\` should be used instead to correspond to the updated tool-calling API. + +This represents the result of a function call. In addition to \`role\` and \`content\`, this message has a \`name\` parameter which conveys the name of the function that was called to produce this result. + +### Prompt templates + + + +Prompt templates help to translate user input and parameters into instructions for a language model. +This can be used to guide a model's response, helping it understand the context and generate relevant and coherent language-based output. + +Prompt Templates take as input an object, where each key represents a variable in the prompt template to fill in. + +Prompt Templates output a PromptValue. This PromptValue can be passed to an LLM or a ChatModel, and can also be cast to a string or an array of messages. +The reason this PromptValue exists is to make it easy to switch between strings and messages. + +There are a few different types of prompt templates: + +#### String PromptTemplates + +These prompt templates are used to format a single string, and generally are used for simpler inputs. +For example, a common way to construct and use a PromptTemplate is as follows: + +\`\`\`typescript +import { PromptTemplate } from "@langchain/core/prompts"; + +const promptTemplate = PromptTemplate.fromTemplate( + "Tell me a joke about {topic}" +); + +await promptTemplate.invoke({ topic: "cats" }); +\`\`\` + +#### ChatPromptTemplates + +These prompt templates are used to format an array of messages. These "templates" consist of an array of templates themselves. +For example, a common way to construct and use a ChatPromptTemplate is as follows: + +\`\`\`typescript +import { ChatPromptTemplate } from "@langchain/core/prompts"; + +const promptTemplate = ChatPromptTemplate.fromMessages([ + ["system", "You are a helpful assistant"], + ["user", "Tell me a joke about {topic}"], +]); + +await promptTemplate.invoke({ topic: "cats" }); +\`\`\` + +In the above example, this ChatPromptTemplate will construct two messages when called. +The first is a system message, that has no variables to format. +The second is a HumanMessage, and will be formatted by the \`topic\` variable the user passes in. + +#### MessagesPlaceholder + + + +This prompt template is responsible for adding an array of messages in a particular place. +In the above ChatPromptTemplate, we saw how we could format two messages, each one a string. +But what if we wanted the user to pass in an array of messages that we would slot into a particular spot? +This is how you use MessagesPlaceholder. + +\`\`\`typescript +import { + ChatPromptTemplate, + MessagesPlaceholder, +} from "@langchain/core/prompts"; +import { HumanMessage } from "@langchain/core/messages"; + +const promptTemplate = ChatPromptTemplate.fromMessages([ + ["system", "You are a helpful assistant"], + new MessagesPlaceholder("msgs"), +]); + +promptTemplate.invoke({ msgs: [new HumanMessage({ content: "hi!" })] }); +\`\`\` + +This will produce an array of two messages, the first one being a system message, and the second one being the HumanMessage we passed in. +If we had passed in 5 messages, then it would have produced 6 messages in total (the system message plus the 5 passed in). +This is useful for letting an array of messages be slotted into a particular spot. + +An alternative way to accomplish the same thing without using the \`MessagesPlaceholder\` class explicitly is: + +\`\`\`typescript +const promptTemplate = ChatPromptTemplate.fromMessages([ + ["system", "You are a helpful assistant"], + ["placeholder", "{msgs}"], // <-- This is the changed part +]); +\`\`\` + +For specifics on how to use prompt templates, see the [relevant how-to guides here](/docs/how_to/#prompt-templates). + +### Example Selectors + +One common prompting technique for achieving better performance is to include examples as part of the prompt. +This gives the language model concrete examples of how it should behave. +Sometimes these examples are hardcoded into the prompt, but for more advanced situations it may be nice to dynamically select them. +Example Selectors are classes responsible for selecting and then formatting examples into prompts. + +For specifics on how to use example selectors, see the [relevant how-to guides here](/docs/how_to/#example-selectors). + +### Output parsers + + + +:::note + +The information here refers to parsers that take a text output from a model try to parse it into a more structured representation. +More and more models are supporting function (or tool) calling, which handles this automatically. +It is recommended to use function/tool calling rather than output parsing. +See documentation for that [here](/docs/concepts/#function-tool-calling). + +::: + +Responsible for taking the output of a model and transforming it to a more suitable format for downstream tasks. +Useful when you are using LLMs to generate structured data, or to normalize output from chat models and LLMs. + +There are two main methods an output parser must implement: + +- "Get format instructions": A method which returns a string containing instructions for how the output of a language model should be formatted. +- "Parse": A method which takes in a string (assumed to be the response from a language model) and parses it into some structure. + +And then one optional one: + +- "Parse with prompt": A method which takes in a string (assumed to be the response from a language model) and a prompt (assumed to be the prompt that generated such a response) and parses it into some structure. The prompt is largely provided in the event the OutputParser wants to retry or fix the output in some way, and needs information from the prompt to do so. + +Output parsers accept a string or \`BaseMessage\` as input and can return an arbitrary type. + +LangChain has many different types of output parsers. This is a list of output parsers LangChain supports. The table below has various pieces of information: + +**Name**: The name of the output parser + +**Supports Streaming**: Whether the output parser supports streaming. + +**Input Type**: Expected input type. Most output parsers work on both strings and messages, but some (like OpenAI Functions) need a message with specific arguments. + +**Output Type**: The output type of the object returned by the parser. + +**Description**: Our commentary on this output parser and when to use it. + +The current date is ${new Date().toISOString()}`; + +test.skip("system prompt caching", async () => { + const model = new ChatOpenAI({ + model: "gpt-4o-mini-2024-07-18", + }); + const date = new Date().toISOString(); + const messages = [ + { + role: "system", + content: `You are a pirate. Always respond in pirate dialect. The current date is ${date}.\nUse the following as context when answering questions: ${CACHED_TEXT}`, + }, + { + role: "user", + content: "What types of messages are supported in LangChain?", + }, + ]; + const res = await model.invoke(messages); + expect(res.response_metadata?.usage.prompt_tokens_details.cached_tokens).toBe( + 0 + ); + await new Promise((resolve) => setTimeout(resolve, 5000)); + const res2 = await model.invoke(messages); + expect( + res2.response_metadata?.usage.prompt_tokens_details.cached_tokens + ).toBeGreaterThan(0); + let aggregate; + for await (const chunk of await model.stream(messages)) { + aggregate = aggregate ? concat(aggregate, chunk) : chunk; + } + expect( + aggregate?.response_metadata?.usage.prompt_tokens_details.cached_tokens + ).toBeGreaterThan(0); +}); diff --git a/libs/langchain-openai/src/tests/chat_models.int.test.ts b/libs/langchain-openai/src/tests/chat_models.int.test.ts index 4e0957d25ec2..a5bed0811e61 100644 --- a/libs/langchain-openai/src/tests/chat_models.int.test.ts +++ b/libs/langchain-openai/src/tests/chat_models.int.test.ts @@ -1,5 +1,5 @@ /* eslint-disable no-process-env */ - +/* eslint-disable @typescript-eslint/no-explicit-any */ import { test, jest, expect } from "@jest/globals"; import { AIMessageChunk, @@ -19,6 +19,7 @@ import { import { CallbackManager } from "@langchain/core/callbacks/manager"; import { NewTokenIndices } from "@langchain/core/callbacks/base"; import { InMemoryCache } from "@langchain/core/caches"; +import { concat } from "@langchain/core/utils/stream"; import { ChatOpenAI } from "../chat_models.js"; // Save the original value of the 'LANGCHAIN_CALLBACKS_BACKGROUND' environment variable @@ -43,6 +44,41 @@ test("Test ChatOpenAI Generate", async () => { // console.log({ res }); }); +test("Test ChatOpenAI invoke fails with proper error", async () => { + const chat = new ChatOpenAI({ + model: "gpt-4o-mini", + maxTokens: 10, + n: 2, + apiKey: "bad", + }); + const message = new HumanMessage("Hello!"); + let authError; + try { + await chat.invoke([message]); + } catch (e) { + authError = e; + } + expect(authError).toBeDefined(); + expect((authError as any)?.lc_error_code).toEqual("MODEL_AUTHENTICATION"); +}); + +test("Test ChatOpenAI invoke to unknown model fails with proper error", async () => { + const chat = new ChatOpenAI({ + model: "badbadbad", + maxTokens: 10, + n: 2, + }); + const message = new HumanMessage("Hello!"); + let authError; + try { + await chat.invoke([message]); + } catch (e) { + authError = e; + } + expect(authError).toBeDefined(); + expect((authError as any)?.lc_error_code).toEqual("MODEL_NOT_FOUND"); +}); + test("Test ChatOpenAI Generate throws when one of the calls fails", async () => { const chat = new ChatOpenAI({ modelName: "gpt-3.5-turbo", @@ -951,3 +987,182 @@ test("Test ChatOpenAI stream method", async () => { } expect(chunks.length).toEqual(1); }); + +describe("Audio output", () => { + test("Audio output", async () => { + const model = new ChatOpenAI({ + maxRetries: 0, + model: "gpt-4o-audio-preview", + temperature: 0, + modalities: ["text", "audio"], + audio: { + voice: "alloy", + format: "wav", + }, + }); + + const response = await model.invoke("Make me an audio clip of you yelling"); + expect(response.additional_kwargs.audio).toBeTruthy(); + if (!response.additional_kwargs.audio) { + throw new Error("Not in additional kwargs"); + } + // console.log( + // "response.additional_kwargs.audio", + // response.additional_kwargs.audio + // ); + expect(Object.keys(response.additional_kwargs.audio).sort()).toEqual([ + "data", + "expires_at", + "id", + "transcript", + ]); + }); + + test("Audio output can stream", async () => { + const model = new ChatOpenAI({ + maxRetries: 0, + model: "gpt-4o-audio-preview", + temperature: 0, + modalities: ["text", "audio"], + audio: { + voice: "alloy", + format: "pcm16", + }, + }); + + const stream = await model.stream("Make me an audio clip of you yelling"); + let finalMsg: AIMessageChunk | undefined; + for await (const chunk of stream) { + finalMsg = finalMsg ? concat(finalMsg, chunk) : chunk; + } + if (!finalMsg) { + throw new Error("No final message found"); + } + + expect(finalMsg.additional_kwargs.audio).toBeTruthy(); + if (!finalMsg.additional_kwargs.audio) { + throw new Error("Not in additional kwargs"); + } + // console.log( + // "response.additional_kwargs.audio", + // finalMsg.additional_kwargs.audio + // ); + expect(Object.keys(finalMsg.additional_kwargs.audio).sort()).toEqual([ + "data", + "expires_at", + "id", + "index", + "transcript", + ]); + }); + + test("Can bind audio output args", async () => { + const model = new ChatOpenAI({ + maxRetries: 0, + model: "gpt-4o-audio-preview", + temperature: 0, + }).bind({ + modalities: ["text", "audio"], + audio: { + voice: "alloy", + format: "wav", + }, + }); + + const response = await model.invoke("Make me an audio clip of you yelling"); + expect(response.additional_kwargs.audio).toBeTruthy(); + if (!response.additional_kwargs.audio) { + throw new Error("Not in additional kwargs"); + } + expect(Object.keys(response.additional_kwargs.audio).sort()).toEqual([ + "data", + "expires_at", + "id", + "transcript", + ]); + }); + + test("Audio output in chat history", async () => { + const model = new ChatOpenAI({ + model: "gpt-4o-audio-preview", + temperature: 0, + modalities: ["text", "audio"], + audio: { + voice: "alloy", + format: "wav", + }, + maxRetries: 0, + }); + + const input = [ + { + role: "user", + content: "Make me an audio clip of you yelling", + }, + ]; + + const response = await model.invoke(input); + expect(response.additional_kwargs.audio).toBeTruthy(); + expect( + (response.additional_kwargs.audio as Record).transcript + .length + ).toBeGreaterThan(1); + // console.log("response", (response.additional_kwargs.audio as any).transcript); + const response2 = await model.invoke([ + ...input, + response, + { + role: "user", + content: "What did you just say?", + }, + ]); + // console.log("response2", (response2.additional_kwargs.audio as any).transcript); + expect(response2.additional_kwargs.audio).toBeTruthy(); + expect( + (response2.additional_kwargs.audio as Record).transcript + .length + ).toBeGreaterThan(1); + }); + + test("Users can pass audio as inputs", async () => { + const model = new ChatOpenAI({ + maxRetries: 0, + model: "gpt-4o-audio-preview", + temperature: 0, + modalities: ["text", "audio"], + audio: { + voice: "alloy", + format: "wav", + }, + }); + + const response = await model.invoke("Make me an audio clip of you yelling"); + // console.log("response", (response.additional_kwargs.audio as any).transcript); + expect(response.additional_kwargs.audio).toBeTruthy(); + expect( + (response.additional_kwargs.audio as Record).transcript + .length + ).toBeGreaterThan(1); + + const userInput = { + type: "input_audio", + input_audio: { + data: (response.additional_kwargs.audio as any).data, + format: "wav", + }, + }; + + const userInputRes = await model.invoke([ + new HumanMessage({ + content: [userInput], + }), + ]); + // console.log("userInputRes.content", userInputRes.content); + // console.log("userInputRes.additional_kwargs.audio", userInputRes.additional_kwargs.audio); + expect(userInputRes.additional_kwargs.audio).toBeTruthy(); + expect( + (userInputRes.additional_kwargs.audio as Record).transcript + .length + ).toBeGreaterThan(1); + }); +}); diff --git a/libs/langchain-openai/src/tests/chat_models.standard.int.test.ts b/libs/langchain-openai/src/tests/chat_models.standard.int.test.ts index f16536d91997..1dd34509ed04 100644 --- a/libs/langchain-openai/src/tests/chat_models.standard.int.test.ts +++ b/libs/langchain-openai/src/tests/chat_models.standard.int.test.ts @@ -1,9 +1,15 @@ /* eslint-disable no-process-env */ -import { test, expect } from "@jest/globals"; +import { test } from "@jest/globals"; import { ChatModelIntegrationTests } from "@langchain/standard-tests"; -import { AIMessageChunk } from "@langchain/core/messages"; +import { AIMessage, AIMessageChunk } from "@langchain/core/messages"; +import { readFileSync } from "fs"; +import { join } from "path"; +import { concat } from "@langchain/core/utils/stream"; +import { BaseLanguageModelInput } from "@langchain/core/language_models/base"; import { ChatOpenAI, ChatOpenAICallOptions } from "../chat_models.js"; +const REPO_ROOT_DIR = process.cwd(); + class ChatOpenAIStandardIntegrationTests extends ChatModelIntegrationTests< ChatOpenAICallOptions, AIMessageChunk @@ -25,6 +31,48 @@ class ChatOpenAIStandardIntegrationTests extends ChatModelIntegrationTests< }); } + supportedUsageMetadataDetails: { + invoke: Array< + | "audio_input" + | "audio_output" + | "reasoning_output" + | "cache_read_input" + | "cache_creation_input" + >; + stream: Array< + | "audio_input" + | "audio_output" + | "reasoning_output" + | "cache_read_input" + | "cache_creation_input" + >; + } = { invoke: ["cache_read_input", "reasoning_output"], stream: [] }; + + async invokeWithReasoningOutput(stream: boolean) { + const chatModel = new ChatOpenAI({ + model: "o1-mini", + streamUsage: true, + temperature: 1, + }); + const input = + "explain the relationship between the 2008/9 economic crisis and the startup ecosystem in the early 2010s"; + + return invoke(chatModel, input, stream); + } + + async invokeWithCacheReadInput(stream: boolean = false): Promise { + const readme = readFileSync(join(REPO_ROOT_DIR, "README.md"), "utf-8"); + + const input = `What's langchain? Here's the langchain README: + + ${readme} + `; + const llm = new ChatOpenAI({ modelName: "gpt-4o-mini", streamUsage: true }); + await invoke(llm, input, stream); + // invoke twice so first invocation is cached + return invoke(llm, input, stream); + } + async testUsageMetadataStreaming() { // ChatOpenAI does not support streaming tokens by // default, so we must pass in a call option to @@ -65,3 +113,22 @@ test("ChatOpenAIStandardIntegrationTests", async () => { const testResults = await testClass.runTests(); expect(testResults).toBe(true); }); + +async function invoke( + chatModel: ChatOpenAI, + input: BaseLanguageModelInput, + stream: boolean +): Promise { + if (stream) { + let finalChunks: AIMessageChunk | undefined; + + // Stream the response for a simple "Hello" prompt + for await (const chunk of await chatModel.stream(input)) { + // Concatenate chunks to get the final result + finalChunks = finalChunks ? concat(finalChunks, chunk) : chunk; + } + return finalChunks as AIMessage; + } else { + return chatModel.invoke(input); + } +} diff --git a/libs/langchain-openai/src/tests/prompts.int.test.ts b/libs/langchain-openai/src/tests/prompts.int.test.ts new file mode 100644 index 000000000000..5e54da012fe0 --- /dev/null +++ b/libs/langchain-openai/src/tests/prompts.int.test.ts @@ -0,0 +1,25 @@ +import OpenAI from "openai"; +import { ChatPromptTemplate } from "@langchain/core/prompts"; + +import { convertPromptToOpenAI } from "../utils/prompts.js"; + +test("Convert hub prompt to OpenAI payload and invoke", async () => { + const prompt = ChatPromptTemplate.fromMessages([ + ["system", "You are a world class comedian"], + ["human", "Tell me a joke about {topic}"], + ]); + const formattedPrompt = await prompt.invoke({ + topic: "cats", + }); + + const { messages } = convertPromptToOpenAI(formattedPrompt); + + const openAIClient = new OpenAI(); + + const openAIResponse = await openAIClient.chat.completions.create({ + model: "gpt-4o-mini", + messages, + }); + + expect(openAIResponse.choices.length).toBeGreaterThan(0); +}); diff --git a/libs/langchain-openai/src/types.ts b/libs/langchain-openai/src/types.ts index 04d8865713d4..1eb449443791 100644 --- a/libs/langchain-openai/src/types.ts +++ b/libs/langchain-openai/src/types.ts @@ -167,6 +167,27 @@ export interface OpenAIChatInput extends OpenAIBaseInput { * If `undefined` the `strict` argument will not be passed to OpenAI. */ supportsStrictToolCalling?: boolean; + + /** + * Output types that you would like the model to generate for this request. Most + * models are capable of generating text, which is the default: + * + * `["text"]` + * + * The `gpt-4o-audio-preview` model can also be used to + * [generate audio](https://platform.openai.com/docs/guides/audio). To request that + * this model generate both text and audio responses, you can use: + * + * `["text", "audio"]` + */ + modalities?: Array; + + /** + * Parameters for audio output. Required when audio output is requested with + * `modalities: ["audio"]`. + * [Learn more](https://platform.openai.com/docs/guides/audio). + */ + audio?: OpenAIClient.Chat.ChatCompletionAudioParam; } export declare interface AzureOpenAIInput { @@ -216,12 +237,19 @@ export declare interface AzureOpenAIInput { azureOpenAIApiCompletionsDeploymentName?: string; /** - * Custom endpoint for Azure OpenAI API. This is useful in case you have a deployment in another region. + * Custom base url for Azure OpenAI API. This is useful in case you have a deployment in another region. * e.g. setting this value to "https://westeurope.api.cognitive.microsoft.com/openai/deployments" * will be result in the endpoint URL: https://westeurope.api.cognitive.microsoft.com/openai/deployments/{DeploymentName}/ */ azureOpenAIBasePath?: string; + /** + * Custom endpoint for Azure OpenAI API. This is useful in case you have a deployment in another region. + * e.g. setting this value to "https://westeurope.api.cognitive.microsoft.com/" + * will be result in the endpoint URL: https://westeurope.api.cognitive.microsoft.com/openai/deployments/{DeploymentName}/ + */ + azureOpenAIEndpoint?: string; + /** * A function that returns an access token for Microsoft Entra (formerly known as Azure Active Directory), * which will be invoked on every request. diff --git a/libs/langchain-openai/src/utils/azure.ts b/libs/langchain-openai/src/utils/azure.ts index 0f3fa68eb4f8..2379dc4db6f9 100644 --- a/libs/langchain-openai/src/utils/azure.ts +++ b/libs/langchain-openai/src/utils/azure.ts @@ -5,6 +5,7 @@ export interface OpenAIEndpointConfig { azureADTokenProvider?: () => Promise; azureOpenAIBasePath?: string; baseURL?: string | null; + azureOpenAIEndpoint?: string; } /** @@ -14,13 +15,15 @@ export interface OpenAIEndpointConfig { * @param {OpenAIEndpointConfig} config - The configuration object for the (Azure) endpoint. * * @property {string} config.azureOpenAIApiDeploymentName - The deployment name of Azure OpenAI. - * @property {string} config.azureOpenAIApiInstanceName - The instance name of Azure OpenAI. + * @property {string} config.azureOpenAIApiInstanceName - The instance name of Azure OpenAI, e.g. `example-resource`. * @property {string} config.azureOpenAIApiKey - The API Key for Azure OpenAI. - * @property {string} config.azureOpenAIBasePath - The base path for Azure OpenAI. + * @property {string} config.azureOpenAIBasePath - The base path for Azure OpenAI, e.g. `https://example-resource.azure.openai.com/openai/deployments/`. * @property {string} config.baseURL - Some other custom base path URL. + * @property {string} config.azureOpenAIEndpoint - The endpoint for the Azure OpenAI instance, e.g. `https://example-resource.azure.openai.com/`. * * The function operates as follows: * - If both `azureOpenAIBasePath` and `azureOpenAIApiDeploymentName` (plus `azureOpenAIApiKey`) are provided, it returns an URL combining these two parameters (`${azureOpenAIBasePath}/${azureOpenAIApiDeploymentName}`). + * - If both `azureOpenAIEndpoint` and `azureOpenAIApiDeploymentName` (plus `azureOpenAIApiKey`) are provided, it returns an URL combining these two parameters (`${azureOpenAIEndpoint}/openai/deployments/${azureOpenAIApiDeploymentName}`). * - If `azureOpenAIApiKey` is provided, it checks for `azureOpenAIApiInstanceName` and `azureOpenAIApiDeploymentName` and throws an error if any of these is missing. If both are provided, it generates an URL incorporating these parameters. * - If none of the above conditions are met, return any custom `baseURL`. * - The function returns the generated URL as a string, or undefined if no custom paths are specified. @@ -37,6 +40,7 @@ export function getEndpoint(config: OpenAIEndpointConfig) { azureOpenAIBasePath, baseURL, azureADTokenProvider, + azureOpenAIEndpoint, } = config; if ( @@ -46,6 +50,13 @@ export function getEndpoint(config: OpenAIEndpointConfig) { ) { return `${azureOpenAIBasePath}/${azureOpenAIApiDeploymentName}`; } + if ( + (azureOpenAIApiKey || azureADTokenProvider) && + azureOpenAIEndpoint && + azureOpenAIApiDeploymentName + ) { + return `${azureOpenAIEndpoint}/openai/deployments/${azureOpenAIApiDeploymentName}`; + } if (azureOpenAIApiKey || azureADTokenProvider) { if (!azureOpenAIApiInstanceName) { diff --git a/libs/langchain-openai/src/utils/errors.ts b/libs/langchain-openai/src/utils/errors.ts new file mode 100644 index 000000000000..463c67cdc86f --- /dev/null +++ b/libs/langchain-openai/src/utils/errors.ts @@ -0,0 +1,22 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-param-reassign */ + +// Duplicate of core +// TODO: Remove once we stop supporting 0.2.x core versions +export type LangChainErrorCodes = + | "INVALID_PROMPT_INPUT" + | "INVALID_TOOL_RESULTS" + | "MESSAGE_COERCION_FAILURE" + | "MODEL_AUTHENTICATION" + | "MODEL_NOT_FOUND" + | "MODEL_RATE_LIMIT" + | "OUTPUT_PARSING_FAILURE"; + +export function addLangChainErrorFields( + error: any, + lc_error_code: LangChainErrorCodes +) { + (error as any).lc_error_code = lc_error_code; + error.message = `${error.message}\n\nTroubleshooting URL: https://js.langchain.com/docs/troubleshooting/errors/${lc_error_code}/\n`; + return error; +} diff --git a/libs/langchain-openai/src/utils/openai.ts b/libs/langchain-openai/src/utils/openai.ts index e95297e56b64..a81240f67c02 100644 --- a/libs/langchain-openai/src/utils/openai.ts +++ b/libs/langchain-openai/src/utils/openai.ts @@ -9,6 +9,7 @@ import { convertToOpenAIFunction, convertToOpenAITool, } from "@langchain/core/utils/function_calling"; +import { addLangChainErrorFields } from "./errors.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function wrapOpenAIClientError(e: any) { @@ -19,6 +20,14 @@ export function wrapOpenAIClientError(e: any) { } else if (e.constructor.name === APIUserAbortError.name) { error = new Error(e.message); error.name = "AbortError"; + } else if (e.status === 400 && e.message.includes("tool_calls")) { + error = addLangChainErrorFields(e, "INVALID_TOOL_RESULTS"); + } else if (e.status === 401) { + error = addLangChainErrorFields(e, "MODEL_AUTHENTICATION"); + } else if (e.status === 429) { + error = addLangChainErrorFields(e, "MODEL_RATE_LIMIT"); + } else if (e.status === 404) { + error = addLangChainErrorFields(e, "MODEL_NOT_FOUND"); } else { error = e; } diff --git a/libs/langchain-openai/src/utils/prompts.ts b/libs/langchain-openai/src/utils/prompts.ts new file mode 100644 index 000000000000..8951ed63a0d9 --- /dev/null +++ b/libs/langchain-openai/src/utils/prompts.ts @@ -0,0 +1,47 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import type { BasePromptValue } from "@langchain/core/prompt_values"; +import type { OpenAI } from "openai"; + +import { _convertMessagesToOpenAIParams } from "../chat_models.js"; + +/** + * Convert a formatted LangChain prompt (e.g. pulled from the hub) into + * a format expected by OpenAI's JS SDK. + * + * Requires the "@langchain/openai" package to be installed in addition + * to the OpenAI SDK. + * + * @example + * ```ts + * import { convertPromptToOpenAI } from "langsmith/utils/hub/openai"; + * import { pull } from "langchain/hub"; + * + * import OpenAI from 'openai'; + * + * const prompt = await pull("jacob/joke-generator"); + * const formattedPrompt = await prompt.invoke({ + * topic: "cats", + * }); + * + * const { messages } = convertPromptToOpenAI(formattedPrompt); + * + * const openAIClient = new OpenAI(); + * + * const openaiResponse = await openAIClient.chat.completions.create({ + * model: "gpt-4o", + * messages, + * }); + * ``` + * @param formattedPrompt + * @returns A partial OpenAI payload. + */ +export function convertPromptToOpenAI(formattedPrompt: BasePromptValue): { + messages: OpenAI.Chat.ChatCompletionMessageParam[]; +} { + const messages = formattedPrompt.toChatMessages(); + return { + messages: _convertMessagesToOpenAIParams( + messages + ) as OpenAI.Chat.ChatCompletionMessageParam[], + }; +} diff --git a/libs/langchain-pinecone/package.json b/libs/langchain-pinecone/package.json index fb6c623e43e8..1d06ca43267d 100644 --- a/libs/langchain-pinecone/package.json +++ b/libs/langchain-pinecone/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/pinecone", - "version": "0.0.9", + "version": "0.1.1", "description": "LangChain integration for Pinecone's vector database", "type": "module", "engines": { diff --git a/libs/langchain-pinecone/src/tests/vectorstores.test.ts b/libs/langchain-pinecone/src/tests/vectorstores.test.ts index c83d0efe3af5..5ca1d284248e 100644 --- a/libs/langchain-pinecone/src/tests/vectorstores.test.ts +++ b/libs/langchain-pinecone/src/tests/vectorstores.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { jest, test, expect } from "@jest/globals"; +import { expect, jest, test } from "@jest/globals"; import { FakeEmbeddings } from "@langchain/core/utils/testing"; import { PineconeStore } from "../vectorstores.js"; @@ -119,6 +119,34 @@ test("PineconeStore with string arrays", async () => { ]); }); +describe("PineconeStore with null pageContent", () => { + it("should handle null pageContent correctly in _formatMatches", async () => { + const mockQueryResponse = { + matches: [ + { + id: "1", + score: 0.9, + metadata: { textKey: null, otherKey: "value" }, + }, + ], + }; + + const client = { + namespace: jest.fn().mockReturnValue({ + query: jest.fn().mockResolvedValue(mockQueryResponse), + }), + }; + const embeddings = new FakeEmbeddings(); + + const store = new PineconeStore(embeddings, { + pineconeIndex: client as any, + }); + + const results = await store.similaritySearchVectorWithScore([], 0); + expect(results[0][0].pageContent).toEqual(""); + }); +}); + test("PineconeStore can instantiate without passing in client", async () => { const embeddings = new FakeEmbeddings(); diff --git a/libs/langchain-pinecone/src/vectorstores.ts b/libs/langchain-pinecone/src/vectorstores.ts index 2228d2456744..3e4db8bbc30c 100644 --- a/libs/langchain-pinecone/src/vectorstores.ts +++ b/libs/langchain-pinecone/src/vectorstores.ts @@ -425,7 +425,7 @@ export class PineconeStore extends VectorStore { documentsWithScores.push([ new Document({ id, - pageContent: pageContent.toString(), + pageContent: pageContent?.toString() ?? "", metadata, }), score, diff --git a/libs/langchain-qdrant/package.json b/libs/langchain-qdrant/package.json index 07481bd0fc78..a0bba7310e9d 100644 --- a/libs/langchain-qdrant/package.json +++ b/libs/langchain-qdrant/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/qdrant", - "version": "0.0.5", + "version": "0.1.0", "description": "LangChain.js integration for the Qdrant vector database", "type": "module", "engines": { diff --git a/libs/langchain-qdrant/src/tests/vectorstores.int.test.ts b/libs/langchain-qdrant/src/tests/vectorstores.int.test.ts index f0a6afca6549..288283b0da80 100644 --- a/libs/langchain-qdrant/src/tests/vectorstores.int.test.ts +++ b/libs/langchain-qdrant/src/tests/vectorstores.int.test.ts @@ -5,6 +5,7 @@ import { QdrantClient } from "@qdrant/js-client-rest"; import { faker } from "@faker-js/faker"; import { Document } from "@langchain/core/documents"; import { SyntheticEmbeddings } from "@langchain/core/utils/testing"; +import { v4 } from "uuid"; import { QdrantVectorStore } from "../vectorstores.js"; describe("QdrantVectorStore testcase", () => { @@ -19,12 +20,13 @@ describe("QdrantVectorStore testcase", () => { }); const pageContent = faker.lorem.sentence(5); + const id = v4(); - await qdrantVectorStore.addDocuments([{ pageContent, metadata: {} }]); + await qdrantVectorStore.addDocuments([{ pageContent, metadata: {}, id }]); const results = await qdrantVectorStore.similaritySearch(pageContent, 1); - expect(results[0]).toEqual(new Document({ metadata: {}, pageContent })); + expect(results[0]).toEqual(new Document({ metadata: {}, pageContent, id })); expect(qdrantVectorStore.maxMarginalRelevanceSearch).toBeDefined(); @@ -35,7 +37,9 @@ describe("QdrantVectorStore testcase", () => { } ); expect(mmrResults.length).toBe(1); - expect(mmrResults[0]).toEqual(new Document({ metadata: {}, pageContent })); + expect(mmrResults[0]).toEqual( + new Document({ metadata: {}, pageContent, id }) + ); }); test("passing client directly with a model that creates embeddings with a different number of dimensions", async () => { @@ -59,6 +63,7 @@ describe("QdrantVectorStore testcase", () => { const results = await qdrantVectorStore.similaritySearch(pageContent, 1); - expect(results[0]).toEqual(new Document({ metadata: {}, pageContent })); + expect(results[0].metadata).toEqual({}); + expect(results[0].pageContent).toEqual(pageContent); }); }); diff --git a/libs/langchain-qdrant/src/vectorstores.ts b/libs/langchain-qdrant/src/vectorstores.ts index 80d98e1226b0..4e48421f71f2 100644 --- a/libs/langchain-qdrant/src/vectorstores.ts +++ b/libs/langchain-qdrant/src/vectorstores.ts @@ -149,7 +149,7 @@ export class QdrantVectorStore extends VectorStore { await this.ensureCollection(); const points = vectors.map((embedding, idx) => ({ - id: uuid(), + id: documents[idx].id ?? uuid(), vector: embedding, payload: { [this.contentPayloadKey]: documents[idx].pageContent, @@ -206,6 +206,7 @@ export class QdrantVectorStore extends VectorStore { results as QdrantSearchResponse[] ).map((res) => [ new Document({ + id: res.id as string, // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata: res.payload[this.metadataPayloadKey] as Record, pageContent: res.payload[this.contentPayloadKey] as string, @@ -264,6 +265,7 @@ export class QdrantVectorStore extends VectorStore { const result = (topMmrMatches as QdrantSearchResponse[]).map( (res) => new Document({ + id: res.id as string, // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata: res.payload[this.metadataPayloadKey] as Record, pageContent: res.payload[this.contentPayloadKey] as string, diff --git a/libs/langchain-redis/package.json b/libs/langchain-redis/package.json index 7470a3bb9f46..1423dd7f5e19 100644 --- a/libs/langchain-redis/package.json +++ b/libs/langchain-redis/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/redis", - "version": "0.0.5", + "version": "0.1.0", "description": "Sample integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-scripts/package.json b/libs/langchain-scripts/package.json index ffea6eb6d6f1..c7aca4e17ea6 100644 --- a/libs/langchain-scripts/package.json +++ b/libs/langchain-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/scripts", - "version": "0.1.2", + "version": "0.1.3", "description": "Shared scripts for LangChain.js", "type": "module", "engines": { @@ -21,7 +21,7 @@ "scripts": { "build": "yarn clean && yarn turbo:command build:internal --filter=@langchain/scripts", "build:internal": "tsc --project ./tsconfig.build.json && yarn move:artifacts && yarn build:generated", - "move:artifacts": "mkdir -p ./dist && mv ./dist_build/* ./dist/", + "move:artifacts": "rimraf dist && mkdir -p dist && mv dist_build/* dist/", "build:generated": "node bin/build.js --create-entrypoints --pre --tree-shaking", "build:turbo": "yarn turbo:command build --filter=@langchain/scripts", "lint:eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --cache --ext .ts,.js src/", diff --git a/libs/langchain-scripts/src/build/index.ts b/libs/langchain-scripts/src/build/index.ts index f224d3678383..ce4ef18b937e 100644 --- a/libs/langchain-scripts/src/build/index.ts +++ b/libs/langchain-scripts/src/build/index.ts @@ -465,8 +465,11 @@ function listEntrypoints(packageJson: Record) { * @param {string} entrypoint * @returns {Promise} Whether or not the file has side effects which are explicitly marked as allowed. */ -const checkAllowSideEffects = async (entrypoint: string): Promise => { - let entrypointContent: Buffer | undefined; +const checkAllowSideEffects = async ( + entrypoint: string, + filename?: string +): Promise => { + let entrypointContent; try { entrypointContent = await fs.promises.readFile(`./dist/${entrypoint}.js`); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -476,16 +479,28 @@ const checkAllowSideEffects = async (entrypoint: string): Promise => { entrypointContent = await fs.promises.readFile( `./dist/${entrypoint}/index.js` ); + } else { + entrypointContent = Buffer.from(""); } } + let fileContent; + try { + fileContent = await fs.promises.readFile(`./${filename}`); + } catch (e) { + fileContent = Buffer.from(""); + } + // Allow escaping side effects strictly within code directly // within an entrypoint - return entrypointContent - ? entrypointContent - .toString() - .includes("/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */") - : false; + return ( + entrypointContent + .toString() + .includes("/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */") || + fileContent + .toString() + .includes("/* __LC_ALLOW_ENTRYPOINT_SIDE_EFFECTS__ */") + ); }; async function checkTreeShaking(config: LangChainConfig) { @@ -499,12 +514,14 @@ async function checkTreeShaking(config: LangChainConfig) { const reportMap = new Map(); for (const entrypoint of entrypoints) { - let sideEffects = ""; + const sideEffects: { log: string; filename?: string }[] = []; console.info = function (...args) { const line = args.length ? args.join(" ") : ""; if (line.includes("First side effect in")) { - sideEffects += `${line}\n`; + const match = line.match(/First side effect in (.+?) is at/); + const filename = match ? match[1] : undefined; + sideEffects.push({ log: `${line}\n`, filename }); } }; @@ -514,17 +531,29 @@ async function checkTreeShaking(config: LangChainConfig) { experimentalLogSideEffects: true, }); - let hasUnexpectedSideEffects = sideEffects.length > 0; - if (hasUnexpectedSideEffects) { - // Map the entrypoint back to the actual file entrypoint using the LangChainConfig file - const actualEntrypoint = - config.entrypoints[entrypoint.replace(/^\.\/|\.js$/g, "")]; - hasUnexpectedSideEffects = !(await checkAllowSideEffects( - actualEntrypoint - )); + let hasUnexpectedSideEffects = false; + for (const sideEffect of sideEffects) { + if (sideEffect.filename) { + // Map the entrypoint back to the actual file entrypoint using the LangChainConfig file + const actualEntrypoint = + config.entrypoints[entrypoint.replace(/^\.\/|\.js$/g, "")]; + const allowSideEffects = await checkAllowSideEffects( + actualEntrypoint, + sideEffect.filename + ); + if (!allowSideEffects) { + hasUnexpectedSideEffects = true; + break; + } + } else { + // If we can't determine the filename, we'll consider it an unexpected side effect + hasUnexpectedSideEffects = true; + break; + } } + reportMap.set(entrypoint, { - log: sideEffects, + log: sideEffects.map(({ log }) => log).join(""), hasUnexpectedSideEffects, }); } diff --git a/libs/langchain-scripts/src/tests/__mdx__/modules/index.mdx b/libs/langchain-scripts/src/tests/__mdx__/modules/index.mdx index 5535f82d125e..5bf7d9bf49b7 100644 --- a/libs/langchain-scripts/src/tests/__mdx__/modules/index.mdx +++ b/libs/langchain-scripts/src/tests/__mdx__/modules/index.mdx @@ -224,13 +224,13 @@ You will have to make `fetch` available globally, either: You'll also need to [polyfill `ReadableStream`](https://www.npmjs.com/package/web-streams-polyfill) by installing: ```bash npm2yarn -npm i web-streams-polyfill +npm i web-streams-polyfill@4 ``` And then adding it to the global namespace in your main entrypoint: ```typescript -import "web-streams-polyfill/es6"; +import "web-streams-polyfill/polyfill"; ``` Additionally you'll have to polyfill `structuredClone`, eg. by installing `core-js` and following the instructions [here](https://github.com/zloirock/core-js). diff --git a/libs/langchain-scripts/src/tests/__mdx__/modules/two.mdx b/libs/langchain-scripts/src/tests/__mdx__/modules/two.mdx index 7825b4109b27..55a1feda31e8 100644 --- a/libs/langchain-scripts/src/tests/__mdx__/modules/two.mdx +++ b/libs/langchain-scripts/src/tests/__mdx__/modules/two.mdx @@ -223,13 +223,13 @@ You will have to make `fetch` available globally, either: You'll also need to [polyfill `ReadableStream`](https://www.npmjs.com/package/web-streams-polyfill) by installing: ```bash npm2yarn -npm i web-streams-polyfill +npm i web-streams-polyfill@4 ``` And then adding it to the global namespace in your main entrypoint: ```typescript -import "web-streams-polyfill/es6"; +import "web-streams-polyfill/polyfill"; ``` Additionally you'll have to polyfill `structuredClone`, eg. by installing `core-js` and following the instructions [here](https://github.com/zloirock/core-js). diff --git a/libs/langchain-standard-tests/src/integration_tests/chat_models.ts b/libs/langchain-standard-tests/src/integration_tests/chat_models.ts index 2da82e80c6ed..3116b5f2b356 100644 --- a/libs/langchain-standard-tests/src/integration_tests/chat_models.ts +++ b/libs/langchain-standard-tests/src/integration_tests/chat_models.ts @@ -95,6 +95,24 @@ export abstract class ChatModelIntegrationTests< supportsParallelToolCalls = false; + // Add these new properties + supportedUsageMetadataDetails: { + invoke: Array< + | "audio_input" + | "audio_output" + | "reasoning_output" + | "cache_read_input" + | "cache_creation_input" + >; + stream: Array< + | "audio_input" + | "audio_output" + | "reasoning_output" + | "cache_read_input" + | "cache_creation_input" + >; + } = { invoke: [], stream: [] }; + constructor( fields: ChatModelIntegrationTestsFields< CallOptions, @@ -374,6 +392,162 @@ export abstract class ChatModelIntegrationTests< // Check that total_tokens is a number expect(typeof usageMetadata.total_tokens).toBe("number"); + + // Test additional usage metadata details + if (this.supportedUsageMetadataDetails.invoke.includes("audio_input")) { + const msgWithAudioInput = await this.invokeWithAudioInput(false); + this.assertAudioInputMetadata(msgWithAudioInput); + } + + if (this.supportedUsageMetadataDetails.invoke.includes("audio_output")) { + const msgWithAudioOutput = await this.invokeWithAudioOutput(false); + this.assertAudioOutputMetadata(msgWithAudioOutput); + } + + if ( + this.supportedUsageMetadataDetails.invoke.includes("reasoning_output") + ) { + const msgWithReasoningOutput = await this.invokeWithReasoningOutput( + false + ); + this.assertReasoningOutputMetadata(msgWithReasoningOutput); + } + + if ( + this.supportedUsageMetadataDetails.invoke.includes("cache_read_input") + ) { + const msgWithCacheReadInput = await this.invokeWithCacheReadInput(false); + this.assertCacheReadInputMetadata(msgWithCacheReadInput); + } + + if ( + this.supportedUsageMetadataDetails.invoke.includes("cache_creation_input") + ) { + const msgWithCacheCreationInput = await this.invokeWithCacheCreationInput( + false + ); + this.assertCacheCreationInputMetadata(msgWithCacheCreationInput); + } + } + + async invokeWithAudioInput(_stream: boolean): Promise { + // Initialize the model so we can access the `.getName()` method + // for better error messages. + const chatModel = new this.Cls(this.constructorArgs); + throw new Error( + `invokeWithAudioInput is not implemented on ${chatModel.getName()}` + + "standard integration tests." + ); + } + + async invokeWithAudioOutput(_stream: boolean): Promise { + // Initialize the model so we can access the `.getName()` method + // for better error messages. + const chatModel = new this.Cls(this.constructorArgs); + throw new Error( + `invokeWithAudioOutput is not implemented on ${chatModel.getName()}` + + "standard integration tests." + ); + } + + async invokeWithReasoningOutput(_stream: boolean): Promise { + // Initialize the model so we can access the `.getName()` method + // for better error messages. + const chatModel = new this.Cls(this.constructorArgs); + throw new Error( + `invokeWithReasoningOutput is not implemented on ${chatModel.getName()}` + + "standard integration tests." + ); + } + + async invokeWithCacheReadInput(_stream: boolean): Promise { + // Initialize the model so we can access the `.getName()` method + // for better error messages. + const chatModel = new this.Cls(this.constructorArgs); + throw new Error( + `invokeWithCacheReadInput is not implemented on ${chatModel.getName()}` + + "standard integration tests." + ); + } + + async invokeWithCacheCreationInput(_stream: boolean): Promise { + // Initialize the model so we can access the `.getName()` method + // for better error messages. + const chatModel = new this.Cls(this.constructorArgs); + throw new Error( + `invokeWithCacheCreationInput is not implemented on ${chatModel.getName()}` + + "standard integration tests." + ); + } + + private assertAudioInputMetadata(msg: AIMessage) { + expect(msg.usage_metadata).toBeDefined(); + expect(msg.usage_metadata?.input_token_details).toBeDefined(); + expect(typeof msg.usage_metadata?.input_token_details?.audio).toBe( + "number" + ); + expect(msg.usage_metadata?.input_tokens).toBeGreaterThanOrEqual( + Object.values(msg.usage_metadata?.input_token_details ?? {}).reduce( + (a, b) => (a ?? 0) + (b ?? 0), + 0 + ) + ); + } + + private assertAudioOutputMetadata(msg: AIMessage) { + expect(msg.usage_metadata).toBeDefined(); + expect(msg.usage_metadata?.output_token_details).toBeDefined(); + expect(typeof msg.usage_metadata?.output_token_details?.audio).toBe( + "number" + ); + expect(msg.usage_metadata?.output_tokens).toBeGreaterThanOrEqual( + Object.values(msg.usage_metadata?.output_token_details ?? {}).reduce( + (a, b) => (a ?? 0) + (b ?? 0), + 0 + ) + ); + } + + private assertReasoningOutputMetadata(msg: AIMessage) { + expect(msg.usage_metadata).toBeDefined(); + expect(msg.usage_metadata?.output_token_details).toBeDefined(); + expect(typeof msg.usage_metadata?.output_token_details?.reasoning).toBe( + "number" + ); + expect(msg.usage_metadata?.output_tokens).toBeGreaterThanOrEqual( + Object.values(msg.usage_metadata?.output_token_details ?? {}).reduce( + (a, b) => (a ?? 0) + (b ?? 0), + 0 + ) + ); + } + + private assertCacheReadInputMetadata(msg: AIMessage) { + expect(msg.usage_metadata).toBeDefined(); + expect(msg.usage_metadata?.input_token_details).toBeDefined(); + expect(typeof msg.usage_metadata?.input_token_details?.cache_read).toBe( + "number" + ); + expect(msg.usage_metadata?.input_tokens).toBeGreaterThanOrEqual( + Object.values(msg.usage_metadata?.input_token_details ?? {}).reduce( + (a, b) => (a ?? 0) + (b ?? 0), + 0 + ) + ); + } + + private assertCacheCreationInputMetadata(msg: AIMessage) { + expect(msg.usage_metadata).toBeDefined(); + expect(msg.usage_metadata?.input_token_details).toBeDefined(); + expect(typeof msg.usage_metadata?.input_token_details?.cache_creation).toBe( + "number" + ); + expect(msg.usage_metadata?.input_tokens).toBeGreaterThanOrEqual( + Object.values(msg.usage_metadata?.input_token_details ?? {}).reduce( + (a, b) => (a ?? 0) + (b ?? 0), + 0 + ) + ); } /** @@ -426,6 +600,40 @@ export abstract class ChatModelIntegrationTests< expect(typeof usageMetadata.input_tokens).toBe("number"); expect(typeof usageMetadata.output_tokens).toBe("number"); expect(typeof usageMetadata.total_tokens).toBe("number"); + + // Test additional usage metadata details + if (this.supportedUsageMetadataDetails.invoke.includes("audio_input")) { + const msgWithAudioInput = await this.invokeWithAudioInput(true); + this.assertAudioInputMetadata(msgWithAudioInput); + } + + if (this.supportedUsageMetadataDetails.invoke.includes("audio_output")) { + const msgWithAudioOutput = await this.invokeWithAudioOutput(true); + this.assertAudioOutputMetadata(msgWithAudioOutput); + } + + if ( + this.supportedUsageMetadataDetails.invoke.includes("reasoning_output") + ) { + const msgWithReasoningOutput = await this.invokeWithReasoningOutput(true); + this.assertReasoningOutputMetadata(msgWithReasoningOutput); + } + + if ( + this.supportedUsageMetadataDetails.invoke.includes("cache_read_input") + ) { + const msgWithCacheReadInput = await this.invokeWithCacheReadInput(true); + this.assertCacheReadInputMetadata(msgWithCacheReadInput); + } + + if ( + this.supportedUsageMetadataDetails.invoke.includes("cache_creation_input") + ) { + const msgWithCacheCreationInput = await this.invokeWithCacheCreationInput( + true + ); + this.assertCacheCreationInputMetadata(msgWithCacheCreationInput); + } } /** diff --git a/libs/langchain-weaviate/package.json b/libs/langchain-weaviate/package.json index 1d33624a21cb..efbb3a14ed22 100644 --- a/libs/langchain-weaviate/package.json +++ b/libs/langchain-weaviate/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/weaviate", - "version": "0.1.0-rc.0", + "version": "0.1.0", "description": "Weaviate integration for LangChain.js", "type": "module", "engines": { diff --git a/libs/langchain-yandex/package.json b/libs/langchain-yandex/package.json index 71509be4b1db..3c919f593460 100644 --- a/libs/langchain-yandex/package.json +++ b/libs/langchain-yandex/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/yandex", - "version": "0.0.3", + "version": "0.1.0", "description": "Yandex integration for LangChain.js", "type": "module", "engines": { diff --git a/package.json b/package.json index 2bd507f968aa..d647515dcbcf 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "dpdm@^3.12.0": "patch:dpdm@npm%3A3.12.0#./.yarn/patches/dpdm-npm-3.12.0-0dfdd8e3b8.patch", "typedoc-plugin-markdown@next": "patch:typedoc-plugin-markdown@npm%3A4.0.0-next.6#./.yarn/patches/typedoc-plugin-markdown-npm-4.0.0-next.6-96b4b47746.patch", "voy-search@0.6.2": "patch:voy-search@npm%3A0.6.2#./.yarn/patches/voy-search-npm-0.6.2-d4aca30a0e.patch", - "@langchain/core": "workspace:*", "protobufjs": "^7.2.5", "zod": "3.23.8" }, diff --git a/yarn.lock b/yarn.lock index 741b54f429d1..8c9766673e28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -226,9 +226,9 @@ __metadata: languageName: node linkType: hard -"@anthropic-ai/sdk@npm:^0.25.2": - version: 0.25.2 - resolution: "@anthropic-ai/sdk@npm:0.25.2" +"@anthropic-ai/sdk@npm:^0.27.3": + version: 0.27.3 + resolution: "@anthropic-ai/sdk@npm:0.27.3" dependencies: "@types/node": ^18.11.18 "@types/node-fetch": ^2.6.4 @@ -237,7 +237,7 @@ __metadata: form-data-encoder: 1.7.2 formdata-node: ^4.3.2 node-fetch: ^2.6.7 - checksum: b38f6a43f6f678e49f1b53226cc55654c23cf0fd1902cf3fcf98c0ae78f4c229518964f4cb31bc39da41925c806e7f4cc7ec14c511d387f07db3136b111bc744 + checksum: 8000fc5a4e545057d8711f978a0de59c9a174398a81f700c9d279213790aaa4b2c100f96f2ef79447b8f1f3a04b8f094d60db66a06df8df96b31a3240d69cb5a languageName: node linkType: hard @@ -251,24 +251,6 @@ __metadata: languageName: node linkType: hard -"@apache-arrow/ts@npm:^12.0.0": - version: 12.0.0 - resolution: "@apache-arrow/ts@npm:12.0.0" - dependencies: - "@types/command-line-args": 5.2.0 - "@types/command-line-usage": 5.0.2 - "@types/node": 18.14.5 - "@types/pad-left": 2.1.1 - command-line-args: 5.2.1 - command-line-usage: 6.1.3 - flatbuffers: 23.3.3 - json-bignum: ^0.0.3 - pad-left: ^2.1.0 - tslib: ^2.5.0 - checksum: 67b2791e14d5377b1d160a0d8390decc386e013c517713f8b9c100737a0e478a394086d91a8c846848d4e30289070a119d8e65191998f4c2555b18a29564df50 - languageName: node - linkType: hard - "@apify/consts@npm:^2.13.0, @apify/consts@npm:^2.9.0": version: 2.13.0 resolution: "@apify/consts@npm:2.13.0" @@ -8645,44 +8627,51 @@ __metadata: languageName: node linkType: hard -"@couchbase/couchbase-darwin-arm64-napi@npm:4.3.0": - version: 4.3.0 - resolution: "@couchbase/couchbase-darwin-arm64-napi@npm:4.3.0" +"@couchbase/couchbase-darwin-arm64-napi@npm:4.4.2": + version: 4.4.2 + resolution: "@couchbase/couchbase-darwin-arm64-napi@npm:4.4.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@couchbase/couchbase-darwin-x64-napi@npm:4.3.0": - version: 4.3.0 - resolution: "@couchbase/couchbase-darwin-x64-napi@npm:4.3.0" +"@couchbase/couchbase-darwin-x64-napi@npm:4.4.2": + version: 4.4.2 + resolution: "@couchbase/couchbase-darwin-x64-napi@npm:4.4.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@couchbase/couchbase-linux-arm64-napi@npm:4.3.0": - version: 4.3.0 - resolution: "@couchbase/couchbase-linux-arm64-napi@npm:4.3.0" +"@couchbase/couchbase-linux-arm64-napi@npm:4.4.2": + version: 4.4.2 + resolution: "@couchbase/couchbase-linux-arm64-napi@npm:4.4.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@couchbase/couchbase-linux-x64-napi@npm:4.3.0": - version: 4.3.0 - resolution: "@couchbase/couchbase-linux-x64-napi@npm:4.3.0" +"@couchbase/couchbase-linux-x64-napi@npm:4.4.2": + version: 4.4.2 + resolution: "@couchbase/couchbase-linux-x64-napi@npm:4.4.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@couchbase/couchbase-linuxmusl-x64-napi@npm:4.3.0": - version: 4.3.0 - resolution: "@couchbase/couchbase-linuxmusl-x64-napi@npm:4.3.0" +"@couchbase/couchbase-linuxmusl-arm64-napi@npm:4.4.2": + version: 4.4.2 + resolution: "@couchbase/couchbase-linuxmusl-arm64-napi@npm:4.4.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@couchbase/couchbase-linuxmusl-x64-napi@npm:4.4.2": + version: 4.4.2 + resolution: "@couchbase/couchbase-linuxmusl-x64-napi@npm:4.4.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@couchbase/couchbase-win32-x64-napi@npm:4.3.0": - version: 4.3.0 - resolution: "@couchbase/couchbase-win32-x64-napi@npm:4.3.0" +"@couchbase/couchbase-win32-x64-napi@npm:4.4.2": + version: 4.4.2 + resolution: "@couchbase/couchbase-win32-x64-napi@npm:4.4.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -11112,24 +11101,46 @@ __metadata: languageName: node linkType: hard -"@langchain/anthropic@npm:^0.2.8": - version: 0.2.17 - resolution: "@langchain/anthropic@npm:0.2.17" - dependencies: - "@anthropic-ai/sdk": ^0.25.2 - "@langchain/core": ">=0.2.21 <0.3.0" - fast-xml-parser: ^4.4.1 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.4 - checksum: c238df1fed3834bee3054645cb363aab7b30b22c917d05731abac4af5f55122b59e86797e9ed8a1a57f9ce7335e4264130f652555d5cc47727b8d274249f02f0 +"@lancedb/vectordb-darwin-arm64@npm:0.4.20": + version: 0.4.20 + resolution: "@lancedb/vectordb-darwin-arm64@npm:0.4.20" + conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@langchain/anthropic@workspace:*, @langchain/anthropic@workspace:libs/langchain-anthropic": +"@lancedb/vectordb-darwin-x64@npm:0.4.20": + version: 0.4.20 + resolution: "@lancedb/vectordb-darwin-x64@npm:0.4.20" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@lancedb/vectordb-linux-arm64-gnu@npm:0.4.20": + version: 0.4.20 + resolution: "@lancedb/vectordb-linux-arm64-gnu@npm:0.4.20" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@lancedb/vectordb-linux-x64-gnu@npm:0.4.20": + version: 0.4.20 + resolution: "@lancedb/vectordb-linux-x64-gnu@npm:0.4.20" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@lancedb/vectordb-win32-x64-msvc@npm:0.4.20": + version: 0.4.20 + resolution: "@lancedb/vectordb-win32-x64-msvc@npm:0.4.20" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@langchain/anthropic@*, @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.25.2 + "@anthropic-ai/sdk": ^0.27.3 "@anthropic-ai/vertex-sdk": ^0.4.1 "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" @@ -11160,21 +11171,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/aws@npm:^0.0.5": - version: 0.0.5 - resolution: "@langchain/aws@npm:0.0.5" - dependencies: - "@aws-sdk/client-bedrock-agent-runtime": ^3.583.0 - "@aws-sdk/client-bedrock-runtime": ^3.602.0 - "@aws-sdk/client-kendra": ^3.352.0 - "@aws-sdk/credential-provider-node": ^3.600.0 - "@langchain/core": ">=0.2.16 <0.3.0" - zod-to-json-schema: ^3.22.5 - checksum: 57388fc13bc7e05aa2539c029a1105b46be583155fed62d3cc907256e2d95ef44019aa593f6a14a5a115fffd6ba3a7fbf47eaab37a34fd6afba6279c0e4eb85f - languageName: node - linkType: hard - -"@langchain/aws@workspace:*, @langchain/aws@workspace:libs/langchain-aws": +"@langchain/aws@*, @langchain/aws@workspace:*, @langchain/aws@workspace:libs/langchain-aws": version: 0.0.0-use.local resolution: "@langchain/aws@workspace:libs/langchain-aws" dependencies: @@ -11328,7 +11325,7 @@ __metadata: "@baiducloud/qianfan": ^0.1.6 "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/openai": ~0.1.0 + "@langchain/openai": ~0.3.0 "@langchain/scripts": ">=0.1.0 <0.2.0" "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 @@ -11365,7 +11362,6 @@ __metadata: "@cloudflare/workers-types": ^4.20240909.0 "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/langgraph": ~0.0.31 "@langchain/scripts": ">=0.1.0 <0.2.0" "@langchain/standard-tests": 0.0.0 "@swc/core": ^1.3.90 @@ -11392,14 +11388,10 @@ __metadata: uuid: ^10.0.0 peerDependencies: "@langchain/core": ">=0.2.21 <0.4.0" - "@langchain/langgraph": "*" - peerDependenciesMeta: - "@langchain/langgraph": - optional: true languageName: unknown linkType: soft -"@langchain/cohere@^0.2.1, @langchain/cohere@workspace:*, @langchain/cohere@workspace:libs/langchain-cohere": +"@langchain/cohere@*, @langchain/cohere@workspace:*, @langchain/cohere@workspace:libs/langchain-cohere": version: 0.0.0-use.local resolution: "@langchain/cohere@workspace:libs/langchain-cohere" dependencies: @@ -11412,7 +11404,7 @@ __metadata: "@tsconfig/recommended": ^1.0.3 "@typescript-eslint/eslint-plugin": ^6.12.0 "@typescript-eslint/parser": ^6.12.0 - cohere-ai: ^7.10.5 + cohere-ai: ^7.14.0 dotenv: ^16.3.1 dpdm: ^3.12.0 eslint: ^8.33.0 @@ -11473,11 +11465,11 @@ __metadata: "@huggingface/inference": ^2.6.4 "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/langgraph": ~0.0.26 "@langchain/openai": ">=0.2.0 <0.4.0" "@langchain/scripts": ">=0.1.0 <0.2.0" "@langchain/standard-tests": 0.0.0 "@layerup/layerup-security": ^1.5.12 + "@libsql/client": ^0.14.0 "@mendable/firecrawl-js": ^0.0.36 "@mlc-ai/web-llm": ">=0.2.62 <0.3.0" "@mozilla/readability": ^0.4.4 @@ -11488,7 +11480,7 @@ __metadata: "@planetscale/database": ^1.8.0 "@premai/prem-sdk": ^0.3.25 "@qdrant/js-client-rest": ^1.8.2 - "@raycast/api": ^1.55.2 + "@raycast/api": ^1.83.1 "@rockset/client": ^0.9.1 "@smithy/eventstream-codec": ^2.0.5 "@smithy/protocol-http": ^3.0.6 @@ -11519,7 +11511,7 @@ __metadata: "@types/ws": ^8 "@typescript-eslint/eslint-plugin": ^5.58.0 "@typescript-eslint/parser": ^5.58.0 - "@upstash/ratelimit": ^1.2.1 + "@upstash/ratelimit": ^2.0.3 "@upstash/redis": ^1.32.0 "@upstash/vector": ^1.1.1 "@vercel/kv": ^0.2.3 @@ -11578,7 +11570,7 @@ __metadata: jsdom: ^22.1.0 jsonwebtoken: ^9.0.2 langchain: ">=0.2.3 <0.4.0" - langsmith: ~0.1.30 + langsmith: ~0.1.56 llmonitor: ^0.5.9 lodash: ^4.17.21 lunary: ^0.7.10 @@ -11586,7 +11578,7 @@ __metadata: mongodb: ^5.2.0 mysql2: ^3.9.8 neo4j-driver: ^5.17.0 - node-llama-cpp: 2.7.3 + node-llama-cpp: ^2 notion-to-md: ^3.1.0 officeparser: ^4.0.4 pdf-parse: 1.1.1 @@ -11610,7 +11602,7 @@ __metadata: typesense: ^1.5.3 usearch: ^1.1.1 uuid: ^10.0.0 - vectordb: ^0.1.4 + vectordb: ^0.9.0 voy-search: 0.6.2 weaviate-ts-client: ^1.4.0 web-auth-library: ^1.0.3 @@ -11647,8 +11639,8 @@ __metadata: "@gradientai/nodejs-sdk": ^1.2.0 "@huggingface/inference": ^2.6.4 "@langchain/core": ">=0.2.21 <0.4.0" - "@langchain/langgraph": "*" "@layerup/layerup-security": ^1.5.12 + "@libsql/client": ^0.14.0 "@mendable/firecrawl-js": ^0.0.13 "@mlc-ai/web-llm": "*" "@mozilla/readability": "*" @@ -11670,7 +11662,7 @@ __metadata: "@tensorflow-models/universal-sentence-encoder": "*" "@tensorflow/tfjs-converter": "*" "@tensorflow/tfjs-core": "*" - "@upstash/ratelimit": ^1.1.3 + "@upstash/ratelimit": ^1.1.3 || ^2.0.3 "@upstash/redis": ^1.20.6 "@upstash/vector": ^1.1.1 "@vercel/kv": ^0.2.3 @@ -11691,7 +11683,6 @@ __metadata: closevector-web: 0.1.6 cohere-ai: "*" convex: ^1.3.1 - couchbase: ^4.3.0 crypto-js: ^4.2.0 d3-dsv: ^2.0.0 discord.js: ^14.14.1 @@ -11717,7 +11708,6 @@ __metadata: mongodb: ">=5.2.0" mysql2: ^3.9.8 neo4j-driver: "*" - node-llama-cpp: "*" notion-to-md: ^3.1.0 officeparser: ^4.0.4 pdf-parse: 1.1.1 @@ -11797,10 +11787,10 @@ __metadata: optional: true "@huggingface/inference": optional: true - "@langchain/langgraph": - optional: true "@layerup/layerup-security": optional: true + "@libsql/client": + optional: true "@mendable/firecrawl-js": optional: true "@mlc-ai/web-llm": @@ -11885,8 +11875,6 @@ __metadata: optional: true convex: optional: true - couchbase: - optional: true crypto-js: optional: true d3-dsv: @@ -11937,8 +11925,6 @@ __metadata: optional: true neo4j-driver: optional: true - node-llama-cpp: - optional: true notion-to-md: optional: true officeparser: @@ -12014,7 +12000,7 @@ __metadata: jest: ^29.5.0 jest-environment-node: ^29.6.4 js-tiktoken: ^1.0.12 - langsmith: ^0.1.43 + langsmith: ^0.1.65 ml-matrix: ^6.10.4 mustache: ^4.2.0 p-queue: ^6.6.2 @@ -12025,7 +12011,7 @@ __metadata: ts-jest: ^29.1.0 typescript: ~5.1.6 uuid: ^10.0.0 - web-streams-polyfill: ^3.3.3 + web-streams-polyfill: ^4.0.0 zod: ^3.22.4 zod-to-json-schema: ^3.22.3 languageName: unknown @@ -12065,7 +12051,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-common@^0.0.27, @langchain/google-common@workspace:*, @langchain/google-common@workspace:libs/langchain-google-common, @langchain/google-common@~0.0.27": +"@langchain/google-common@^0.1.0, @langchain/google-common@workspace:*, @langchain/google-common@workspace:libs/langchain-google-common, @langchain/google-common@~0.1.0": version: 0.0.0-use.local resolution: "@langchain/google-common@workspace:libs/langchain-google-common" dependencies: @@ -12100,13 +12086,13 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-gauth@workspace:libs/langchain-google-gauth, @langchain/google-gauth@~0.0.27": +"@langchain/google-gauth@workspace:libs/langchain-google-gauth, @langchain/google-gauth@~0.1.0": version: 0.0.0-use.local resolution: "@langchain/google-gauth@workspace:libs/langchain-google-gauth" dependencies: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/google-common": ~0.0.27 + "@langchain/google-common": ~0.1.0 "@langchain/scripts": ">=0.1.0 <0.2.0" "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 @@ -12135,7 +12121,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-genai@^0.0.26, @langchain/google-genai@workspace:*, @langchain/google-genai@workspace:libs/langchain-google-genai": +"@langchain/google-genai@*, @langchain/google-genai@workspace:*, @langchain/google-genai@workspace:libs/langchain-google-genai": version: 0.0.0-use.local resolution: "@langchain/google-genai@workspace:libs/langchain-google-genai" dependencies: @@ -12178,8 +12164,8 @@ __metadata: dependencies: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/google-common": ^0.0.27 - "@langchain/google-webauth": ~0.0.27 + "@langchain/google-common": ^0.1.0 + "@langchain/google-webauth": ~0.1.0 "@langchain/scripts": ">=0.1.0 <0.2.0" "@langchain/standard-tests": 0.0.0 "@swc/core": ^1.3.90 @@ -12208,14 +12194,14 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-vertexai@workspace:*, @langchain/google-vertexai@workspace:libs/langchain-google-vertexai, @langchain/google-vertexai@~0.0": +"@langchain/google-vertexai@*, @langchain/google-vertexai@workspace:*, @langchain/google-vertexai@workspace:libs/langchain-google-vertexai": version: 0.0.0-use.local resolution: "@langchain/google-vertexai@workspace:libs/langchain-google-vertexai" dependencies: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/google-common": ^0.0.27 - "@langchain/google-gauth": ~0.0.27 + "@langchain/google-common": ^0.1.0 + "@langchain/google-gauth": ~0.1.0 "@langchain/scripts": ">=0.1.0 <0.2.0" "@langchain/standard-tests": 0.0.0 "@swc/core": ^1.3.90 @@ -12244,13 +12230,13 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-webauth@workspace:libs/langchain-google-webauth, @langchain/google-webauth@~0.0.27": +"@langchain/google-webauth@workspace:libs/langchain-google-webauth, @langchain/google-webauth@~0.1.0": version: 0.0.0-use.local resolution: "@langchain/google-webauth@workspace:libs/langchain-google-webauth" dependencies: "@jest/globals": ^29.5.0 "@langchain/core": "workspace:*" - "@langchain/google-common": ~0.0.27 + "@langchain/google-common": ~0.1.0 "@langchain/scripts": ">=0.1.0 <0.2.0" "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 @@ -12279,20 +12265,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/groq@npm:^0.0.15": - version: 0.0.15 - resolution: "@langchain/groq@npm:0.0.15" - dependencies: - "@langchain/core": ">=0.2.16 <0.3.0" - "@langchain/openai": ~0.2.4 - groq-sdk: ^0.3.2 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.5 - checksum: b9c9450ad481b36273ca42bb5499584febdd33f00c00962bf86321bc4ecf7a2d42ecdd86a0386f1931a556fb3038efa8a284e5256a4a124dabcd32c3293f1215 - languageName: node - linkType: hard - -"@langchain/groq@workspace:*, @langchain/groq@workspace:libs/langchain-groq": +"@langchain/groq@*, @langchain/groq@workspace:*, @langchain/groq@workspace:libs/langchain-groq": version: 0.0.0-use.local resolution: "@langchain/groq@workspace:libs/langchain-groq" dependencies: @@ -12330,68 +12303,32 @@ __metadata: languageName: unknown linkType: soft -"@langchain/langgraph@npm:0.0.28, @langchain/langgraph@npm:^0.0.28": - version: 0.0.28 - resolution: "@langchain/langgraph@npm:0.0.28" - dependencies: - "@langchain/core": ">=0.2.16 <0.3.0" - uuid: ^10.0.0 - zod: ^3.23.8 - peerDependencies: - better-sqlite3: ^9.5.0 - peerDependenciesMeta: - better-sqlite3: - optional: true - checksum: 1465791026ccd6eaa13a2f2d03b8fb9f0972a8c23b9da1cfd581074f413ea60ef860de6d704c6a3b49f7425f23d6ba49c23255167ae83ab7d70dc00cc0560ce2 - languageName: node - linkType: hard - -"@langchain/langgraph@npm:~0.0.26": - version: 0.0.34 - resolution: "@langchain/langgraph@npm:0.0.34" +"@langchain/langgraph-checkpoint@npm:~0.0.6": + version: 0.0.6 + resolution: "@langchain/langgraph-checkpoint@npm:0.0.6" dependencies: - "@langchain/core": ">=0.2.20 <0.3.0" uuid: ^10.0.0 - zod: ^3.23.8 peerDependencies: - better-sqlite3: ^9.5.0 - peerDependenciesMeta: - better-sqlite3: - optional: true - checksum: efa3c6b4eb40fdeb696531fe63f1f8e2b5d23b7adfc9404103cc9265db4aab6caee7de9ba1e01e14f2765d98a8b536bfbc473795543b0314e84c72b3b349a258 + "@langchain/core": ">=0.2.31 <0.4.0" + checksum: 7303a0f92f6474757bc9484314f36566a0785d8167ead65483fdada2921447e153a24754629ee601a168d2a39b6cc6d0c57ec34d607e10225619b9d5d30c8a5a languageName: node linkType: hard -"@langchain/langgraph@npm:~0.0.31": - version: 0.0.31 - resolution: "@langchain/langgraph@npm:0.0.31" +"@langchain/langgraph@npm:0.2.3, @langchain/langgraph@npm:^0.2.3": + version: 0.2.3 + resolution: "@langchain/langgraph@npm:0.2.3" dependencies: - "@langchain/core": ">=0.2.18 <0.3.0" + "@langchain/langgraph-checkpoint": ~0.0.6 + double-ended-queue: ^2.1.0-0 uuid: ^10.0.0 zod: ^3.23.8 peerDependencies: - better-sqlite3: ^9.5.0 - peerDependenciesMeta: - better-sqlite3: - optional: true - checksum: 74c0af490dab5c1f38d426cdeb0530fd300606bd28bb099d27b0ace029a02800a75fcc047f6755d853b485e78728b472170a19173803014dcc54bafe85939d9f + "@langchain/core": ">=0.2.31 <0.4.0" + checksum: 09d423228633f4bfee24b15e9f81eee57e96fecf9914f1e1c1f40f9436d120de912b8c987da356591c6eae0ee2bcf3759ab8e0ff762f87bc4f3f73e4098c12e9 languageName: node linkType: hard -"@langchain/mistralai@npm:^0.0.26": - version: 0.0.26 - resolution: "@langchain/mistralai@npm:0.0.26" - dependencies: - "@langchain/core": ">=0.2.16 <0.3.0" - "@mistralai/mistralai": ^0.4.0 - uuid: ^10.0.0 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.4 - checksum: 97c545994b16166044c96357c211cbaf8d9a2dbcc45a8bfdd1a90089ccd62f7e294aaa9d9fdd23bcea888b1a4eade818e96cf9a6f9c972e05deb2abf7a7fc8fc - languageName: node - linkType: hard - -"@langchain/mistralai@workspace:*, @langchain/mistralai@workspace:libs/langchain-mistralai": +"@langchain/mistralai@*, @langchain/mistralai@workspace:*, @langchain/mistralai@workspace:libs/langchain-mistralai": version: 0.0.0-use.local resolution: "@langchain/mistralai@workspace:libs/langchain-mistralai" dependencies: @@ -12534,18 +12471,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/ollama@npm:^0.0.2": - version: 0.0.2 - resolution: "@langchain/ollama@npm:0.0.2" - dependencies: - "@langchain/core": ">=0.2.17 <0.3.0" - ollama: ^0.5.6 - uuid: ^10.0.0 - checksum: 33b9259842b4df5221bb492b5d99e0545964486a9a2e6ab899b645fd8eb514b534280d8e6e3f1d0efd406513e624e9c030c1457910163e9a1ee10908e316a54e - languageName: node - linkType: hard - -"@langchain/ollama@workspace:*, @langchain/ollama@workspace:libs/langchain-ollama": +"@langchain/ollama@*, @langchain/ollama@workspace:*, @langchain/ollama@workspace:libs/langchain-ollama": version: 0.0.0-use.local resolution: "@langchain/ollama@workspace:libs/langchain-ollama" dependencies: @@ -12582,7 +12508,7 @@ __metadata: languageName: unknown linkType: soft -"@langchain/openai@>=0.1.0 <0.4.0, @langchain/openai@>=0.2.0 <0.4.0, @langchain/openai@workspace:*, @langchain/openai@workspace:^, @langchain/openai@workspace:libs/langchain-openai": +"@langchain/openai@>=0.1.0 <0.4.0, @langchain/openai@>=0.2.0 <0.4.0, @langchain/openai@workspace:*, @langchain/openai@workspace:^, @langchain/openai@workspace:libs/langchain-openai, @langchain/openai@~0.3.0": version: 0.0.0-use.local resolution: "@langchain/openai@workspace:libs/langchain-openai" dependencies: @@ -12604,7 +12530,7 @@ __metadata: jest: ^29.5.0 jest-environment-node: ^29.6.4 js-tiktoken: ^1.0.12 - openai: ^4.57.3 + openai: ^4.68.0 prettier: ^2.8.3 release-it: ^17.6.0 rimraf: ^5.0.1 @@ -12617,45 +12543,6 @@ __metadata: languageName: unknown linkType: soft -"@langchain/openai@npm:>=0.1.0 <0.3.0": - version: 0.2.11 - resolution: "@langchain/openai@npm:0.2.11" - dependencies: - "@langchain/core": ">=0.2.26 <0.3.0" - js-tiktoken: ^1.0.12 - openai: ^4.57.3 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - checksum: afd192901ff5996008f5d2988ceba09847701b9832503ca0b75c4ba919d3df1be547571c78c8e024aef29ba592e4d4c6f7e16bcb487f21077667ec311da7cd55 - languageName: node - linkType: hard - -"@langchain/openai@npm:~0.1.0": - version: 0.1.3 - resolution: "@langchain/openai@npm:0.1.3" - dependencies: - "@langchain/core": ">=0.2.5 <0.3.0" - js-tiktoken: ^1.0.12 - openai: ^4.49.1 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - checksum: 43aaafad2e1416bc23846c6c5084502025d3972c0b11b2ef2bcefd9da3dc1cd933cdf838c596404584af024adccc26e34476fededed0e614a19b47a4e5a8c8e5 - languageName: node - linkType: hard - -"@langchain/openai@npm:~0.2.4": - version: 0.2.10 - resolution: "@langchain/openai@npm:0.2.10" - dependencies: - "@langchain/core": ">=0.2.26 <0.3.0" - js-tiktoken: ^1.0.12 - openai: ^4.57.3 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - checksum: ebaef6c95ebbc467f66ca844cecfadc3e911697a25a4020aa9c461a3ee70cbffd749fa5ee2017f995cecd1b3b2dacc5c5436c34830cbef93fa77874c5b130290 - languageName: node - linkType: hard - "@langchain/pinecone@workspace:*, @langchain/pinecone@workspace:libs/langchain-pinecone": version: 0.0.0-use.local resolution: "@langchain/pinecone@workspace:libs/langchain-pinecone" @@ -12874,16 +12761,6 @@ __metadata: languageName: unknown linkType: soft -"@langchain/textsplitters@npm:~0.0.0": - version: 0.0.3 - resolution: "@langchain/textsplitters@npm:0.0.3" - dependencies: - "@langchain/core": ">0.2.0 <0.3.0" - js-tiktoken: ^1.0.12 - checksum: f0b32d65c863a280ce7104bff4d367734b8f76f2ec42b741fb690fbc20737bb4a3a412b82d8ba308a524441b6084ecd59cf61c3ce13cbb9639fbd02241c341d1 - languageName: node - linkType: hard - "@langchain/weaviate@workspace:*, @langchain/weaviate@workspace:libs/langchain-weaviate": version: 0.0.0-use.local resolution: "@langchain/weaviate@workspace:libs/langchain-weaviate" @@ -12996,6 +12873,106 @@ __metadata: languageName: node linkType: hard +"@libsql/client@npm:^0.14.0": + version: 0.14.0 + resolution: "@libsql/client@npm:0.14.0" + dependencies: + "@libsql/core": ^0.14.0 + "@libsql/hrana-client": ^0.7.0 + js-base64: ^3.7.5 + libsql: ^0.4.4 + promise-limit: ^2.7.0 + checksum: 7eeaf95d76da8870544c27fcf1206c377a0fc3df72b174393a1fb17fc92ca568d9a42d24d65e771ca77edc5108e853a7ac18540ad0242da759f2ec1191103d99 + languageName: node + linkType: hard + +"@libsql/core@npm:^0.14.0": + version: 0.14.0 + resolution: "@libsql/core@npm:0.14.0" + dependencies: + js-base64: ^3.7.5 + checksum: dae12491a277e03c3729de069bee9af9689f0c178b0fd1ef342f03aab94e4ccba8801e11d0393e0e3a77596e34f101b69b7f07f16f6b34a09eca698e340bed36 + languageName: node + linkType: hard + +"@libsql/darwin-arm64@npm:0.4.6": + version: 0.4.6 + resolution: "@libsql/darwin-arm64@npm:0.4.6" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@libsql/darwin-x64@npm:0.4.6": + version: 0.4.6 + resolution: "@libsql/darwin-x64@npm:0.4.6" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@libsql/hrana-client@npm:^0.7.0": + version: 0.7.0 + resolution: "@libsql/hrana-client@npm:0.7.0" + dependencies: + "@libsql/isomorphic-fetch": ^0.3.1 + "@libsql/isomorphic-ws": ^0.1.5 + js-base64: ^3.7.5 + node-fetch: ^3.3.2 + checksum: 0d36931ca3a01144dc14294d1d9666ee64724c6ab4889890ff0bc45564369503f6abccbc448518485af107bd69f49d35878059c46d98dacb34db4757b52c406a + languageName: node + linkType: hard + +"@libsql/isomorphic-fetch@npm:^0.3.1": + version: 0.3.1 + resolution: "@libsql/isomorphic-fetch@npm:0.3.1" + checksum: 9f131cae3b14c39712f1140e21b2ab1ccc81b5f6ad2aa90d739dc8df0602109a5c4c0ea820dcd39632ace7a4b247bc31e2a5e79cd6efaf5f1777650aac9ac694 + languageName: node + linkType: hard + +"@libsql/isomorphic-ws@npm:^0.1.5": + version: 0.1.5 + resolution: "@libsql/isomorphic-ws@npm:0.1.5" + dependencies: + "@types/ws": ^8.5.4 + ws: ^8.13.0 + checksum: 8255a0f4cae8ea66c94d6ab02ca57ddc7d6472c43700fd089e615e2df56028bf3723f694c91fbd76db403772f43f49cf2545e29e7ac18f77aa482fcfed71c940 + languageName: node + linkType: hard + +"@libsql/linux-arm64-gnu@npm:0.4.6": + version: 0.4.6 + resolution: "@libsql/linux-arm64-gnu@npm:0.4.6" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@libsql/linux-arm64-musl@npm:0.4.6": + version: 0.4.6 + resolution: "@libsql/linux-arm64-musl@npm:0.4.6" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@libsql/linux-x64-gnu@npm:0.4.6": + version: 0.4.6 + resolution: "@libsql/linux-x64-gnu@npm:0.4.6" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@libsql/linux-x64-musl@npm:0.4.6": + version: 0.4.6 + resolution: "@libsql/linux-x64-musl@npm:0.4.6" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@libsql/win32-x64-msvc@npm:0.4.6": + version: 0.4.6 + resolution: "@libsql/win32-x64-msvc@npm:0.4.6" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@mapbox/node-pre-gyp@npm:^1.0.0": version: 1.0.10 resolution: "@mapbox/node-pre-gyp@npm:1.0.10" @@ -13184,6 +13161,20 @@ __metadata: languageName: node linkType: hard +"@neon-rs/load@npm:^0.0.4": + version: 0.0.4 + resolution: "@neon-rs/load@npm:0.0.4" + checksum: ceed42a681980f4c96152857f6846434e3a89e25cac14228604a55e7992e96af01f30629026a498341984b405a2687099e56256a9eded9fee5393facca1ef762 + languageName: node + linkType: hard + +"@neon-rs/load@npm:^0.0.74": + version: 0.0.74 + resolution: "@neon-rs/load@npm:0.0.74" + checksum: d26ec9b08cdf1a7c5aeefe98f77112d205d11b4005a7934b21fe8fd27528847e08e4749e7e6c3fc05ae9f701175a58c11a095ae6af449634df3991a2c82e1dfa + languageName: node + linkType: hard + "@neondatabase/serverless@npm:0.6.0": version: 0.6.0 resolution: "@neondatabase/serverless@npm:0.6.0" @@ -14279,25 +14270,27 @@ __metadata: languageName: node linkType: hard -"@raycast/api@npm:^1.55.2": - version: 1.55.2 - resolution: "@raycast/api@npm:1.55.2" +"@raycast/api@npm:^1.83.1": + version: 1.83.1 + resolution: "@raycast/api@npm:1.83.1" dependencies: - "@types/node": 18.8.3 - "@types/react": 18.0.9 - react: 18.1.0 - react-reconciler: 0.28.0 + "@types/node": ^20.8.10 + "@types/react": ^18.3.3 + react: 18.3.1 peerDependencies: - "@types/node": 18.8.3 - "@types/react": 18.0.9 + "@types/node": 20.8.10 + "@types/react": 18.3.3 + react-devtools: 5.2.0 peerDependenciesMeta: "@types/node": optional: true "@types/react": optional: true + react-devtools: + optional: true bin: ray: bin/ray - checksum: 1154dc3d02d3ca1a9ee11d638682058464e2c01fb30df43b1f2ef0e6205744f34fcef10a413e16bfc93a08d19d6e569bbe7151660587a0d50c8df074f21c663d + checksum: 166ee04610ea90c327da9ea898fec474cd14b9b83e2205447b979edd4d6e36682e6b0121c238a8380439a932e5d6654b63e2afb17ea408c85fe8066f1a8e3070 languageName: node linkType: hard @@ -19155,13 +19148,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.8.3": - version: 18.8.3 - resolution: "@types/node@npm:18.8.3" - checksum: 9201adc6dc389644c9f478f950ef8926a93e5827865dcd80d7d12fefacab665c96879c87cd6ec74d5eccdd998c4603d02e1e07a35d71a63fe4c20670a381f6ef - languageName: node - linkType: hard - "@types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": version: 18.15.11 resolution: "@types/node@npm:18.15.11" @@ -19224,6 +19210,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.8.10": + version: 20.16.10 + resolution: "@types/node@npm:20.16.10" + dependencies: + undici-types: ~6.19.2 + checksum: 8b4d5a0af828871627e80da095df50ef8ae7ca458f39ccde1d060e3bf97b0bc76af9547bd5d75ce4ccb1dd80466804b2fe4f12553c11cb8691c6667effd9a9d7 + languageName: node + linkType: hard + "@types/offscreencanvas@npm:~2019.3.0": version: 2019.3.0 resolution: "@types/offscreencanvas@npm:2019.3.0" @@ -19347,13 +19342,6 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:^6.9.15": - version: 6.9.15 - resolution: "@types/qs@npm:6.9.15" - checksum: 97d8208c2b82013b618e7a9fc14df6bd40a73e1385ac479b6896bafc7949a46201c15f42afd06e86a05e914f146f495f606b6fb65610cc60cf2e0ff743ec38a2 - languageName: node - linkType: hard - "@types/range-parser@npm:*": version: 1.2.4 resolution: "@types/range-parser@npm:1.2.4" @@ -19413,25 +19401,24 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:18.0.9": - version: 18.0.9 - resolution: "@types/react@npm:18.0.9" +"@types/react@npm:^18": + version: 18.2.35 + resolution: "@types/react@npm:18.2.35" dependencies: "@types/prop-types": "*" "@types/scheduler": "*" csstype: ^3.0.2 - checksum: 162364dad716d9017ee34aabf2ea37499709ebbdef70392ae1b39225985971e1a46f121efb9c5c7da92144ee1d96d4525df806a7c1c03a5db7fd31dd034ddc7a + checksum: d3f1c39116c82e3a504f3332b9198f94f34a33d8535604434b1e387ecabd41ec995961775e2e1c08feefdde878a5b86220879350900137980d681ae3983db462 languageName: node linkType: hard -"@types/react@npm:^18": - version: 18.2.35 - resolution: "@types/react@npm:18.2.35" +"@types/react@npm:^18.3.3": + version: 18.3.10 + resolution: "@types/react@npm:18.3.10" dependencies: "@types/prop-types": "*" - "@types/scheduler": "*" csstype: ^3.0.2 - checksum: d3f1c39116c82e3a504f3332b9198f94f34a33d8535604434b1e387ecabd41ec995961775e2e1c08feefdde878a5b86220879350900137980d681ae3983db462 + checksum: 04261654b5f4bc9584e9d882c7dfd5b36dc58963f958f8c3efd24cb68c9d205bc2d57558a1479b86d7827f0e5116d5bd111791d1253583d1e1c165f0aeb48c48 languageName: node linkType: hard @@ -19601,6 +19588,13 @@ __metadata: languageName: node linkType: hard +"@types/uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "@types/uuid@npm:10.0.0" + checksum: e3958f8b0fe551c86c14431f5940c3470127293280830684154b91dc7eb3514aeb79fe3216968833cf79d4d1c67f580f054b5be2cd562bebf4f728913e73e944 + languageName: node + linkType: hard + "@types/uuid@npm:^9": version: 9.0.1 resolution: "@types/uuid@npm:9.0.1" @@ -19608,13 +19602,6 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9.0.1": - version: 9.0.2 - resolution: "@types/uuid@npm:9.0.2" - checksum: 1754bcf3444e1e3aeadd6e774fc328eb53bc956665e2e8fb6ec127aa8e1f43d9a224c3d22a9a6233dca8dd81a12dc7fed4d84b8876dd5ec82d40f574f7ff8b68 - languageName: node - linkType: hard - "@types/validate-npm-package-name@npm:3.0.0": version: 3.0.0 resolution: "@types/validate-npm-package-name@npm:3.0.0" @@ -19682,6 +19669,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.5.4": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "*" + checksum: ddefb6ad1671f70ce73b38a5f47f471d4d493864fca7c51f002a86e5993d031294201c5dced6d5018fb8905ad46888d65c7f20dd54fc165910b69f42fba9a6d0 + languageName: node + linkType: hard + "@types/ws@npm:^8.5.5": version: 8.5.7 resolution: "@types/ws@npm:8.5.7" @@ -20222,21 +20218,21 @@ __metadata: languageName: node linkType: hard -"@upstash/core-analytics@npm:^0.0.9": - version: 0.0.9 - resolution: "@upstash/core-analytics@npm:0.0.9" +"@upstash/core-analytics@npm:^0.0.10": + version: 0.0.10 + resolution: "@upstash/core-analytics@npm:0.0.10" dependencies: "@upstash/redis": ^1.28.3 - checksum: cfb49ce9d7c745a98de2b4e35973855d6311502c90840683dbe4b6a58304d880c6c356c6de275f32cfe5ec5add865a3dfe860c96d989e8a06e823788e90af2b9 + checksum: 5b435a5e67666a6ecb19e59ab73f314124634fdadb66390f8b2d6f7e20cac461262e5b81d113d77a5d093d8d11b2cfe3ecd5c573cde77505557f8bbf0f8b1e27 languageName: node linkType: hard -"@upstash/ratelimit@npm:^1.2.1": - version: 1.2.1 - resolution: "@upstash/ratelimit@npm:1.2.1" +"@upstash/ratelimit@npm:^2.0.3": + version: 2.0.3 + resolution: "@upstash/ratelimit@npm:2.0.3" dependencies: - "@upstash/core-analytics": ^0.0.9 - checksum: c23112312c51d91916ab75610a2ad9bd7b8eea41c2f6765eb84e572334eb0a4bc59c2420c9332c22a91e42398570c47ce0231884e6362d56e50057935d5f12f8 + "@upstash/core-analytics": ^0.0.10 + checksum: eae88d62948f9069bf1c89e9a7fb5c0e2a1e784d04af2319d079a60b02dc67d8e316af9252931d29fafb93dedb496479b0e92a340d62bdddd96d60e3fbe44c63 languageName: node linkType: hard @@ -20919,26 +20915,6 @@ __metadata: languageName: node linkType: hard -"apache-arrow@npm:^12.0.0": - version: 12.0.0 - resolution: "apache-arrow@npm:12.0.0" - dependencies: - "@types/command-line-args": 5.2.0 - "@types/command-line-usage": 5.0.2 - "@types/node": 18.14.5 - "@types/pad-left": 2.1.1 - command-line-args: 5.2.1 - command-line-usage: 6.1.3 - flatbuffers: 23.3.3 - json-bignum: ^0.0.3 - pad-left: ^2.1.0 - tslib: ^2.5.0 - bin: - arrow2csv: bin/arrow2csv.js - checksum: 3285189517c2b298cda42852321ce127754918513116eade6e4914c57983f68b6ba96605cfaa2202796d3d6e14755d3b3758f76c1374492affa3d95714eaca40 - languageName: node - linkType: hard - "apache-arrow@npm:^12.0.1": version: 12.0.1 resolution: "apache-arrow@npm:12.0.1" @@ -21748,13 +21724,6 @@ __metadata: languageName: node linkType: hard -"base-64@npm:^0.1.0": - version: 0.1.0 - resolution: "base-64@npm:0.1.0" - checksum: 5a42938f82372ab5392cbacc85a5a78115cbbd9dbef9f7540fa47d78763a3a8bd7d598475f0d92341f66285afd377509851a9bb5c67bbecb89686e9255d5b3eb - languageName: node - linkType: hard - "base16@npm:^1.0.0": version: 1.0.0 resolution: "base16@npm:1.0.0" @@ -22621,13 +22590,6 @@ __metadata: languageName: node linkType: hard -"charenc@npm:0.0.2": - version: 0.0.2 - resolution: "charenc@npm:0.0.2" - checksum: 81dcadbe57e861d527faf6dd3855dc857395a1c4d6781f4847288ab23cffb7b3ee80d57c15bba7252ffe3e5e8019db767757ee7975663ad2ca0939bb8fcaf2e5 - languageName: node - linkType: hard - "cheerio-select@npm:^2.1.0": version: 2.1.0 resolution: "cheerio-select@npm:2.1.0" @@ -23115,6 +23077,29 @@ __metadata: languageName: node linkType: hard +"cmake-js@npm:^7.3.0": + version: 7.3.0 + resolution: "cmake-js@npm:7.3.0" + dependencies: + axios: ^1.6.5 + debug: ^4 + fs-extra: ^11.2.0 + lodash.isplainobject: ^4.0.6 + memory-stream: ^1.0.0 + node-api-headers: ^1.1.0 + npmlog: ^6.0.2 + rc: ^1.2.7 + semver: ^7.5.4 + tar: ^6.2.0 + url-join: ^4.0.1 + which: ^2.0.2 + yargs: ^17.7.2 + bin: + cmake-js: bin/cmake-js + checksum: 9a0da5bde0e4c03879f5835a6bf9e9e28113831a6ce9663917e4463e3056a98470bce4336416f3a0136360b68771031b53cbaf4ae17bd920352d2f2b1a8774c8 + languageName: node + linkType: hard + "cmd-shim@npm:^6.0.0": version: 6.0.2 resolution: "cmd-shim@npm:6.0.2" @@ -23150,15 +23135,15 @@ __metadata: languageName: node linkType: hard -"cohere-ai@npm:^7.10.5": - version: 7.10.5 - resolution: "cohere-ai@npm:7.10.5" +"cohere-ai@npm:^7.14.0": + version: 7.14.0 + resolution: "cohere-ai@npm:7.14.0" dependencies: "@aws-sdk/client-sagemaker": ^3.583.0 "@aws-sdk/credential-providers": ^3.583.0 "@aws-sdk/protocol-http": ^3.374.0 "@aws-sdk/signature-v4": ^3.374.0 - form-data: 4.0.0 + form-data: ^4.0.0 form-data-encoder: ^4.0.2 formdata-node: ^6.0.3 js-base64: 3.7.2 @@ -23166,8 +23151,7 @@ __metadata: qs: 6.11.2 readable-stream: ^4.5.2 url-join: 4.0.1 - web-streams-polyfill: ^4.0.0 - checksum: 80f87d1bd831c0115936a9d9cd1a364bde184ad96d461396acb346e2d3601ef084b6736a1344553f6e05713b7e117a8c295ff508111fca9f7b6c2a16e722e035 + checksum: 423426458f65947259bba6616d05b456f08163ee1d07e1e7d573fec2a1ba5eac737eb20d2769149b83bdb16f37ae9a7de77348016d49a1a5db47862e79ff29d4 languageName: node linkType: hard @@ -23659,7 +23643,7 @@ __metadata: "@docusaurus/preset-classic": 2.4.3 "@docusaurus/remark-plugin-npm2yarn": 2.4.3 "@docusaurus/theme-mermaid": 2.4.3 - "@langchain/langgraph": 0.0.28 + "@langchain/langgraph": 0.2.3 "@langchain/scripts": "workspace:*" "@mdx-js/react": ^1.6.22 "@microsoft/fetch-event-source": ^2.0.1 @@ -23791,17 +23775,18 @@ __metadata: linkType: hard "couchbase@npm:^4.3.0": - version: 4.3.0 - resolution: "couchbase@npm:4.3.0" - dependencies: - "@couchbase/couchbase-darwin-arm64-napi": 4.3.0 - "@couchbase/couchbase-darwin-x64-napi": 4.3.0 - "@couchbase/couchbase-linux-arm64-napi": 4.3.0 - "@couchbase/couchbase-linux-x64-napi": 4.3.0 - "@couchbase/couchbase-linuxmusl-x64-napi": 4.3.0 - "@couchbase/couchbase-win32-x64-napi": 4.3.0 - cmake-js: ^7.2.1 - node-addon-api: ^7.0.0 + version: 4.4.2 + resolution: "couchbase@npm:4.4.2" + dependencies: + "@couchbase/couchbase-darwin-arm64-napi": 4.4.2 + "@couchbase/couchbase-darwin-x64-napi": 4.4.2 + "@couchbase/couchbase-linux-arm64-napi": 4.4.2 + "@couchbase/couchbase-linux-x64-napi": 4.4.2 + "@couchbase/couchbase-linuxmusl-arm64-napi": 4.4.2 + "@couchbase/couchbase-linuxmusl-x64-napi": 4.4.2 + "@couchbase/couchbase-win32-x64-napi": 4.4.2 + cmake-js: ^7.3.0 + node-addon-api: ^8.0.0 dependenciesMeta: "@couchbase/couchbase-darwin-arm64-napi": optional: true @@ -23811,11 +23796,13 @@ __metadata: optional: true "@couchbase/couchbase-linux-x64-napi": optional: true + "@couchbase/couchbase-linuxmusl-arm64-napi": + optional: true "@couchbase/couchbase-linuxmusl-x64-napi": optional: true "@couchbase/couchbase-win32-x64-napi": optional: true - checksum: 99c24663cd7ab524668f26f151f92a78ecf850ddeca919b71f26eebe1134c492218639f9f491ff95e749628bd108164f78f13884e85a1de27959152247f8ecab + checksum: 452dcdc01f9277fc6e15668eb4ab67b6afff0945a8832a598b1009068fe82edcb76d4fbf0704d9351deba74348ee25759ddba5efbfed14dae23fca13cf4714a5 languageName: node linkType: hard @@ -23915,13 +23902,6 @@ __metadata: languageName: node linkType: hard -"crypt@npm:0.0.2": - version: 0.0.2 - resolution: "crypt@npm:0.0.2" - checksum: baf4c7bbe05df656ec230018af8cf7dbe8c14b36b98726939cef008d473f6fe7a4fad906cfea4062c93af516f1550a3f43ceb4d6615329612c6511378ed9fe34 - languageName: node - linkType: hard - "crypto-js@npm:^4.2.0": version: 4.2.0 resolution: "crypto-js@npm:4.2.0" @@ -25114,6 +25094,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:2.0.2, detect-libc@npm:^2.0.2": + version: 2.0.2 + resolution: "detect-libc@npm:2.0.2" + checksum: 2b2cd3649b83d576f4be7cc37eb3b1815c79969c8b1a03a40a4d55d83bc74d010753485753448eacb98784abf22f7dbd3911fd3b60e29fda28fed2d1a997944d + languageName: node + linkType: hard + "detect-libc@npm:^2.0.0": version: 2.0.1 resolution: "detect-libc@npm:2.0.1" @@ -25121,13 +25108,6 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.2": - version: 2.0.2 - resolution: "detect-libc@npm:2.0.2" - checksum: 2b2cd3649b83d576f4be7cc37eb3b1815c79969c8b1a03a40a4d55d83bc74d010753485753448eacb98784abf22f7dbd3911fd3b60e29fda28fed2d1a997944d - languageName: node - linkType: hard - "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -25196,16 +25176,6 @@ __metadata: languageName: node linkType: hard -"digest-fetch@npm:^1.3.0": - version: 1.3.0 - resolution: "digest-fetch@npm:1.3.0" - dependencies: - base-64: ^0.1.0 - md5: ^2.3.0 - checksum: 8ebdb4b9ef02b1ac0da532d25c7d08388f2552813dfadabfe7c4630e944bb4a48093b997fc926440a10e1ccf4912f2ce9adcf2d6687b0518dab8480e08f22f9d - languageName: node - linkType: hard - "dingbat-to-unicode@npm:^1.0.1": version: 1.0.1 resolution: "dingbat-to-unicode@npm:1.0.1" @@ -25451,6 +25421,13 @@ __metadata: languageName: node linkType: hard +"double-ended-queue@npm:^2.1.0-0": + version: 2.1.0-0 + resolution: "double-ended-queue@npm:2.1.0-0" + checksum: 3030cf9dcf6f8e7d8cb6ae5b7304890445d7c32233a614e400ba7b378086ad76f5822d0e501afd5ffe0af1de4bcb842fa23d4c79174d54f6566399435fafc271 + languageName: node + linkType: hard + "dpdm@npm:3.12.0": version: 3.12.0 resolution: "dpdm@npm:3.12.0" @@ -27234,7 +27211,7 @@ __metadata: "@langchain/google-vertexai": "workspace:*" "@langchain/google-vertexai-web": "workspace:*" "@langchain/groq": "workspace:*" - "@langchain/langgraph": ^0.0.28 + "@langchain/langgraph": ^0.2.3 "@langchain/mistralai": "workspace:*" "@langchain/mongodb": "workspace:*" "@langchain/nomic": "workspace:*" @@ -27253,7 +27230,6 @@ __metadata: "@planetscale/database": ^1.8.0 "@prisma/client": ^4.11.0 "@qdrant/js-client-rest": ^1.9.0 - "@raycast/api": ^1.55.2 "@rockset/client": ^0.9.1 "@supabase/supabase-js": ^2.45.0 "@tensorflow/tfjs-backend-cpu": ^4.4.0 @@ -27269,8 +27245,8 @@ __metadata: "@zilliz/milvus2-sdk-node": ^2.3.5 axios: ^0.26.0 chromadb: ^1.5.3 + cohere-ai: ^7.14.0 convex: ^1.3.1 - couchbase: ^4.3.0 date-fns: ^3.3.1 dotenv: ^16.0.3 duck-duck-scrape: ^2.2.5 @@ -27288,7 +27264,7 @@ __metadata: ioredis: ^5.3.2 js-yaml: ^4.1.0 langchain: "workspace:*" - langsmith: ^0.1.43 + langsmith: ^0.1.56 mongodb: ^6.3.0 pg: ^8.11.0 pickleparser: ^0.2.1 @@ -27302,7 +27278,7 @@ __metadata: typescript: ~5.1.6 typesense: ^1.5.3 uuid: ^10.0.0 - vectordb: ^0.1.4 + vectordb: ^0.9.0 voy-search: 0.6.2 weaviate-ts-client: ^2.0.0 zod: ^3.22.4 @@ -29349,23 +29325,6 @@ __metadata: languageName: node linkType: hard -"groq-sdk@npm:^0.3.2": - version: 0.3.3 - resolution: "groq-sdk@npm:0.3.3" - dependencies: - "@types/node": ^18.11.18 - "@types/node-fetch": ^2.6.4 - abort-controller: ^3.0.0 - agentkeepalive: ^4.2.1 - digest-fetch: ^1.3.0 - form-data-encoder: 1.7.2 - formdata-node: ^4.3.2 - node-fetch: ^2.6.7 - web-streams-polyfill: ^3.2.1 - checksum: 822af994d44947072d861879bec959b2bad0762b16339cf1c4308d454bff09af3721126c45aa2f78d112e38365be92b36ec632bfc60fc6ece4e2f7c1b8ff327a - languageName: node - linkType: hard - "groq-sdk@npm:^0.5.0": version: 0.5.0 resolution: "groq-sdk@npm:0.5.0" @@ -30548,13 +30507,6 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:~1.1.6": - version: 1.1.6 - resolution: "is-buffer@npm:1.1.6" - checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 - languageName: node - linkType: hard - "is-callable@npm:^1.1.3, is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" @@ -32247,7 +32199,7 @@ __metadata: languageName: node linkType: hard -"js-base64@npm:3.7.7": +"js-base64@npm:3.7.7, js-base64@npm:^3.7.5": version: 3.7.7 resolution: "js-base64@npm:3.7.7" checksum: d1b02971db9dc0fd35baecfaf6ba499731fb44fe3373e7e1d6681fbd3ba665f29e8d9d17910254ef8104e2cb8b44117fe4202d3dc54c7cafe9ba300fe5433358 @@ -32726,221 +32678,21 @@ __metadata: languageName: node linkType: hard -"langchain@npm:>=0.2.3 <0.4.0": - version: 0.2.19 - resolution: "langchain@npm:0.2.19" +"langchain@>=0.2.3 <0.4.0, langchain@workspace:*, langchain@workspace:langchain": + version: 0.0.0-use.local + resolution: "langchain@workspace:langchain" dependencies: - "@langchain/core": ">=0.2.21 <0.3.0" - "@langchain/openai": ">=0.1.0 <0.3.0" - "@langchain/textsplitters": ~0.0.0 - binary-extensions: ^2.2.0 - js-tiktoken: ^1.0.12 - js-yaml: ^4.1.0 - jsonpointer: ^5.0.1 - langsmith: ~0.1.40 - openapi-types: ^12.1.3 - p-retry: 4 - uuid: ^10.0.0 - yaml: ^2.2.1 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - peerDependencies: - "@aws-sdk/client-s3": "*" - "@aws-sdk/client-sagemaker-runtime": "*" - "@aws-sdk/client-sfn": "*" - "@aws-sdk/credential-provider-node": "*" - "@azure/storage-blob": "*" - "@browserbasehq/sdk": "*" - "@gomomento/sdk": "*" - "@gomomento/sdk-core": "*" - "@gomomento/sdk-web": ^1.51.1 + "@faker-js/faker": ^7.6.0 + "@jest/globals": ^29.5.0 "@langchain/anthropic": "*" "@langchain/aws": "*" "@langchain/cohere": "*" - "@langchain/community": "*" + "@langchain/core": "workspace:*" "@langchain/google-genai": "*" "@langchain/google-vertexai": "*" "@langchain/groq": "*" "@langchain/mistralai": "*" "@langchain/ollama": "*" - "@mendable/firecrawl-js": "*" - "@notionhq/client": "*" - "@pinecone-database/pinecone": "*" - "@supabase/supabase-js": "*" - "@vercel/kv": "*" - "@xata.io/client": "*" - apify-client: "*" - assemblyai: "*" - axios: "*" - cheerio: "*" - chromadb: "*" - convex: "*" - couchbase: "*" - d3-dsv: "*" - epub2: "*" - fast-xml-parser: "*" - handlebars: ^4.7.8 - html-to-text: "*" - ignore: "*" - ioredis: "*" - jsdom: "*" - mammoth: "*" - mongodb: "*" - node-llama-cpp: "*" - notion-to-md: "*" - officeparser: "*" - pdf-parse: "*" - peggy: ^3.0.2 - playwright: "*" - puppeteer: "*" - pyodide: ">=0.24.1 <0.27.0" - redis: "*" - sonix-speech-recognition: "*" - srt-parser-2: "*" - typeorm: "*" - weaviate-ts-client: "*" - web-auth-library: "*" - ws: "*" - youtube-transcript: "*" - youtubei.js: "*" - peerDependenciesMeta: - "@aws-sdk/client-s3": - optional: true - "@aws-sdk/client-sagemaker-runtime": - optional: true - "@aws-sdk/client-sfn": - optional: true - "@aws-sdk/credential-provider-node": - optional: true - "@azure/storage-blob": - optional: true - "@browserbasehq/sdk": - optional: true - "@gomomento/sdk": - optional: true - "@gomomento/sdk-core": - optional: true - "@gomomento/sdk-web": - optional: true - "@langchain/anthropic": - optional: true - "@langchain/aws": - optional: true - "@langchain/cohere": - optional: true - "@langchain/community": - optional: true - "@langchain/google-genai": - optional: true - "@langchain/google-vertexai": - optional: true - "@langchain/groq": - optional: true - "@langchain/mistralai": - optional: true - "@langchain/ollama": - optional: true - "@mendable/firecrawl-js": - optional: true - "@notionhq/client": - optional: true - "@pinecone-database/pinecone": - optional: true - "@supabase/supabase-js": - optional: true - "@vercel/kv": - optional: true - "@xata.io/client": - optional: true - apify-client: - optional: true - assemblyai: - optional: true - axios: - optional: true - cheerio: - optional: true - chromadb: - optional: true - convex: - optional: true - couchbase: - optional: true - d3-dsv: - optional: true - epub2: - optional: true - faiss-node: - optional: true - fast-xml-parser: - optional: true - handlebars: - optional: true - html-to-text: - optional: true - ignore: - optional: true - ioredis: - optional: true - jsdom: - optional: true - mammoth: - optional: true - mongodb: - optional: true - node-llama-cpp: - optional: true - notion-to-md: - optional: true - officeparser: - optional: true - pdf-parse: - optional: true - peggy: - optional: true - playwright: - optional: true - puppeteer: - optional: true - pyodide: - optional: true - redis: - optional: true - sonix-speech-recognition: - optional: true - srt-parser-2: - optional: true - typeorm: - optional: true - weaviate-ts-client: - optional: true - web-auth-library: - optional: true - ws: - optional: true - youtube-transcript: - optional: true - youtubei.js: - optional: true - checksum: 4fb49c9ba34536b87098d1b2dcd8594efcce7b7c8650cbeb1ec06c68fa6c142e45fd9e1ea56f43bc2113d6d53e1e56f320688df684b8d1fa1cef9dacbf50c091 - languageName: node - linkType: hard - -"langchain@workspace:*, langchain@workspace:langchain": - version: 0.0.0-use.local - resolution: "langchain@workspace:langchain" - dependencies: - "@faker-js/faker": ^7.6.0 - "@jest/globals": ^29.5.0 - "@langchain/anthropic": ^0.2.8 - "@langchain/aws": ^0.0.5 - "@langchain/cohere": ^0.2.1 - "@langchain/core": "workspace:*" - "@langchain/google-genai": ^0.0.26 - "@langchain/google-vertexai": ~0.0 - "@langchain/groq": ^0.0.15 - "@langchain/mistralai": ^0.0.26 - "@langchain/ollama": ^0.0.2 "@langchain/openai": ">=0.1.0 <0.4.0" "@langchain/scripts": ">=0.1.0 <0.2.0" "@langchain/textsplitters": ">=0.0.0 <0.2.0" @@ -32972,7 +32724,7 @@ __metadata: js-tiktoken: ^1.0.12 js-yaml: ^4.1.0 jsonpointer: ^5.0.1 - langsmith: ~0.1.40 + langsmith: ^0.1.56 openai: ^4.41.1 openapi-types: ^12.1.3 p-retry: 4 @@ -32993,7 +32745,6 @@ __metadata: "@langchain/anthropic": "*" "@langchain/aws": "*" "@langchain/cohere": "*" - "@langchain/community": "*" "@langchain/core": ">=0.2.21 <0.4.0" "@langchain/google-genai": "*" "@langchain/google-vertexai": "*" @@ -33012,8 +32763,6 @@ __metadata: optional: true "@langchain/cohere": optional: true - "@langchain/community": - optional: true "@langchain/google-genai": optional: true "@langchain/google-vertexai": @@ -33054,77 +32803,41 @@ __metadata: languageName: unknown linkType: soft -"langsmith@npm:^0.1.43": - version: 0.1.43 - resolution: "langsmith@npm:0.1.43" +"langsmith@npm:^0.1.56, langsmith@npm:~0.1.56": + version: 0.1.56 + resolution: "langsmith@npm:0.1.56" dependencies: - "@types/uuid": ^9.0.1 + "@types/uuid": ^10.0.0 commander: ^10.0.1 p-queue: ^6.6.2 p-retry: 4 semver: ^7.6.3 - uuid: ^9.0.0 - peerDependencies: - "@langchain/core": "*" - langchain: "*" - openai: "*" - peerDependenciesMeta: - "@langchain/core": - optional: true - langchain: - optional: true - openai: - optional: true - checksum: 83ded542eb9a4f3c7f75671ba9a85b9bb0558251cbc5ad506098068b7c5abcb6062101857cd5e2b324d5eb1e3f4984aff0d08b7b913c3ad6e41d4cbc65492a13 - languageName: node - linkType: hard - -"langsmith@npm:~0.1.30": - version: 0.1.39 - resolution: "langsmith@npm:0.1.39" - dependencies: - "@types/uuid": ^9.0.1 - commander: ^10.0.1 - p-queue: ^6.6.2 - p-retry: 4 - uuid: ^9.0.0 + uuid: ^10.0.0 peerDependencies: - "@langchain/core": "*" - langchain: "*" openai: "*" peerDependenciesMeta: - "@langchain/core": - optional: true - langchain: - optional: true openai: optional: true - checksum: df21332662ec3a2d2d5cf915acede52b96aedf2a286259435d683f230af5926500b129cab1f0275450e0d3de6d9d8476e410ac46f5e994beb43f2e2df8a1965f + checksum: 61db6dc3016e35d14d25e78a8ecebcc6356f2efc00310f5582dce9d28a88377525425622d1b98f053e73c0b3233d44c5a2f9d5654ca72ee2e61163edd5be2d28 languageName: node linkType: hard -"langsmith@npm:~0.1.40": - version: 0.1.40 - resolution: "langsmith@npm:0.1.40" +"langsmith@npm:^0.1.65": + version: 0.1.65 + resolution: "langsmith@npm:0.1.65" dependencies: - "@types/uuid": ^9.0.1 + "@types/uuid": ^10.0.0 commander: ^10.0.1 p-queue: ^6.6.2 p-retry: 4 semver: ^7.6.3 - uuid: ^9.0.0 + uuid: ^10.0.0 peerDependencies: - "@langchain/core": "*" - langchain: "*" openai: "*" peerDependenciesMeta: - "@langchain/core": - optional: true - langchain: - optional: true openai: optional: true - checksum: 8c5bcf5137e93a9a17203fbe21d6a61f45c98fccafc2040d56e9cc15a4ee432456d986adf0e590d8c436b72d18143053ce6e65f021115f1596dd4519ec2805d7 + checksum: ca44f26733fbb20675b84f2586b90622b8cf1aedc82123f5574af04e88ba29348e28b2b63f410479aeb7e5c174d2fef13b4bd9eb68581d93a104950b1fafa40f languageName: node linkType: hard @@ -33238,6 +32951,39 @@ __metadata: languageName: node linkType: hard +"libsql@npm:^0.4.4": + version: 0.4.6 + resolution: "libsql@npm:0.4.6" + dependencies: + "@libsql/darwin-arm64": 0.4.6 + "@libsql/darwin-x64": 0.4.6 + "@libsql/linux-arm64-gnu": 0.4.6 + "@libsql/linux-arm64-musl": 0.4.6 + "@libsql/linux-x64-gnu": 0.4.6 + "@libsql/linux-x64-musl": 0.4.6 + "@libsql/win32-x64-msvc": 0.4.6 + "@neon-rs/load": ^0.0.4 + detect-libc: 2.0.2 + dependenciesMeta: + "@libsql/darwin-arm64": + optional: true + "@libsql/darwin-x64": + optional: true + "@libsql/linux-arm64-gnu": + optional: true + "@libsql/linux-arm64-musl": + optional: true + "@libsql/linux-x64-gnu": + optional: true + "@libsql/linux-x64-musl": + optional: true + "@libsql/win32-x64-msvc": + optional: true + checksum: 0c2e864172d43bbf9555eeb1e5aba41e1fe86cb3eb05f99a8ed1b92afc702ec84193fb207163d7e3b291a65d86735f45a86138c03b919fcf0edca054cd0ea1b3 + conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=wasm32) + languageName: node + linkType: hard + "lie@npm:~3.3.0": version: 3.3.0 resolution: "lie@npm:3.3.0" @@ -34016,17 +33762,6 @@ __metadata: languageName: node linkType: hard -"md5@npm:^2.3.0": - version: 2.3.0 - resolution: "md5@npm:2.3.0" - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: ~1.1.6 - checksum: a63cacf4018dc9dee08c36e6f924a64ced735b37826116c905717c41cebeb41a522f7a526ba6ad578f9c80f02cb365033ccd67fe186ffbcc1a1faeb75daa9b6e - languageName: node - linkType: hard - "mdast-squeeze-paragraphs@npm:^4.0.0": version: 4.0.0 resolution: "mdast-squeeze-paragraphs@npm:4.0.0" @@ -35036,6 +34771,13 @@ __metadata: languageName: node linkType: hard +"node-api-headers@npm:^1.1.0": + version: 1.3.0 + resolution: "node-api-headers@npm:1.3.0" + checksum: 4da6c55dd3f39487e24eb5c0862a28899c0b8c4c03041bb452a7b8940efc9ffe781dafdc6e2fbe381d3d0495e08aaee4e2cc873d7a35a0b33b39248c376e873b + languageName: node + linkType: hard + "node-domexception@npm:1.0.0, node-domexception@npm:^1.0.0": version: 1.0.0 resolution: "node-domexception@npm:1.0.0" @@ -35202,9 +34944,9 @@ __metadata: languageName: node linkType: hard -"node-llama-cpp@npm:2.7.3": - version: 2.7.3 - resolution: "node-llama-cpp@npm:2.7.3" +"node-llama-cpp@npm:^2": + version: 2.8.16 + resolution: "node-llama-cpp@npm:2.8.16" dependencies: chalk: ^5.3.0 chmodrp: ^1.0.2 @@ -35222,9 +34964,14 @@ __metadata: uuid: ^9.0.0 which: ^4.0.0 yargs: ^17.7.2 + peerDependencies: + typescript: ">=5.0.0" + peerDependenciesMeta: + typescript: + optional: true bin: node-llama-cpp: dist/cli/cli.js - checksum: 5be984ab4711d3e6f3f29e6246e5d3b437b0e48631376f361c832a969530ffa4da5cdc78e8cf8e003a86411ced42c7cf022ad6d5b50c24f276cd841dbb077e0b + checksum: 51607ec21f02a702a440fae6a8c06b00b039cbfcb09a7d92eaa16e97972ff71874285d9d2561429bae03b52da81e0d12d2d22356dd162f30ce92d37c928014c4 languageName: node linkType: hard @@ -35763,37 +35510,17 @@ __metadata: languageName: node linkType: hard -"openai@npm:^4.49.1": - version: 4.49.1 - resolution: "openai@npm:4.49.1" - 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 - web-streams-polyfill: ^3.2.1 - bin: - openai: bin/cli - checksum: b9bc845d25412d6b6ad827fb1363a4029935d8eb85a8708e55f5cf2852a0551b8720c8099edcbb0a2c2ab2be2d8f652a97061d9898b908e29d9bb2f727304b6e - languageName: node - linkType: hard - -"openai@npm:^4.57.3": - version: 4.57.3 - resolution: "openai@npm:4.57.3" +"openai@npm:^4.68.0": + version: 4.68.0 + resolution: "openai@npm:4.68.0" dependencies: "@types/node": ^18.11.18 "@types/node-fetch": ^2.6.4 - "@types/qs": ^6.9.15 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 - qs: ^6.10.3 peerDependencies: zod: ^3.23.8 peerDependenciesMeta: @@ -35801,7 +35528,7 @@ __metadata: optional: true bin: openai: bin/cli - checksum: 6e8cef99975af5fd8e9a06685f05396a6fabecda38bd77fa62db4b7ea9bdfa0b4c762c5f74e99e42212af81f74f50748c5034bf78c9abcf74cc6eb984f3dcffa + checksum: 2866e54ac1b34e074055dde7cc809bcc33d1172f0ab289dacd54ced04a62ab3c2b9f584fdb84ece981edc5c30939497af4e91fe33646f71d5c6ced5d7106a797 languageName: node linkType: hard @@ -37602,6 +37329,13 @@ __metadata: languageName: node linkType: hard +"promise-limit@npm:^2.7.0": + version: 2.7.0 + resolution: "promise-limit@npm:2.7.0" + checksum: 3e20a46d752ab41c921feceb668b3a6d000438573e71342d42bf63373fdbafbf4b6a3f88ef5ff902a2a37be28152dc74e618e863134fd4c08c196448f4a6d28f + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -37934,15 +37668,6 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.3": - version: 6.13.0 - resolution: "qs@npm:6.13.0" - dependencies: - side-channel: ^1.0.6 - checksum: e9404dc0fc2849245107108ce9ec2766cde3be1b271de0bf1021d049dc5b98d1a2901e67b431ac5509f865420a7ed80b7acb3980099fe1c118a1c5d2e1432ad8 - languageName: node - linkType: hard - "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -38188,18 +37913,6 @@ __metadata: languageName: node linkType: hard -"react-reconciler@npm:0.28.0": - version: 0.28.0 - resolution: "react-reconciler@npm:0.28.0" - dependencies: - loose-envify: ^1.1.0 - scheduler: ^0.22.0 - peerDependencies: - react: ^18.1.0 - checksum: 6421f9500827219556ead3d86cbdf0553c47d633ecda2144720bf235d404f5e4e1e585336c4dfa80e26a04f1a6117c455a60964917d5600c73a91d8374f0232f - languageName: node - linkType: hard - "react-router-config@npm:^5.1.1": version: 5.1.1 resolution: "react-router-config@npm:5.1.1" @@ -38261,12 +37974,12 @@ __metadata: languageName: node linkType: hard -"react@npm:18.1.0": - version: 18.1.0 - resolution: "react@npm:18.1.0" +"react@npm:18.3.1": + version: 18.3.1 + resolution: "react@npm:18.3.1" dependencies: loose-envify: ^1.1.0 - checksum: 5bb296b561b43ef2220395da4faac86c14a087c8c80e1a7598a5740f01ee605c11eaf249985c1e2000971c4cd32ccb46d40f00479bbd9fb6b1c7cf857393b7d4 + checksum: a27bcfa8ff7c15a1e50244ad0d0c1cb2ad4375eeffefd266a64889beea6f6b64c4966c9b37d14ee32d6c9fcd5aa6ba183b6988167ab4d127d13e7cb5b386a376 languageName: node linkType: hard @@ -39379,15 +39092,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.22.0": - version: 0.22.0 - resolution: "scheduler@npm:0.22.0" - dependencies: - loose-envify: ^1.1.0 - checksum: a8ef5cab769c020cd6382ad9ecc3f72dbde56a50a36639b3a42ad9c11f7724f03700bcad373044059b8067d4a6365154dc7c0ca8027ef20ff4900cf58a0fc2c5 - languageName: node - linkType: hard - "scheduler@npm:^0.23.0": version: 0.23.0 resolution: "scheduler@npm:0.23.0" @@ -41038,6 +40742,20 @@ __metadata: languageName: node linkType: hard +"tar@npm:^6.2.0": + version: 6.2.1 + resolution: "tar@npm:6.2.1" + dependencies: + chownr: ^2.0.0 + fs-minipass: ^2.0.0 + minipass: ^5.0.0 + minizlib: ^2.1.1 + mkdirp: ^1.0.3 + yallist: ^4.0.0 + checksum: f1322768c9741a25356c11373bce918483f40fa9a25c69c59410c8a1247632487edef5fe76c5f12ac51a6356d2f1829e96d2bc34098668a2fc34d76050ac2b6c + languageName: node + linkType: hard + "teeny-request@npm:^9.0.0": version: 9.0.0 resolution: "teeny-request@npm:9.0.0" @@ -42239,6 +41957,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.19.2": + version: 6.19.8 + resolution: "undici-types@npm:6.19.8" + checksum: de51f1b447d22571cf155dfe14ff6d12c5bdaec237c765085b439c38ca8518fc360e88c70f99469162bf2e14188a7b0bcb06e1ed2dc031042b984b0bb9544017 + languageName: node + linkType: hard + "undici@npm:5.27.2": version: 5.27.2 resolution: "undici@npm:5.27.2" @@ -42924,13 +42649,32 @@ __metadata: languageName: node linkType: hard -"vectordb@npm:^0.1.4": - version: 0.1.4 - resolution: "vectordb@npm:0.1.4" - dependencies: - "@apache-arrow/ts": ^12.0.0 - apache-arrow: ^12.0.0 - checksum: 8a40abf4466479b0b9e61687416b5ab232458401917bf9a1d5f3d8ea8c8320ecc5691174f4d4c0cfef0bb6c16328a9088419fd90ac85fd7267dbccdd1f9e55d7 +"vectordb@npm:^0.9.0": + version: 0.9.0 + resolution: "vectordb@npm:0.9.0" + dependencies: + "@lancedb/vectordb-darwin-arm64": 0.4.20 + "@lancedb/vectordb-darwin-x64": 0.4.20 + "@lancedb/vectordb-linux-arm64-gnu": 0.4.20 + "@lancedb/vectordb-linux-x64-gnu": 0.4.20 + "@lancedb/vectordb-win32-x64-msvc": 0.4.20 + "@neon-rs/load": ^0.0.74 + axios: ^1.4.0 + peerDependencies: + "@apache-arrow/ts": ^14.0.2 + apache-arrow: ^14.0.2 + dependenciesMeta: + "@lancedb/vectordb-darwin-arm64": + optional: true + "@lancedb/vectordb-darwin-x64": + optional: true + "@lancedb/vectordb-linux-arm64-gnu": + optional: true + "@lancedb/vectordb-linux-x64-gnu": + optional: true + "@lancedb/vectordb-win32-x64-msvc": + optional: true + conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64) languageName: node linkType: hard @@ -43125,13 +42869,6 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:^3.3.3": - version: 3.3.3 - resolution: "web-streams-polyfill@npm:3.3.3" - checksum: 21ab5ea08a730a2ef8023736afe16713b4f2023ec1c7085c16c8e293ee17ed085dff63a0ad8722da30c99c4ccbd4ccd1b2e79c861829f7ef2963d7de7004c2cb - languageName: node - linkType: hard - "web-streams-polyfill@npm:^4.0.0": version: 4.0.0 resolution: "web-streams-polyfill@npm:4.0.0"