diff --git a/docs/docs/how_to/convert_runnable_to_tool.ipynb b/docs/docs/how_to/convert_runnable_to_tool.ipynb
new file mode 100644
index 0000000000..ed4b51e097
--- /dev/null
+++ b/docs/docs/how_to/convert_runnable_to_tool.ipynb
@@ -0,0 +1,541 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "9a8bceb3-95bd-4496-bb9e-57655136e070",
+ "metadata": {},
+ "source": [
+ "# How to use Runnables as Tools\n",
+ "\n",
+ ":::info Prerequisites\n",
+ "\n",
+ "This guide assumes familiarity with the following concepts:\n",
+ "\n",
+ "- [Runnables](/docs/concepts#runnable-interface)\n",
+ "- [Tools](/docs/concepts#tools)\n",
+ "- [Agents](/docs/tutorials/agents)\n",
+ "\n",
+ ":::\n",
+ "\n",
+ "Here we will demonstrate how to convert a LangChain `Runnable` into a tool that can be used by agents, chains, or chat models.\n",
+ "\n",
+ "## Dependencies\n",
+ "\n",
+ "**Note**: this guide requires `langchain-core` >= 0.2.13. We will also use [OpenAI](/docs/integrations/platforms/openai/) for embeddings, but any LangChain embeddings should suffice. We will use a simple [LangGraph](https://langchain-ai.github.io/langgraph/) agent for demonstration purposes."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "92341f48-2c29-4ce9-8ab8-0a7c7a7c98a1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%capture --no-stderr\n",
+ "%pip install -U langchain-core langchain-openai langgraph"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2b0dcc1a-48e8-4a81-b920-3563192ce076",
+ "metadata": {},
+ "source": [
+ "LangChain [tools](/docs/concepts#tools) are interfaces that an agent, chain, or chat model can use to interact with the world. See [here](/docs/how_to/#tools) for how-to guides covering tool-calling, built-in tools, custom tools, and more information.\n",
+ "\n",
+ "LangChain tools-- instances of [BaseTool](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.BaseTool.html)-- are [Runnables](/docs/concepts/#runnable-interface) with additional constraints that enable them to be invoked effectively by language models:\n",
+ "\n",
+ "- Their inputs are constrained to be serializable, specifically strings and Python `dict` objects;\n",
+ "- They contain names and descriptions indicating how and when they should be used;\n",
+ "- They may contain a detailed [args_schema](https://python.langchain.com/v0.2/docs/how_to/custom_tools/) for their arguments. That is, while a tool (as a `Runnable`) might accept a single `dict` input, the specific keys and type information needed to populate a dict should be specified in the `args_schema`.\n",
+ "\n",
+ "Runnables that accept string or `dict` input can be converted to tools using the [as_tool](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable.as_tool) method, which allows for the specification of names, descriptions, and additional schema information for arguments."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b4d76680-1b6b-4862-8c4f-22766a1d41f2",
+ "metadata": {},
+ "source": [
+ "## Basic usage\n",
+ "\n",
+ "With typed `dict` input:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "b2cc4231-64a3-4733-a284-932dcbf2fcc3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import List\n",
+ "\n",
+ "from langchain_core.runnables import RunnableLambda\n",
+ "from typing_extensions import TypedDict\n",
+ "\n",
+ "\n",
+ "class Args(TypedDict):\n",
+ " a: int\n",
+ " b: List[int]\n",
+ "\n",
+ "\n",
+ "def f(x: Args) -> str:\n",
+ " return str(x[\"a\"] * max(x[\"b\"]))\n",
+ "\n",
+ "\n",
+ "runnable = RunnableLambda(f)\n",
+ "as_tool = runnable.as_tool(\n",
+ " name=\"My tool\",\n",
+ " description=\"Explanation of when to use tool.\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "57f2d435-624d-459a-903d-8509fbbde610",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Explanation of when to use tool.\n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": [
+ "{'title': 'My tool',\n",
+ " 'type': 'object',\n",
+ " 'properties': {'a': {'title': 'A', 'type': 'integer'},\n",
+ " 'b': {'title': 'B', 'type': 'array', 'items': {'type': 'integer'}}},\n",
+ " 'required': ['a', 'b']}"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "print(as_tool.description)\n",
+ "\n",
+ "as_tool.args_schema.schema()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "54ae7384-a03d-4fa4-8cdf-9604a4bc39ee",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'6'"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "as_tool.invoke({\"a\": 3, \"b\": [1, 2]})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9038f587-4613-4f50-b349-135f9e7e3b15",
+ "metadata": {},
+ "source": [
+ "Without typing information, arg types can be specified via `arg_types`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "169f733c-4936-497f-8577-ee769dc16b88",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import Any, Dict\n",
+ "\n",
+ "\n",
+ "def g(x: Dict[str, Any]) -> str:\n",
+ " return str(x[\"a\"] * max(x[\"b\"]))\n",
+ "\n",
+ "\n",
+ "runnable = RunnableLambda(g)\n",
+ "as_tool = runnable.as_tool(\n",
+ " name=\"My tool\",\n",
+ " description=\"Explanation of when to use tool.\",\n",
+ " arg_types={\"a\": int, \"b\": List[int]},\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "32b1a992-8997-4c98-8eb2-c9fe9431b799",
+ "metadata": {},
+ "source": [
+ "Alternatively, we can add typing information via [Runnable.with_types](https://api.python.langchain.com/en/latest/runnables/langchain_core.runnables.base.Runnable.html#langchain_core.runnables.base.Runnable.with_types):"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "eb102705-89b7-48dc-9158-d36d5f98ae8e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "as_tool = runnable.with_types(input_type=Args).as_tool(\n",
+ " name=\"My tool\",\n",
+ " description=\"Explanation of when to use tool.\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7c474d85-4e01-4fae-9bba-0c6c8c26475c",
+ "metadata": {},
+ "source": [
+ "String input is also supported:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "c475282a-58d6-4c2b-af7d-99b73b7d8a13",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def f(x: str) -> str:\n",
+ " return x + \"a\"\n",
+ "\n",
+ "\n",
+ "def g(x: str) -> str:\n",
+ " return x + \"z\"\n",
+ "\n",
+ "\n",
+ "runnable = RunnableLambda(f) | g\n",
+ "as_tool = runnable.as_tool()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "ad6d8d96-3a87-40bd-a2ac-44a8acde0a8e",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "'baz'"
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "as_tool.invoke(\"b\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "89fdb3a7-d228-48f0-8f73-262af4febb58",
+ "metadata": {},
+ "source": [
+ "## In agents\n",
+ "\n",
+ "Below we will incorporate LangChain Runnables as tools in an [agent](/docs/concepts/#agents) application. We will demonstrate with:\n",
+ "\n",
+ "- a document [retriever](/docs/concepts/#retrievers);\n",
+ "- a simple [RAG](/docs/tutorials/rag/) chain, allowing an agent to delegate relevant queries to it.\n",
+ "\n",
+ "We first instantiate a chat model that supports [tool calling](/docs/how_to/tool_calling/):\n",
+ "\n",
+ "```{=mdx}\n",
+ "\n",
+ "```"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "d06c9f2a-4475-450f-9106-54db1d99623b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# | output: false\n",
+ "# | echo: false\n",
+ "\n",
+ "from langchain_openai import ChatOpenAI\n",
+ "\n",
+ "llm = ChatOpenAI(model=\"gpt-3.5-turbo-0125\", temperature=0)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e8a2038a-d762-4196-b5e3-fdb89c11e71d",
+ "metadata": {},
+ "source": [
+ "Following the [RAG tutorial](/docs/tutorials/rag/), let's first construct a retriever:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "23d2a47e-6712-4294-81c8-2c1d76b4bb81",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langchain_core.documents import Document\n",
+ "from langchain_core.vectorstores import InMemoryVectorStore\n",
+ "from langchain_openai import OpenAIEmbeddings\n",
+ "\n",
+ "documents = [\n",
+ " Document(\n",
+ " page_content=\"Dogs are great companions, known for their loyalty and friendliness.\",\n",
+ " ),\n",
+ " Document(\n",
+ " page_content=\"Cats are independent pets that often enjoy their own space.\",\n",
+ " ),\n",
+ "]\n",
+ "\n",
+ "vectorstore = InMemoryVectorStore.from_documents(\n",
+ " documents, embedding=OpenAIEmbeddings()\n",
+ ")\n",
+ "\n",
+ "retriever = vectorstore.as_retriever(\n",
+ " search_type=\"similarity\",\n",
+ " search_kwargs={\"k\": 1},\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9ba737ac-43a2-4a6f-b855-5bd0305017f1",
+ "metadata": {},
+ "source": [
+ "We next create use a simple pre-built [LangGraph agent](https://python.langchain.com/v0.2/docs/tutorials/agents/) and provide it the tool:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "c939cf2a-60e9-4afd-8b47-84d76ccb13f5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from langgraph.prebuilt import create_react_agent\n",
+ "\n",
+ "tools = [\n",
+ " retriever.as_tool(\n",
+ " name=\"pet_info_retriever\",\n",
+ " description=\"Get information about pets.\",\n",
+ " )\n",
+ "]\n",
+ "agent = create_react_agent(llm, tools)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "be29437b-a187-4a0a-9a5d-419c56f2434e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_W8cnfOjwqEn4cFcg19LN9mYD', 'function': {'arguments': '{\"__arg1\":\"dogs\"}', 'name': 'pet_info_retriever'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 60, 'total_tokens': 79}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-d7f81de9-1fb7-4caf-81ed-16dcdb0b2ab4-0', tool_calls=[{'name': 'pet_info_retriever', 'args': {'__arg1': 'dogs'}, 'id': 'call_W8cnfOjwqEn4cFcg19LN9mYD'}], usage_metadata={'input_tokens': 60, 'output_tokens': 19, 'total_tokens': 79})]}}\n",
+ "----\n",
+ "{'tools': {'messages': [ToolMessage(content=\"[Document(id='86f835fe-4bbe-4ec6-aeb4-489a8b541707', page_content='Dogs are great companions, known for their loyalty and friendliness.')]\", name='pet_info_retriever', tool_call_id='call_W8cnfOjwqEn4cFcg19LN9mYD')]}}\n",
+ "----\n",
+ "{'agent': {'messages': [AIMessage(content='Dogs are known for being great companions, known for their loyalty and friendliness.', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 134, 'total_tokens': 152}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-9ca5847a-a5eb-44c0-a774-84cc2c5bbc5b-0', usage_metadata={'input_tokens': 134, 'output_tokens': 18, 'total_tokens': 152})]}}\n",
+ "----\n"
+ ]
+ }
+ ],
+ "source": [
+ "for chunk in agent.stream({\"messages\": [(\"human\", \"What are dogs known for?\")]}):\n",
+ " print(chunk)\n",
+ " print(\"----\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "96f2ac9c-36f4-4b7a-ae33-f517734c86aa",
+ "metadata": {},
+ "source": [
+ "See [LangSmith trace](https://smith.langchain.com/public/44e438e3-2faf-45bd-b397-5510fc145eb9/r) for the above run."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a722fd8a-b957-4ba7-b408-35596b76835f",
+ "metadata": {},
+ "source": [
+ "Going further, we can create a simple [RAG](/docs/tutorials/rag/) chain that takes an additional parameter-- here, the \"style\" of the answer."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "bea518c9-c711-47c2-b8cc-dbd102f71f09",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from operator import itemgetter\n",
+ "\n",
+ "from langchain_core.output_parsers import StrOutputParser\n",
+ "from langchain_core.prompts import ChatPromptTemplate\n",
+ "from langchain_core.runnables import RunnablePassthrough\n",
+ "\n",
+ "system_prompt = \"\"\"\n",
+ "You are an assistant for question-answering tasks.\n",
+ "Use the below context to answer the question. If\n",
+ "you don't know the answer, say you don't know.\n",
+ "Use three sentences maximum and keep the answer\n",
+ "concise.\n",
+ "\n",
+ "Answer in the style of {answer_style}.\n",
+ "\n",
+ "Question: {question}\n",
+ "\n",
+ "Context: {context}\n",
+ "\"\"\"\n",
+ "\n",
+ "prompt = ChatPromptTemplate.from_messages([(\"system\", system_prompt)])\n",
+ "\n",
+ "rag_chain = (\n",
+ " {\n",
+ " \"context\": itemgetter(\"question\") | retriever,\n",
+ " \"question\": itemgetter(\"question\"),\n",
+ " \"answer_style\": itemgetter(\"answer_style\"),\n",
+ " }\n",
+ " | prompt\n",
+ " | llm\n",
+ " | StrOutputParser()\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "955a23db-5218-4c34-8486-450a2ddb3443",
+ "metadata": {},
+ "source": [
+ "Note that the input schema for our chain contains the required arguments, so it converts to a tool without further specification:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "2c9f6e61-80ed-4abb-8e77-84de3ccbc891",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'title': 'RunnableParallelInput',\n",
+ " 'type': 'object',\n",
+ " 'properties': {'question': {'title': 'Question'},\n",
+ " 'answer_style': {'title': 'Answer Style'}}}"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "rag_chain.input_schema.schema()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "a3f9cf5b-8c71-4b0f-902b-f92e028780c9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "rag_tool = rag_chain.as_tool(\n",
+ " name=\"pet_expert\",\n",
+ " description=\"Get information about pets.\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4570615b-8f96-4d97-ae01-1c08b14be584",
+ "metadata": {},
+ "source": [
+ "Below we again invoke the agent. Note that the agent populates the required parameters in its `tool_calls`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "06409913-a2ad-400f-a202-7b8dd2ef483a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "{'agent': {'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_17iLPWvOD23zqwd1QVQ00Y63', 'function': {'arguments': '{\"question\":\"What are dogs known for according to pirates?\",\"answer_style\":\"quote\"}', 'name': 'pet_expert'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 28, 'prompt_tokens': 59, 'total_tokens': 87}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-7fef44f3-7bba-4e63-8c51-2ad9c5e65e2e-0', tool_calls=[{'name': 'pet_expert', 'args': {'question': 'What are dogs known for according to pirates?', 'answer_style': 'quote'}, 'id': 'call_17iLPWvOD23zqwd1QVQ00Y63'}], usage_metadata={'input_tokens': 59, 'output_tokens': 28, 'total_tokens': 87})]}}\n",
+ "----\n",
+ "{'tools': {'messages': [ToolMessage(content='\"Dogs are known for their loyalty and friendliness, making them great companions for pirates on long sea voyages.\"', name='pet_expert', tool_call_id='call_17iLPWvOD23zqwd1QVQ00Y63')]}}\n",
+ "----\n",
+ "{'agent': {'messages': [AIMessage(content='According to pirates, dogs are known for their loyalty and friendliness, making them great companions for pirates on long sea voyages.', response_metadata={'token_usage': {'completion_tokens': 27, 'prompt_tokens': 119, 'total_tokens': 146}, 'model_name': 'gpt-3.5-turbo-0125', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-5a30edc3-7be0-4743-b980-ca2f8cad9b8d-0', usage_metadata={'input_tokens': 119, 'output_tokens': 27, 'total_tokens': 146})]}}\n",
+ "----\n"
+ ]
+ }
+ ],
+ "source": [
+ "agent = create_react_agent(llm, [rag_tool])\n",
+ "\n",
+ "for chunk in agent.stream(\n",
+ " {\"messages\": [(\"human\", \"What would a pirate say dogs are known for?\")]}\n",
+ "):\n",
+ " print(chunk)\n",
+ " print(\"----\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "96cc9bc3-e79e-49a8-9915-428ea225358b",
+ "metadata": {},
+ "source": [
+ "See [LangSmith trace](https://smith.langchain.com/public/147ae4e6-4dfb-4dd9-8ca0-5c5b954f08ac/r) for the above run."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.4"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/docs/how_to/index.mdx b/docs/docs/how_to/index.mdx
index 1fb57fab48..422b06e11e 100644
--- a/docs/docs/how_to/index.mdx
+++ b/docs/docs/how_to/index.mdx
@@ -187,6 +187,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: convert Runnables to tools](/docs/how_to/convert_runnable_to_tool)
- [How to: use 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)
diff --git a/libs/community/langchain_community/tools/e2b_data_analysis/tool.py b/libs/community/langchain_community/tools/e2b_data_analysis/tool.py
index 6de115e0b4..f0498aeb25 100644
--- a/libs/community/langchain_community/tools/e2b_data_analysis/tool.py
+++ b/libs/community/langchain_community/tools/e2b_data_analysis/tool.py
@@ -234,7 +234,7 @@ class E2BDataAnalysisTool(BaseTool):
]
self.description = self.description + "\n" + self.uploaded_files_description
- def as_tool(self) -> Tool:
+ def as_tool(self) -> Tool: # type: ignore[override]
return Tool.from_function(
func=self._run,
name=self.name,
diff --git a/libs/core/langchain_core/runnables/base.py b/libs/core/langchain_core/runnables/base.py
index 75656ed599..4b24b8b1aa 100644
--- a/libs/core/langchain_core/runnables/base.py
+++ b/libs/core/langchain_core/runnables/base.py
@@ -92,6 +92,7 @@ if TYPE_CHECKING:
from langchain_core.runnables.fallbacks import (
RunnableWithFallbacks as RunnableWithFallbacksT,
)
+ from langchain_core.tools import BaseTool
from langchain_core.tracers.log_stream import (
RunLog,
RunLogPatch,
@@ -2006,6 +2007,78 @@ class Runnable(Generic[Input, Output], ABC):
if hasattr(iterator_, "aclose"):
await iterator_.aclose()
+ @beta_decorator.beta(message="This API is in beta and may change in the future.")
+ def as_tool(
+ self,
+ *,
+ name: Optional[str] = None,
+ description: Optional[str] = None,
+ arg_types: Optional[Dict[str, Type]] = None,
+ ) -> BaseTool:
+ """Create a BaseTool from a Runnable.
+
+ ``as_tool`` will instantiate a BaseTool with a name, description, and
+ ``args_schema`` from a runnable. Where possible, schemas are inferred
+ from ``runnable.get_input_schema``. Alternatively (e.g., if the
+ runnable takes a dict as input and the specific dict keys are not typed),
+ pass ``arg_types`` to specify the required arguments.
+
+ Typed dict input:
+
+ .. code-block:: python
+
+ from typing import List
+ from typing_extensions import TypedDict
+ from langchain_core.runnables import RunnableLambda
+
+ class Args(TypedDict):
+ a: int
+ b: List[int]
+
+ def f(x: Args) -> str:
+ return str(x["a"] * max(x["b"]))
+
+ runnable = RunnableLambda(f)
+ as_tool = runnable.as_tool()
+ as_tool.invoke({"a": 3, "b": [1, 2]})
+
+ ``dict`` input, specifying schema:
+
+ .. code-block:: python
+
+ from typing import Any, Dict, List
+ from langchain_core.runnables import RunnableLambda
+
+ def f(x: Dict[str, Any]) -> str:
+ return str(x["a"] * max(x["b"]))
+
+ runnable = RunnableLambda(f)
+ as_tool = runnable.as_tool(arg_types={"a": int, "b": List[int]})
+ as_tool.invoke({"a": 3, "b": [1, 2]})
+
+ String input:
+
+ .. code-block:: python
+
+ from langchain_core.runnables import RunnableLambda
+
+ def f(x: str) -> str:
+ return x + "a"
+
+ def g(x: str) -> str:
+ return x + "z"
+
+ runnable = RunnableLambda(f) | g
+ as_tool = runnable.as_tool()
+ as_tool.invoke("b")
+ """
+ # Avoid circular import
+ from langchain_core.tools import convert_runnable_to_tool
+
+ return convert_runnable_to_tool(
+ self, name=name, description=description, arg_types=arg_types
+ )
+
class RunnableSerializable(Serializable, Runnable[Input, Output]):
"""Runnable that can be serialized to JSON."""
diff --git a/libs/core/langchain_core/tools.py b/libs/core/langchain_core/tools.py
index ae5e9853c3..49ba78f469 100644
--- a/libs/core/langchain_core/tools.py
+++ b/libs/core/langchain_core/tools.py
@@ -39,6 +39,7 @@ from typing import (
Tuple,
Type,
Union,
+ get_type_hints,
)
from typing_extensions import Annotated, get_args, get_origin
@@ -1218,3 +1219,76 @@ class BaseToolkit(BaseModel, ABC):
@abstractmethod
def get_tools(self) -> List[BaseTool]:
"""Get the tools in the toolkit."""
+
+
+def _get_description_from_runnable(runnable: Runnable) -> str:
+ """Generate a placeholder description of a runnable."""
+ input_schema = runnable.input_schema.schema()
+ return f"Takes {input_schema}."
+
+
+def _get_schema_from_runnable_and_arg_types(
+ runnable: Runnable,
+ name: str,
+ arg_types: Optional[Dict[str, Type]] = None,
+) -> Type[BaseModel]:
+ """Infer args_schema for tool."""
+ if arg_types is None:
+ try:
+ arg_types = get_type_hints(runnable.InputType)
+ except TypeError as e:
+ raise TypeError(
+ "Tool input must be str or dict. If dict, dict arguments must be "
+ "typed. Either annotate types (e.g., with TypedDict) or pass "
+ f"arg_types into `.as_tool` to specify. {str(e)}"
+ )
+ fields = {key: (key_type, Field(...)) for key, key_type in arg_types.items()}
+ return create_model(name, **fields) # type: ignore
+
+
+def convert_runnable_to_tool(
+ runnable: Runnable,
+ name: Optional[str] = None,
+ description: Optional[str] = None,
+ arg_types: Optional[Dict[str, Type]] = None,
+) -> BaseTool:
+ """Convert a Runnable into a BaseTool."""
+ description = description or _get_description_from_runnable(runnable)
+ name = name or runnable.get_name()
+
+ schema = runnable.input_schema.schema()
+ if schema.get("type") == "string":
+ return Tool(
+ name=name,
+ func=runnable.invoke,
+ coroutine=runnable.ainvoke,
+ description=description,
+ )
+ else:
+
+ async def ainvoke_wrapper(
+ callbacks: Optional[Callbacks] = None, **kwargs: Any
+ ) -> Any:
+ return await runnable.ainvoke(kwargs, config={"callbacks": callbacks})
+
+ def invoke_wrapper(callbacks: Optional[Callbacks] = None, **kwargs: Any) -> Any:
+ return runnable.invoke(kwargs, config={"callbacks": callbacks})
+
+ if (
+ arg_types is None
+ and schema.get("type") == "object"
+ and schema.get("properties")
+ ):
+ args_schema = runnable.input_schema
+ else:
+ args_schema = _get_schema_from_runnable_and_arg_types(
+ runnable, name, arg_types=arg_types
+ )
+
+ return StructuredTool.from_function(
+ name=name,
+ func=invoke_wrapper,
+ coroutine=ainvoke_wrapper,
+ description=description,
+ args_schema=args_schema,
+ )
diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py
index 968b2a03d4..7760e90f78 100644
--- a/libs/core/tests/unit_tests/test_tools.py
+++ b/libs/core/tests/unit_tests/test_tools.py
@@ -11,14 +11,14 @@ from functools import partial
from typing import Any, Callable, Dict, List, Optional, Type, Union
import pytest
-from typing_extensions import Annotated
+from typing_extensions import Annotated, TypedDict
from langchain_core.callbacks import (
AsyncCallbackManagerForToolRun,
CallbackManagerForToolRun,
)
from langchain_core.pydantic_v1 import BaseModel, ValidationError
-from langchain_core.runnables import ensure_config
+from langchain_core.runnables import Runnable, RunnableLambda, ensure_config
from langchain_core.tools import (
BaseTool,
SchemaAnnotationError,
@@ -987,3 +987,85 @@ def test_tool_annotated_descriptions() -> None:
},
"required": ["bar", "baz"],
}
+
+
+def test_convert_from_runnable_dict() -> None:
+ # Test with typed dict input
+ class Args(TypedDict):
+ a: int
+ b: List[int]
+
+ def f(x: Args) -> str:
+ return str(x["a"] * max(x["b"]))
+
+ runnable: Runnable = RunnableLambda(f)
+ as_tool = runnable.as_tool()
+ args_schema = as_tool.args_schema
+ assert args_schema is not None
+ assert args_schema.schema() == {
+ "title": "f",
+ "type": "object",
+ "properties": {
+ "a": {"title": "A", "type": "integer"},
+ "b": {"title": "B", "type": "array", "items": {"type": "integer"}},
+ },
+ "required": ["a", "b"],
+ }
+ assert as_tool.description
+ result = as_tool.invoke({"a": 3, "b": [1, 2]})
+ assert result == "6"
+
+ as_tool = runnable.as_tool(name="my tool", description="test description")
+ assert as_tool.name == "my tool"
+ assert as_tool.description == "test description"
+
+ # Dict without typed input-- must supply arg types
+ def g(x: Dict[str, Any]) -> str:
+ return str(x["a"] * max(x["b"]))
+
+ runnable = RunnableLambda(g)
+ as_tool = runnable.as_tool(arg_types={"a": int, "b": List[int]})
+ result = as_tool.invoke({"a": 3, "b": [1, 2]})
+ assert result == "6"
+
+ # Test with config
+ def h(x: Dict[str, Any]) -> str:
+ config = ensure_config()
+ assert config["configurable"]["foo"] == "not-bar"
+ return str(x["a"] * max(x["b"]))
+
+ runnable = RunnableLambda(h)
+ as_tool = runnable.as_tool(arg_types={"a": int, "b": List[int]})
+ result = as_tool.invoke(
+ {"a": 3, "b": [1, 2]}, config={"configurable": {"foo": "not-bar"}}
+ )
+ assert result == "6"
+
+
+def test_convert_from_runnable_other() -> None:
+ # String input
+ def f(x: str) -> str:
+ return x + "a"
+
+ def g(x: str) -> str:
+ return x + "z"
+
+ runnable: Runnable = RunnableLambda(f) | g
+ as_tool = runnable.as_tool()
+ args_schema = as_tool.args_schema
+ assert args_schema is None
+ assert as_tool.description
+
+ result = as_tool.invoke("b")
+ assert result == "baz"
+
+ # Test with config
+ def h(x: str) -> str:
+ config = ensure_config()
+ assert config["configurable"]["foo"] == "not-bar"
+ return x + "a"
+
+ runnable = RunnableLambda(h)
+ as_tool = runnable.as_tool()
+ result = as_tool.invoke("b", config={"configurable": {"foo": "not-bar"}})
+ assert result == "ba"
diff --git a/libs/core/tests/unit_tests/utils/test_function_calling.py b/libs/core/tests/unit_tests/utils/test_function_calling.py
index 6702b5ad63..32c6349a5f 100644
--- a/libs/core/tests/unit_tests/utils/test_function_calling.py
+++ b/libs/core/tests/unit_tests/utils/test_function_calling.py
@@ -4,10 +4,11 @@ from typing import Any, Callable, Dict, List, Literal, Optional, Type
import pytest
from pydantic import BaseModel as BaseModelV2Maybe # pydantic: ignore
from pydantic import Field as FieldV2Maybe # pydantic: ignore
-from typing_extensions import Annotated
+from typing_extensions import Annotated, TypedDict
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from langchain_core.pydantic_v1 import BaseModel, Field
+from langchain_core.runnables import Runnable, RunnableLambda
from langchain_core.tools import BaseTool, tool
from langchain_core.utils.function_calling import (
convert_to_openai_function,
@@ -52,6 +53,18 @@ def function() -> Callable:
return dummy_function
+@pytest.fixture()
+def runnable() -> Runnable:
+ class Args(TypedDict):
+ arg1: Annotated[int, "foo"]
+ arg2: Annotated[Literal["bar", "baz"], "one of 'bar', 'baz'"]
+
+ def dummy_function(input_dict: Args) -> None:
+ pass
+
+ return RunnableLambda(dummy_function)
+
+
@pytest.fixture()
def dummy_tool() -> BaseTool:
class Schema(BaseModel):
@@ -141,6 +154,7 @@ def test_convert_to_openai_function(
json_schema: Dict,
annotated_function: Callable,
dummy_pydantic: Type[BaseModel],
+ runnable: Runnable,
) -> None:
expected = {
"name": "dummy_function",
@@ -173,6 +187,23 @@ def test_convert_to_openai_function(
actual = convert_to_openai_function(fn) # type: ignore
assert actual == expected
+ # Test runnables
+ actual = convert_to_openai_function(runnable.as_tool(description="dummy function"))
+ parameters = {
+ "type": "object",
+ "properties": {
+ "arg1": {"type": "integer"},
+ "arg2": {
+ "enum": ["bar", "baz"],
+ "type": "string",
+ },
+ },
+ "required": ["arg1", "arg2"],
+ }
+ runnable_expected = expected.copy()
+ runnable_expected["parameters"] = parameters
+ assert actual == runnable_expected
+
def test_convert_to_openai_function_nested() -> None:
class Nested(BaseModel):