mirror of
https://github.com/hwchase17/langchain.git
synced 2025-06-23 15:19:33 +00:00
core[patch]: propagate parse_docstring
to tool decorator (#24123)
Disabled by default. ```python from langchain_core.tools import tool @tool(parse_docstring=True) def foo(bar: str, baz: int) -> str: """The foo. Args: bar: this is the bar baz: this is the baz """ return bar foo.args_schema.schema() ``` ```json { "title": "fooSchema", "description": "The foo.", "type": "object", "properties": { "bar": { "title": "Bar", "description": "this is the bar", "type": "string" }, "baz": { "title": "Baz", "description": "this is the baz", "type": "integer" } }, "required": [ "bar", "baz" ] } ```
This commit is contained in:
parent
4121d4151f
commit
8ee8ca7c83
@ -16,13 +16,15 @@
|
|||||||
"| args_schema | Pydantic BaseModel | Optional but recommended, can be used to provide more information (e.g., few-shot examples) or validation for expected parameters |\n",
|
"| args_schema | Pydantic BaseModel | Optional but recommended, can be used to provide more information (e.g., few-shot examples) or validation for expected parameters |\n",
|
||||||
"| return_direct | boolean | Only relevant for agents. When True, after invoking the given tool, the agent will stop and return the result direcly to the user. |\n",
|
"| return_direct | boolean | Only relevant for agents. When True, after invoking the given tool, the agent will stop and return the result direcly to the user. |\n",
|
||||||
"\n",
|
"\n",
|
||||||
"LangChain provides 3 ways to create tools:\n",
|
"LangChain supports the creation of tools from:\n",
|
||||||
"\n",
|
"\n",
|
||||||
"1. Using [@tool decorator](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.tool.html#langchain_core.tools.tool) -- the simplest way to define a custom tool.\n",
|
"1. Functions;\n",
|
||||||
"2. Using [StructuredTool.from_function](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.StructuredTool.html#langchain_core.tools.StructuredTool.from_function) class method -- this is similar to the `@tool` decorator, but allows more configuration and specification of both sync and async implementations.\n",
|
"2. LangChain [Runnables](/docs/concepts#runnable-interface);\n",
|
||||||
"3. By sub-classing from [BaseTool](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.BaseTool.html) -- This is the most flexible method, it provides the largest degree of control, at the expense of more effort and code.\n",
|
"3. By sub-classing from [BaseTool](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.BaseTool.html) -- This is the most flexible method, it provides the largest degree of control, at the expense of more effort and code.\n",
|
||||||
"\n",
|
"\n",
|
||||||
"The `@tool` or the `StructuredTool.from_function` class method should be sufficient for most use cases.\n",
|
"Creating tools from functions may be sufficient for most use cases, and can be done via a simple [@tool decorator](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.tool.html#langchain_core.tools.tool). If more configuration is needed-- e.g., specification of both sync and async implementations-- one can also use the [StructuredTool.from_function](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.StructuredTool.html#langchain_core.tools.StructuredTool.from_function) class method.\n",
|
||||||
|
"\n",
|
||||||
|
"In this guide we provide an overview of these methods.\n",
|
||||||
"\n",
|
"\n",
|
||||||
":::{.callout-tip}\n",
|
":::{.callout-tip}\n",
|
||||||
"\n",
|
"\n",
|
||||||
@ -35,7 +37,9 @@
|
|||||||
"id": "c7326b23",
|
"id": "c7326b23",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"## @tool decorator\n",
|
"## Creating tools from functions\n",
|
||||||
|
"\n",
|
||||||
|
"### @tool decorator\n",
|
||||||
"\n",
|
"\n",
|
||||||
"This `@tool` decorator is the simplest way to define a custom tool. The decorator uses the function name as the tool name by default, but this can be overridden by passing a string as the first argument. Additionally, the decorator will use the function's docstring as the tool's description - so a docstring MUST be provided. "
|
"This `@tool` decorator is the simplest way to define a custom tool. The decorator uses the function name as the tool name by default, but this can be overridden by passing a string as the first argument. Additionally, the decorator will use the function's docstring as the tool's description - so a docstring MUST be provided. "
|
||||||
]
|
]
|
||||||
@ -51,7 +55,7 @@
|
|||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"text": [
|
||||||
"multiply\n",
|
"multiply\n",
|
||||||
"multiply(a: int, b: int) -> int - Multiply two numbers.\n",
|
"Multiply two numbers.\n",
|
||||||
"{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}\n"
|
"{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}\n"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -96,6 +100,57 @@
|
|||||||
" return a * b"
|
" return a * b"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "8f0edc51-c586-414c-8941-c8abe779943f",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"Note that `@tool` supports parsing of annotations, nested schemas, and other features:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "5626423f-053e-4a66-adca-1d794d835397",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{'title': 'multiply_by_maxSchema',\n",
|
||||||
|
" 'description': 'Multiply a by the maximum of b.',\n",
|
||||||
|
" 'type': 'object',\n",
|
||||||
|
" 'properties': {'a': {'title': 'A',\n",
|
||||||
|
" 'description': 'scale factor',\n",
|
||||||
|
" 'type': 'string'},\n",
|
||||||
|
" 'b': {'title': 'B',\n",
|
||||||
|
" 'description': 'list of ints over which to take maximum',\n",
|
||||||
|
" 'type': 'array',\n",
|
||||||
|
" 'items': {'type': 'integer'}}},\n",
|
||||||
|
" 'required': ['a', 'b']}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 3,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"from typing import Annotated, List\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"@tool\n",
|
||||||
|
"def multiply_by_max(\n",
|
||||||
|
" a: Annotated[str, \"scale factor\"],\n",
|
||||||
|
" b: Annotated[List[int], \"list of ints over which to take maximum\"],\n",
|
||||||
|
") -> int:\n",
|
||||||
|
" \"\"\"Multiply a by the maximum of b.\"\"\"\n",
|
||||||
|
" return a * max(b)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"multiply_by_max.args_schema.schema()"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"id": "98d6eee9",
|
"id": "98d6eee9",
|
||||||
@ -106,7 +161,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 3,
|
"execution_count": 4,
|
||||||
"id": "9216d03a-f6ea-4216-b7e1-0661823a4c0b",
|
"id": "9216d03a-f6ea-4216-b7e1-0661823a4c0b",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -115,7 +170,7 @@
|
|||||||
"output_type": "stream",
|
"output_type": "stream",
|
||||||
"text": [
|
"text": [
|
||||||
"multiplication-tool\n",
|
"multiplication-tool\n",
|
||||||
"multiplication-tool(a: int, b: int) -> int - Multiply two numbers.\n",
|
"Multiply two numbers.\n",
|
||||||
"{'a': {'title': 'A', 'description': 'first number', 'type': 'integer'}, 'b': {'title': 'B', 'description': 'second number', 'type': 'integer'}}\n",
|
"{'a': {'title': 'A', 'description': 'first number', 'type': 'integer'}, 'b': {'title': 'B', 'description': 'second number', 'type': 'integer'}}\n",
|
||||||
"True\n"
|
"True\n"
|
||||||
]
|
]
|
||||||
@ -143,19 +198,84 @@
|
|||||||
"print(multiply.return_direct)"
|
"print(multiply.return_direct)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "33a9e94d-0b60-48f3-a4c2-247dce096e66",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"#### Docstring parsing"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "6d0cb586-93d4-4ff1-9779-71df7853cb68",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"`@tool` can optionally parse [Google Style docstrings](https://google.github.io/styleguide/pyguide.html#383-functions-and-methods) and associate the docstring components (such as arg descriptions) to the relevant parts of the tool schema. To toggle this behavior, specify `parse_docstring`:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"id": "336f5538-956e-47d5-9bde-b732559f9e61",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{'title': 'fooSchema',\n",
|
||||||
|
" 'description': 'The foo.',\n",
|
||||||
|
" 'type': 'object',\n",
|
||||||
|
" 'properties': {'bar': {'title': 'Bar',\n",
|
||||||
|
" 'description': 'The bar.',\n",
|
||||||
|
" 'type': 'string'},\n",
|
||||||
|
" 'baz': {'title': 'Baz', 'description': 'The baz.', 'type': 'integer'}},\n",
|
||||||
|
" 'required': ['bar', 'baz']}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"@tool(parse_docstring=True)\n",
|
||||||
|
"def foo(bar: str, baz: int) -> str:\n",
|
||||||
|
" \"\"\"The foo.\n",
|
||||||
|
"\n",
|
||||||
|
" Args:\n",
|
||||||
|
" bar: The bar.\n",
|
||||||
|
" baz: The baz.\n",
|
||||||
|
" \"\"\"\n",
|
||||||
|
" return bar\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"foo.args_schema.schema()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "f18a2503-5393-421b-99fa-4a01dd824d0e",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
":::{.callout-caution}\n",
|
||||||
|
"By default, `@tool(parse_docstring=True)` will raise `ValueError` if the docstring does not parse correctly. See [API Reference](https://api.python.langchain.com/en/latest/tools/langchain_core.tools.tool.html) for detail and examples.\n",
|
||||||
|
":::"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"id": "b63fcc3b",
|
"id": "b63fcc3b",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"source": [
|
"source": [
|
||||||
"## StructuredTool\n",
|
"### StructuredTool\n",
|
||||||
"\n",
|
"\n",
|
||||||
"The `StrurcturedTool.from_function` class method provides a bit more configurability than the `@tool` decorator, without requiring much additional code."
|
"The `StrurcturedTool.from_function` class method provides a bit more configurability than the `@tool` decorator, without requiring much additional code."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 4,
|
"execution_count": 6,
|
||||||
"id": "564fbe6f-11df-402d-b135-ef6ff25e1e63",
|
"id": "564fbe6f-11df-402d-b135-ef6ff25e1e63",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -198,7 +318,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 5,
|
"execution_count": 7,
|
||||||
"id": "6bc055d4-1fbe-4db5-8881-9c382eba6b1b",
|
"id": "6bc055d4-1fbe-4db5-8881-9c382eba6b1b",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -208,7 +328,7 @@
|
|||||||
"text": [
|
"text": [
|
||||||
"6\n",
|
"6\n",
|
||||||
"Calculator\n",
|
"Calculator\n",
|
||||||
"Calculator(a: int, b: int) -> int - multiply numbers\n",
|
"multiply numbers\n",
|
||||||
"{'a': {'title': 'A', 'description': 'first number', 'type': 'integer'}, 'b': {'title': 'B', 'description': 'second number', 'type': 'integer'}}\n"
|
"{'a': {'title': 'A', 'description': 'first number', 'type': 'integer'}, 'b': {'title': 'B', 'description': 'second number', 'type': 'integer'}}\n"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -239,6 +359,63 @@
|
|||||||
"print(calculator.args)"
|
"print(calculator.args)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "5517995d-54e3-449b-8fdb-03561f5e4647",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Creating tools from Runnables\n",
|
||||||
|
"\n",
|
||||||
|
"LangChain [Runnables](/docs/concepts#runnable-interface) 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.\n",
|
||||||
|
"\n",
|
||||||
|
"Example usage:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"id": "8ef593c5-cf72-4c10-bfc9-7d21874a0c24",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"{'answer_style': {'title': 'Answer Style', 'type': 'string'}}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 9,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"from langchain_core.language_models import GenericFakeChatModel\n",
|
||||||
|
"from langchain_core.output_parsers import StrOutputParser\n",
|
||||||
|
"from langchain_core.prompts import ChatPromptTemplate\n",
|
||||||
|
"\n",
|
||||||
|
"prompt = ChatPromptTemplate.from_messages(\n",
|
||||||
|
" [(\"human\", \"Hello. Please respond in the style of {answer_style}.\")]\n",
|
||||||
|
")\n",
|
||||||
|
"\n",
|
||||||
|
"# Placeholder LLM\n",
|
||||||
|
"llm = GenericFakeChatModel(messages=iter([\"hello matey\"]))\n",
|
||||||
|
"\n",
|
||||||
|
"chain = prompt | llm | StrOutputParser()\n",
|
||||||
|
"\n",
|
||||||
|
"as_tool = chain.as_tool(\n",
|
||||||
|
" name=\"Style responder\", description=\"Description of when to use tool.\"\n",
|
||||||
|
")\n",
|
||||||
|
"as_tool.args"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "0521b787-a146-45a6-8ace-ae1ac4669dd7",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"See [this guide](/docs/how_to/convert_runnable_to_tool) for more detail."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"id": "b840074b-9c10-4ca0-aed8-626c52b2398f",
|
"id": "b840074b-9c10-4ca0-aed8-626c52b2398f",
|
||||||
@ -251,7 +428,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 16,
|
"execution_count": 10,
|
||||||
"id": "1dad8f8e",
|
"id": "1dad8f8e",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@ -300,7 +477,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 11,
|
||||||
"id": "bb551c33",
|
"id": "bb551c33",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -351,7 +528,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 8,
|
"execution_count": 12,
|
||||||
"id": "6615cb77-fd4c-4676-8965-f92cc71d4944",
|
"id": "6615cb77-fd4c-4676-8965-f92cc71d4944",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -383,7 +560,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 9,
|
"execution_count": 13,
|
||||||
"id": "bb2af583-eadd-41f4-a645-bf8748bd3dcd",
|
"id": "bb2af583-eadd-41f4-a645-bf8748bd3dcd",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -428,7 +605,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 10,
|
"execution_count": 14,
|
||||||
"id": "4ad0932c-8610-4278-8c57-f9218f654c8a",
|
"id": "4ad0932c-8610-4278-8c57-f9218f654c8a",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -473,7 +650,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 11,
|
"execution_count": 15,
|
||||||
"id": "7094c0e8-6192-4870-a942-aad5b5ae48fd",
|
"id": "7094c0e8-6192-4870-a942-aad5b5ae48fd",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
@ -496,7 +673,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 12,
|
"execution_count": 16,
|
||||||
"id": "b4d22022-b105-4ccc-a15b-412cb9ea3097",
|
"id": "b4d22022-b105-4ccc-a15b-412cb9ea3097",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -506,7 +683,7 @@
|
|||||||
"'Error: There is no city by the name of foobar.'"
|
"'Error: There is no city by the name of foobar.'"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 12,
|
"execution_count": 16,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@ -530,7 +707,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 13,
|
"execution_count": 17,
|
||||||
"id": "3fad1728-d367-4e1b-9b54-3172981271cf",
|
"id": "3fad1728-d367-4e1b-9b54-3172981271cf",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -540,7 +717,7 @@
|
|||||||
"\"There is no such city, but it's probably above 0K there!\""
|
"\"There is no such city, but it's probably above 0K there!\""
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 13,
|
"execution_count": 17,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@ -564,7 +741,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 14,
|
"execution_count": 18,
|
||||||
"id": "ebfe7c1f-318d-4e58-99e1-f31e69473c46",
|
"id": "ebfe7c1f-318d-4e58-99e1-f31e69473c46",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@ -574,7 +751,7 @@
|
|||||||
"'The following errors occurred during tool execution: `Error: There is no city by the name of foobar.`'"
|
"'The following errors occurred during tool execution: `Error: There is no city by the name of foobar.`'"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 14,
|
"execution_count": 18,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@ -609,7 +786,7 @@
|
|||||||
"name": "python",
|
"name": "python",
|
||||||
"nbconvert_exporter": "python",
|
"nbconvert_exporter": "python",
|
||||||
"pygments_lexer": "ipython3",
|
"pygments_lexer": "ipython3",
|
||||||
"version": "3.11.4"
|
"version": "3.10.4"
|
||||||
},
|
},
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"interpreter": {
|
"interpreter": {
|
||||||
|
@ -85,6 +85,8 @@ from langchain_core.runnables.config import (
|
|||||||
)
|
)
|
||||||
from langchain_core.runnables.utils import accepts_context
|
from langchain_core.runnables.utils import accepts_context
|
||||||
|
|
||||||
|
FILTERED_ARGS = ("run_manager", "callbacks")
|
||||||
|
|
||||||
|
|
||||||
class SchemaAnnotationError(TypeError):
|
class SchemaAnnotationError(TypeError):
|
||||||
"""Raised when 'args_schema' is missing or has an incorrect type annotation."""
|
"""Raised when 'args_schema' is missing or has an incorrect type annotation."""
|
||||||
@ -149,14 +151,27 @@ def _get_filtered_args(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _parse_python_function_docstring(function: Callable) -> Tuple[str, dict]:
|
def _parse_python_function_docstring(
|
||||||
|
function: Callable, annotations: dict, error_on_invalid_docstring: bool = False
|
||||||
|
) -> Tuple[str, dict]:
|
||||||
"""Parse the function and argument descriptions from the docstring of a function.
|
"""Parse the function and argument descriptions from the docstring of a function.
|
||||||
|
|
||||||
Assumes the function docstring follows Google Python style guide.
|
Assumes the function docstring follows Google Python style guide.
|
||||||
"""
|
"""
|
||||||
|
invalid_docstring_error = ValueError(
|
||||||
|
f"Found invalid Google-Style docstring for {function}."
|
||||||
|
)
|
||||||
docstring = inspect.getdoc(function)
|
docstring = inspect.getdoc(function)
|
||||||
if docstring:
|
if docstring:
|
||||||
docstring_blocks = docstring.split("\n\n")
|
docstring_blocks = docstring.split("\n\n")
|
||||||
|
if error_on_invalid_docstring:
|
||||||
|
filtered_annotations = {
|
||||||
|
arg for arg in annotations if arg not in (*(FILTERED_ARGS), "return")
|
||||||
|
}
|
||||||
|
if filtered_annotations and (
|
||||||
|
len(docstring_blocks) < 2 or not docstring_blocks[1].startswith("Args:")
|
||||||
|
):
|
||||||
|
raise (invalid_docstring_error)
|
||||||
descriptors = []
|
descriptors = []
|
||||||
args_block = None
|
args_block = None
|
||||||
past_descriptors = False
|
past_descriptors = False
|
||||||
@ -173,6 +188,8 @@ def _parse_python_function_docstring(function: Callable) -> Tuple[str, dict]:
|
|||||||
continue
|
continue
|
||||||
description = " ".join(descriptors)
|
description = " ".join(descriptors)
|
||||||
else:
|
else:
|
||||||
|
if error_on_invalid_docstring:
|
||||||
|
raise (invalid_docstring_error)
|
||||||
description = ""
|
description = ""
|
||||||
args_block = None
|
args_block = None
|
||||||
arg_descriptions = {}
|
arg_descriptions = {}
|
||||||
@ -187,20 +204,38 @@ def _parse_python_function_docstring(function: Callable) -> Tuple[str, dict]:
|
|||||||
return description, arg_descriptions
|
return description, arg_descriptions
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_docstring_args_against_annotations(
|
||||||
|
arg_descriptions: dict, annotations: dict
|
||||||
|
) -> None:
|
||||||
|
"""Raise error if docstring arg is not in type annotations."""
|
||||||
|
for docstring_arg in arg_descriptions:
|
||||||
|
if docstring_arg not in annotations:
|
||||||
|
raise ValueError(
|
||||||
|
f"Arg {docstring_arg} in docstring not found in function signature."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _infer_arg_descriptions(
|
def _infer_arg_descriptions(
|
||||||
fn: Callable, *, parse_docstring: bool = False
|
fn: Callable,
|
||||||
|
*,
|
||||||
|
parse_docstring: bool = False,
|
||||||
|
error_on_invalid_docstring: bool = False,
|
||||||
) -> Tuple[str, dict]:
|
) -> Tuple[str, dict]:
|
||||||
"""Infer argument descriptions from a function's docstring."""
|
"""Infer argument descriptions from a function's docstring."""
|
||||||
if parse_docstring:
|
|
||||||
description, arg_descriptions = _parse_python_function_docstring(fn)
|
|
||||||
else:
|
|
||||||
description = inspect.getdoc(fn) or ""
|
|
||||||
arg_descriptions = {}
|
|
||||||
if hasattr(inspect, "get_annotations"):
|
if hasattr(inspect, "get_annotations"):
|
||||||
# This is for python < 3.10
|
# This is for python < 3.10
|
||||||
annotations = inspect.get_annotations(fn) # type: ignore
|
annotations = inspect.get_annotations(fn) # type: ignore
|
||||||
else:
|
else:
|
||||||
annotations = getattr(fn, "__annotations__", {})
|
annotations = getattr(fn, "__annotations__", {})
|
||||||
|
if parse_docstring:
|
||||||
|
description, arg_descriptions = _parse_python_function_docstring(
|
||||||
|
fn, annotations, error_on_invalid_docstring=error_on_invalid_docstring
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
description = inspect.getdoc(fn) or ""
|
||||||
|
arg_descriptions = {}
|
||||||
|
if parse_docstring:
|
||||||
|
_validate_docstring_args_against_annotations(arg_descriptions, annotations)
|
||||||
for arg, arg_type in annotations.items():
|
for arg, arg_type in annotations.items():
|
||||||
if arg in arg_descriptions:
|
if arg in arg_descriptions:
|
||||||
continue
|
continue
|
||||||
@ -222,6 +257,7 @@ def create_schema_from_function(
|
|||||||
*,
|
*,
|
||||||
filter_args: Optional[Sequence[str]] = None,
|
filter_args: Optional[Sequence[str]] = None,
|
||||||
parse_docstring: bool = False,
|
parse_docstring: bool = False,
|
||||||
|
error_on_invalid_docstring: bool = False,
|
||||||
) -> Type[BaseModel]:
|
) -> Type[BaseModel]:
|
||||||
"""Create a pydantic schema from a function's signature.
|
"""Create a pydantic schema from a function's signature.
|
||||||
Args:
|
Args:
|
||||||
@ -230,20 +266,22 @@ def create_schema_from_function(
|
|||||||
filter_args: Optional list of arguments to exclude from the schema
|
filter_args: Optional list of arguments to exclude from the schema
|
||||||
parse_docstring: Whether to parse the function's docstring for descriptions
|
parse_docstring: Whether to parse the function's docstring for descriptions
|
||||||
for each argument.
|
for each argument.
|
||||||
|
error_on_invalid_docstring: if ``parse_docstring`` is provided, configures
|
||||||
|
whether to raise ValueError on invalid Google Style docstrings.
|
||||||
Returns:
|
Returns:
|
||||||
A pydantic model with the same arguments as the function
|
A pydantic model with the same arguments as the function
|
||||||
"""
|
"""
|
||||||
# https://docs.pydantic.dev/latest/usage/validation_decorator/
|
# https://docs.pydantic.dev/latest/usage/validation_decorator/
|
||||||
validated = validate_arguments(func, config=_SchemaConfig) # type: ignore
|
validated = validate_arguments(func, config=_SchemaConfig) # type: ignore
|
||||||
inferred_model = validated.model # type: ignore
|
inferred_model = validated.model # type: ignore
|
||||||
filter_args = (
|
filter_args = filter_args if filter_args is not None else FILTERED_ARGS
|
||||||
filter_args if filter_args is not None else ("run_manager", "callbacks")
|
|
||||||
)
|
|
||||||
for arg in filter_args:
|
for arg in filter_args:
|
||||||
if arg in inferred_model.__fields__:
|
if arg in inferred_model.__fields__:
|
||||||
del inferred_model.__fields__[arg]
|
del inferred_model.__fields__[arg]
|
||||||
description, arg_descriptions = _infer_arg_descriptions(
|
description, arg_descriptions = _infer_arg_descriptions(
|
||||||
func, parse_docstring=parse_docstring
|
func,
|
||||||
|
parse_docstring=parse_docstring,
|
||||||
|
error_on_invalid_docstring=error_on_invalid_docstring,
|
||||||
)
|
)
|
||||||
# Pydantic adds placeholder virtual fields we need to strip
|
# Pydantic adds placeholder virtual fields we need to strip
|
||||||
valid_properties = _get_filtered_args(inferred_model, func, filter_args=filter_args)
|
valid_properties = _get_filtered_args(inferred_model, func, filter_args=filter_args)
|
||||||
@ -909,6 +947,8 @@ class StructuredTool(BaseTool):
|
|||||||
return_direct: bool = False,
|
return_direct: bool = False,
|
||||||
args_schema: Optional[Type[BaseModel]] = None,
|
args_schema: Optional[Type[BaseModel]] = None,
|
||||||
infer_schema: bool = True,
|
infer_schema: bool = True,
|
||||||
|
parse_docstring: bool = False,
|
||||||
|
error_on_invalid_docstring: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> StructuredTool:
|
) -> StructuredTool:
|
||||||
"""Create tool from a given function.
|
"""Create tool from a given function.
|
||||||
@ -923,6 +963,10 @@ class StructuredTool(BaseTool):
|
|||||||
return_direct: Whether to return the result directly or as a callback
|
return_direct: Whether to return the result directly or as a callback
|
||||||
args_schema: The schema of the tool's input arguments
|
args_schema: The schema of the tool's input arguments
|
||||||
infer_schema: Whether to infer the schema from the function's signature
|
infer_schema: Whether to infer the schema from the function's signature
|
||||||
|
parse_docstring: if ``infer_schema`` and ``parse_docstring``, will attempt
|
||||||
|
to parse parameter descriptions from Google Style function docstrings.
|
||||||
|
error_on_invalid_docstring: if ``parse_docstring`` is provided, configures
|
||||||
|
whether to raise ValueError on invalid Google Style docstrings.
|
||||||
**kwargs: Additional arguments to pass to the tool
|
**kwargs: Additional arguments to pass to the tool
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@ -963,7 +1007,12 @@ class StructuredTool(BaseTool):
|
|||||||
_args_schema = args_schema
|
_args_schema = args_schema
|
||||||
if _args_schema is None and infer_schema:
|
if _args_schema is None and infer_schema:
|
||||||
# schema name is appended within function
|
# schema name is appended within function
|
||||||
_args_schema = create_schema_from_function(name, source_function)
|
_args_schema = create_schema_from_function(
|
||||||
|
name,
|
||||||
|
source_function,
|
||||||
|
parse_docstring=parse_docstring,
|
||||||
|
error_on_invalid_docstring=error_on_invalid_docstring,
|
||||||
|
)
|
||||||
return cls(
|
return cls(
|
||||||
name=name,
|
name=name,
|
||||||
func=func,
|
func=func,
|
||||||
@ -980,6 +1029,8 @@ def tool(
|
|||||||
return_direct: bool = False,
|
return_direct: bool = False,
|
||||||
args_schema: Optional[Type[BaseModel]] = None,
|
args_schema: Optional[Type[BaseModel]] = None,
|
||||||
infer_schema: bool = True,
|
infer_schema: bool = True,
|
||||||
|
parse_docstring: bool = False,
|
||||||
|
error_on_invalid_docstring: bool = True,
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
"""Make tools out of functions, can be used with or without arguments.
|
"""Make tools out of functions, can be used with or without arguments.
|
||||||
|
|
||||||
@ -991,6 +1042,10 @@ def tool(
|
|||||||
infer_schema: Whether to infer the schema of the arguments from
|
infer_schema: Whether to infer the schema of the arguments from
|
||||||
the function's signature. This also makes the resultant tool
|
the function's signature. This also makes the resultant tool
|
||||||
accept a dictionary input to its `run()` function.
|
accept a dictionary input to its `run()` function.
|
||||||
|
parse_docstring: if ``infer_schema`` and ``parse_docstring``, will attempt to
|
||||||
|
parse parameter descriptions from Google Style function docstrings.
|
||||||
|
error_on_invalid_docstring: if ``parse_docstring`` is provided, configures
|
||||||
|
whether to raise ValueError on invalid Google Style docstrings.
|
||||||
|
|
||||||
Requires:
|
Requires:
|
||||||
- Function must be of type (str) -> str
|
- Function must be of type (str) -> str
|
||||||
@ -1008,6 +1063,78 @@ def tool(
|
|||||||
def search_api(query: str) -> str:
|
def search_api(query: str) -> str:
|
||||||
# Searches the API for the query.
|
# Searches the API for the query.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
.. versionadded:: 0.2.14
|
||||||
|
Parse Google-style docstrings:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@tool(parse_docstring=True)
|
||||||
|
def foo(bar: str, baz: int) -> str:
|
||||||
|
\"\"\"The foo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bar: The bar.
|
||||||
|
baz: The baz.
|
||||||
|
\"\"\"
|
||||||
|
return bar
|
||||||
|
|
||||||
|
foo.args_schema.schema()
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
{
|
||||||
|
"title": "fooSchema",
|
||||||
|
"description": "The foo.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bar": {
|
||||||
|
"title": "Bar",
|
||||||
|
"description": "The bar.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"baz": {
|
||||||
|
"title": "Baz",
|
||||||
|
"description": "The baz.",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"bar",
|
||||||
|
"baz"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Note that parsing by default will raise ``ValueError`` if the docstring
|
||||||
|
is considered invalid. A docstring is considered invalid if it contains
|
||||||
|
arguments not in the function signature, or is unable to be parsed into
|
||||||
|
a summary and "Args:" blocks. Examples below:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
# No args section
|
||||||
|
def invalid_docstring_1(bar: str, baz: int) -> str:
|
||||||
|
\"\"\"The foo.\"\"\"
|
||||||
|
return bar
|
||||||
|
|
||||||
|
# Improper whitespace between summary and args section
|
||||||
|
def invalid_docstring_2(bar: str, baz: int) -> str:
|
||||||
|
\"\"\"The foo.
|
||||||
|
Args:
|
||||||
|
bar: The bar.
|
||||||
|
baz: The baz.
|
||||||
|
\"\"\"
|
||||||
|
return bar
|
||||||
|
|
||||||
|
# Documented args absent from function signature
|
||||||
|
def invalid_docstring_3(bar: str, baz: int) -> str:
|
||||||
|
\"\"\"The foo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
banana: The bar.
|
||||||
|
monkey: The baz.
|
||||||
|
\"\"\"
|
||||||
|
return bar
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _make_with_name(tool_name: str) -> Callable:
|
def _make_with_name(tool_name: str) -> Callable:
|
||||||
@ -1052,6 +1179,8 @@ def tool(
|
|||||||
return_direct=return_direct,
|
return_direct=return_direct,
|
||||||
args_schema=schema,
|
args_schema=schema,
|
||||||
infer_schema=infer_schema,
|
infer_schema=infer_schema,
|
||||||
|
parse_docstring=parse_docstring,
|
||||||
|
error_on_invalid_docstring=error_on_invalid_docstring,
|
||||||
)
|
)
|
||||||
# If someone doesn't want a schema applied, we must treat it as
|
# If someone doesn't want a schema applied, we must treat it as
|
||||||
# a simple string->string function
|
# a simple string->string function
|
||||||
|
@ -138,7 +138,11 @@ def convert_python_function_to_openai_function(
|
|||||||
|
|
||||||
func_name = _get_python_function_name(function)
|
func_name = _get_python_function_name(function)
|
||||||
model = tools.create_schema_from_function(
|
model = tools.create_schema_from_function(
|
||||||
func_name, function, filter_args=(), parse_docstring=True
|
func_name,
|
||||||
|
function,
|
||||||
|
filter_args=(),
|
||||||
|
parse_docstring=True,
|
||||||
|
error_on_invalid_docstring=False,
|
||||||
)
|
)
|
||||||
return convert_pydantic_to_openai_function(
|
return convert_pydantic_to_openai_function(
|
||||||
model,
|
model,
|
||||||
|
@ -959,6 +959,84 @@ def test_tool_arg_descriptions() -> None:
|
|||||||
"required": ["bar", "baz"],
|
"required": ["bar", "baz"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Test parses docstring
|
||||||
|
foo2 = tool(foo, parse_docstring=True)
|
||||||
|
args_schema = foo2.args_schema.schema() # type: ignore
|
||||||
|
expected = {
|
||||||
|
"title": "fooSchema",
|
||||||
|
"description": "The foo.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"bar": {"title": "Bar", "description": "The bar.", "type": "string"},
|
||||||
|
"baz": {"title": "Baz", "description": "The baz.", "type": "integer"},
|
||||||
|
},
|
||||||
|
"required": ["bar", "baz"],
|
||||||
|
}
|
||||||
|
assert args_schema == expected
|
||||||
|
|
||||||
|
# Test parsing with run_manager does not raise error
|
||||||
|
def foo3(
|
||||||
|
bar: str, baz: int, run_manager: Optional[CallbackManagerForToolRun] = None
|
||||||
|
) -> str:
|
||||||
|
"""The foo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bar: The bar.
|
||||||
|
baz: The baz.
|
||||||
|
"""
|
||||||
|
return bar
|
||||||
|
|
||||||
|
as_tool = tool(foo3, parse_docstring=True)
|
||||||
|
args_schema = as_tool.args_schema.schema() # type: ignore
|
||||||
|
assert args_schema["description"] == expected["description"]
|
||||||
|
assert args_schema["properties"] == expected["properties"]
|
||||||
|
|
||||||
|
# Test parameterless tool does not raise error for missing Args section
|
||||||
|
# in docstring.
|
||||||
|
def foo4() -> str:
|
||||||
|
"""The foo."""
|
||||||
|
return "bar"
|
||||||
|
|
||||||
|
as_tool = tool(foo4, parse_docstring=True)
|
||||||
|
args_schema = as_tool.args_schema.schema() # type: ignore
|
||||||
|
assert args_schema["description"] == expected["description"]
|
||||||
|
|
||||||
|
def foo5(run_manager: Optional[CallbackManagerForToolRun] = None) -> str:
|
||||||
|
"""The foo."""
|
||||||
|
return "bar"
|
||||||
|
|
||||||
|
as_tool = tool(foo5, parse_docstring=True)
|
||||||
|
args_schema = as_tool.args_schema.schema() # type: ignore
|
||||||
|
assert args_schema["description"] == expected["description"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_invalid_docstrings() -> None:
|
||||||
|
# Test invalid docstrings
|
||||||
|
def foo3(bar: str, baz: int) -> str:
|
||||||
|
"""The foo."""
|
||||||
|
return bar
|
||||||
|
|
||||||
|
def foo4(bar: str, baz: int) -> str:
|
||||||
|
"""The foo.
|
||||||
|
Args:
|
||||||
|
bar: The bar.
|
||||||
|
baz: The baz.
|
||||||
|
"""
|
||||||
|
return bar
|
||||||
|
|
||||||
|
def foo5(bar: str, baz: int) -> str:
|
||||||
|
"""The foo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
banana: The bar.
|
||||||
|
monkey: The baz.
|
||||||
|
"""
|
||||||
|
return bar
|
||||||
|
|
||||||
|
for func in [foo3, foo4, foo5]:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_ = tool(func, parse_docstring=True)
|
||||||
|
|
||||||
|
|
||||||
def test_tool_annotated_descriptions() -> None:
|
def test_tool_annotated_descriptions() -> None:
|
||||||
def foo(
|
def foo(
|
||||||
|
Loading…
Reference in New Issue
Block a user