Skip to content

feat: v2.31.0 — replace speedict with sqlite3 and require Python 3.13+ - #61

Merged
drawal1 merged 2 commits into
radiantlogicinc:mainfrom
dharrawal:feat/v2.31.0-speedict-to-sqlite
Aug 8, 2026
Merged

feat: v2.31.0 — replace speedict with sqlite3 and require Python 3.13+#61
drawal1 merged 2 commits into
radiantlogicinc:mainfrom
dharrawal:feat/v2.31.0-speedict-to-sqlite

Conversation

@dharrawal

@dharrawal dharrawal commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace abandoned speedict/RocksDB with stdlib sqlite3 (KVStore + float32 BLOB UtteranceCacheStore) across enablecache, NLU cache, utterance matching, and conversation store
  • Raise requires-python to >=3.13,<3.15 (unblocks 3.13 installs; no sdist/wheel existed for speedict on 3.13+)
  • Abandon pre-existing .rdb / RocksDB cache.db dirs (no data migration); coerce classifier ndarray labels before JSON store
  • Epic: fix-lvl

Test plan

  • Focused migration tests (test_kvstore, enablecache, conversation concurrency, cache matching, memory bounds, fastapi service paths)
  • Full suite on Python 3.13.14: 1715 passed, 15 skipped (PYTEST_EXIT=0, ~48 min)
  • Confirm downstream consumers can drop speedictrocksdict shims after upgrade
  • Operators may delete abandoned .rdb / ___convo_info/cache.db RocksDB directories when ready

Made with Cursor

Summary by Sourcery

Replace RocksDB/speedict-based persistence and caches with a new SQLite-backed key-value store and float32 BLOB utterance cache, and raise the minimum supported Python version to 3.13.

New Features:

  • Introduce a SQLite-backed KVStore and UtteranceCacheStore for durable JSON key-value storage and efficient embedding caches.
  • Add new tests for KVStore, utterance cache matching, enablecache behavior, and cross-process ConversationStore concurrency in WAL mode.

Enhancements:

  • Migrate conversation persistence, workflow enablecache, NLU caches, and utterance matching from speedict/RocksDB files to SQLite .sqlite3 files.
  • Simplify ConversationStore turn handling by relying exclusively on per-turn keys and abandoning legacy inline turns migration behavior.
  • Ensure enablecache only persists JSON-serialisable results and provide clearer errors for non-JSON outputs.
  • Update FastAPI MCP admin tooling, soak tests, and documentation to reflect SQLite-based stores and new file naming conventions.

Build:

  • Bump project version to 2.31.0 and adjust Poetry configuration to require Python >=3.13,<3.15 while removing the speedict dependency.

Documentation:

  • Refresh README to document Python 3.13–3.14 support, the sqlite3-backed storage layout, and guidance on removing legacy RocksDB directories and shims.

Tests:

  • Add focused tests covering KVStore semantics, multiprocessing concurrency, conversation store concurrency under WAL, utterance cache matching via float32 BLOBs, and memory-bound behavior against the new SQLite-backed store.

…+ (fix-lvl)

Drop the abandoned speedict/RocksDB dependency in favor of stdlib sqlite3
(WAL, JSON values, float32 BLOB utterance cache). Unblocks Python 3.13
installs, removes pickle-on-disk from first-party stores, and fixes
process-exclusive LOCK failures under concurrent writers. requires-python
is now >=3.13,<3.15; pre-existing .rdb / cache.db RocksDB dirs are abandoned.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sourcery-ai

sourcery-ai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

Replaces the abandoned speedict/RocksDB dependency with a stdlib sqlite3-backed KVStore (including a specialized UtteranceCacheStore) across caches and conversation persistence, raises the Python version requirement to 3.13–3.14, and adds concurrency and behavior tests to validate the migration and JSON-only storage.

Sequence diagram for utterance caching and matching with UtteranceCacheStore

sequenceDiagram
    actor User
    participant IntentDetection as intent_detection
    participant CacheMatching as cache_matching
    participant Store as UtteranceCacheStore

    User->>IntentDetection: store_utterance_cache(cache_path, utterance, label, model_pipeline)
    IntentDetection->>CacheMatching: store_utterance_cache(cache_path, utterance, label, model_pipeline)
    CacheMatching->>Store: UtteranceCacheStore(cache_path)
    CacheMatching->>CacheMatching: get_embedding(utterance, model_pipeline)
    CacheMatching->>Store: get(utterance_hash)
    Store-->>CacheMatching: existing entry or None
    CacheMatching->>Store: upsert(utterance_hash, utterance, command_mapping, embedding)
    CacheMatching->>Store: close()

    User->>IntentDetection: cache_match(cache_path, utterance, model_pipeline)
    IntentDetection->>CacheMatching: cache_match(cache_path, utterance, model_pipeline)
    CacheMatching->>Store: UtteranceCacheStore(cache_path)
    CacheMatching->>CacheMatching: get_embedding(utterance, model_pipeline)
    CacheMatching->>Store: iter_entries()
    Store-->>CacheMatching: (hash_key, entry) list
    CacheMatching->>CacheMatching: cosine_similarity(query_embedding, cached_embedding)
    CacheMatching-->>IntentDetection: best label / None
    CacheMatching->>Store: close()
Loading

File-Level Changes

Change Details Files
Introduce SQLite-backed KVStore and UtteranceCacheStore to replace speedict/RocksDB and support JSON values plus float32 BLOB embeddings with WAL concurrency.
  • Add KVStore implementation using sqlite3 with a kv table, WAL mode, JSON-serialised values, and basic dict-like methods (get/set/del/keys/contains).
  • Add UtteranceCacheStore implementation using sqlite3 with a dedicated utterance_cache table storing metadata as JSON and embeddings as float32 BLOBs, plus helpers to pack/unpack numpy arrays.
  • Ensure KVStore and UtteranceCacheStore can safely share a single SQLite file via separate tables and directory auto-creation.
fastworkflow/kvstore.py
Migrate conversation persistence from Rdict .rdb files to SQLite-backed KVStore .sqlite3 files and simplify turn storage semantics.
  • Switch ConversationStore to use KVStore instead of speedict.Rdict, updating db_path to use .sqlite3 filenames and adapting helper methods to KVStore types.
  • Drop support for inline 'turns' lists and legacy migration logic; authoritative turns are now stored only in per-turn keys referenced by appended_turn_count.
  • Update tests that describe conversation storage to refer to SQLite-backed stores, adjust byte-counting to use JSON encoding instead of pickle, and change durable store metrics to count .sqlite3 files.
  • Update dump_all_conversations admin endpoint to scan .sqlite3 files instead of .rdb and derive channel_id from the new extension.
fastworkflow/run_fastapi_mcp/conversation_store.py
tests/test_fastapi_memory_bounds.py
tests/soak/memory_soak.py
fastworkflow/run_fastapi_mcp/__main__.py
Move enablecache decorator and NLU caches (utterance store, suggested commands, counts) from speedict/RocksDB to SQLite KVStore, enforcing JSON-serialisable values and new cache file naming.
  • Replace speedict.Rdict usage in workflow.enablecache with KVStore, constructing cache.sqlite3 files under the existing per-function cache directory and raising TypeError when values are not JSON-serialisable.
  • Update NLU intent_detection caches to use KVStore instead of Rdict and rename cache file extensions from .db/cache.db to .sqlite3, coercing ndarray labels and flag_type to JSON-friendly types when storing suggested commands.
  • Adjust helper methods that store/read utterances, suggested commands, and counts to use KVStore semantics and close connections appropriately.
fastworkflow/workflow.py
fastworkflow/_workflows/command_metadata_extraction/intent_detection.py
Switch utterance/embedding cache matching from a JSON blob stored via Rdict to UtteranceCacheStore float32 BLOB rows with compatible semantics.
  • Replace Rdict usage in fastworkflow.cache_matching with UtteranceCacheStore for store_utterance_cache and cache_match, using per-utterance hash keys mapped to JSON metadata plus float32 embedding blobs.
  • Refactor store_utterance_cache to read/update per-utterance records via UtteranceCacheStore.get/upsert instead of manipulating a single 'cache' dict, ensuring embeddings are stored as numpy arrays when present and None otherwise.
  • Refactor cache_match to iterate over UtteranceCacheStore.iter_entries(), perform cosine similarity on numpy float32 embeddings, and only consider entries with non-empty embeddings; update variable naming to track best key/mapping rather than a central cache dict.
fastworkflow/cache_matching.py
Raise the project’s Python requirement to 3.13–3.14 and remove the speedict dependency from packaging and documentation.
  • Update pyproject.toml to bump the package version from 2.30.1 to 2.31.0 and change the Python requirement from >=3.11,<3.14 to >=3.13,<3.15, removing speedict from dependencies.
  • Update README installation notes to require Python 3.13+, mention sqlite3 replacing speedict/RocksDB, document new .sqlite3-based conversation and NLU cache locations, and advise operators to delete unused .rdb/cache.db directories and downstream rocksdict shims.
  • Align CLI docs and FastAPI MCP notes to describe SQLite-backed conversation persistence instead of Rdict-backed storage.
pyproject.toml
README.md
fastworkflow/run_fastapi_mcp/__main__.py
Add test coverage for KVStore/UtteranceCacheStore behavior, cross-process concurrency, cache-matching via BLOB rows, and enablecache’s JSON-only constraint.
  • Add tests for KVStore basic semantics (round trip, KeyError), int key coercion, keys() materialisation behavior for concurrent mutation, JSON-not-pickle storage, UtteranceCacheStore round-trip, and sharing a SQLite file with KVStore; include a multi-process concurrency test exercising KVStore writes from four processes.
  • Add tests for ConversationStore concurrency across processes using WAL mode, validating append_conversation_turns, count_conversation_turns, get_conversation, and .sqlite3 path expectations.
  • Add tests verifying utterance cache matching via UtteranceCacheStore BLOB embeddings (with fake pipeline), and ensuring no legacy 'cache' JSON blob remains in the shared SQLite file.
  • Add tests for enablecache decorator using KVStore, including successful caching of JSON-serialisable return values and TypeError raised for non-JSON-serialisable results when SPEEDDICT_FOLDERNAME environment/config is set.
tests/test_kvstore.py
tests/test_conversation_store_concurrency.py
tests/test_cache_matching_sqlite.py
tests/test_enablecache_kvstore.py
tests/test_fastapi_memory_bounds.py
Update behavior around legacy inline 'turns' fields to ensure they are ignored post-SQLite migration rather than used as authoritative data.
  • Change ConversationStore.count_conversation_turns and append/update paths to no longer consider inline 'turns' lists, relying solely on appended_turn_count and per-turn keys.
  • Update the memory bounds test describing downgrade/upgrade behavior to assert that a stale inline 'turns' field is ignored, preventing duplication or reordering of durable per-turn entries.
  • Modify tests that manually poison a conversation record with inline 'turns' to expect that the durable per-turn entries remain authoritative and that newly appended turns extend that sequence correctly.
fastworkflow/run_fastapi_mcp/conversation_store.py
tests/test_fastapi_memory_bounds.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 security issues, 3 other issues, and left some high level feedback:

Security issues:

  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)

General comments:

  • Now that KVStore and UtteranceCacheStore implement context managers, consider using with in the call sites (e.g., cache_matching, intent_detection helpers) instead of manual close() calls to make lifecycle management more robust and less error-prone.
  • KVStore stores values as JSON TEXT in a single v column; if you anticipate large or complex values (e.g., big payloads), you may want to revisit this schema (e.g., BLOB or additional typed columns) to avoid unnecessary UTF-8 encode/decode overhead and potential size-related performance issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Now that KVStore and UtteranceCacheStore implement context managers, consider using `with` in the call sites (e.g., cache_matching, intent_detection helpers) instead of manual `close()` calls to make lifecycle management more robust and less error-prone.
- KVStore stores values as JSON `TEXT` in a single `v` column; if you anticipate large or complex values (e.g., big payloads), you may want to revisit this schema (e.g., `BLOB` or additional typed columns) to avoid unnecessary UTF-8 encode/decode overhead and potential size-related performance issues.

## Individual Comments

### Comment 1
<location path="fastworkflow/kvstore.py" line_range="168-174" />
<code_context>
+        )
+        self._conn.commit()
+
+    def iter_entries(self) -> Iterator[tuple[str, dict[str, Any]]]:
+        rows = self._conn.execute(
+            "SELECT k, meta, vec FROM utterance_cache"
+        ).fetchall()
+        for key, meta_json, vec in rows:
+            meta = json.loads(meta_json)
+            yield key, {
+                "utterance": meta.get("utterance", ""),
+                "command_mapping": meta.get("command_mapping", {}),
</code_context>
<issue_to_address>
**suggestion (performance):** UtteranceCacheStore.iter_entries uses fetchall(), which may not scale for large caches.

iter_entries() loads the entire `utterance_cache` table into memory via `fetchall()`, which can cause high memory usage and GC overhead for large caches, especially when `cache_match` builds `list(db.iter_entries())`. Consider iterating directly over the cursor (e.g. `for key, meta_json, vec in self._conn.execute(...):`) and having `cache_match` consume that iterator instead of materialising a list when only the best match is required.
</issue_to_address>

### Comment 2
<location path="tests/test_kvstore.py" line_range="115-124" />
<code_context>
+        raise SystemExit(1) from exc
+
+
+def test_kvstore_four_process_concurrency(tmp_path: Path):
+    path = str(tmp_path / "concurrent.sqlite3")
+    n_workers = 4
+    n_ops = 50
+    result_paths = [str(tmp_path / f"result_{i}.txt") for i in range(n_workers)]
+    procs = [
+        multiprocessing.Process(
+            target=_mp_writer, args=(path, i, n_ops, result_paths[i])
+        )
+        for i in range(n_workers)
+    ]
+    for p in procs:
+        p.start()
+    for p in procs:
+        p.join(timeout=60)
+        assert p.exitcode == 0, f"worker exit {p.exitcode}"
+
+    for rp in result_paths:
+        assert Path(rp).read_text(encoding="utf-8") == "ok"
+
+    with KVStore(path) as db:
+        keys = list(db.keys())
+    assert len(keys) == n_workers * n_ops
</code_context>
<issue_to_address>
**suggestion (testing):** Concurrency test should also validate stored values, not just key count

This test only checks the total key count. To catch silent data corruption under concurrent writes, it should also validate that each key’s stored `(worker, i)` payload is correct (e.g., iterate `db.keys()` and assert `db[key]['worker']` / `db[key]['i']` follow the expected pattern). That would more robustly exercise WAL + `busy_timeout` under contention.
</issue_to_address>

### Comment 3
<location path="tests/test_kvstore.py" line_range="64-73" />
<code_context>
+    assert b"DefinitelyNotPickle" in raw  # plain JSON text
+
+
+def test_utterance_cache_float32_blob_round_trip(tmp_path: Path):
+    path = str(tmp_path / "utt.sqlite3")
+    vec = np.arange(8, dtype=np.float32)
+    with UtteranceCacheStore(path) as store:
+        store.upsert(
+            "123",
+            utterance="hello",
+            command_mapping={"cmd": {"frequency": 1, "feedback_date": "t"}},
+            embedding=vec,
+        )
+        entry = store.get("123")
+        assert entry is not None
+        assert entry["utterance"] == "hello"
+        assert entry["command_mapping"]["cmd"]["frequency"] == 1
+        np.testing.assert_array_equal(entry["embedding"], vec)
+
+        entries = list(store.iter_entries())
+        assert len(entries) == 1
+        assert entries[0][0] == "123"
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for UtteranceCacheStore handling of None/empty embeddings

The current round-trip test only covers non-empty float32 vectors. To exercise the BLOB packing/unpacking edge cases in `_pack_vec/_unpack_vec`, please add tests that upsert and then read back entries with `embedding=None` and `embedding=np.array([], dtype=np.float32)`. These should confirm that both paths yield `embedding is None` on read and that `cache_match` safely skips such entries.

Suggested implementation:

```python
from fastworkflow.kvstore import KVStore, UtteranceCacheStore


def test_utterance_cache_none_embedding_round_trip(tmp_path: Path):
    path = str(tmp_path / "utt_none.sqlite3")
    with UtteranceCacheStore(path) as store:
        store.upsert(
            "none-emb",
            utterance="hello-none",
            command_mapping={"cmd": {"frequency": 1, "feedback_date": "t"}},
            embedding=None,
        )
        entry = store.get("none-emb")
        assert entry is not None
        assert entry["utterance"] == "hello-none"
        assert entry["command_mapping"]["cmd"]["frequency"] == 1
        assert entry["embedding"] is None

        entries = list(store.iter_entries())
        assert len(entries) == 1
        assert entries[0][0] == "none-emb"
        assert entries[0][1]["embedding"] is None


def test_utterance_cache_empty_embedding_round_trip(tmp_path: Path):
    path = str(tmp_path / "utt_empty.sqlite3")
    empty_vec = np.array([], dtype=np.float32)
    with UtteranceCacheStore(path) as store:
        store.upsert(
            "empty-emb",
            utterance="hello-empty",
            command_mapping={"cmd": {"frequency": 1, "feedback_date": "t"}},
            embedding=empty_vec,
        )
        entry = store.get("empty-emb")
        assert entry is not None
        assert entry["utterance"] == "hello-empty"
        assert entry["command_mapping"]["cmd"]["frequency"] == 1
        # Empty vectors should round-trip to a None embedding, not an empty ndarray
        assert entry["embedding"] is None

        entries = list(store.iter_entries())
        assert len(entries) == 1
        assert entries[0][0] == "empty-emb"
        assert entries[0][1]["embedding"] is None

```

To fully cover the `cache_match` behavior, you should also:
1. Import the `cache_match` helper from `fastworkflow.kvstore` (or wherever it is defined).
2. Add tests that:
   - Create a `UtteranceCacheStore` containing entries with `embedding=None` and empty embeddings.
   - Call `cache_match(...)` with a non-empty query embedding.
   - Assert that:
     * No matches are returned for the entries with `embedding is None`, or
     * At minimum, `cache_match` does not raise and does not attempt to compute similarity against the `None`/empty embeddings.
You will need to adapt the test according to the actual `cache_match` signature and return type (e.g., list of matches, best match, etc.).
</issue_to_address>

### Comment 4
<location path="fastworkflow/kvstore.py" line_range="36" />
<code_context>
        self._conn.execute(f"PRAGMA busy_timeout={int(timeout * 1000)}")
</code_context>
<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location path="fastworkflow/kvstore.py" line_range="107" />
<code_context>
        self._conn.execute(f"PRAGMA busy_timeout={int(timeout * 1000)}")
</code_context>
<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread fastworkflow/kvstore.py
Comment thread tests/test_kvstore.py
Comment thread tests/test_kvstore.py
Comment thread fastworkflow/kvstore.py Outdated
Comment thread fastworkflow/kvstore.py Outdated
Stream utterance_cache rows instead of fetchall, prefer context managers
at call sites, rely on sqlite3.connect(timeout=) instead of interpolating
PRAGMA busy_timeout, and harden concurrency/empty-embedding tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@drawal1
drawal1 merged commit 092ea6a into radiantlogicinc:main Aug 8, 2026
1 of 2 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CVE automation follow-up (post-merge)

Multi-scanner pass completed against the merged v2.31.0 tree (092ea6a).

Tool Result
Syft / Trivy / Grype / osv-scanner OK — 1 Python advisory (diskcache CVE-2025-69872), OpenVEX suppresses it
Dockle / Dive OK on proxy image python:3.13-slim-bookworm (no first-party Dockerfile)
Snyk / Docker Scout Skipped (no SNYK_TOKEN / Docker Hub login)

Easy pyproject.toml remediations: none (diskcache still has no PyPI fix).

Human-review HTML: all current findings recommended IGNORE with rationale (diskcache VEX’d; proxy-image OS/CIS findings out of product scope).

Follow-up PR with refreshed reports: #62

View PR

Open in Web View Automation 

Sent by Cursor Automation: Untitled

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants