Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `HotdataToolCache` and `cached()` in `hotdata_langchain.cache`: cache LangChain tool
results in a Hotdata managed table, keyed by tool name and arguments. Works on any
plain function/tool, not just this package's own. `make_hotdata_tools` gains `cache`
and `cache_ttl` parameters that wire the two read-only tools (`hotdata_execute_sql`,
`hotdata_list_managed_databases`) through it; the mutating tools are never cached.

### Changed

- Bump `hotdata-framework` to `>=0.8.0` and `hotdata` to `>=0.8.0` — both released 0.8.0
today, adding native key-based `mode="upsert"`/`"update"`/`"delete"` loads on managed
tables (used by the new cache backend) and a per-call `key=` override on
`load_managed_table`.
- Add `pyarrow` as a direct dependency (already a transitive dependency of
`hotdata-framework`; the new cache module writes parquet directly).

## [0.2.2] - 2026-06-27

Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,60 @@ Limit how many rows are returned to the LLM. Useful for keeping responses within
tools = hl.make_hotdata_tools(client, max_rows=50)
```

## Caching tool calls

LangChain caches LLM calls (`set_llm_cache`) but has no equivalent for tool calls — every
`StructuredTool` invocation re-executes, even when an agent retries the same query.
`HotdataToolCache` fills that gap using a Hotdata managed table as the backing store, keyed
by tool name and arguments:

```python
from hotdata_langchain import HotdataToolCache

cache = HotdataToolCache(client) # construct once per process and reuse
tools = hl.make_hotdata_tools(client, cache=cache)
```

This serves repeated calls to the read-only tools (`hotdata_execute_sql`,
`hotdata_list_managed_databases`) from the cache instead of re-running them.
`hotdata_create_managed_database` and `hotdata_load_managed_table` are never cached —
skipping a mutation on a cache hit would be a correctness bug, not caching.

Pass `cache_ttl=timedelta(...)` to `make_hotdata_tools` to expire entries after a fixed age.

### Caching arbitrary tools

`cached()` works on any plain function or tool, not just this package's own — Hotdata can
act as a shared, queryable cache backend for database queries, API calls, or search
results from any part of your agent:

```python
from hotdata_langchain.cache import cached

cached_search = cached(my_search_function, cache=cache, tool_name="search")
```

### Known limitations

- **Database-resolution race**: on first use, `HotdataToolCache` resolves-or-creates a
managed database by name. Managed-database names are not unique or identifying, so two
processes racing on first use can each create a distinct database with the same name,
silently splitting the cache. Pass `database_id=` (a database you created once, e.g. at
deploy time) to pin a specific database and avoid this entirely — recommended for any
multi-process deployment.
- **Shared-table write ceiling**: managed-table loads serialize at the per-table lock
regardless of key, so all cache writes funnel through one lock. Fine for moderate
concurrency; sharding by tool/hash-prefix is not implemented.
- **Per-instance memoization**: construct one long-lived `HotdataToolCache` per process and
reuse it — a fresh instance per call defeats memoization and re-races the database
resolution on every call.

## Run the examples

```bash
uv run python examples/langchain_basic.py
uv run python examples/langchain_managed_db.py
uv run python examples/langchain_cached_tools.py
```

## Development
Expand Down
52 changes: 52 additions & 0 deletions examples/langchain_cached_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Cache LangChain tool results in a Hotdata managed table."""

import time

import hotdata_langchain as hl
from hotdata_langchain.cache import cached


def main() -> None:
client = hl.from_env()

# HotdataToolCache is a pluggable cache backend for the two read/idempotent tools —
# repeated calls with the same SQL are served from a managed table instead of
# re-running the query. Construct one instance per process and reuse it.
cache = hl.HotdataToolCache(client)
tools = hl.make_hotdata_tools(client, cache=cache)
by_name = {tool.name: tool for tool in tools}

sql_tool = by_name["hotdata_execute_sql"]
print("First call (miss, runs the query):")
print(sql_tool.invoke({"sql": "SELECT 1 AS ok"}))

print("\nSecond call, same SQL (hit, served from the cache):")
print(sql_tool.invoke({"sql": "SELECT 1 AS ok"}))

# cached() also works on any plain function — not just this package's own tools —
# which is the more general story: Hotdata as a cache backend for arbitrary
# LangChain tools (database queries, API calls, search results).
calls = {"n": 0}

def slow_search(query: str) -> str:
calls["n"] += 1
time.sleep(1) # stand in for a real API call or search request
return f"{calls['n']} result(s) for {query!r}"

cached_search = cached(slow_search, cache=cache, tool_name="slow_search")

print("\nArbitrary function, first call (miss, ~1s):")
start = time.monotonic()
print(cached_search("hotdata langchain"))
print(f"took {time.monotonic() - start:.2f}s")

print("\nSame function, same query (hit, instant):")
start = time.monotonic()
print(cached_search("hotdata langchain"))
print(f"took {time.monotonic() - start:.2f}s")

client.close()


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions hotdata_langchain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from hotdata_framework import HotdataClient, QueryResult, from_env

from hotdata_langchain.cache import HotdataToolCache, cached
from hotdata_langchain.databases import (
create_managed_database,
list_managed_databases_json,
Expand All @@ -24,8 +25,10 @@

__all__ = [
"HotdataClient",
"HotdataToolCache",
"QueryResult",
"__version__",
"cached",
"create_managed_database",
"execute_sql_json",
"from_env",
Expand Down
Loading
Loading