Cache LangChain tool results in a Hotdata managed table - #33
Draft
rohan-hotdata wants to merge 1 commit into
Draft
Cache LangChain tool results in a Hotdata managed table#33rohan-hotdata wants to merge 1 commit into
rohan-hotdata wants to merge 1 commit into
Conversation
LangChain caches LLM calls but has no equivalent for tool calls -- every StructuredTool invocation re-executes, even on a retry or a repeated question. HotdataToolCache + cached() fill that gap: a pluggable, persistent, queryable cache backend for any tool call (not just this package's own), keyed by tool name and arguments. Wires into make_hotdata_tools via cache=/cache_ttl= params, applied only to the two read/idempotent tools -- the mutating tools are never cached, since skipping a mutation on a cache hit would be a correctness bug. Unblocked by hotdata-framework 0.8.0's native key-based mode="upsert" on load_managed_table (shipped 2026-07-09), avoiding the full-table read-modify-write hotdata-dlt-destination has to emulate on older managed-table APIs. Bumps hotdata/hotdata-framework to >=0.8.0 (both released 0.8.0 the same day this was scoped) and adds pyarrow as a direct dependency. Verified with 15 new unit tests (fake backend round-tripping through real pyarrow parquet writes/reads) plus a live run against a real workspace: confirmed read/write fidelity, and found that a managed table with zero rows ever loaded 400s on query instead of returning empty -- handled by the existing fail-open policy in cached(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rohan-hotdata
added a commit
that referenced
this pull request
Jul 24, 2026
Archives the benchmark work that asked whether HotdataToolCache (draft PR #33) is the right store for tool-result caching, given that nothing in its design is Hotdata-specific. benchmarks/backends.py SqliteToolCache, LayeredToolCache, ToolCache protocol benchmarks/bench_*.py query cost, cache primitives, end-to-end, fleet, server-side (network excluded) benchmarks/crossover.py model over the measured constants, incl. an RTT sweep benchmarks/provision_tpch.py idempotent TPCH sf=1 fixture via DuckDB dbgen tests/test_backend_parity.py 49 tests asserting both backends behave identically What it found, in short: a cache lookup costs the engine 71-79ms against Q1's 98-101ms of compute, so the ceiling for a same-region deployment is ~1.3x on a cheap query and ~3.4x on an expensive one. Larger cached results make it worse, not better. Full write-up in benchmarks/FINDINGS.md, including three defects in the PR #33 write path and why Redis rather than SQLite is the real comparison. Based on feat/tool-result-caching, since every module imports hotdata_langchain.cache. Runs as-is on this branch; not meant to merge on its own, and parked rather than pursued. pyproject: allow print in benchmarks/, matching examples/ and scripts/.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
LangChain caches LLM calls (
set_llm_cache/BaseCache) but has no equivalent for toolcalls -- confirmed by reading the installed
langchain_coresource directly: neitherBaseToolnorRunnableexposes a cache hook. EveryStructuredToolinvocationre-executes, even on an agent retry or a repeated question across sessions.
HotdataToolCache(hotdata_langchain/cache.py) -- the storage backend. Keys anentry by
sha256(version + tool_name + args), stores it in a Hotdata managed table(
cache_key,tool_name,args_json,result_json,created_at) viaload_managed_table(..., mode="upsert"), reads it back with a plain SQLSELECT.cached()-- wraps any plain function or tool callable, not just this package'sown. Fails open: a broken cache backend degrades to "uncached", never to a broken tool
call.
make_hotdata_tools(client, cache=..., cache_ttl=...)wires this into the two readtools only (
hotdata_execute_sql,hotdata_list_managed_databases) -- the mutatingtools are never cached, since skipping a mutation on a cache hit would be a correctness
bug, not caching.
Why now
This was originally blocked: managed tables historically only supported full-table
replacewrites (perhotdata-dlt-destination's own docs, which still describeread-modify-write emulation), which would have made a per-cache-entry write cost
O(cache size). Reading
hotdata_framework's actual installed source and runtimedb's githistory directly showed this is now out of date -- native, key-based
mode="upsert"shipped server-side on 2026-07-09. That's what unblocked a simple per-call write path, and
is why this PR also bumps
hotdata/hotdata-frameworkto>=0.8.0(both released 0.8.0the same day this was scoped -- newer than Dependabot's open #30/#31, which target the
same version and can likely be closed once this merges) and adds
pyarrowas a directdependency (already transitive via
hotdata-framework; the new module writes parquetdirectly).
Verification
tests/test_cache.pyagainst a fake backend thatround-trips through real pyarrow parquet writes/reads (not just mocked return values).
ruff check/formatclean;mypyclean on all source files.managed table with zero rows ever loaded returns HTTP 400 on query instead of an empty
result --
cached()'s fail-open logic absorbs this correctly (tool call stillsucceeds), confirmed it doesn't recur once the table holds at least one row.
lineitem, a 6-way joinfor revenue-by-nation) showed 3.5-4.8x faster over 12 calls once the cache warms up;
cold-write cost (~4-6s) breaks even in 2-3 repeats for queries at this scale. Trivial
queries (
SELECT 1) don't clearly benefit -- a cache lookup has its own ~0.6-0.7sfloor, since it's still a network round trip. This scoping (expensive + repeated, not
cheap + occasional) is called out in the new README section.
Known limitations (documented in README)
processes can each create a distinct database with the same name, silently splitting
the cache. Mitigation: pass
database_id=to pin one explicitly.HotdataToolCacheis memoized per-instance -- construct one per process and reuse it.Not in this PR
Semantic/fuzzy cache matching (embedding-based similarity for near-duplicate calls) is a
separate follow-up that would reuse Hotdata's vector-index infrastructure; not a
dependency of this change.
Status
Draft -- not ready to merge. Opening for early visibility/feedback.
🤖 Generated with Claude Code