From 3d64f4343c64abb7235fb480603b02d3a11ba871 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Tue, 16 Jul 2024 15:57:48 -0700 Subject: [PATCH 01/15] Adds docs on passing tool results to a model (#6095) --- docs/core_docs/docs/how_to/index.mdx | 1 + .../docs/how_to/tool_artifacts.ipynb | 2 +- .../how_to/tool_results_pass_to_model.ipynb | 269 ++++++++++++++++++ 3 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 docs/core_docs/docs/how_to/tool_results_pass_to_model.ipynb diff --git a/docs/core_docs/docs/how_to/index.mdx b/docs/core_docs/docs/how_to/index.mdx index d5686da7387f1..88370fdf968fe 100644 --- a/docs/core_docs/docs/how_to/index.mdx +++ b/docs/core_docs/docs/how_to/index.mdx @@ -161,6 +161,7 @@ LangChain [Tools](/docs/concepts/#tools) contain a description of the tool (to p - [How to: create custom tools](/docs/how_to/custom_tools) - [How to: use built-in tools and built-in toolkits](/docs/how_to/tools_builtin) - [How to: use a chat model to call tools](/docs/how_to/tool_calling/) +- [How to: pass tool results back to model](/docs/how_to/tool_results_pass_to_model/) - [How to: add ad-hoc tool calling capability to LLMs and Chat Models](/docs/how_to/tools_prompting) - [How to: return extra artifacts from a custom tool](/docs/how_to/tool_artifacts) diff --git a/docs/core_docs/docs/how_to/tool_artifacts.ipynb b/docs/core_docs/docs/how_to/tool_artifacts.ipynb index f95f757d992f2..0c684ad080051 100644 --- a/docs/core_docs/docs/how_to/tool_artifacts.ipynb +++ b/docs/core_docs/docs/how_to/tool_artifacts.ipynb @@ -12,7 +12,7 @@ "This guide assumes familiarity with the following concepts:\n", "\n", "- [Tools](/docs/concepts/#tools)\n", - "- [Tool calling](/docs/concepts/#tool-calling)\n", + "- [Tool calling](/docs/concepts/#functiontool-calling)\n", "\n", ":::\n", "```\n", diff --git a/docs/core_docs/docs/how_to/tool_results_pass_to_model.ipynb b/docs/core_docs/docs/how_to/tool_results_pass_to_model.ipynb new file mode 100644 index 0000000000000..827c8580163c8 --- /dev/null +++ b/docs/core_docs/docs/how_to/tool_results_pass_to_model.ipynb @@ -0,0 +1,269 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to pass tool outputs to the model\n", + "\n", + "```{=mdx}\n", + ":::info Prerequisites\n", + "This guide assumes familiarity with the following concepts:\n", + "\n", + "- [Tools](/docs/concepts/#tools)\n", + "- [Tool calling](/docs/concepts/#functiontool-calling)\n", + "\n", + ":::\n", + "```\n", + "\n", + "If we're using the model-generated tool invocations to actually call tools and want to pass the tool results back to the model, we can do so using `ToolMessage`s and `ToolCall`s. First, let's define some tools and a chat model instance." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "\n", + "const addTool = tool(async ({ a, b }) => {\n", + " return a + b;\n", + "}, {\n", + " name: \"add\",\n", + " schema: z.object({\n", + " a: z.number(),\n", + " b: z.number(),\n", + " }),\n", + " description: \"Adds a and b.\",\n", + "});\n", + "\n", + "const multiplyTool = tool(async ({ a, b }) => {\n", + " return a * b;\n", + "}, {\n", + " name: \"multiply\",\n", + " schema: z.object({\n", + " a: z.number(),\n", + " b: z.number(),\n", + " }),\n", + " description: \"Multiplies a and b.\",\n", + "});\n", + "\n", + "const tools = [addTool, multiplyTool];" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we invoke a tool with a `ToolCall`, we'll automatically get back a `ToolMessage` that can be fed back to the model: \n", + "\n", + "```{=mdx}\n", + ":::caution Compatibility\n", + "\n", + "This functionality requires `@langchain/core>=0.2.16`. Please see here for a [guide on upgrading](/docs/how_to/installation/#installing-integration-packages).\n", + "\n", + ":::\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\n", + " HumanMessage {\n", + " lc_serializable: true,\n", + " lc_kwargs: {\n", + " content: 'What is 3 * 12? Also, what is 11 + 49?',\n", + " additional_kwargs: {},\n", + " response_metadata: {}\n", + " },\n", + " lc_namespace: [ 'langchain_core', 'messages' ],\n", + " content: 'What is 3 * 12? Also, what is 11 + 49?',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined\n", + " },\n", + " AIMessage {\n", + " lc_serializable: true,\n", + " lc_kwargs: {\n", + " content: '',\n", + " tool_calls: [Array],\n", + " invalid_tool_calls: [],\n", + " additional_kwargs: [Object],\n", + " id: 'chatcmpl-9llAzVKdHCJkcUCnwGx62bqesSJPB',\n", + " response_metadata: {}\n", + " },\n", + " lc_namespace: [ 'langchain_core', 'messages' ],\n", + " content: '',\n", + " name: undefined,\n", + " additional_kwargs: { function_call: undefined, tool_calls: [Array] },\n", + " response_metadata: { tokenUsage: [Object], finish_reason: 'tool_calls' },\n", + " id: 'chatcmpl-9llAzVKdHCJkcUCnwGx62bqesSJPB',\n", + " tool_calls: [ [Object], [Object] ],\n", + " invalid_tool_calls: [],\n", + " usage_metadata: { input_tokens: 87, output_tokens: 50, total_tokens: 137 }\n", + " },\n", + " ToolMessage {\n", + " lc_serializable: true,\n", + " lc_kwargs: {\n", + " content: '36',\n", + " artifact: undefined,\n", + " tool_call_id: 'call_7P5ZjvqWc7jrXjWDkhZ6MU4b',\n", + " name: 'multiply',\n", + " additional_kwargs: {},\n", + " response_metadata: {}\n", + " },\n", + " lc_namespace: [ 'langchain_core', 'messages' ],\n", + " content: '36',\n", + " name: 'multiply',\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_call_id: 'call_7P5ZjvqWc7jrXjWDkhZ6MU4b',\n", + " artifact: undefined\n", + " },\n", + " ToolMessage {\n", + " lc_serializable: true,\n", + " lc_kwargs: {\n", + " content: '60',\n", + " artifact: undefined,\n", + " tool_call_id: 'call_jbyowegkI0coHbnnHs7HLELC',\n", + " name: 'add',\n", + " additional_kwargs: {},\n", + " response_metadata: {}\n", + " },\n", + " lc_namespace: [ 'langchain_core', 'messages' ],\n", + " content: '60',\n", + " name: 'add',\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_call_id: 'call_jbyowegkI0coHbnnHs7HLELC',\n", + " artifact: undefined\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "import { HumanMessage } from \"@langchain/core/messages\";\n", + "\n", + "const messages = [\n", + " new HumanMessage(\"What is 3 * 12? Also, what is 11 + 49?\"),\n", + "];\n", + "\n", + "const aiMessage = await llmWithTools.invoke(messages);\n", + "\n", + "messages.push(aiMessage);\n", + "\n", + "const toolsByName = {\n", + " add: addTool,\n", + " multiply: multiplyTool,\n", + "}\n", + "\n", + "for (const toolCall of aiMessage.tool_calls) {\n", + " const selectedTool = toolsByName[toolCall.name];\n", + " const toolMessage = await selectedTool.invoke(toolCall);\n", + " messages.push(toolMessage);\n", + "}\n", + "\n", + "console.log(messages);" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AIMessage {\n", + " lc_serializable: true,\n", + " lc_kwargs: {\n", + " content: '3 * 12 is 36, and 11 + 49 is 60.',\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " additional_kwargs: { function_call: undefined, tool_calls: undefined },\n", + " id: 'chatcmpl-9llB0VVQNdufqhJHHtY9yCPeQeKLZ',\n", + " response_metadata: {}\n", + " },\n", + " lc_namespace: [ 'langchain_core', 'messages' ],\n", + " content: '3 * 12 is 36, and 11 + 49 is 60.',\n", + " name: undefined,\n", + " additional_kwargs: { function_call: undefined, tool_calls: undefined },\n", + " response_metadata: {\n", + " tokenUsage: { completionTokens: 19, promptTokens: 153, totalTokens: 172 },\n", + " finish_reason: 'stop'\n", + " },\n", + " id: 'chatcmpl-9llB0VVQNdufqhJHHtY9yCPeQeKLZ',\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " usage_metadata: { input_tokens: 153, output_tokens: 19, total_tokens: 172 }\n", + "}\n" + ] + } + ], + "source": [ + "await llmWithTools.invoke(messages);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that we pass back the same `tool_call_id` in the `ToolMessage` as what we receive from the model in order to help the model match tool responses with tool calls.\n", + "\n", + "## Related\n", + "\n", + "You've now seen how to pass tool calls back to a model.\n", + "\n", + "These guides may interest you next:\n", + "\n", + "- [Creating custom tools](/docs/how_to/custom_tools)\n", + "- [Building agents with LangGraph](https://langchain-ai.github.io/langgraphjs/)" + ] + } + ], + "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": 4 +} From 5b67687dfaf218661a35a3881ab208e1861d5494 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Tue, 16 Jul 2024 17:21:10 -0700 Subject: [PATCH 02/15] docs[patch]: Update tool runtime docs (#6097) * Update tool runtime docs * Add tool configure docs * Update tool_configure.ipynb --- docs/core_docs/docs/how_to/index.mdx | 2 + .../docs/how_to/tool_configure.ipynb | 114 ++++++++++++++++ docs/core_docs/docs/how_to/tool_runtime.ipynb | 128 +++++++++--------- 3 files changed, 178 insertions(+), 66 deletions(-) create mode 100644 docs/core_docs/docs/how_to/tool_configure.ipynb diff --git a/docs/core_docs/docs/how_to/index.mdx b/docs/core_docs/docs/how_to/index.mdx index 88370fdf968fe..aec6c78c15cab 100644 --- a/docs/core_docs/docs/how_to/index.mdx +++ b/docs/core_docs/docs/how_to/index.mdx @@ -163,6 +163,8 @@ LangChain [Tools](/docs/concepts/#tools) contain a description of the tool (to p - [How to: use a chat model to call tools](/docs/how_to/tool_calling/) - [How to: pass tool results back to model](/docs/how_to/tool_results_pass_to_model/) - [How to: add ad-hoc tool calling capability to LLMs and Chat Models](/docs/how_to/tools_prompting) +- [How to: pass run time values to tools](/docs/how_to/tool_runtime) +- [How to: access the `RunnableConfig` object within a custom tool](/docs/how_to/tool_configure) - [How to: return extra artifacts from a custom tool](/docs/how_to/tool_artifacts) ### Agents diff --git a/docs/core_docs/docs/how_to/tool_configure.ipynb b/docs/core_docs/docs/how_to/tool_configure.ipynb new file mode 100644 index 0000000000000..e2c5e2f9b7f8e --- /dev/null +++ b/docs/core_docs/docs/how_to/tool_configure.ipynb @@ -0,0 +1,114 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to access the RunnableConfig object within a custom tool\n", + "\n", + "```{=mdx}\n", + ":::info Prerequisites\n", + "\n", + "This guide assumes familiarity with the following concepts:\n", + "\n", + "- [LangChain Tools](/docs/concepts/#tools)\n", + "- [Custom tools](/docs/how_to/custom_tools)\n", + "- [LangChain Expression Language (LCEL)](/docs/concepts/#langchain-expression-language)\n", + "\n", + ":::\n", + "```\n", + "\n", + "Tools are runnables, and you can treat them the same way as any other runnable at the interface level - you can call `invoke()`, `batch()`, and `stream()` on them as normal. However, when writing custom tools, you may want to invoke other runnables like chat models or retrievers. In order to properly trace and configure those sub-invocations, you'll need to manually access and pass in the tool's current [`RunnableConfig`](https://api.js.langchain.com/interfaces/langchain_core_runnables.RunnableConfig.html) object.\n", + "\n", + "This guide covers how to do this for custom tools created in different ways.\n", + "\n", + "## From the `tool` method\n", + "\n", + "Accessing the `RunnableConfig` object for a custom tool created with the [`tool`](https://api.js.langchain.com/functions/langchain_core_tools.tool-1.html) helper method is simple - it's always the second parameter passed into your custom function. Here's an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "import type { RunnableConfig } from \"@langchain/core/runnables\";\n", + "\n", + "const reverseTool = tool(\n", + " async (input: { text: string }, config?: RunnableConfig) => {\n", + " const originalString = input.text + (config?.configurable?.additional_field ?? \"\");\n", + " return originalString.split(\"\").reverse().join(\"\");\n", + " }, {\n", + " name: \"reverse\",\n", + " description: \"A test tool that combines input text with a configurable parameter.\",\n", + " schema: z.object({\n", + " text: z.string()\n", + " }),\n", + " }\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then, if we invoke the tool with a `config` containing a `configurable` field, we can see that `additional_field` is passed through correctly:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "321cba\n" + ] + } + ], + "source": [ + "await reverseTool.invoke(\n", + " {text: \"abc\"}, {configurable: {additional_field: \"123\"}}\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "You've now seen how to configure and stream events from within a tool. Next, check out the following guides for more on using tools:\n", + "\n", + "- Pass [tool results back to a model](/docs/how_to/tool_results_pass_to_model)\n", + "- Building [tool-using chains and agents](/docs/how_to#tools)\n", + "- Getting [structured outputs](/docs/how_to/structured_output/) from models" + ] + } + ], + "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/how_to/tool_runtime.ipynb b/docs/core_docs/docs/how_to/tool_runtime.ipynb index 588263a1885de..1d2ab289ba4c1 100644 --- a/docs/core_docs/docs/how_to/tool_runtime.ipynb +++ b/docs/core_docs/docs/how_to/tool_runtime.ipynb @@ -6,6 +6,7 @@ "source": [ "# How to pass run time values to a tool\n", "\n", + "```{=mdx}\n", ":::info Prerequisites\n", "\n", "This guide assumes familiarity with the following concepts:\n", @@ -21,6 +22,7 @@ "You can find a [list of all models that support tool calling](/docs/integrations/chat/).\n", "\n", ":::\n", + "```\n", "\n", "You may need to bind values to a tool that are only known at runtime. For example, the tool logic may require using the ID of the user who made the request.\n", "\n", @@ -28,7 +30,7 @@ "\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 simple design pattern that creates the tool dynamically at run time and binds to them appropriate values." + "This how-to guide shows a design pattern that creates the tool dynamically at run time and binds to them appropriate values." ] }, { @@ -58,58 +60,47 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import { z } from \"zod\";\n", - "import { StructuredTool } from \"@langchain/core/tools\";\n", + "import { tool } from \"@langchain/core/tools\";\n", "\n", "const userToPets: Record = {};\n", "\n", - "function generateToolsForUser(userId: string): StructuredTool[] {\n", - " class UpdateFavoritePets extends StructuredTool {\n", - " name = \"update_favorite_pets\";\n", - " description = \"Add the list of favorite pets.\";\n", - " schema = z.object({\n", - " pets: z.array(z.string())\n", - " })\n", - "\n", - " async _call(input: { pets: string[] }): Promise {\n", - " userToPets[userId] = input.pets;\n", - " return \"update_favorite_pets called.\"\n", - " }\n", - " }\n", - "\n", - " class DeleteFavoritePets extends StructuredTool {\n", - " name = \"delete_favorite_pets\";\n", - " description = \"Delete the list of favorite pets.\";\n", - "\n", - " schema = z.object({\n", - " no_op: z.boolean().optional().describe(\"No operation.\")\n", - " })\n", - "\n", - " async _call(input: never): Promise {\n", - " if (userId in userToPets) {\n", - " delete userToPets[userId];\n", - " }\n", - " return \"delete_favorite_pets called.\"\n", - " }\n", - " }\n", - "\n", - " class ListFavoritePets extends StructuredTool {\n", - " name = \"list_favorite_pets\";\n", - " description = \"List favorite pets if any.\";\n", - " schema = z.object({\n", - " no_op: z.boolean().optional().describe(\"No operation.\")\n", - " })\n", - "\n", - " async _call(input: never): Promise {\n", - " return JSON.stringify(userToPets[userId]) || JSON.stringify([]);\n", + "function generateToolsForUser(userId: string) {\n", + " const updateFavoritePets = tool(async (input) => {\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", + " });\n", + "\n", + " const deleteFavoritePets = tool(async () => {\n", + " if (userId in userToPets) {\n", + " delete userToPets[userId];\n", " }\n", - " }\n", - "\n", - " return [new UpdateFavoritePets(), new DeleteFavoritePets(), new ListFavoritePets()];\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", "}" ] }, @@ -122,7 +113,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -135,40 +126,42 @@ } ], "source": [ - "const [updatePets, deletePets, listPets] = generateToolsForUser(\"brace\")\n", - "await updatePets.invoke({ pets: [\"cat\", \"dog\"] })\n", - "console.log(userToPets)\n", - "console.log(await listPets.invoke({}))" + "const [updatePets, deletePets, listPets] = generateToolsForUser(\"brace\");\n", + "\n", + "await updatePets.invoke({ pets: [\"cat\", \"dog\"] });\n", + "\n", + "console.log(userToPets);\n", + "console.log(await listPets.invoke({}));" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import { BaseChatModel } from \"@langchain/core/language_models/chat_models\";\n", "\n", "async function handleRunTimeRequest(userId: string, query: string, llm: BaseChatModel): Promise {\n", - " if (!llm.bindTools) {\n", - " throw new Error(\"Language model does not support tools.\");\n", - " }\n", - " const tools = generateToolsForUser(userId);\n", - " const llmWithTools = llm.bindTools(tools);\n", - " return llmWithTools.invoke(query);\n", - " }" + " if (!llm.bindTools) {\n", + " throw new Error(\"Language model does not support tools.\");\n", + " }\n", + " const tools = generateToolsForUser(userId);\n", + " const llmWithTools = llm.bindTools(tools);\n", + " return llmWithTools.invoke(query);\n", + "}" ] }, { "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!" + "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": 12, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -178,31 +171,34 @@ "{\n", " name: 'update_favorite_pets',\n", " args: { pets: [ 'cats', 'parrots' ] },\n", - " id: 'call_to1cbIVqMNuahHCdFO9oQzpN'\n", + " type: 'tool_call',\n", + " id: 'call_97h0nQ3B3cr0m58HOwq9ZyUz'\n", "}\n" ] } ], "source": [ "const aiMessage = await handleRunTimeRequest(\n", - " \"brace\", \"my favorite animals are cats and parrots.\", llm,\n", - ")\n", - "console.log(aiMessage.tool_calls[0])" + " \"brace\", \"my favorite animals are cats and parrots.\", llm,\n", + ");\n", + "console.log(aiMessage.tool_calls[0]);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "```{=mdx}\n", ":::tip\n", "Click [here](https://smith.langchain.com/public/3d766ecc-8f28-400b-8636-632e6f1598c7/r) to see the LangSmith trace for the above run.\n", ":::\n", "\n", ":::tip\n", - "Chat models only output requests to invoke tools, they don't actually invoke the underlying tools.\n", + "Chat models only output requests to invoke tools. They don't actually invoke the underlying tools.\n", "\n", "To see how to invoke the tools, please refer to [how to use a model to call tools](/docs/how_to/tool_calling/).\n", - ":::" + ":::\n", + "```" ] } ], From dbb3b59623ee5ce4bf675bbf41a6e3e044ae0802 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Tue, 16 Jul 2024 19:21:35 -0700 Subject: [PATCH 03/15] docs[patch]: Adds tool stream events and error handling guides (#6098) * Adds tool stream events and error handling guides * Add tool streaming doc --- docs/core_docs/docs/how_to/index.mdx | 4 + .../docs/how_to/tool_stream_events.ipynb | 682 ++++++++++++++++++ .../docs/how_to/tool_streaming.ipynb | 451 ++++++++++++ docs/core_docs/docs/how_to/tools_error.ipynb | 244 +++++++ 4 files changed, 1381 insertions(+) create mode 100644 docs/core_docs/docs/how_to/tool_stream_events.ipynb create mode 100644 docs/core_docs/docs/how_to/tool_streaming.ipynb create mode 100644 docs/core_docs/docs/how_to/tools_error.ipynb diff --git a/docs/core_docs/docs/how_to/index.mdx b/docs/core_docs/docs/how_to/index.mdx index aec6c78c15cab..802aa03435dfd 100644 --- a/docs/core_docs/docs/how_to/index.mdx +++ b/docs/core_docs/docs/how_to/index.mdx @@ -73,6 +73,8 @@ These are the core building blocks you can use when building applications. - [How to: get log probabilities](/docs/how_to/logprobs) - [How to: stream a response back](/docs/how_to/chat_streaming) - [How to: track token usage](/docs/how_to/chat_token_usage_tracking) +- [How to: stream tool calls](/docs/how_to/tool_streaming) +- [How to: few shot prompt tool behavior](/docs/how_to/tool_calling#few-shotting-with-tools) ### Messages @@ -164,7 +166,9 @@ LangChain [Tools](/docs/concepts/#tools) contain a description of the tool (to p - [How to: pass tool results back to model](/docs/how_to/tool_results_pass_to_model/) - [How to: add ad-hoc tool calling capability to LLMs and Chat Models](/docs/how_to/tools_prompting) - [How to: pass run time values to tools](/docs/how_to/tool_runtime) +- [How to: handle errors when calling tools](/docs/how_to/tools_error) - [How to: access the `RunnableConfig` object within a custom tool](/docs/how_to/tool_configure) +- [How to: stream events from child runs within a custom tool](/docs/how_to/tool_stream_events) - [How to: return extra artifacts from a custom tool](/docs/how_to/tool_artifacts) ### Agents diff --git a/docs/core_docs/docs/how_to/tool_stream_events.ipynb b/docs/core_docs/docs/how_to/tool_stream_events.ipynb new file mode 100644 index 0000000000000..34ab592821766 --- /dev/null +++ b/docs/core_docs/docs/how_to/tool_stream_events.ipynb @@ -0,0 +1,682 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to stream events from child runs within a custom tool\n", + "\n", + "```{=mdx}\n", + ":::info Prerequisites\n", + "\n", + "This guide assumes familiarity with the following concepts:\n", + "- [LangChain Tools](/docs/concepts/#tools)\n", + "- [Custom tools](/docs/how_to/custom_tools)\n", + "- [Using stream events](/docs/how_to/streaming/#using-stream-events)\n", + "- [Accessing RunnableConfig within a custom tool](/docs/how_to/tool_configure/)\n", + "\n", + ":::\n", + "```\n", + "\n", + "If you have tools that call chat models, retrievers, or other runnables, you may want to access internal events from those runnables or configure them with additional properties. This guide shows you how to manually pass parameters properly so that you can do this using the [`.streamEvents()`](/docs/how_to/streaming/#using-stream-events) method.\n", + "\n", + "```{=mdx}\n", + ":::caution Compatibility\n", + "\n", + "In order to support a wider variety of JavaScript environments, the base LangChain package does not automatically propagate configuration to child runnables by default. This includes callbacks necessary for `.streamEvents()`. This is a common reason why you may fail to see events being emitted from custom runnables or tools.\n", + "\n", + "You will need to manually propagate the `RunnableConfig` object to the child runnable. For an example of how to manually propagate the config, see the implementation of the `bar` RunnableLambda below.\n", + "\n", + "This guide also requires `@langchain/core>=0.2.16`.\n", + ":::\n", + "```\n", + "\n", + "Say you have a custom tool that calls a chain that condenses its input by prompting a chat model to return only 10 words, then reversing the output. First, define it in a naive way:\n", + "\n", + "```{=mdx}\n", + "import ChatModelTabs from \"@theme/ChatModelTabs\";\n", + "\n", + "\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import { ChatAnthropic } from \"@langchain/anthropic\";\n", + "const model = new ChatAnthropic({\n", + " model: \"claude-3-5-sonnet-20240620\",\n", + " temperature: 0,\n", + "});" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "import { ChatPromptTemplate } from \"@langchain/core/prompts\";\n", + "import { StringOutputParser } from \"@langchain/core/output_parsers\";\n", + "\n", + "const specialSummarizationTool = tool(async (input) => {\n", + " const prompt = ChatPromptTemplate.fromTemplate(\n", + " \"You are an expert writer. Summarize the following text in 10 words or less:\\n\\n{long_text}\"\n", + " );\n", + " const reverse = (x: string) => {\n", + " return x.split(\"\").reverse().join(\"\");\n", + " };\n", + " const chain = prompt\n", + " .pipe(model)\n", + " .pipe(new StringOutputParser())\n", + " .pipe(reverse);\n", + " const summary = await chain.invoke({ long_text: input.long_text });\n", + " return summary;\n", + "}, {\n", + " name: \"special_summarization_tool\",\n", + " description: \"A tool that summarizes input text using advanced techniques.\",\n", + " schema: z.object({\n", + " long_text: z.string(),\n", + " }),\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Invoking the tool directly works just fine:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ".yad noitaudarg rof tiftuo sesoohc yrraB ;scisyhp seifed eeB\n" + ] + } + ], + "source": [ + "const LONG_TEXT = `\n", + "NARRATOR:\n", + "(Black screen with text; The sound of buzzing bees can be heard)\n", + "According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground. The bee, of course, flies anyway because bees don't care what humans think is impossible.\n", + "BARRY BENSON:\n", + "(Barry is picking out a shirt)\n", + "Yellow, black. Yellow, black. Yellow, black. Yellow, black. Ooh, black and yellow! Let's shake it up a little.\n", + "JANET BENSON:\n", + "Barry! Breakfast is ready!\n", + "BARRY:\n", + "Coming! Hang on a second.`;\n", + "\n", + "await specialSummarizationTool.invoke({ long_text: LONG_TEXT });" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "But if you wanted to access the raw output from the chat model rather than the full tool, you might try to use the [`.streamEvents()`](/docs/how_to/streaming/#using-stream-events) method and look for an `on_chat_model_end` event. Here's what happens:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "const stream = await specialSummarizationTool.streamEvents(\n", + " { long_text: LONG_TEXT },\n", + " { version: \"v2\" },\n", + ");\n", + "\n", + "for await (const event of stream) {\n", + " if (event.event === \"on_chat_model_end\") {\n", + " // Never triggers!\n", + " console.log(event);\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You'll notice that there are no chat model events emitted from the child run!\n", + "\n", + "This is because the example above does not pass the tool's config object into the internal chain. To fix this, redefine your tool to take a special parameter typed as `RunnableConfig` (see [this guide](/docs/how_to/tool_configure) for more details). You'll also need to pass that parameter through into the internal chain when executing it:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "const specialSummarizationToolWithConfig = tool(async (input, config) => {\n", + " const prompt = ChatPromptTemplate.fromTemplate(\n", + " \"You are an expert writer. Summarize the following text in 10 words or less:\\n\\n{long_text}\"\n", + " );\n", + " const reverse = (x: string) => {\n", + " return x.split(\"\").reverse().join(\"\");\n", + " };\n", + " const chain = prompt\n", + " .pipe(model)\n", + " .pipe(new StringOutputParser())\n", + " .pipe(reverse);\n", + " // Pass the \"config\" object as an argument to any executed runnables\n", + " const summary = await chain.invoke({ long_text: input.long_text }, config);\n", + " return summary;\n", + "}, {\n", + " name: \"special_summarization_tool\",\n", + " description: \"A tool that summarizes input text using advanced techniques.\",\n", + " schema: z.object({\n", + " long_text: z.string(),\n", + " }),\n", + "});" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now try the same `.streamEvents()` call as before with your new tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " event: 'on_chat_model_end',\n", + " data: {\n", + " output: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: 'Bee defies physics; Barry chooses outfit for graduation day.',\n", + " name: undefined,\n", + " additional_kwargs: [Object],\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: [Object]\n", + " },\n", + " input: { messages: [Array] }\n", + " },\n", + " run_id: '27ac7b2e-591c-4adc-89ec-64d96e233ec8',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const stream = await specialSummarizationToolWithConfig.streamEvents(\n", + " { long_text: LONG_TEXT },\n", + " { version: \"v2\" },\n", + ");\n", + "\n", + "for await (const event of stream) {\n", + " if (event.event === \"on_chat_model_end\") {\n", + " // Never triggers!\n", + " console.log(event);\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Awesome! This time there's an event emitted.\n", + "\n", + "For streaming, `.streamEvents()` automatically calls internal runnables in a chain with streaming enabled if possible, so if you wanted to a stream of tokens as they are generated from the chat model, you could simply filter to look for `on_chat_model_stream` events with no other changes:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: 'Bee',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: ' def',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: 'ies physics',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: ';',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: ' Barry',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: ' cho',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: 'oses outfit',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: ' for',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: ' graduation',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: ' day',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n", + "{\n", + " event: 'on_chat_model_stream',\n", + " data: {\n", + " chunk: AIMessageChunk {\n", + " lc_serializable: true,\n", + " lc_kwargs: [Object],\n", + " lc_namespace: [Array],\n", + " content: '.',\n", + " name: undefined,\n", + " additional_kwargs: {},\n", + " response_metadata: {},\n", + " id: undefined,\n", + " tool_calls: [],\n", + " invalid_tool_calls: [],\n", + " tool_call_chunks: [],\n", + " usage_metadata: undefined\n", + " }\n", + " },\n", + " run_id: '938c0469-83c6-4dbd-862e-cd73381165de',\n", + " name: 'ChatAnthropic',\n", + " tags: [ 'seq:step:2' ],\n", + " metadata: {\n", + " ls_provider: 'anthropic',\n", + " ls_model_name: 'claude-3-5-sonnet-20240620',\n", + " ls_model_type: 'chat',\n", + " ls_temperature: 0,\n", + " ls_max_tokens: 2048,\n", + " ls_stop: undefined\n", + " }\n", + "}\n" + ] + } + ], + "source": [ + "const stream = await specialSummarizationToolWithConfig.streamEvents(\n", + " { long_text: LONG_TEXT },\n", + " { version: \"v2\" },\n", + ");\n", + "\n", + "for await (const event of stream) {\n", + " if (event.event === \"on_chat_model_stream\") {\n", + " // Never triggers!\n", + " console.log(event);\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Automatically passing config (Advanced)\n", + "\n", + "If you've used [LangGraph](https://langchain-ai.github.io/langgraphjs/), you may have noticed that you don't need to pass config in nested calls. This is because LangGraph takes advantage of an API called [`async_hooks`](https://nodejs.org/api/async_hooks.html), which is not supported in many, but not all environments.\n", + "\n", + "If you wish, you can enable automatic configuration passing by running the following code to import and enable `AsyncLocalStorage` globally:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import { AsyncLocalStorageProviderSingleton } from \"@langchain/core/singletons\";\n", + "import { AsyncLocalStorage } from \"async_hooks\";\n", + "\n", + "AsyncLocalStorageProviderSingleton.initializeGlobalInstance(\n", + " new AsyncLocalStorage()\n", + ");" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "You've now seen how to stream events from within a tool. Next, check out the following guides for more on using tools:\n", + "\n", + "- Pass [runtime values to tools](/docs/how_to/tool_runtime)\n", + "- Pass [tool results back to a model](/docs/how_to/tool_results_pass_to_model)\n", + "- [Dispatch custom callback events](/docs/how_to/callbacks_custom_events)\n", + "\n", + "You can also check out some more specific uses of tool calling:\n", + "\n", + "- Building [tool-using chains and agents](/docs/how_to#tools)\n", + "- Getting [structured outputs](/docs/how_to/structured_output/) from models" + ] + } + ], + "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": 4 +} diff --git a/docs/core_docs/docs/how_to/tool_streaming.ipynb b/docs/core_docs/docs/how_to/tool_streaming.ipynb new file mode 100644 index 0000000000000..141fb06748591 --- /dev/null +++ b/docs/core_docs/docs/how_to/tool_streaming.ipynb @@ -0,0 +1,451 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to stream tool calls\n", + "\n", + "When tools are called in a streaming context, \n", + "[message chunks](https://api.js.langchain.com/classes/langchain_core_messages.AIMessageChunk.html) \n", + "will be populated with [tool call chunk](https://api.js.langchain.com/types/langchain_core_messages_tool.ToolCallChunk.html) \n", + "objects in a list via the `.tool_call_chunks` attribute. A `ToolCallChunk` includes \n", + "optional string fields for the tool `name`, `args`, and `id`, and includes an optional \n", + "integer field `index` that can be used to join chunks together. Fields are optional \n", + "because portions of a tool call may be streamed across different chunks (e.g., a chunk \n", + "that includes a substring of the arguments may have null values for the tool name and id).\n", + "\n", + "Because message chunks inherit from their parent message class, an \n", + "[`AIMessageChunk`](https://api.js.langchain.com/classes/langchain_core_messages.AIMessageChunk.html) \n", + "with tool call chunks will also include `.tool_calls` and `.invalid_tool_calls` fields. \n", + "These fields are parsed best-effort from the message's tool call chunks.\n", + "\n", + "Note that not all providers currently support streaming for tool calls. Before we start let's define our tools and our model." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import { z } from \"zod\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "\n", + "const addTool = tool(async (input) => {\n", + " return input.a + input.b;\n", + "}, {\n", + " name: \"add\",\n", + " description: \"Adds a and b.\",\n", + " schema: z.object({\n", + " a: z.number(),\n", + " b: z.number(),\n", + " }),\n", + "});\n", + "\n", + "const multiplyTool = tool(async (input) => {\n", + " return input.a * input.b;\n", + "}, {\n", + " name: \"multiply\",\n", + " description: \"Multiplies a and b.\",\n", + " schema: z.object({\n", + " a: z.number(),\n", + " b: z.number(),\n", + " }),\n", + "});\n", + "\n", + "const tools = [addTool, multiplyTool];\n", + "\n", + "const model = new ChatOpenAI({\n", + " model: \"gpt-4o\",\n", + " temperature: 0,\n", + "});\n", + "\n", + "const modelWithTools = model.bindTools(tools);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's define our query and stream our output:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '',\n", + " id: 'call_MdIlJL5CAYD7iz9gTm5lwWtJ',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: '{\"a\"',\n", + " id: undefined,\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: ': 3, ',\n", + " id: undefined,\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: '\"b\": 1',\n", + " id: undefined,\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: '2}',\n", + " id: undefined,\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'add',\n", + " args: '',\n", + " id: 'call_ihL9W6ylSRlYigrohe9SClmW',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: '{\"a\"',\n", + " id: undefined,\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: ': 11,',\n", + " id: undefined,\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: ' \"b\": ',\n", + " id: undefined,\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: undefined,\n", + " args: '49}',\n", + " id: undefined,\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[]\n", + "[]\n" + ] + } + ], + "source": [ + "const query = \"What is 3 * 12? Also, what is 11 + 49?\";\n", + "\n", + "const stream = await modelWithTools.stream(query);\n", + "\n", + "for await (const chunk of stream) {\n", + " console.log(chunk.tool_call_chunks);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that adding message chunks will merge their corresponding tool call chunks. This is the principle by which LangChain's various [tool output parsers](/docs/how_to/output_parser_structured) support streaming.\n", + "\n", + "For example, below we accumulate tool call chunks:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\"',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, ',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 1',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " },\n", + " {\n", + " name: 'add',\n", + " args: '',\n", + " id: 'call_ufY7lDSeCQwWbdq1XQQ2PBHR',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " },\n", + " {\n", + " name: 'add',\n", + " args: '{\"a\"',\n", + " id: 'call_ufY7lDSeCQwWbdq1XQQ2PBHR',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " },\n", + " {\n", + " name: 'add',\n", + " args: '{\"a\": 11,',\n", + " id: 'call_ufY7lDSeCQwWbdq1XQQ2PBHR',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " },\n", + " {\n", + " name: 'add',\n", + " args: '{\"a\": 11, \"b\": ',\n", + " id: 'call_ufY7lDSeCQwWbdq1XQQ2PBHR',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " },\n", + " {\n", + " name: 'add',\n", + " args: '{\"a\": 11, \"b\": 49}',\n", + " id: 'call_ufY7lDSeCQwWbdq1XQQ2PBHR',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " },\n", + " {\n", + " name: 'add',\n", + " args: '{\"a\": 11, \"b\": 49}',\n", + " id: 'call_ufY7lDSeCQwWbdq1XQQ2PBHR',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n", + "[\n", + " {\n", + " name: 'multiply',\n", + " args: '{\"a\": 3, \"b\": 12}',\n", + " id: 'call_0zGpgVz81Ew0HA4oKblG0s0a',\n", + " index: 0,\n", + " type: 'tool_call_chunk'\n", + " },\n", + " {\n", + " name: 'add',\n", + " args: '{\"a\": 11, \"b\": 49}',\n", + " id: 'call_ufY7lDSeCQwWbdq1XQQ2PBHR',\n", + " index: 1,\n", + " type: 'tool_call_chunk'\n", + " }\n", + "]\n" + ] + } + ], + "source": [ + "import { concat } from \"@langchain/core/utils/stream\";\n", + "\n", + "const stream = await modelWithTools.stream(query);\n", + "\n", + "let gathered = undefined;\n", + "\n", + "for await (const chunk of stream) {\n", + " gathered = gathered !== undefined ? concat(gathered, chunk) : chunk;\n", + " console.log(gathered.tool_call_chunks);\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At the end, we can see the final aggregated tool call chunks include the fully gathered raw string value:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "string\n" + ] + } + ], + "source": [ + "console.log(typeof gathered.tool_call_chunks[0].args);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And we can also see the fully parsed tool call as an object at the end:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "object\n" + ] + } + ], + "source": [ + "console.log(typeof gathered.tool_calls[0].args);" + ] + } + ], + "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/how_to/tools_error.ipynb b/docs/core_docs/docs/how_to/tools_error.ipynb new file mode 100644 index 0000000000000..fdf2825dd974e --- /dev/null +++ b/docs/core_docs/docs/how_to/tools_error.ipynb @@ -0,0 +1,244 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5d60cbb9-2a6a-43ea-a9e9-f67b16ddd2b2", + "metadata": {}, + "source": [ + "# How to handle tool errors\n", + "\n", + "```{=mdx}\n", + ":::info Prerequisites\n", + "\n", + "This guide assumes familiarity with the following concepts:\n", + "- [Chat models](/docs/concepts/#chat-models)\n", + "- [LangChain Tools](/docs/concepts/#tools)\n", + "- [How to use a model to call tools](/docs/how_to/tool_calling)\n", + "\n", + ":::\n", + "```\n", + "\n", + "Calling tools with an LLM isn't perfect. The model may try to call a tool that doesn't exist or fail to return arguments that match the requested schema. Strategies like keeping schemas simple, reducing the number of tools you pass at once, and having good names and descriptions can help mitigate this risk, but aren't foolproof.\n", + "\n", + "This guide covers some ways to build error handling into your chains to mitigate these failure modes." + ] + }, + { + "cell_type": "markdown", + "id": "0a50f93a-5d6f-4691-8f98-27239a1c2f95", + "metadata": {}, + "source": [ + "## Chain\n", + "\n", + "Suppose we have the following (dummy) tool and tool-calling chain. We'll make our tool intentionally convoluted to try and trip up the model." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1d20604e-c4d1-4d21-841b-23e4f61aec36", + "metadata": {}, + "outputs": [], + "source": [ + "import { z } from \"zod\";\n", + "import { ChatOpenAI } from \"@langchain/openai\";\n", + "import { tool } from \"@langchain/core/tools\";\n", + "\n", + "const llm = new ChatOpenAI({\n", + " model: \"gpt-3.5-turbo-0125\",\n", + " temperature: 0,\n", + "});\n", + "\n", + "const complexTool = tool(async (params) => {\n", + " return params.int_arg * params.float_arg;\n", + "}, {\n", + " name: \"complex_tool\",\n", + " description: \"Do something complex with a complex tool.\",\n", + " schema: z.object({\n", + " int_arg: z.number(),\n", + " float_arg: z.number(),\n", + " number_arg: z.object({}),\n", + " })\n", + "});\n", + "\n", + "const llmWithTools = llm.bindTools([complexTool]);\n", + "\n", + "const chain = llmWithTools\n", + " .pipe((message) => message.tool_calls?.[0].args)\n", + " .pipe(complexTool);" + ] + }, + { + "cell_type": "markdown", + "id": "c34f005e-63f0-4841-9461-ca36c36607fc", + "metadata": {}, + "source": [ + "We can see that when we try to invoke this chain the model fails to correctly call the tool:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d354664c-ac44-4967-a35f-8912b3ad9477", + "metadata": {}, + "outputs": [ + { + "ename": "Error", + "evalue": "Received tool input did not match expected schema", + "output_type": "error", + "traceback": [ + "Stack trace:", + "Error: Received tool input did not match expected schema", + " at DynamicStructuredTool.call (file:///Users/jacoblee/Library/Caches/deno/npm/registry.npmjs.org/@langchain/core/0.2.16/dist/tools/index.js:100:19)", + " at eventLoopTick (ext:core/01_core.js:63:7)", + " at async RunnableSequence.invoke (file:///Users/jacoblee/Library/Caches/deno/npm/registry.npmjs.org/@langchain/core/0.2.16_1/dist/runnables/base.js:1139:27)", + " at async :1:22" + ] + } + ], + "source": [ + "await chain.invoke(\n", + " \"use complex tool. the args are 5, 2.1, potato\"\n", + ");" + ] + }, + { + "cell_type": "markdown", + "id": "890d989d-2d39-4571-9a55-d3496b9b5d27", + "metadata": {}, + "source": [ + "## Try/except tool call\n", + "\n", + "The simplest way to more gracefully handle errors is to try/except the tool-calling step and return a helpful message on errors:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8fedb550-683d-45ae-8876-ae7acb332019", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Calling tool with arguments:\n", + "\n", + "{\"int_arg\":5,\"float_arg\":2.1,\"number_arg\":\"potato\"}\n", + "\n", + "raised the following error:\n", + "\n", + "Error: Received tool input did not match expected schema\n" + ] + } + ], + "source": [ + "const tryExceptToolWrapper = async (input, config) => {\n", + " try {\n", + " const result = await complexTool.invoke(input);\n", + " return result;\n", + " } catch (e) {\n", + " return `Calling tool with arguments:\\n\\n${JSON.stringify(input)}\\n\\nraised the following error:\\n\\n${e}`\n", + " }\n", + "}\n", + "\n", + "const chain = llmWithTools\n", + " .pipe((message) => message.tool_calls?.[0].args)\n", + " .pipe(tryExceptToolWrapper);\n", + "\n", + "const res = await chain.invoke(\"use complex tool. the args are 5, 2.1, potato\");\n", + "\n", + "console.log(res);" + ] + }, + { + "cell_type": "markdown", + "id": "3b2f6393-cb47-49d0-921c-09550a049fe4", + "metadata": {}, + "source": [ + "## Fallbacks\n", + "\n", + "We can also try to fallback to a better model in the event of a tool invocation error. In this case we'll fall back to an identical chain that uses `gpt-4-1106-preview` instead of `gpt-3.5-turbo`." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "02cc4223-35fa-4240-976a-012299ca703c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[33m10.5\u001b[39m" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "const chain = llmWithTools\n", + " .pipe((message) => message.tool_calls?.[0].args)\n", + " .pipe(complexTool);\n", + "\n", + "const betterModel = new ChatOpenAI({\n", + " model: \"gpt-4-1106-preview\",\n", + " temperature: 0,\n", + "}).bindTools([complexTool]);\n", + "\n", + "const betterChain = betterModel\n", + " .pipe((message) => message.tool_calls?.[0].args)\n", + " .pipe(complexTool);\n", + "\n", + "const chainWithFallback = chain.withFallbacks({ fallbacks: [betterChain] });\n", + "\n", + "await chainWithFallback.invoke(\"use complex tool. the args are 5, 2.1, potato\");" + ] + }, + { + "cell_type": "markdown", + "id": "412f8c4e-cc83-4d87-84a1-5ba2f8edb1e9", + "metadata": {}, + "source": [ + "Looking at the [LangSmith trace](https://smith.langchain.com/public/ea31e7ca-4abc-48e3-9943-700100c86622/r) for this chain run, we can see that the first chain call fails as expected and it's the fallback that succeeds." + ] + }, + { + "cell_type": "markdown", + "id": "6b97af9f", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "Now you've seen some strategies how to handle tool calling errors. Next, you can learn more about how to use tools:\n", + "\n", + "- Few shot prompting [with tools](/docs/how_to/tool_calling#few-shotting-with-tools)\n", + "- Stream [tool calls](/docs/how_to/tool_streaming/)\n", + "- Pass [runtime values to tools](/docs/how_to/tool_runtime)\n", + "\n", + "You can also check out some more specific uses of tool calling:\n", + "\n", + "- Getting [structured outputs](/docs/how_to/structured_output/) from models" + ] + } + ], + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 56334842fb6e4077ded08b3be2903fe67a83a403 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Wed, 17 Jul 2024 08:54:25 -0700 Subject: [PATCH 04/15] openai-experimental[patch]: Adds experimental raw response field to OpenAI chat models (#6087) * Adds experimental raw response field to OpenAI chat models * Fix lint * Refactor --- libs/langchain-openai/src/chat_models.ts | 42 +++++++++++++------ .../src/tests/azure/chat_models.int.test.ts | 18 ++++++++ .../azure/chat_models.standard.int.test.ts | 18 ++++++++ .../src/tests/azure/embeddings.int.test.ts | 19 +++++++++ .../src/tests/azure/llms.int.test.ts | 18 ++++++++ .../tests/chat_models-extended.int.test.ts | 26 ++++++++++++ libs/langchain-openai/src/types.ts | 8 +++- 7 files changed, 136 insertions(+), 13 deletions(-) diff --git a/libs/langchain-openai/src/chat_models.ts b/libs/langchain-openai/src/chat_models.ts index c3f1f03ffc1b9..31712178e896b 100644 --- a/libs/langchain-openai/src/chat_models.ts +++ b/libs/langchain-openai/src/chat_models.ts @@ -129,7 +129,8 @@ export function messageToOpenAIRole(message: BaseMessage): OpenAIRoleEnum { function openAIResponseToChatMessage( message: OpenAIClient.Chat.Completions.ChatCompletionMessage, - messageId: string + rawResponse: OpenAIClient.Chat.Completions.ChatCompletion, + includeRawResponse?: boolean ): BaseMessage { const rawToolCalls: OpenAIToolCall[] | undefined = message.tool_calls as | OpenAIToolCall[] @@ -146,15 +147,19 @@ function openAIResponseToChatMessage( invalidToolCalls.push(makeInvalidToolCall(rawToolCall, e.message)); } } + const additional_kwargs: Record = { + function_call: message.function_call, + tool_calls: rawToolCalls, + }; + if (includeRawResponse !== undefined) { + additional_kwargs.__raw_response = rawResponse; + } return new AIMessage({ content: message.content || "", tool_calls: toolCalls, invalid_tool_calls: invalidToolCalls, - additional_kwargs: { - function_call: message.function_call, - tool_calls: rawToolCalls, - }, - id: messageId, + additional_kwargs, + id: rawResponse.id, }); } default: @@ -165,12 +170,13 @@ function openAIResponseToChatMessage( function _convertDeltaToMessageChunk( // eslint-disable-next-line @typescript-eslint/no-explicit-any delta: Record, - messageId: string, - defaultRole?: OpenAIRoleEnum + rawResponse: OpenAIClient.Chat.Completions.ChatCompletionChunk, + defaultRole?: OpenAIRoleEnum, + includeRawResponse?: boolean ) { const role = delta.role ?? defaultRole; const content = delta.content ?? ""; - let additional_kwargs; + let additional_kwargs: Record; if (delta.function_call) { additional_kwargs = { function_call: delta.function_call, @@ -182,6 +188,9 @@ function _convertDeltaToMessageChunk( } else { additional_kwargs = {}; } + if (includeRawResponse) { + additional_kwargs.__raw_response = rawResponse; + } if (role === "user") { return new HumanMessageChunk({ content }); } else if (role === "assistant") { @@ -201,7 +210,7 @@ function _convertDeltaToMessageChunk( content, tool_call_chunks: toolCallChunks, additional_kwargs, - id: messageId, + id: rawResponse.id, }); } else if (role === "system") { return new SystemMessageChunk({ content }); @@ -415,6 +424,8 @@ export class ChatOpenAI< organization?: string; + __includeRawResponse?: boolean; + protected client: OpenAIClient; protected clientConfig: ClientOptions; @@ -485,6 +496,7 @@ export class ChatOpenAI< this.stop = fields?.stopSequences ?? fields?.stop; this.stopSequences = this?.stop; this.user = fields?.user; + this.__includeRawResponse = fields?.__includeRawResponse; if (this.azureOpenAIApiKey || this.azureADTokenProvider) { if (!this.azureOpenAIApiInstanceName && !this.azureOpenAIBasePath) { @@ -648,7 +660,12 @@ export class ChatOpenAI< if (!delta) { continue; } - const chunk = _convertDeltaToMessageChunk(delta, data.id, defaultRole); + const chunk = _convertDeltaToMessageChunk( + delta, + data, + defaultRole, + this.__includeRawResponse + ); defaultRole = delta.role ?? defaultRole; const newTokenIndices = { prompt: options.promptIndex ?? 0, @@ -797,7 +814,8 @@ export class ChatOpenAI< text, message: openAIResponseToChatMessage( part.message ?? { role: "assistant" }, - data.id + data, + this.__includeRawResponse ), }; generation.generationInfo = { diff --git a/libs/langchain-openai/src/tests/azure/chat_models.int.test.ts b/libs/langchain-openai/src/tests/azure/chat_models.int.test.ts index 65fcedce93bff..31de416cbfd4c 100644 --- a/libs/langchain-openai/src/tests/azure/chat_models.int.test.ts +++ b/libs/langchain-openai/src/tests/azure/chat_models.int.test.ts @@ -28,6 +28,24 @@ import { AzureChatOpenAI } from "../../azure/chat_models.js"; // Save the original value of the 'LANGCHAIN_CALLBACKS_BACKGROUND' environment variable const originalBackground = process.env.LANGCHAIN_CALLBACKS_BACKGROUND; +beforeAll(() => { + if (!process.env.AZURE_OPENAI_API_KEY) { + process.env.AZURE_OPENAI_API_KEY = process.env.TEST_AZURE_OPENAI_API_KEY; + } + if (!process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME) { + process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = + process.env.TEST_AZURE_OPENAI_API_DEPLOYMENT_NAME; + } + if (!process.env.AZURE_OPENAI_BASE_PATH) { + process.env.AZURE_OPENAI_BASE_PATH = + process.env.TEST_AZURE_OPENAI_BASE_PATH; + } + if (!process.env.AZURE_OPENAI_API_VERSION) { + process.env.AZURE_OPENAI_API_VERSION = + process.env.TEST_AZURE_OPENAI_API_VERSION; + } +}); + test("Test Azure ChatOpenAI call method", async () => { const chat = new AzureChatOpenAI({ modelName: "gpt-3.5-turbo", diff --git a/libs/langchain-openai/src/tests/azure/chat_models.standard.int.test.ts b/libs/langchain-openai/src/tests/azure/chat_models.standard.int.test.ts index cfec5cfe8eb88..5f003828e331c 100644 --- a/libs/langchain-openai/src/tests/azure/chat_models.standard.int.test.ts +++ b/libs/langchain-openai/src/tests/azure/chat_models.standard.int.test.ts @@ -5,6 +5,24 @@ import { AIMessageChunk } from "@langchain/core/messages"; import { AzureChatOpenAI } from "../../azure/chat_models.js"; import { ChatOpenAICallOptions } from "../../chat_models.js"; +beforeAll(() => { + if (!process.env.AZURE_OPENAI_API_KEY) { + process.env.AZURE_OPENAI_API_KEY = process.env.TEST_AZURE_OPENAI_API_KEY; + } + if (!process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME) { + process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = + process.env.TEST_AZURE_OPENAI_API_DEPLOYMENT_NAME; + } + if (!process.env.AZURE_OPENAI_BASE_PATH) { + process.env.AZURE_OPENAI_BASE_PATH = + process.env.TEST_AZURE_OPENAI_BASE_PATH; + } + if (!process.env.AZURE_OPENAI_API_VERSION) { + process.env.AZURE_OPENAI_API_VERSION = + process.env.TEST_AZURE_OPENAI_API_VERSION; + } +}); + class AzureChatOpenAIStandardIntegrationTests extends ChatModelIntegrationTests< ChatOpenAICallOptions, AIMessageChunk diff --git a/libs/langchain-openai/src/tests/azure/embeddings.int.test.ts b/libs/langchain-openai/src/tests/azure/embeddings.int.test.ts index 7362fe1a73e8d..634cca967d74e 100644 --- a/libs/langchain-openai/src/tests/azure/embeddings.int.test.ts +++ b/libs/langchain-openai/src/tests/azure/embeddings.int.test.ts @@ -1,6 +1,25 @@ +/* eslint-disable no-process-env */ import { test, expect } from "@jest/globals"; import { AzureOpenAIEmbeddings as OpenAIEmbeddings } from "../../azure/embeddings.js"; +beforeAll(() => { + if (!process.env.AZURE_OPENAI_API_KEY) { + process.env.AZURE_OPENAI_API_KEY = process.env.TEST_AZURE_OPENAI_API_KEY; + } + if (!process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME) { + process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = + process.env.TEST_AZURE_OPENAI_API_DEPLOYMENT_NAME; + } + if (!process.env.AZURE_OPENAI_BASE_PATH) { + process.env.AZURE_OPENAI_BASE_PATH = + process.env.TEST_AZURE_OPENAI_BASE_PATH; + } + if (!process.env.AZURE_OPENAI_API_VERSION) { + process.env.AZURE_OPENAI_API_VERSION = + process.env.TEST_AZURE_OPENAI_API_VERSION; + } +}); + test("Test AzureOpenAIEmbeddings.embedQuery", async () => { const embeddings = new OpenAIEmbeddings(); const res = await embeddings.embedQuery("Hello world"); diff --git a/libs/langchain-openai/src/tests/azure/llms.int.test.ts b/libs/langchain-openai/src/tests/azure/llms.int.test.ts index fa91c27e5dc4c..42f84c1634293 100644 --- a/libs/langchain-openai/src/tests/azure/llms.int.test.ts +++ b/libs/langchain-openai/src/tests/azure/llms.int.test.ts @@ -15,6 +15,24 @@ import { AzureOpenAI } from "../../azure/llms.js"; // Save the original value of the 'LANGCHAIN_CALLBACKS_BACKGROUND' environment variable const originalBackground = process.env.LANGCHAIN_CALLBACKS_BACKGROUND; +beforeAll(() => { + if (!process.env.AZURE_OPENAI_API_KEY) { + process.env.AZURE_OPENAI_API_KEY = process.env.TEST_AZURE_OPENAI_API_KEY; + } + if (!process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME) { + process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME = + process.env.TEST_AZURE_OPENAI_API_DEPLOYMENT_NAME; + } + if (!process.env.AZURE_OPENAI_BASE_PATH) { + process.env.AZURE_OPENAI_BASE_PATH = + process.env.TEST_AZURE_OPENAI_BASE_PATH; + } + if (!process.env.AZURE_OPENAI_API_VERSION) { + process.env.AZURE_OPENAI_API_VERSION = + process.env.TEST_AZURE_OPENAI_API_VERSION; + } +}); + test("Test Azure OpenAI invoke", async () => { const model = new AzureOpenAI({ maxTokens: 5, 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 3eca7ca0ff4fd..cec4151519228 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 @@ -291,3 +291,29 @@ test("Few shotting with tool calls", async () => { console.log(res); expect(res.content).toContain("24"); }); + +test("Test ChatOpenAI with raw response", async () => { + const chat = new ChatOpenAI({ + modelName: "gpt-3.5-turbo-1106", + maxTokens: 128, + __includeRawResponse: true, + }); + const message = new HumanMessage("Hello!"); + const res = await chat.invoke([message]); + expect(res.additional_kwargs.__raw_response).toBeDefined(); +}); + +test("Test ChatOpenAI with raw response", async () => { + const chat = new ChatOpenAI({ + modelName: "gpt-3.5-turbo-1106", + maxTokens: 128, + __includeRawResponse: true, + }); + const message = new HumanMessage("Hello!"); + const stream = await chat.stream([message]); + for await (const chunk of stream) { + expect( + chunk.additional_kwargs.__raw_response || chunk.usage_metadata + ).toBeDefined(); + } +}); diff --git a/libs/langchain-openai/src/types.ts b/libs/langchain-openai/src/types.ts index 7bcf4e8cf16eb..19e6af483d7d5 100644 --- a/libs/langchain-openai/src/types.ts +++ b/libs/langchain-openai/src/types.ts @@ -148,7 +148,13 @@ export interface OpenAIChatInput extends OpenAIBaseInput { topLogprobs?: number; /** ChatGPT messages to pass as a prefix to the prompt */ - prefixMessages?: OpenAIClient.Chat.CreateChatCompletionRequestMessage[]; + prefixMessages?: OpenAIClient.Chat.ChatCompletionMessageParam[]; + + /** + * Whether to include the raw OpenAI response in the output message's "additional_kwargs" field. + * Currently in experimental beta. + */ + __includeRawResponse?: boolean; } export declare interface AzureOpenAIInput { From 7e2e254401518cd58fd276eb54f07ff1669d5934 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Wed, 17 Jul 2024 09:35:05 -0700 Subject: [PATCH 05/15] openai[patch]: Adds OpenAI system_fingerprint to response_metadata (#6107) * Adds OpenAI system_fingerprint to response_metadata * Fix lint --- libs/langchain-openai/src/chat_models.ts | 10 ++++++++++ .../src/tests/chat_models-extended.int.test.ts | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/libs/langchain-openai/src/chat_models.ts b/libs/langchain-openai/src/chat_models.ts index 31712178e896b..487b54ffab9bd 100644 --- a/libs/langchain-openai/src/chat_models.ts +++ b/libs/langchain-openai/src/chat_models.ts @@ -154,11 +154,18 @@ function openAIResponseToChatMessage( if (includeRawResponse !== undefined) { additional_kwargs.__raw_response = rawResponse; } + let response_metadata: Record | undefined; + if (rawResponse.system_fingerprint) { + response_metadata = { + system_fingerprint: rawResponse.system_fingerprint, + }; + } return new AIMessage({ content: message.content || "", tool_calls: toolCalls, invalid_tool_calls: invalidToolCalls, additional_kwargs, + response_metadata, id: rawResponse.id, }); } @@ -681,6 +688,9 @@ export class ChatOpenAI< const generationInfo: Record = { ...newTokenIndices }; if (choice.finish_reason !== undefined) { generationInfo.finish_reason = choice.finish_reason; + // Only include system fingerprint in the last chunk for now + // to avoid concatenation issues + generationInfo.system_fingerprint = data.system_fingerprint; } if (this.logprobs) { generationInfo.logprobs = choice.logprobs; 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 cec4151519228..cf32376df624b 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 @@ -26,10 +26,18 @@ test("Test ChatOpenAI seed", async () => { seed: 123454930394983, }); const message = new HumanMessage("Say something random!"); + const res = await chat.invoke([message]); - console.log(JSON.stringify(res)); const res2 = await chat.invoke([message]); + + expect(res.response_metadata.system_fingerprint).toBeDefined(); + expect(res2.response_metadata.system_fingerprint).toBeDefined(); + + // These are unfortunately not consistently the same + delete res.response_metadata.system_fingerprint; + delete res2.response_metadata.system_fingerprint; + const resAsObject = { ...res, id: undefined, @@ -40,7 +48,6 @@ test("Test ChatOpenAI seed", async () => { id: undefined, lc_kwargs: { ...res2.lc_kwargs, id: undefined }, }; - expect(resAsObject).toEqual(res2AsObject); }); From 2c000ddcb787dc1f7d0bec8b4072fa2c0c85cdd0 Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Wed, 17 Jul 2024 09:44:47 -0700 Subject: [PATCH 06/15] openai[patch]: Release 0.2.3 (#6108) --- libs/langchain-openai/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langchain-openai/package.json b/libs/langchain-openai/package.json index 9330e917afb0d..a34552c9a5407 100644 --- a/libs/langchain-openai/package.json +++ b/libs/langchain-openai/package.json @@ -1,6 +1,6 @@ { "name": "@langchain/openai", - "version": "0.2.2", + "version": "0.2.3", "description": "OpenAI integrations for LangChain.js", "type": "module", "engines": { From 1c4b2d83cacea6e4bd0b537356cc3bc0970ef848 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 11:03:12 -0700 Subject: [PATCH 07/15] core[patch]: Allow runnable tools to take single string/`ToolCall` inputs (#6096) * core[patch]: Allow runnable tools to take single string inputs * add test for tool func * chore: lint files * cr * cr * cr * fix types * rename ZodAny to ZodObjectAny * docstring nits * fiox * cr * cr --- examples/package.json | 1 + langchain-core/src/runnables/base.ts | 46 +++++++- .../runnables/tests/runnable_tools.test.ts | 42 ++++++++ langchain-core/src/tools/index.ts | 102 +++++++++--------- langchain-core/src/tools/tests/tools.test.ts | 16 +++ langchain-core/src/tools/utils.ts | 24 +++++ langchain-core/src/types/zod.ts | 2 +- yarn.lock | 17 +++ 8 files changed, 194 insertions(+), 56 deletions(-) create mode 100644 langchain-core/src/tools/utils.ts diff --git a/examples/package.json b/examples/package.json index 56fb064ed4561..8f81162219bd1 100644 --- a/examples/package.json +++ b/examples/package.json @@ -48,6 +48,7 @@ "@langchain/google-vertexai": "workspace:*", "@langchain/google-vertexai-web": "workspace:*", "@langchain/groq": "workspace:*", + "@langchain/langgraph": "^0.0.28", "@langchain/mistralai": "workspace:*", "@langchain/mongodb": "workspace:*", "@langchain/nomic": "workspace:*", diff --git a/langchain-core/src/runnables/base.ts b/langchain-core/src/runnables/base.ts index ac099057ffc3b..ea0f00bd402bf 100644 --- a/langchain-core/src/runnables/base.ts +++ b/langchain-core/src/runnables/base.ts @@ -52,6 +52,8 @@ import { isIterableIterator, isIterator, } from "./iter.js"; +import { _isToolCall, ToolInputParsingException } from "../tools/utils.js"; +import { ToolCall } from "../messages/tool.js"; export { type RunnableInterface, RunnableBatchOptions }; @@ -1095,7 +1097,7 @@ export abstract class Runnable< name?: string; description?: string; schema: z.ZodType; - }): RunnableToolLike, RunOutput> { + }): RunnableToolLike, RunOutput> { return convertRunnableToTool(this, fields); } } @@ -2828,8 +2830,29 @@ export class RunnableToolLike< schema: RunInput; constructor(fields: RunnableToolLikeArgs) { + const sequence = RunnableSequence.from([ + RunnableLambda.from(async (input) => { + let toolInput: z.TypeOf; + + if (_isToolCall(input)) { + try { + toolInput = await this.schema.parseAsync(input.args); + } catch (e) { + throw new ToolInputParsingException( + `Received tool input did not match expected schema`, + JSON.stringify(input.args) + ); + } + } else { + toolInput = input; + } + return toolInput; + }).withConfig({ runName: `${fields.name}:parse_input` }), + fields.bound, + ]).withConfig({ runName: fields.name }); + super({ - bound: fields.bound, + bound: sequence, config: fields.config ?? {}, }); @@ -2863,11 +2886,24 @@ export function convertRunnableToTool( description?: string; schema: z.ZodType; } -): RunnableToolLike, RunOutput> { +): RunnableToolLike, RunOutput> { const name = fields.name ?? runnable.getName(); - const description = fields.description ?? fields.schema.description; + const description = fields.description ?? fields.schema?.description; + + if (fields.schema.constructor === z.ZodString) { + return new RunnableToolLike, RunOutput>({ + name, + description, + schema: z + .object({ + input: z.string(), + }) + .transform((input) => input.input) as z.ZodType, + bound: runnable, + }); + } - return new RunnableToolLike, RunOutput>({ + return new RunnableToolLike, RunOutput>({ name, description, schema: fields.schema, diff --git a/langchain-core/src/runnables/tests/runnable_tools.test.ts b/langchain-core/src/runnables/tests/runnable_tools.test.ts index 4c16aea077c71..b8ac95cf49403 100644 --- a/langchain-core/src/runnables/tests/runnable_tools.test.ts +++ b/langchain-core/src/runnables/tests/runnable_tools.test.ts @@ -1,5 +1,7 @@ import { z } from "zod"; import { RunnableLambda, RunnableToolLike } from "../base.js"; +import { FakeRetriever } from "../../utils/testing/index.js"; +import { Document } from "../../documents/document.js"; test("Runnable asTool works", async () => { const schema = z.object({ @@ -137,3 +139,43 @@ test("Runnable asTool uses Zod schema description if not provided", async () => expect(tool.description).toBe(description); }); + +test("Runnable asTool can accept a string zod schema", async () => { + const lambda = RunnableLambda.from((input) => { + return `${input}a`; + }).asTool({ + name: "string_tool", + description: "A tool that appends 'a' to the input string", + schema: z.string(), + }); + + const result = await lambda.invoke("b"); + expect(result).toBe("ba"); +}); + +test("Runnables which dont accept ToolCalls as inputs can accept ToolCalls", async () => { + const pageContent = "Dogs are pretty cool, man!"; + const retriever = new FakeRetriever({ + output: [ + new Document({ + pageContent, + }), + ], + }); + const tool = retriever.asTool({ + name: "pet_info_retriever", + description: "Get information about pets.", + schema: z.string(), + }); + + const result = await tool.invoke({ + type: "tool_call", + name: "pet_info_retriever", + args: { + input: "dogs", + }, + id: "string", + }); + expect(result).toHaveLength(1); + expect(result[0].pageContent).toBe(pageContent); +}); diff --git a/langchain-core/src/tools/index.ts b/langchain-core/src/tools/index.ts index 09a36c41aff92..8286bb7cc33d7 100644 --- a/langchain-core/src/tools/index.ts +++ b/langchain-core/src/tools/index.ts @@ -16,9 +16,12 @@ import { } from "../runnables/config.js"; import type { RunnableFunc, RunnableInterface } from "../runnables/base.js"; import { ToolCall, ToolMessage } from "../messages/tool.js"; -import { ZodAny } from "../types/zod.js"; +import { ZodObjectAny } from "../types/zod.js"; import { MessageContent } from "../messages/base.js"; import { AsyncLocalStorageProviderSingleton } from "../singletons/index.js"; +import { _isToolCall, ToolInputParsingException } from "./utils.js"; + +export { ToolInputParsingException }; export type ResponseFormat = "content" | "content_and_artifact" | string; @@ -44,21 +47,7 @@ export interface ToolParams extends BaseLangChainParams { responseFormat?: ResponseFormat; } -/** - * Custom error class used to handle exceptions related to tool input parsing. - * It extends the built-in `Error` class and adds an optional `output` - * property that can hold the output that caused the exception. - */ -export class ToolInputParsingException extends Error { - output?: string; - - constructor(message: string, output?: string) { - super(message); - this.output = output; - } -} - -export interface StructuredToolInterface +export interface StructuredToolInterface extends RunnableInterface< (z.output extends string ? string : never) | z.input | ToolCall, ToolReturnType @@ -96,7 +85,7 @@ export interface StructuredToolInterface * Base class for Tools that accept input of any shape defined by a Zod schema. */ export abstract class StructuredTool< - T extends ZodAny = ZodAny + T extends ZodObjectAny = ZodObjectAny > extends BaseLangChain< (z.output extends string ? string : never) | z.input | ToolCall, ToolReturnType @@ -259,7 +248,7 @@ export abstract class StructuredTool< } } -export interface ToolInterface +export interface ToolInterface extends StructuredToolInterface { /** * @deprecated Use .invoke() instead. Will be removed in 0.3.0. @@ -279,7 +268,7 @@ export interface ToolInterface /** * Base class for Tools that accept input as a string. */ -export abstract class Tool extends StructuredTool { +export abstract class Tool extends StructuredTool { schema = z .object({ input: z.string().optional() }) .transform((obj) => obj.input); @@ -328,8 +317,9 @@ export interface DynamicToolInput extends BaseDynamicToolInput { /** * Interface for the input parameters of the DynamicStructuredTool class. */ -export interface DynamicStructuredToolInput - extends BaseDynamicToolInput { +export interface DynamicStructuredToolInput< + T extends ZodObjectAny = ZodObjectAny +> extends BaseDynamicToolInput { func: ( input: BaseDynamicToolInput["responseFormat"] extends "content_and_artifact" ? ToolCall @@ -393,7 +383,7 @@ export class DynamicTool extends Tool { * provided function when the tool is called. */ export class DynamicStructuredTool< - T extends ZodAny = ZodAny + T extends ZodObjectAny = ZodObjectAny > extends StructuredTool { static lc_name() { return "DynamicStructuredTool"; @@ -456,11 +446,11 @@ export abstract class BaseToolkit { /** * Parameters for the tool function. - * @template {ZodAny} RunInput The input schema for the tool. - * @template {any} RunOutput The output type for the tool. + * @template {ZodObjectAny | z.ZodString = ZodObjectAny} RunInput The input schema for the tool. Either any Zod object, or a Zod string. */ -interface ToolWrapperParams - extends ToolParams { +interface ToolWrapperParams< + RunInput extends ZodObjectAny | z.ZodString = ZodObjectAny +> extends ToolParams { /** * The name of the tool. If using with an LLM, this * will be passed as the tool name. @@ -491,33 +481,54 @@ interface ToolWrapperParams /** * Creates a new StructuredTool instance with the provided function, name, description, and schema. + * * @function - * @template {RunInput extends ZodAny = ZodAny} RunInput The input schema for the tool. This corresponds to the input type when the tool is invoked. - * @template {RunOutput = any} RunOutput The output type for the tool. This corresponds to the output type when the tool is invoked. - * @template {FuncInput extends z.infer | ToolCall = z.infer} FuncInput The input type for the function. + * @template {ZodObjectAny | z.ZodString = ZodObjectAny} T The input schema for the tool. Either any Zod object, or a Zod string. * - * @param {RunnableFunc | ToolCall, RunOutput>} func - The function to invoke when the tool is called. - * @param fields - An object containing the following properties: + * @param {RunnableFunc, ToolReturnType>} func - The function to invoke when the tool is called. + * @param {ToolWrapperParams} fields - An object containing the following properties: * @param {string} fields.name The name of the tool. * @param {string | undefined} fields.description The description of the tool. Defaults to either the description on the Zod schema, or `${fields.name} tool`. - * @param {z.ZodObject} fields.schema The Zod schema defining the input for the tool. + * @param {ZodObjectAny | z.ZodString | undefined} fields.schema The Zod schema defining the input for the tool. If undefined, it will default to a Zod string schema. * - * @returns {DynamicStructuredTool} A new StructuredTool instance. + * @returns {DynamicStructuredTool} A new StructuredTool instance. */ -export function tool( +export function tool( func: RunnableFunc, ToolReturnType>, fields: ToolWrapperParams -): DynamicStructuredTool { - const schema = - fields.schema ?? - z.object({ input: z.string().optional() }).transform((obj) => obj.input); +): DynamicTool; + +export function tool( + func: RunnableFunc, ToolReturnType>, + fields: ToolWrapperParams +): DynamicStructuredTool; + +export function tool( + func: RunnableFunc, ToolReturnType>, + fields: ToolWrapperParams +): + | DynamicStructuredTool + | DynamicTool { + // If the schema is not provided, or it's a string schema, create a DynamicTool + if (!fields.schema || !("shape" in fields.schema) || !fields.schema.shape) { + return new DynamicTool({ + name: fields.name, + description: + fields.description ?? + fields.schema?.description ?? + `${fields.name} tool`, + responseFormat: fields.responseFormat, + func, + }); + } const description = - fields.description ?? schema.description ?? `${fields.name} tool`; - return new DynamicStructuredTool({ + fields.description ?? fields.schema.description ?? `${fields.name} tool`; + + return new DynamicStructuredTool({ name: fields.name, description, - schema: schema as T, + schema: fields.schema as T extends ZodObjectAny ? T : ZodObjectAny, // TODO: Consider moving into DynamicStructuredTool constructor func: async (input, runManager, config) => { return new Promise((resolve, reject) => { @@ -540,15 +551,6 @@ export function tool( }); } -function _isToolCall(toolCall?: unknown): toolCall is ToolCall { - return !!( - toolCall && - typeof toolCall === "object" && - "type" in toolCall && - toolCall.type === "tool_call" - ); -} - function _formatToolOutput(params: { content: unknown; name: string; diff --git a/langchain-core/src/tools/tests/tools.test.ts b/langchain-core/src/tools/tests/tools.test.ts index b514b99c1827e..bf577a4a1dc9e 100644 --- a/langchain-core/src/tools/tests/tools.test.ts +++ b/langchain-core/src/tools/tests/tools.test.ts @@ -99,3 +99,19 @@ test("Returns tool message if responseFormat is content_and_artifact and returns expect(toolResult.artifact).toEqual({ location: "San Francisco" }); expect(toolResult.name).toBe("weather"); }); + +test("Tool can accept single string input", async () => { + const stringTool = tool( + (input: string): string => { + return `${input}a`; + }, + { + name: "string_tool", + description: "A tool that appends 'a' to the input string", + schema: z.string(), + } + ); + + const result = await stringTool.invoke("b"); + expect(result).toBe("ba"); +}); diff --git a/langchain-core/src/tools/utils.ts b/langchain-core/src/tools/utils.ts new file mode 100644 index 0000000000000..b9c5bd8b384db --- /dev/null +++ b/langchain-core/src/tools/utils.ts @@ -0,0 +1,24 @@ +import { ToolCall } from "../messages/tool.js"; + +export function _isToolCall(toolCall?: unknown): toolCall is ToolCall { + return !!( + toolCall && + typeof toolCall === "object" && + "type" in toolCall && + toolCall.type === "tool_call" + ); +} + +/** + * Custom error class used to handle exceptions related to tool input parsing. + * It extends the built-in `Error` class and adds an optional `output` + * property that can hold the output that caused the exception. + */ +export class ToolInputParsingException extends Error { + output?: string; + + constructor(message: string, output?: string) { + super(message); + this.output = output; + } +} diff --git a/langchain-core/src/types/zod.ts b/langchain-core/src/types/zod.ts index d864170ddafa9..faaa92b3bff24 100644 --- a/langchain-core/src/types/zod.ts +++ b/langchain-core/src/types/zod.ts @@ -1,4 +1,4 @@ import type { z } from "zod"; // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ZodAny = z.ZodObject; +export type ZodObjectAny = z.ZodObject; diff --git a/yarn.lock b/yarn.lock index 2497a15f11f9b..8fcc427d9b2e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11341,6 +11341,22 @@ __metadata: languageName: node linkType: hard +"@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/mistralai@workspace:*, @langchain/mistralai@workspace:libs/langchain-mistralai": version: 0.0.0-use.local resolution: "@langchain/mistralai@workspace:libs/langchain-mistralai" @@ -24929,6 +24945,7 @@ __metadata: "@langchain/google-vertexai": "workspace:*" "@langchain/google-vertexai-web": "workspace:*" "@langchain/groq": "workspace:*" + "@langchain/langgraph": ^0.0.28 "@langchain/mistralai": "workspace:*" "@langchain/mongodb": "workspace:*" "@langchain/nomic": "workspace:*" From 8050a2aaba4fb20b9a24dc0077ca5a539f904d50 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 11:09:37 -0700 Subject: [PATCH 08/15] google-genai[patch]: Fix removing additional properties from schema (#6109) * google-genai[patch]: Fix removing additional properties from schema * chore: lint files --- .../src/tests/chat_models.test.ts | 46 +++++++++++++------ .../src/utils/zod_to_genai_parameters.ts | 5 +- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/libs/langchain-google-genai/src/tests/chat_models.test.ts b/libs/langchain-google-genai/src/tests/chat_models.test.ts index 22c1de19cc4ab..ca41b8b5e100f 100644 --- a/libs/langchain-google-genai/src/tests/chat_models.test.ts +++ b/libs/langchain-google-genai/src/tests/chat_models.test.ts @@ -15,6 +15,19 @@ import { convertMessageContentToParts, } from "../utils/common.js"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extractKeys(obj: Record, keys: string[] = []) { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + keys.push(key); + if (typeof obj[key] === "object" && obj[key] !== null) { + extractKeys(obj[key], keys); + } + } + } + return keys; +} + test("Google AI - `temperature` must be in range [0.0,1.0]", async () => { expect( () => @@ -89,19 +102,6 @@ test("Google AI - `safetySettings` category array must be unique", async () => { }); test("removeAdditionalProperties can remove all instances of additionalProperties", async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function extractKeys(obj: Record, keys: string[] = []) { - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - keys.push(key); - if (typeof obj[key] === "object" && obj[key] !== null) { - extractKeys(obj[key], keys); - } - } - } - return keys; - } - const idealResponseSchema = z.object({ idealResponse: z .string() @@ -137,6 +137,26 @@ test("removeAdditionalProperties can remove all instances of additionalPropertie expect( arrSchemaObj.find((key) => key === "additionalProperties") ).toBeUndefined(); + + const analysisSchema = z.object({ + decision: z.enum(["UseAPI", "UseFallback"]), + explanation: z.string(), + apiDetails: z + .object({ + serviceName: z.string(), + endpointName: z.string(), + parameters: z.record(z.unknown()), + extractionPath: z.string(), + }) + .optional(), + }); + const parsedAnalysisSchema = removeAdditionalProperties( + zodToJsonSchema(analysisSchema) + ); + const analysisSchemaObj = extractKeys(parsedAnalysisSchema); + expect( + analysisSchemaObj.find((key) => key === "additionalProperties") + ).toBeUndefined(); }); test("convertMessageContentToParts correctly handles message types", () => { diff --git a/libs/langchain-google-genai/src/utils/zod_to_genai_parameters.ts b/libs/langchain-google-genai/src/utils/zod_to_genai_parameters.ts index e43285cc66391..dd59665164c83 100644 --- a/libs/langchain-google-genai/src/utils/zod_to_genai_parameters.ts +++ b/libs/langchain-google-genai/src/utils/zod_to_genai_parameters.ts @@ -24,10 +24,7 @@ export function removeAdditionalProperties( if (typeof obj === "object" && obj !== null) { const newObj = { ...obj }; - if ( - "additionalProperties" in newObj && - typeof newObj.additionalProperties === "boolean" - ) { + if ("additionalProperties" in newObj) { delete newObj.additionalProperties; } From 1d79eca7488ccd37f749e03c1052614da384bdb4 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 11:15:14 -0700 Subject: [PATCH 09/15] google-common[patch]: Release 0.0.23 (#6113) --- libs/langchain-google-genai/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langchain-google-genai/package.json b/libs/langchain-google-genai/package.json index e5c4b61874685..3cf012e763aca 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.22", + "version": "0.0.23", "description": "Sample integration for LangChain.js", "type": "module", "engines": { From 8848c7da8605c0cd51d00e865d18b8612369aa46 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 11:17:01 -0700 Subject: [PATCH 10/15] google-common[patch]: Fix removing all instances of additionalProperties (#6110) * google-common[patch]: Fix removing all instances of additionalProperties * add tests --- .../src/tests/chat_models.test.ts | 74 +++++++++++++++++++ .../src/utils/zod_to_gemini_parameters.ts | 5 +- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/libs/langchain-google-common/src/tests/chat_models.test.ts b/libs/langchain-google-common/src/tests/chat_models.test.ts index dfd7de527e38f..cb6674459c32f 100644 --- a/libs/langchain-google-common/src/tests/chat_models.test.ts +++ b/libs/langchain-google-common/src/tests/chat_models.test.ts @@ -10,11 +10,14 @@ import { ToolMessage, } from "@langchain/core/messages"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; import { ChatGoogleBase, ChatGoogleBaseInput } from "../chat_models.js"; import { authOptions, MockClient, MockClientAuthInfo, mockId } from "./mock.js"; import { GeminiTool, GoogleAIBaseLLMInput } from "../types.js"; import { GoogleAbstractedClient } from "../auth.js"; import { GoogleAISafetyError } from "../utils/safety.js"; +import { removeAdditionalProperties } from "../utils/zod_to_gemini_parameters.js"; class ChatGoogle extends ChatGoogleBase { constructor(fields?: ChatGoogleBaseInput) { @@ -840,3 +843,74 @@ describe("Mock ChatGoogle", () => { console.log(JSON.stringify(record?.opts?.data, null, 1)); }); }); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extractKeys(obj: Record, keys: string[] = []) { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + keys.push(key); + if (typeof obj[key] === "object" && obj[key] !== null) { + extractKeys(obj[key], keys); + } + } + } + return keys; +} + +test("removeAdditionalProperties can remove all instances of additionalProperties", async () => { + const idealResponseSchema = z.object({ + idealResponse: z + .string() + .optional() + .describe("The ideal response to the question"), + }); + const questionSchema = z.object({ + question: z.string().describe("Question text"), + type: z.enum(["singleChoice", "multiChoice"]).describe("Question type"), + options: z.array(z.string()).describe("List of possible answers"), + correctAnswer: z + .string() + .optional() + .describe("correct answer from the possible answers"), + idealResponses: z + .array(idealResponseSchema) + .describe("Array of ideal responses to the question"), + }); + + const schema = z.object({ + questions: z.array(questionSchema).describe("Array of question objects"), + }); + + const parsedSchemaArr = removeAdditionalProperties(zodToJsonSchema(schema)); + const arrSchemaKeys = extractKeys(parsedSchemaArr); + expect( + arrSchemaKeys.find((key) => key === "additionalProperties") + ).toBeUndefined(); + const parsedSchemaObj = removeAdditionalProperties( + zodToJsonSchema(questionSchema) + ); + const arrSchemaObj = extractKeys(parsedSchemaObj); + expect( + arrSchemaObj.find((key) => key === "additionalProperties") + ).toBeUndefined(); + + const analysisSchema = z.object({ + decision: z.enum(["UseAPI", "UseFallback"]), + explanation: z.string(), + apiDetails: z + .object({ + serviceName: z.string(), + endpointName: z.string(), + parameters: z.record(z.unknown()), + extractionPath: z.string(), + }) + .optional(), + }); + const parsedAnalysisSchema = removeAdditionalProperties( + zodToJsonSchema(analysisSchema) + ); + const analysisSchemaObj = extractKeys(parsedAnalysisSchema); + expect( + analysisSchemaObj.find((key) => key === "additionalProperties") + ).toBeUndefined(); +}); diff --git a/libs/langchain-google-common/src/utils/zod_to_gemini_parameters.ts b/libs/langchain-google-common/src/utils/zod_to_gemini_parameters.ts index 229a578fbc210..101005e66bef1 100644 --- a/libs/langchain-google-common/src/utils/zod_to_gemini_parameters.ts +++ b/libs/langchain-google-common/src/utils/zod_to_gemini_parameters.ts @@ -15,10 +15,7 @@ export function removeAdditionalProperties( if (typeof obj === "object" && obj !== null) { const newObj = { ...obj }; - if ( - "additionalProperties" in newObj && - typeof newObj.additionalProperties === "boolean" - ) { + if ("additionalProperties" in newObj) { delete newObj.additionalProperties; } From eccc7d11ff0dc885987508cdeb210500b2783b26 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 11:20:56 -0700 Subject: [PATCH 11/15] google[patch]: Release(common) 0.0.21, Release 0.0.20 (#6114) --- libs/langchain-google-common/package.json | 2 +- libs/langchain-google-gauth/package.json | 4 ++-- libs/langchain-google-vertexai-web/package.json | 4 ++-- libs/langchain-google-vertexai/package.json | 4 ++-- libs/langchain-google-webauth/package.json | 4 ++-- yarn.lock | 14 +++++++------- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/langchain-google-common/package.json b/libs/langchain-google-common/package.json index 1bec8aa3a860d..fa371c0472efb 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.20", + "version": "0.0.21", "description": "Core types and classes for Google services.", "type": "module", "engines": { diff --git a/libs/langchain-google-gauth/package.json b/libs/langchain-google-gauth/package.json index a41e893065d6b..462f4b13744ae 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.19", + "version": "0.0.20", "description": "Google auth based authentication support for Google services", "type": "module", "engines": { @@ -36,7 +36,7 @@ "license": "MIT", "dependencies": { "@langchain/core": ">0.1.56 <0.3.0", - "@langchain/google-common": "~0.0.19", + "@langchain/google-common": "~0.0.21", "google-auth-library": "^8.9.0" }, "devDependencies": { diff --git a/libs/langchain-google-vertexai-web/package.json b/libs/langchain-google-vertexai-web/package.json index 3e1ee7ce94635..7cbcdc643b144 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.19", + "version": "0.0.20", "description": "LangChain.js support for Google Vertex AI Web", "type": "module", "engines": { @@ -41,7 +41,7 @@ "license": "MIT", "dependencies": { "@langchain/core": ">0.1.56 <0.3.0", - "@langchain/google-webauth": "~0.0.19" + "@langchain/google-webauth": "~0.0.20" }, "devDependencies": { "@jest/globals": "^29.5.0", diff --git a/libs/langchain-google-vertexai/package.json b/libs/langchain-google-vertexai/package.json index 6b71a1566de4e..8d37a978879ca 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.19", + "version": "0.0.20", "description": "LangChain.js support for Google Vertex AI", "type": "module", "engines": { @@ -44,7 +44,7 @@ "license": "MIT", "dependencies": { "@langchain/core": ">0.1.56 <0.3.0", - "@langchain/google-gauth": "~0.0.19" + "@langchain/google-gauth": "~0.0.20" }, "devDependencies": { "@jest/globals": "^29.5.0", diff --git a/libs/langchain-google-webauth/package.json b/libs/langchain-google-webauth/package.json index 8e95d85ce11fb..c4319a7031be8 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.19", + "version": "0.0.20", "description": "Web-based authentication support for Google services", "type": "module", "engines": { @@ -41,7 +41,7 @@ "license": "MIT", "dependencies": { "@langchain/core": ">0.1.56 <0.3.0", - "@langchain/google-common": "~0.0.19", + "@langchain/google-common": "~0.0.21", "web-auth-library": "^1.0.3" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 8fcc427d9b2e0..773e2e5c029b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11091,7 +11091,7 @@ __metadata: languageName: node linkType: hard -"@langchain/google-common@workspace:*, @langchain/google-common@workspace:libs/langchain-google-common, @langchain/google-common@~0.0.19": +"@langchain/google-common@workspace:*, @langchain/google-common@workspace:libs/langchain-google-common, @langchain/google-common@~0.0.21": version: 0.0.0-use.local resolution: "@langchain/google-common@workspace:libs/langchain-google-common" dependencies: @@ -11124,13 +11124,13 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-gauth@workspace:libs/langchain-google-gauth, @langchain/google-gauth@~0.0.19": +"@langchain/google-gauth@workspace:libs/langchain-google-gauth, @langchain/google-gauth@~0.0.20": version: 0.0.0-use.local resolution: "@langchain/google-gauth@workspace:libs/langchain-google-gauth" dependencies: "@jest/globals": ^29.5.0 "@langchain/core": ">0.1.56 <0.3.0" - "@langchain/google-common": ~0.0.19 + "@langchain/google-common": ~0.0.21 "@langchain/scripts": ~0.0.14 "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 @@ -11198,7 +11198,7 @@ __metadata: dependencies: "@jest/globals": ^29.5.0 "@langchain/core": ">0.1.56 <0.3.0" - "@langchain/google-webauth": ~0.0.19 + "@langchain/google-webauth": ~0.0.20 "@langchain/scripts": ~0.0.14 "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 @@ -11231,7 +11231,7 @@ __metadata: "@jest/globals": ^29.5.0 "@langchain/core": ">0.1.56 <0.3.0" "@langchain/google-common": latest - "@langchain/google-gauth": ~0.0.19 + "@langchain/google-gauth": ~0.0.20 "@langchain/scripts": ~0.0.14 "@langchain/standard-tests": 0.0.0 "@swc/core": ^1.3.90 @@ -11257,13 +11257,13 @@ __metadata: languageName: unknown linkType: soft -"@langchain/google-webauth@workspace:libs/langchain-google-webauth, @langchain/google-webauth@~0.0.19": +"@langchain/google-webauth@workspace:libs/langchain-google-webauth, @langchain/google-webauth@~0.0.20": version: 0.0.0-use.local resolution: "@langchain/google-webauth@workspace:libs/langchain-google-webauth" dependencies: "@jest/globals": ^29.5.0 "@langchain/core": ">0.1.56 <0.3.0" - "@langchain/google-common": ~0.0.19 + "@langchain/google-common": ~0.0.21 "@langchain/scripts": ~0.0.14 "@swc/core": ^1.3.90 "@swc/jest": ^0.2.29 From f416316bc3f54b5547867ec55a207cd1be92411b Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 13:45:47 -0700 Subject: [PATCH 12/15] docs[patch]: Add note on genai docs for invalid tool schema (#6116) * docs[patch]: Add note on genai docs for invalid tool schema * callout nits --- docs/core_docs/.gitignore | 12 ++++++++++ .../integrations/chat/google_generativeai.mdx | 22 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/core_docs/.gitignore b/docs/core_docs/.gitignore index b952a46c9bf65..fdd37add508fb 100644 --- a/docs/core_docs/.gitignore +++ b/docs/core_docs/.gitignore @@ -57,14 +57,26 @@ docs/how_to/trim_messages.md docs/how_to/trim_messages.mdx docs/how_to/tools_prompting.md docs/how_to/tools_prompting.mdx +docs/how_to/tools_error.md +docs/how_to/tools_error.mdx docs/how_to/tools_builtin.md docs/how_to/tools_builtin.mdx +docs/how_to/tool_streaming.md +docs/how_to/tool_streaming.mdx +docs/how_to/tool_stream_events.md +docs/how_to/tool_stream_events.mdx docs/how_to/tool_runtime.md docs/how_to/tool_runtime.mdx +docs/how_to/tool_results_pass_to_model.md +docs/how_to/tool_results_pass_to_model.mdx +docs/how_to/tool_configure.md +docs/how_to/tool_configure.mdx docs/how_to/tool_calls_multimodal.md docs/how_to/tool_calls_multimodal.mdx docs/how_to/tool_calling.md docs/how_to/tool_calling.mdx +docs/how_to/tool_artifacts.md +docs/how_to/tool_artifacts.mdx docs/how_to/structured_output.md docs/how_to/structured_output.mdx docs/how_to/streaming.md diff --git a/docs/core_docs/docs/integrations/chat/google_generativeai.mdx b/docs/core_docs/docs/integrations/chat/google_generativeai.mdx index 0772cf712159b..a97b768eae1d1 100644 --- a/docs/core_docs/docs/integrations/chat/google_generativeai.mdx +++ b/docs/core_docs/docs/integrations/chat/google_generativeai.mdx @@ -41,6 +41,28 @@ import GoogleGenerativeAI from "@examples/models/chat/googlegenerativeai.ts"; ## Tool calling +:::caution +The Google GenerativeAI package as of version `0.0.23` does not allow tool schemas to contain an object with unknown properties. +The Google VertexAI package (as of version `0.0.20`) does support this pattern. +[Click here for the Google VertexAI package documentation](/docs/integrations/chat/google_vertex_ai). + +For example, the following Zod schema will throw an error: + +```typescript +const schema = z.object({ + properties: z.record(z.unknown()), // Not allowed +}); +``` + +or + +```typescript +const schema = z.record(z.unknown()); // Not allowed +``` + +Instead, you should explicitly define the properties of the object field, or use the Google VertexAI package. +::: + import GoogleGenerativeAIToolCalling from "@examples/models/chat/googlegenerativeai_tools.ts"; {GoogleGenerativeAIToolCalling} From 77904e30132272a23284fc0956cd60f0069e2f56 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 14:36:01 -0700 Subject: [PATCH 13/15] docs[patch]: Fix import path in typesense doc (#6117) --- .../docs/integrations/vectorstores/typesense.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/core_docs/docs/integrations/vectorstores/typesense.mdx b/docs/core_docs/docs/integrations/vectorstores/typesense.mdx index f203d3a8ca31b..e0a0b7a453957 100644 --- a/docs/core_docs/docs/integrations/vectorstores/typesense.mdx +++ b/docs/core_docs/docs/integrations/vectorstores/typesense.mdx @@ -9,14 +9,17 @@ import IntegrationInstallTooltip from "@mdx_components/integration_install_toolt ```bash npm2yarn -npm install @langchain/openai +npm install @langchain/openai @langchain/community ``` ```typescript -import { Typesense, TypesenseConfig } from "langchain/vectorstores/typesense"; +import { + Typesense, + TypesenseConfig, +} from "@lanchain/community/vectorstores/typesense"; import { OpenAIEmbeddings } from "@langchain/openai"; import { Client } from "typesense"; -import { Document } from "langchain/document"; +import { Document } from "@langchain/core/documents"; const vectorTypesenseClient = new Client({ nodes: [ From a64203b7f875e8df830fb68debee7af960f4eb52 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 14:43:16 -0700 Subject: [PATCH 14/15] core[minor]: Standardize tool choice (#6111) * core[minor]: Standardize tool choice * implement in partner pkgs * use basechatmodelcalloptions --- .../src/language_models/chat_models.ts | 21 +++++++- libs/langchain-anthropic/src/chat_models.ts | 42 ++++----------- libs/langchain-anthropic/src/utils.ts | 51 +++++++++++++++++++ libs/langchain-aws/src/chat_models.ts | 13 ++--- libs/langchain-aws/src/common.ts | 8 ++- libs/langchain-groq/src/chat_models.ts | 2 +- libs/langchain-openai/src/chat_models.ts | 10 ++-- libs/langchain-openai/src/utils/openai.ts | 34 ++++++++++++- 8 files changed, 135 insertions(+), 46 deletions(-) create mode 100644 libs/langchain-anthropic/src/utils.ts diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index 1f39802436d09..772964724e58a 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -46,6 +46,9 @@ import { concat } from "../utils/stream.js"; import { RunnablePassthrough } from "../runnables/passthrough.js"; import { isZodSchema } from "../utils/types/is_zod_schema.js"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ToolChoice = string | Record | "auto" | "any"; + /** * Represents a serialized chat model. */ @@ -73,7 +76,23 @@ export type BaseChatModelParams = BaseLanguageModelParams; /** * Represents the call options for a base chat model. */ -export type BaseChatModelCallOptions = BaseLanguageModelCallOptions; +export type BaseChatModelCallOptions = BaseLanguageModelCallOptions & { + /** + * Specifies how the chat model should use tools. + * @default undefined + * + * Possible values: + * - "auto": The model may choose to use any of the provided tools, or none. + * - "any": The model must use one of the provided tools. + * - "none": The model must not use any tools. + * - A string (not "auto", "any", or "none"): The name of a specific tool the model must use. + * - An object: A custom schema specifying tool choice parameters. Specific to the provider. + * + * Note: Not all providers support tool_choice. An error will be thrown + * if used with an unsupporting model. + */ + tool_choice?: ToolChoice; +}; /** * Creates a transform stream for encoding chat message chunks. diff --git a/libs/langchain-anthropic/src/chat_models.ts b/libs/langchain-anthropic/src/chat_models.ts index 8aa8295f2676b..9d1e0bab5fba3 100644 --- a/libs/langchain-anthropic/src/chat_models.ts +++ b/libs/langchain-anthropic/src/chat_models.ts @@ -21,12 +21,12 @@ import { import { getEnvironmentVariable } from "@langchain/core/utils/env"; import { BaseChatModel, + BaseChatModelCallOptions, LangSmithParams, type BaseChatModelParams, } from "@langchain/core/language_models/chat_models"; import { type StructuredOutputMethodOptions, - type BaseLanguageModelCallOptions, type BaseLanguageModelInput, type ToolDefinition, isOpenAITool, @@ -53,6 +53,11 @@ import { extractToolCalls, } from "./output_parsers.js"; import { AnthropicToolResponse } from "./types.js"; +import { + AnthropicToolChoice, + AnthropicToolTypes, + handleToolChoice, +} from "./utils.js"; type AnthropicMessage = Anthropic.MessageParam; type AnthropicMessageCreateParams = Anthropic.MessageCreateParamsNonStreaming; @@ -60,23 +65,11 @@ type AnthropicStreamingMessageCreateParams = Anthropic.MessageCreateParamsStreaming; type AnthropicMessageStreamEvent = Anthropic.MessageStreamEvent; type AnthropicRequestOptions = Anthropic.RequestOptions; -type AnthropicToolChoice = - | { - type: "tool"; - name: string; - } - | "any" - | "auto"; + export interface ChatAnthropicCallOptions - extends BaseLanguageModelCallOptions, + extends BaseChatModelCallOptions, Pick { - tools?: ( - | StructuredToolInterface - | AnthropicTool - | Record - | ToolDefinition - | RunnableToolLike - )[]; + tools?: AnthropicToolTypes[]; /** * Whether or not to specify what tool the model should use * @default "auto" @@ -855,24 +848,11 @@ export class ChatAnthropicMessages< "messages" > & Kwargs { - let tool_choice: + const tool_choice: | MessageCreateParams.ToolChoiceAuto | MessageCreateParams.ToolChoiceAny | MessageCreateParams.ToolChoiceTool - | undefined; - if (options?.tool_choice) { - if (options?.tool_choice === "any") { - tool_choice = { - type: "any", - }; - } else if (options?.tool_choice === "auto") { - tool_choice = { - type: "auto", - }; - } else { - tool_choice = options?.tool_choice; - } - } + | undefined = handleToolChoice(options?.tool_choice); return { model: this.model, diff --git a/libs/langchain-anthropic/src/utils.ts b/libs/langchain-anthropic/src/utils.ts new file mode 100644 index 0000000000000..860dd0f594ff4 --- /dev/null +++ b/libs/langchain-anthropic/src/utils.ts @@ -0,0 +1,51 @@ +import type { + MessageCreateParams, + Tool as AnthropicTool, +} from "@anthropic-ai/sdk/resources/index.mjs"; +import { ToolDefinition } from "@langchain/core/language_models/base"; +import { RunnableToolLike } from "@langchain/core/runnables"; +import { StructuredToolInterface } from "@langchain/core/tools"; + +export type AnthropicToolChoice = + | { + type: "tool"; + name: string; + } + | "any" + | "auto" + | "none" + | string; + +export type AnthropicToolTypes = + | StructuredToolInterface + | AnthropicTool + | Record + | ToolDefinition + | RunnableToolLike; + +export function handleToolChoice( + toolChoice?: AnthropicToolChoice +): + | MessageCreateParams.ToolChoiceAuto + | MessageCreateParams.ToolChoiceAny + | MessageCreateParams.ToolChoiceTool + | undefined { + if (!toolChoice) { + return undefined; + } else if (toolChoice === "any") { + return { + type: "any", + }; + } else if (toolChoice === "auto") { + return { + type: "auto", + }; + } else if (typeof toolChoice === "string") { + return { + type: "tool", + name: toolChoice, + }; + } else { + return toolChoice; + } +} diff --git a/libs/langchain-aws/src/chat_models.ts b/libs/langchain-aws/src/chat_models.ts index 8d5ae1abf00d0..96b47993c3a65 100644 --- a/libs/langchain-aws/src/chat_models.ts +++ b/libs/langchain-aws/src/chat_models.ts @@ -2,7 +2,6 @@ import type { BaseMessage } from "@langchain/core/messages"; import { AIMessageChunk } from "@langchain/core/messages"; import type { ToolDefinition, - BaseLanguageModelCallOptions, BaseLanguageModelInput, } from "@langchain/core/language_models/base"; import { CallbackManagerForLLMRun } from "@langchain/core/callbacks/manager"; @@ -10,6 +9,7 @@ import { type BaseChatModelParams, BaseChatModel, LangSmithParams, + BaseChatModelCallOptions, } from "@langchain/core/language_models/chat_models"; import type { ToolConfiguration, @@ -30,11 +30,7 @@ import { import type { DocumentType as __DocumentType } from "@smithy/types"; import { StructuredToolInterface } from "@langchain/core/tools"; import { Runnable, RunnableToolLike } from "@langchain/core/runnables"; -import { - BedrockToolChoice, - ConverseCommandParams, - CredentialType, -} from "./types.js"; +import { ConverseCommandParams, CredentialType } from "./types.js"; import { convertToConverseTools, convertToBedrockToolChoice, @@ -43,6 +39,7 @@ import { handleConverseStreamContentBlockDelta, handleConverseStreamMetadata, handleConverseStreamContentBlockStart, + BedrockConverseToolChoice, } from "./common.js"; /** @@ -127,7 +124,7 @@ export interface ChatBedrockConverseInput } export interface ChatBedrockConverseCallOptions - extends BaseLanguageModelCallOptions, + extends BaseChatModelCallOptions, Pick< ChatBedrockConverseInput, "additionalModelRequestFields" | "streamUsage" @@ -149,7 +146,7 @@ export interface ChatBedrockConverseCallOptions * or whether to generate text instead. * If a tool name is passed, it will force the model to call that specific tool. */ - tool_choice?: "any" | "auto" | string | BedrockToolChoice; + tool_choice?: BedrockConverseToolChoice; } /** diff --git a/libs/langchain-aws/src/common.ts b/libs/langchain-aws/src/common.ts index 14afc63cad6e3..e9dd95759b594 100644 --- a/libs/langchain-aws/src/common.ts +++ b/libs/langchain-aws/src/common.ts @@ -258,8 +258,14 @@ export function convertToConverseTools( ); } +export type BedrockConverseToolChoice = + | "any" + | "auto" + | string + | BedrockToolChoice; + export function convertToBedrockToolChoice( - toolChoice: string | BedrockToolChoice, + toolChoice: BedrockConverseToolChoice, tools: BedrockTool[] ): BedrockToolChoice { if (typeof toolChoice === "string") { diff --git a/libs/langchain-groq/src/chat_models.ts b/libs/langchain-groq/src/chat_models.ts index eb8f2dffc76d7..ca7c302cfd7f0 100644 --- a/libs/langchain-groq/src/chat_models.ts +++ b/libs/langchain-groq/src/chat_models.ts @@ -69,7 +69,7 @@ import { ToolCallChunk } from "@langchain/core/messages/tool"; export interface ChatGroqCallOptions extends BaseChatModelCallOptions { headers?: Record; tools?: OpenAIClient.ChatCompletionTool[]; - tool_choice?: OpenAIClient.ChatCompletionToolChoiceOption; + tool_choice?: OpenAIClient.ChatCompletionToolChoiceOption | "any" | string; response_format?: { type: "json_object" }; } diff --git a/libs/langchain-openai/src/chat_models.ts b/libs/langchain-openai/src/chat_models.ts index 487b54ffab9bd..db86e6e919404 100644 --- a/libs/langchain-openai/src/chat_models.ts +++ b/libs/langchain-openai/src/chat_models.ts @@ -64,7 +64,11 @@ import type { LegacyOpenAIInput, } from "./types.js"; import { type OpenAIEndpointConfig, getEndpoint } from "./utils/azure.js"; -import { wrapOpenAIClientError } from "./utils/openai.js"; +import { + OpenAIToolChoice, + formatToOpenAIToolChoice, + wrapOpenAIClientError, +} from "./utils/openai.js"; import { FunctionDef, formatFunctionDefinitions, @@ -274,7 +278,7 @@ export interface ChatOpenAICallOptions extends OpenAICallOptions, BaseFunctionCallOptions { tools?: StructuredToolInterface[] | OpenAIClient.ChatCompletionTool[]; - tool_choice?: OpenAIClient.ChatCompletionToolChoiceOption; + tool_choice?: OpenAIToolChoice; promptIndex?: number; response_format?: { type: "json_object" }; seed?: number; @@ -613,7 +617,7 @@ export class ChatOpenAI< tools: isStructuredToolArray(options?.tools) ? options?.tools.map(convertToOpenAITool) : options?.tools, - tool_choice: options?.tool_choice, + tool_choice: formatToOpenAIToolChoice(options?.tool_choice), response_format: options?.response_format, seed: options?.seed, ...streamOptionsConfig, diff --git a/libs/langchain-openai/src/utils/openai.ts b/libs/langchain-openai/src/utils/openai.ts index 990a7f1dc9392..e95297e56b64e 100644 --- a/libs/langchain-openai/src/utils/openai.ts +++ b/libs/langchain-openai/src/utils/openai.ts @@ -1,4 +1,8 @@ -import { APIConnectionTimeoutError, APIUserAbortError } from "openai"; +import { + APIConnectionTimeoutError, + APIUserAbortError, + OpenAI as OpenAIClient, +} from "openai"; import { zodToJsonSchema } from "zod-to-json-schema"; import type { StructuredToolInterface } from "@langchain/core/tools"; import { @@ -36,3 +40,31 @@ export function formatToOpenAIAssistantTool(tool: StructuredToolInterface) { }, }; } + +export type OpenAIToolChoice = + | OpenAIClient.ChatCompletionToolChoiceOption + | "any" + | string; + +export function formatToOpenAIToolChoice( + toolChoice?: OpenAIToolChoice +): OpenAIClient.ChatCompletionToolChoiceOption | undefined { + if (!toolChoice) { + return undefined; + } else if (toolChoice === "any" || toolChoice === "required") { + return "required"; + } else if (toolChoice === "auto") { + return "auto"; + } else if (toolChoice === "none") { + return "none"; + } else if (typeof toolChoice === "string") { + return { + type: "function", + function: { + name: toolChoice, + }, + }; + } else { + return toolChoice; + } +} From 227447f6225eb3a79ff33c829f75610f26131f3d Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 17 Jul 2024 15:44:53 -0700 Subject: [PATCH 15/15] core[patch]: JSDoc nit unsupporting to unsupported (#6120) --- langchain-core/src/language_models/chat_models.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/langchain-core/src/language_models/chat_models.ts b/langchain-core/src/language_models/chat_models.ts index 772964724e58a..30256073c33a5 100644 --- a/langchain-core/src/language_models/chat_models.ts +++ b/langchain-core/src/language_models/chat_models.ts @@ -89,7 +89,7 @@ export type BaseChatModelCallOptions = BaseLanguageModelCallOptions & { * - An object: A custom schema specifying tool choice parameters. Specific to the provider. * * Note: Not all providers support tool_choice. An error will be thrown - * if used with an unsupporting model. + * if used with an unsupported model. */ tool_choice?: ToolChoice; };