langchain-autotools
Generate LangChain Tools and Toolkits from any Python SDK -- no extra code required.
Built for LangChain 1.x (langchain-core >= 1.0).
pip install langchain-autotools
How AutoTools makes a dynamic toolkit
AutoTools wraps any SDK client that exposes CRUD-ish operations (get, update, create, delete) and turns them into an LLM-enabled toolkit without a hand-written tool per call.
There are two core pieces:
- AutoToolWrapper -- wraps the SDK you pass in and emits one tool per allowed method
- CrudControls -- controls the access an agent has to Create, Read, Update and Delete verbs, with flexible pattern matching
Each generated tool carries an args_schema derived from the underlying function
signature, so tool-calling models fill in real, typed arguments:
from langchain_core.utils.function_calling import convert_to_openai_tool
convert_to_openai_tool(toolkit.get_tools()[0])
# {'type': 'function',
# 'function': {'name': 'get_thing',
# 'description': 'Gets Thing',
# 'parameters': {'type': 'object',
# 'properties': {'thing_id': {'type': 'integer'}},
# 'required': ['thing_id']}}}
Quick Start
import boto3
from langchain_autotools import AutoToolWrapper, CrudControls
# Create your SDK client
s3 = boto3.client("s3")
# Configure access controls
crud_controls = CrudControls(
read=True,
read_list=["list_buckets"], # exact match
create=True,
create_list=["create_bucket*"], # wildcard match
)
# Create the toolkit
toolkit = AutoToolWrapper(client=s3, crud_controls=crud_controls)
# View available tools
print([tool.name for tool in toolkit.get_tools()])
AutoToolWrapper is a LangChain BaseToolkit, so get_tools() is the idiomatic way to
read its tools. The toolkit.operations attribute holds the same list.
Pattern Matching in CrudControls
CrudControls supports both regex patterns and glob-style wildcards for matching
function names. The style is detected per pattern, so a single list can mix the two.
# Glob patterns (simple wildcards)
crud_controls = CrudControls(
read=True,
read_list=["get_thing*"], # Matches: get_thing, get_thing_by_id, get_thing_generator
)
# Regex patterns (more precise control)
crud_controls = CrudControls(
read=True,
read_list=[
r"^get_thing$", # Exact match only
r"^get_thing_\w+$", # Require at least one word char after the underscore
],
)
# Mix and match both styles
crud_controls = CrudControls(
read=True,
read_list=[
"get_thing*", # Glob pattern for broad matching
r"^get_other_\w+$", # Regex pattern for precise matching
],
)
Glob patterns support familiar shell-style wildcards:
*matches everything?matches any single character[seq]matches any character in seq[!seq]matches any character not in seq
A pattern is treated as a regex when it uses syntax a glob has no meaning for -- anchors,
groups, quantifiers, escapes (^ $ . + { } | \ ( )) -- or when it is written explicitly
as r"...". Everything else is a glob.
The defaults are read-only: read=True with read_list=["get_*"], and create, update and
delete switched off.
Excluding matches
exclude is a veto applied after the verb lists, so a wildcard can stay broad while
specific matches are dropped. It matters more than it sounds: boto3 clients carry
get_paginator and get_waiter, which match get_*, become tools, and return an object
repr like "<botocore.client.S3.Paginator.ListObjectsV2 object at 0x7f1a...>" when an
agent calls them.
crud_controls = CrudControls(
read=True,
read_list=["get_*", "list_*"],
exclude=["get_paginator", "get_waiter", "*_internal"],
)
Exclude patterns use the same glob-or-regex detection as the verb lists.
Working with Agents
toolkit.get_tools() returns a plain list of tools, so it drops straight into
create_agent:
from langchain.agents import create_agent
agent = create_agent(
"anthropic:claude-opus-5",
toolkit.get_tools(),
system_prompt="You are an assistant that manages AWS resources.",
)
result = agent.invoke({"messages": [{"role": "user", "content": "How many S3 buckets do I have?"}]})
print(result["messages"][-1].content)
NOTE: If your SDK has many callable functions, your tool list could exceed your model's
context length. Use CrudControls pattern matching to limit the tools your agent has
access to -- and see Controlling description size, which
is usually the larger cost of the two.
Controlling description size
Descriptions come from the wrapped function's docstring, and some SDKs write essays.
Wrapping boto3's S3 client with read_list=["get_*", "list_*"] produces 49 tools
carrying ~128,000 tokens of description between them -- get_object alone is ~9,000.
The tool count is rarely the context problem; the docstrings are.
# leading paragraph only
toolkit = AutoToolWrapper(client=sdk, describe="summary")
# hard ceiling, applied after `describe`
toolkit = AutoToolWrapper(client=sdk, max_description_length=1500)
# full control
toolkit = AutoToolWrapper(client=sdk, describe=lambda func, name: my_summary(func))
On the S3 example above, describe="summary" gives ~1,050 tokens -- a 99.2% cut -- with
get_object reading Retrieves an object from Amazon S3. A cap of 1500 gives ~17,800.
Watch out: for a dynamic SDK (see Dynamic SDKs) no args_schema
could be derived, so the docstring is the only record of what a call accepts. Trimming it
takes that away -- with describe="summary", nothing tells the model that get_object
needs a Bucket and a Key. Models often know popular SDKs well enough to manage, and
fixed_args can supply the rest, but test it before trusting it. SDKs with real
signatures are unaffected: their schema carries the arguments.
Pinned arguments
fixed_args supplies values the model never chooses and never sees. They are stripped
from the args_schema and merged in at call time, overriding anything the model sent:
toolkit = AutoToolWrapper(client=s3, fixed_args={"Bucket": "my-scoped-bucket"})
A pin is only applied to operations that can accept it -- one that takes no Bucket is
left alone rather than being called with an argument it would reject.
Errors
An SDK exception is re-raised as a ToolException and, by default, handed back to the
agent as the tool's result ("PermissionError: AccessDenied: not authorized") so it can
read the failure and adapt instead of the run ending. Set handle_tool_error=False on a
tool to let it propagate; the original exception is kept as __cause__.
Raw results
Tools return a JSON string. To also get the underlying Python object without re-parsing, ask for an artifact:
toolkit = AutoToolWrapper(client=sdk, response_format="content_and_artifact")
message = tool.invoke(
{"name": "get_thing", "args": {"thing_id": 3}, "id": "1", "type": "tool_call"}
)
message.content # '{"status": 200, "response": {"id": 3}}' -- what the model sees
message.artifact # {'status': 200, 'response': {'id': 3}} -- the real object
Generators are drained once, so the artifact is the materialized list.
Capping result size
Descriptions are only half the context problem. A single realistic list call:
list_objects(count=5000) # -> 890,014 chars (~222,500 tokens)
That lands mid-run, after the agent has already committed to the call. Set a ceiling:
toolkit = AutoToolWrapper(client=sdk, max_result_length=4000)
A list is cut on an item boundary, so the kept portion is still valid JSON. Anything else is cut on a character boundary. Either way the result ends with a marker naming what was dropped, so the model can tell "that is all of it" from "there is more":
[{"Key": "part-00000.parquet", ...}, ...]
[truncated: 4,992 of 5,000 items omitted. Narrow the request or raise max_result_length.]
Truncation applies to the content the model sees. If you also asked for an artifact, the artifact keeps the whole result -- calling code has no context window to protect.
Knowing which tools are destructive
The CRUD verb that exposed each operation is recorded on the tool, so you can act on it without re-deriving the match:
tool.metadata["crud"] # "read" | "create" | "update" | "delete"
tool.metadata["sdk_function"] # the underlying method name, ignoring any prefix
Which is what you want for putting a human in front of the dangerous half:
from langchain.agents.middleware import HumanInTheLoopMiddleware
destructive = [
tool.name
for tool in toolkit.get_tools()
if tool.metadata["crud"] in ("create", "update", "delete")
]
agent = create_agent(
"anthropic:claude-opus-5",
toolkit.get_tools(),
middleware=[HumanInTheLoopMiddleware(interrupt_on=dict.fromkeys(destructive, True))],
)
When a name matches under more than one verb, the first of create, read, update, delete wins, so the reported verb is stable.
Wrapping more than one SDK
Two SDKs that both expose get_object would hand the agent two identically named tools.
prefix keeps them apart; dispatch still uses the real method name:
s3_tools = AutoToolWrapper(client=s3, prefix="s3_").get_tools()
gcs_tools = AutoToolWrapper(client=gcs, prefix="gcs_").get_tools()
agent = create_agent("anthropic:claude-opus-5", s3_tools + gcs_tools)
Prefixes are validated at construction, since a tool name providers reject is only discovered at inference:
AutoToolWrapper(client=s3, prefix="s3.")
# ValueError: prefix 's3.' contains '.', which is not allowed in a tool name.
# Use letters, digits, underscores or hyphens -- for example 's3_'.
Argument documentation
Where a docstring documents its parameters, that text is attached to the matching schema
field, so the model gets more than a type. Google style (an Args: block) and Sphinx
style (:param name:) are both understood:
def get_thing(self, thing_id: int) -> dict:
"""Gets a thing.
Args:
thing_id: Identifier of the thing, as issued by the registry.
"""
{"thing_id": {"type": "integer",
"description": "Identifier of the thing, as issued by the registry."}}
This survives describe="summary" -- the description is trimmed to Gets a thing. while
the schema keeps the argument detail.
Async
Every generated tool implements the async interface. Coroutine functions on the SDK are awaited directly; synchronous ones are run in a worker thread so they don't block the event loop.
await toolkit.get_tools()[0].ainvoke({"thing_id": 123})
Dynamic SDKs
Some SDKs (boto3 among them) build their methods dynamically, so the signature is just
(*args, **kwargs). There is nothing to derive a schema from, so those tools skip the
args_schema and pass whatever the model supplies straight through to the SDK call.
AWS Authentication Example
If using AWS services, ensure you have your credentials available:
import getpass
import os
os.environ["AWS_ACCESS_KEY_ID"] = getpass.getpass(prompt="AWS Access Key ID: ")
os.environ["AWS_SECRET_ACCESS_KEY"] = getpass.getpass(prompt="AWS Secret Access Key: ")
os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
Development
poetry install
poetry run pytest
poetry run ruff check .
Releasing
The git tag is the version. Pushing a tag matching *.*.* runs the release workflow,
which does poetry version $TAG before building, so version in pyproject.toml is a
0.0.0 placeholder and local builds are always 0.0.0.
# add the release to CHANGELOG.md, then, from an up-to-date main:
git tag 0.2.0 && git push origin 0.2.0
Use a bare tag (0.2.0, not v0.2.0) to match the published versions. Note that the
test workflow runs on pushes to main and on pull requests, not on tags -- a tag
publishes whatever it points at, so tag a commit you have already seen go green.
Release files for langchain-autotools 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| langchain_autotools-0.2.0.tar.gz | 18.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| langchain_autotools-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.3 kB
Release files / langchain_autotools-0.2.0.tar.gz
| Download URL | langchain_autotools-0.2.0.tar.gz |
|---|---|
| Size | 18.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
874b92c793e0daa4ee572877e370f6f11554cf68389f8271d07fab0fe78eaf60
|
|
BLAKE2b-256 checksum How to use checksums |
79c905948d7530c4d095784f20babbbf0f274b6b124dc9f15dc065c25f3b923c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.4.1 CPython/3.12.3 Linux/6.17.0-1022-azure
|
Release files / langchain_autotools-0.2.0-py3-none-any.whl
| Download URL | langchain_autotools-0.2.0-py3-none-any.whl |
|---|---|
| Size | 15.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
066d6e6435996aaac717418105b0707082f34daeec9520f4c5d9bcd7368252f0
|
|
BLAKE2b-256 checksum How to use checksums |
becb9cd533dcfba6546de3eca0ff99e2335b00e4e6401ccd600c7fc0586b1fa8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
poetry/2.4.1 CPython/3.12.3 Linux/6.17.0-1022-azure
|