mirror of
https://github.com/hwchase17/langchain
synced 2024-11-10 01:10:59 +00:00
core[patch]: support conversion of runnables to tools (#23992)
Open to other thoughts on UX. string input: ```python as_tool = retriever.as_tool() as_tool.invoke("cat") # [Document(...), ...] ``` typed dict input: ```python class Args(TypedDict): key: int def f(x: Args) -> str: return str(x["key"] * 2) as_tool = RunnableLambda(f).as_tool( name="my tool", description="description", # name, description are inferred if not supplied ) as_tool.invoke({"key": 3}) # "6" ``` for untyped dict input, allow specification of parameters + types ```python def g(x: Dict[str, Any]) -> str: return str(x["key"] * 2) as_tool = RunnableLambda(g).as_tool(arg_types={"key": int}) result = as_tool.invoke({"key": 3}) # "6" ``` Passing the `arg_types` is slightly awkward but necessary to ensure tool calls populate parameters correctly: ```python from typing import Any, Dict from langchain_core.runnables import RunnableLambda from langchain_openai import ChatOpenAI def f(x: Dict[str, Any]) -> str: return str(x["key"] * 2) runnable = RunnableLambda(f) as_tool = runnable.as_tool(arg_types={"key": int}) llm = ChatOpenAI().bind_tools([as_tool]) result = llm.invoke("Use the tool on 3.") tool_call = result.tool_calls[0] args = tool_call["args"] assert args == {"key": 3} as_tool.run(args) ``` Contrived (?) example with langgraph agent as a tool: ```python from typing import List, Literal from typing_extensions import TypedDict from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent llm = ChatOpenAI(temperature=0) def magic_function(input: int) -> int: """Applies a magic function to an input.""" return input + 2 agent_1 = create_react_agent(llm, [magic_function]) class Message(TypedDict): role: Literal["human"] content: str agent_tool = agent_1.as_tool( arg_types={"messages": List[Message]}, name="Jeeves", description="Ask Jeeves.", ) agent_2 = create_react_agent(llm, [agent_tool]) ``` --------- Co-authored-by: Erick Friis <erick@langchain.dev>
This commit is contained in:
parent
b63a48b7d3
commit
975b6129f6
541
docs/docs/how_to/convert_runnable_to_tool.ipynb
Normal file
541
docs/docs/how_to/convert_runnable_to_tool.ipynb
Normal file
@ -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",
|
||||
"<ChatModelTabs\n",
|
||||
" customVarName=\"llm\"\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': 'RunnableParallel<context,question,answer_style>Input',\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
|
||||
}
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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"
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user