diff --git a/lib/crewai-tools/pyproject.toml b/lib/crewai-tools/pyproject.toml index a683b9967..8e0b6132a 100644 --- a/lib/crewai-tools/pyproject.toml +++ b/lib/crewai-tools/pyproject.toml @@ -8,12 +8,10 @@ authors = [ ] requires-python = ">=3.10, <3.14" dependencies = [ - "lancedb~=0.5.4", "pytube~=15.0.0", "requests~=2.32.5", "docker~=7.1.0", "crewai==1.9.3", - "lancedb~=0.5.4", "tiktoken~=0.8.0", "beautifulsoup4~=4.13.4", "python-docx~=1.2.0", diff --git a/lib/crewai/pyproject.toml b/lib/crewai/pyproject.toml index b4b3e2ecc..30e370072 100644 --- a/lib/crewai/pyproject.toml +++ b/lib/crewai/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "mcp~=1.26.0", "uv~=0.9.13", "aiosqlite~=0.21.0", - "lancedb>=0.4.0", + "lancedb>=0.29.2", ] [project.urls] diff --git a/lib/crewai/src/crewai/cli/memory_tui.py b/lib/crewai/src/crewai/cli/memory_tui.py index 98576670d..3310786e7 100644 --- a/lib/crewai/src/crewai/cli/memory_tui.py +++ b/lib/crewai/src/crewai/cli/memory_tui.py @@ -290,13 +290,20 @@ class MemoryTUI(App[None]): if self._memory is None: panel.update(self._init_error or "No memory loaded.") return + _DISPLAY_LIMIT = 1000 info = self._memory.info(path) self._last_scope_info = info - self._entries = self._memory.list_records(scope=path, limit=200) + self._entries = self._memory.list_records(scope=path, limit=_DISPLAY_LIMIT) panel.update(_format_scope_info(info)) panel.border_title = "Detail" entry_list = self.query_one("#entry-list", OptionList) - entry_list.border_title = f"Entries ({len(self._entries)})" + capped = info.record_count > _DISPLAY_LIMIT + count_label = ( + f"Entries (showing {_DISPLAY_LIMIT} of {info.record_count} — display limit)" + if capped + else f"Entries ({len(self._entries)})" + ) + entry_list.border_title = count_label self._populate_entry_list() def on_option_list_option_highlighted( @@ -376,6 +383,11 @@ class MemoryTUI(App[None]): return info_lines: list[str] = [] + info_lines.append( + "[dim italic]Searched the full dataset" + + (f" within [bold]{scope}[/]" if scope else "") + + " using the recall flow (semantic + recency + importance).[/]\n" + ) if not self._custom_embedder: info_lines.append( "[dim italic]Note: Using default OpenAI embedder. " diff --git a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py index d40999985..e8ca283ce 100644 --- a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py +++ b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py @@ -53,6 +53,7 @@ class LanceDBStorage: path: str | Path | None = None, table_name: str = "memories", vector_dim: int | None = None, + compact_every: int = 100, ) -> None: """Initialize LanceDB storage. @@ -64,6 +65,10 @@ class LanceDBStorage: vector_dim: Dimensionality of the embedding vector. When ``None`` (default), the dimension is auto-detected from the existing table schema or from the first saved embedding. + compact_every: Number of ``save()`` calls between automatic + background compactions. Each ``save()`` creates one new + fragment file; compaction merges them, keeping query + performance consistent. Set to 0 to disable. """ if path is None: storage_dir = os.environ.get("CREWAI_STORAGE_DIR") @@ -78,6 +83,22 @@ class LanceDBStorage: self._table_name = table_name self._db = lancedb.connect(str(self._path)) + # On macOS and Linux the default per-process open-file limit is 256. + # A LanceDB table stores one file per fragment (one fragment per save() + # call by default). With hundreds of fragments, a single full-table + # scan opens all of them simultaneously, exhausting the limit. + # Raise it proactively so scans on large tables never hit OS error 24. + try: + import resource + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + if soft < 4096: + resource.setrlimit(resource.RLIMIT_NOFILE, (min(hard, 4096), hard)) + except Exception: + pass # Windows or already at the max hard limit — safe to ignore + + self._compact_every = compact_every + self._save_count = 0 + # Get or create a shared write lock for this database path. resolved = str(self._path.resolve()) with LanceDBStorage._path_locks_guard: @@ -91,6 +112,11 @@ class LanceDBStorage: try: self._table: lancedb.table.Table | None = self._db.open_table(self._table_name) self._vector_dim: int = self._infer_dim_from_table(self._table) + # Best-effort: create the scope index if it doesn't exist yet. + self._ensure_scope_index() + # Compact in the background if the table has accumulated many + # fragments from previous runs (each save() creates one). + self._compact_if_needed() except Exception: self._table = None self._vector_dim = vector_dim or 0 # 0 = not yet known @@ -178,6 +204,56 @@ class LanceDBStorage: table.delete("id = '__schema_placeholder__'") return table + def _ensure_scope_index(self) -> None: + """Create a BTREE scalar index on the ``scope`` column if not present. + + A scalar index lets LanceDB skip a full table scan when filtering by + scope prefix, which is the hot path for ``list_records``, + ``get_scope_info``, and ``list_scopes``. The call is best-effort: + if the table is empty or the index already exists the exception is + swallowed silently. + """ + if self._table is None: + return + try: + self._table.create_scalar_index("scope", index_type="BTREE", replace=False) + except Exception: + pass # index already exists, table empty, or unsupported version + + # ------------------------------------------------------------------ + # Automatic background compaction + # ------------------------------------------------------------------ + + def _compact_if_needed(self) -> None: + """Spawn a background compaction on startup. + + Called whenever an existing table is opened so that fragments + accumulated in previous sessions are silently merged before the + first query. ``optimize()`` returns quickly when the table is + already compact, so the cost is negligible in the common case. + """ + if self._table is None or self._compact_every <= 0: + return + self._compact_async() + + def _compact_async(self) -> None: + """Fire-and-forget: compact the table in a daemon background thread.""" + threading.Thread( + target=self._compact_safe, + daemon=True, + name="lancedb-compact", + ).start() + + def _compact_safe(self) -> None: + """Run ``table.optimize()`` in a background thread, absorbing errors.""" + try: + if self._table is not None: + self._table.optimize() + # Refresh the scope index so new fragments are covered. + self._ensure_scope_index() + except Exception: + _logger.debug("LanceDB background compaction failed", exc_info=True) + def _ensure_table(self, vector_dim: int | None = None) -> lancedb.table.Table: """Return the table, creating it lazily if needed. @@ -239,6 +315,7 @@ class LanceDBStorage: if r.embedding and len(r.embedding) > 0: dim = len(r.embedding) break + is_new_table = self._table is None with self._write_lock: self._ensure_table(vector_dim=dim) rows = [self._record_to_row(r) for r in records] @@ -246,6 +323,13 @@ class LanceDBStorage: if r["vector"] is None or len(r["vector"]) != self._vector_dim: r["vector"] = [0.0] * self._vector_dim self._retry_write("add", rows) + # Create the scope index on the first save so it covers the initial dataset. + if is_new_table: + self._ensure_scope_index() + # Auto-compact every N saves so fragment files don't pile up. + self._save_count += 1 + if self._compact_every > 0 and self._save_count % self._compact_every == 0: + self._compact_async() def update(self, record: MemoryRecord) -> None: """Update a record by ID. Preserves created_at, updates last_accessed.""" @@ -261,6 +345,10 @@ class LanceDBStorage: def touch_records(self, record_ids: list[str]) -> None: """Update last_accessed to now for the given record IDs. + Uses a single batch ``table.update()`` call instead of N + delete-and-re-add cycles, which is both faster and avoids + unnecessary write amplification. + Args: record_ids: IDs of records to touch. """ @@ -268,25 +356,20 @@ class LanceDBStorage: return with self._write_lock: now = datetime.utcnow().isoformat() - for rid in record_ids: - safe_id = str(rid).replace("'", "''") - rows = ( - self._table.search([0.0] * self._vector_dim) - .where(f"id = '{safe_id}'") - .limit(1) - .to_list() - ) - if rows: - rows[0]["last_accessed"] = now - self._retry_write("delete", f"id = '{safe_id}'") - self._retry_write("add", [rows[0]]) + safe_ids = [str(rid).replace("'", "''") for rid in record_ids] + ids_expr = ", ".join(f"'{rid}'" for rid in safe_ids) + self._retry_write( + "update", + where=f"id IN ({ids_expr})", + values={"last_accessed": now}, + ) def get_record(self, record_id: str) -> MemoryRecord | None: """Return a single record by ID, or None if not found.""" if self._table is None: return None safe_id = str(record_id).replace("'", "''") - rows = self._table.search([0.0] * self._vector_dim).where(f"id = '{safe_id}'").limit(1).to_list() + rows = self._table.search().where(f"id = '{safe_id}'").limit(1).to_list() if not rows: return None return self._row_to_record(rows[0]) @@ -374,13 +457,31 @@ class LanceDBStorage: self._retry_write("delete", where_expr) return before - self._table.count_rows() - def _scan_rows(self, scope_prefix: str | None = None, limit: int = _SCAN_ROWS_LIMIT) -> list[dict[str, Any]]: - """Scan rows optionally filtered by scope prefix.""" + def _scan_rows( + self, + scope_prefix: str | None = None, + limit: int = _SCAN_ROWS_LIMIT, + columns: list[str] | None = None, + ) -> list[dict[str, Any]]: + """Scan rows optionally filtered by scope prefix. + + Uses a full table scan (no vector query) so the limit is applied after + the scope filter, not to ANN candidates before filtering. + + Args: + scope_prefix: Optional scope path prefix to filter by. + limit: Maximum number of rows to return (applied after filtering). + columns: Optional list of column names to fetch. Pass only the + columns you need for metadata operations to avoid reading the + heavy ``vector`` column unnecessarily. + """ if self._table is None: return [] - q = self._table.search([0.0] * self._vector_dim) + q = self._table.search() if scope_prefix is not None and scope_prefix.strip("/"): q = q.where(f"scope LIKE '{scope_prefix.rstrip('/')}%'") + if columns is not None: + q = q.select(columns) return q.limit(limit).to_list() def list_records( @@ -406,7 +507,10 @@ class LanceDBStorage: prefix = scope if scope != "/" else "" if prefix and not prefix.startswith("/"): prefix = "/" + prefix - rows = self._scan_rows(prefix or None) + rows = self._scan_rows( + prefix or None, + columns=["scope", "categories_str", "created_at"], + ) if not rows: return ScopeInfo( path=scope or "/", @@ -453,7 +557,7 @@ class LanceDBStorage: def list_scopes(self, parent: str = "/") -> list[str]: parent = parent.rstrip("/") or "" prefix = (parent + "/") if parent else "/" - rows = self._scan_rows(prefix if prefix != "/" else None) + rows = self._scan_rows(prefix if prefix != "/" else None, columns=["scope"]) children: set[str] = set() for row in rows: sc = str(row.get("scope", "")) @@ -465,7 +569,7 @@ class LanceDBStorage: return sorted(children) def list_categories(self, scope_prefix: str | None = None) -> dict[str, int]: - rows = self._scan_rows(scope_prefix) + rows = self._scan_rows(scope_prefix, columns=["categories_str"]) counts: dict[str, int] = {} for row in rows: cat_str = row.get("categories_str") or "[]" @@ -498,6 +602,21 @@ class LanceDBStorage: if prefix: self._table.delete(f"scope >= '{prefix}' AND scope < '{prefix}/\uFFFF'") + def optimize(self) -> None: + """Compact the table synchronously and refresh the scope index. + + Under normal usage this is called automatically in the background + (every ``compact_every`` saves and on startup when the table is + fragmented). Call this explicitly only when you need the compaction + to be complete before the next operation — for example immediately + after a large bulk import, before a latency-sensitive recall. + It is a no-op if the table does not exist. + """ + if self._table is None: + return + self._table.optimize() + self._ensure_scope_index() + async def asave(self, records: list[MemoryRecord]) -> None: self.save(records) diff --git a/uv.lock b/uv.lock index ea4af006f..dba6ab30c 100644 --- a/uv.lock +++ b/uv.lock @@ -1204,7 +1204,7 @@ requires-dist = [ { name = "json-repair", specifier = "~=0.25.2" }, { name = "json5", specifier = "~=0.10.0" }, { name = "jsonref", specifier = "~=1.1.0" }, - { name = "lancedb", specifier = ">=0.4.0" }, + { name = "lancedb", specifier = ">=0.29.2" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.74.9,<3" }, { name = "mcp", specifier = "~=1.26.0" }, { name = "mem0ai", marker = "extra == 'mem0'", specifier = "~=0.1.94" }, @@ -1286,7 +1286,6 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "crewai" }, { name = "docker" }, - { name = "lancedb" }, { name = "pymupdf" }, { name = "python-docx" }, { name = "pytube" }, @@ -1428,7 +1427,6 @@ requires-dist = [ { name = "firecrawl-py", marker = "extra == 'firecrawl-py'", specifier = ">=1.8.0" }, { name = "gitpython", marker = "extra == 'github'", specifier = "==3.1.38" }, { name = "hyperbrowser", marker = "extra == 'hyperbrowser'", specifier = ">=0.18.0" }, - { name = "lancedb", specifier = "~=0.5.4" }, { name = "langchain-apify", marker = "extra == 'apify'", specifier = ">=0.1.2,<1.0.0" }, { name = "linkup-sdk", marker = "extra == 'linkup-sdk'", specifier = ">=0.2.2" }, { name = "lxml", marker = "extra == 'rag'", specifier = ">=5.3.0,<5.4.0" }, @@ -3226,27 +3224,54 @@ wheels = [ ] [[package]] -name = "lancedb" -version = "0.5.7" +name = "lance-namespace" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lance-namespace-urllib3-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/c6/aec0d7752e15536564b50cf9a8926f0e5d7780aa3ab8ce8bca46daa55659/lance_namespace-0.5.2.tar.gz", hash = "sha256:566cc33091b5631793ab411f095d46c66391db0a62343cd6b4470265bb04d577", size = 10274, upload-time = "2026-02-20T03:14:31.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/3d/737c008d8fb2861e7ce260e2ffab0d5058eae41556181f80f1a1c3b52ef5/lance_namespace-0.5.2-py3-none-any.whl", hash = "sha256:6ccaf5649bf6ee6aa92eed9c535a114b7b4eb08e89f40426f58bc1466cbcffa3", size = 12087, upload-time = "2026-02-20T03:14:35.261Z" }, +] + +[[package]] +name = "lance-namespace-urllib3-client" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "cachetools" }, - { name = "click" }, - { name = "deprecation" }, - { name = "overrides" }, { name = "pydantic" }, - { name = "pylance" }, - { name = "pyyaml" }, - { name = "ratelimiter" }, - { name = "requests" }, - { name = "retry" }, - { name = "semver" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, + { name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" }, + { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/64/51622c93ec8c164483c83b68764e5e76e52286c0137a8247bc6a7fac25f4/lance_namespace_urllib3_client-0.5.2.tar.gz", hash = "sha256:8a3a238006e6eabc01fc9d385ac3de22ba933aef0ae8987558f3c3199c9b3799", size = 172578, upload-time = "2026-02-20T03:14:33.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/10/f86d994498b37f7f35d0b8c2f7626a16fe4cb1949b518c1e5d5052ecf95f/lance_namespace_urllib3_client-0.5.2-py3-none-any.whl", hash = "sha256:83cefb6fd6e5df0b99b5e866ee3d46300d375b75e8af32c27bc16fbf7c1a5978", size = 300351, upload-time = "2026-02-20T03:14:34.236Z" }, +] + +[[package]] +name = "lancedb" +version = "0.29.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "lance-namespace" }, + { name = "numpy" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "pyarrow" }, + { name = "pydantic" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/14/1b/f87a2b6420f6f55ea64e5f8f18f231450cc602a0854739bcf946cebc080a/lancedb-0.5.7.tar.gz", hash = "sha256:878914b493f91d09a77b14f1528104741f273234cbdd6671be705f447701fd51", size = 102890, upload-time = "2024-02-22T20:11:29.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/21/ecb191feff512640a59e17fe1737bd9c33970bc857c59a77fa61d5e314d9/lancedb-0.5.7-py3-none-any.whl", hash = "sha256:6169966f715ef530be545950e1aaf9f3f160967e4ba7456cd67c9f30f678095d", size = 115104, upload-time = "2024-02-22T20:11:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/fbb25946a234928958e016c5448343fd314bd601315f9587568321591a17/lancedb-0.29.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc1faf2e12addb9585569d0fb114ecc25ec3867e4e1aa6934e9343cfb5265ee4", size = 42341708, upload-time = "2026-02-09T06:21:31.677Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/d3a7b6d0237e343ad5b2afef2bdb99423746d5c3e882a9cab68dc041c2d0/lancedb-0.29.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fec19cfc52a5b9d98e060bd2f02a1c9df6a0bfd15b36021b6017327a41893a3", size = 44147347, upload-time = "2026-02-09T06:31:02.567Z" }, + { url = "https://files.pythonhosted.org/packages/66/21/153a42294279c5b66d763f357808dde0899b71c5c8e41ad5ecbeeb8728df/lancedb-0.29.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:636939ab9225d435020ba17c231f5eaba15312a07813bcebcd71128204cc039f", size = 47186355, upload-time = "2026-02-09T06:34:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/f7041ae7d7730332b2754fe7adc2e0bd496f92bf526ac710b7eb3caf1d0a/lancedb-0.29.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f79b32083fcab139009db521d2f7fcd6afe4cca98a78c06c5940ff00a170cc1a", size = 44172354, upload-time = "2026-02-09T06:31:03.834Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/c152497c18cea0f36b523fc03b8e0a48be2b120276cc15a86d79b8b83cde/lancedb-0.29.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:991043a28c1f49f14df2479b554a95c759a85666dc58573cc86c1b9df05db794", size = 47228009, upload-time = "2026-02-09T06:34:40.872Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bd47bca59a87a88a4ca291a0718291422440750d84b34318048c70a537c2/lancedb-0.29.2-cp39-abi3-win_amd64.whl", hash = "sha256:101eb0ac018bb0b643dd9ea22065f6f2102e9d44c9ac58a197477ccbfbc0b9fa", size = 52028768, upload-time = "2026-02-09T07:00:02.272Z" }, ] [[package]] @@ -5414,15 +5439,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, ] -[[package]] -name = "py" -version = "1.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, -] - [[package]] name = "py-rust-stemmers" version = "0.1.5" @@ -5916,22 +5932,6 @@ crypto = [ { name = "cryptography" }, ] -[[package]] -name = "pylance" -version = "0.9.18" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "pyarrow" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/b8/15d4d380f0858dde46d42891776017e3bf9eb40129b3fe222637eecf8f43/pylance-0.9.18-cp38-abi3-macosx_10_15_x86_64.whl", hash = "sha256:fe2445d922c594d90e89111385106f6b152caab27996217db7bb4b8947eb0bea", size = 20319043, upload-time = "2024-02-19T07:36:11.206Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/69f927a215d415362300d14a50b3cbc6575fd640ca5e632d488e022d3af1/pylance-0.9.18-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:a2c424c50f5186edbbcc5a26f34063ed09d9a7390e28033395728ce02b5658f0", size = 18780426, upload-time = "2024-02-19T07:30:10.963Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b8/991e4544cfa21de2c7de5dd6bd8410df454fec5b374680fa96cd8698763b/pylance-0.9.18-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10af06edfde3e8451bf2251381d3980a0a164eab9d4c3d4dc8b6318969e958a6", size = 21584420, upload-time = "2024-02-19T07:32:30.283Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5e/ff80f31d995315790393cbe599565f55d03eb717654cfeb65b701803e887/pylance-0.9.18-cp38-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:d8bb9045d7163cc966b9fe34a917044192be37a90915475b77461e5b7d89e442", size = 19960982, upload-time = "2024-02-19T07:32:49.686Z" }, - { url = "https://files.pythonhosted.org/packages/2d/e5/c0e0a6cad08ab86a9c0bce7e8caef8f666337bb7950e2ab151ea4f88242d/pylance-0.9.18-cp38-abi3-win_amd64.whl", hash = "sha256:5ea80b7bf70d992f3fe63bce2d2f064f742124c04eaedeb76baca408ded85a2c", size = 22089079, upload-time = "2024-02-19T07:42:43.262Z" }, -] - [[package]] name = "pylatexenc" version = "2.10" @@ -6629,15 +6629,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/fd/0d025466f0f84552634f2a94c018df34568fe55cc97184a6bb2c719c5b3a/rapidocr-3.6.0-py3-none-any.whl", hash = "sha256:d16b43872fc4dfa1e60996334dcd0dc3e3f1f64161e2332bc1873b9f65754e6b", size = 15067340, upload-time = "2026-01-28T14:45:04.271Z" }, ] -[[package]] -name = "ratelimiter" -version = "1.2.0.post0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/e0/b36010bddcf91444ff51179c076e4a09c513674a56758d7cfea4f6520e29/ratelimiter-1.2.0.post0.tar.gz", hash = "sha256:5c395dcabdbbde2e5178ef3f89b568a3066454a6ddc223b76473dac22f89b4f7", size = 9182, upload-time = "2017-12-12T00:33:38.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/80/2164fa1e863ad52cc8d870855fba0fbb51edd943edffd516d54b5f6f8ff8/ratelimiter-1.2.0.post0-py3-none-any.whl", hash = "sha256:a52be07bc0bb0b3674b4b304550f10c769bbb00fead3072e035904474259809f", size = 6642, upload-time = "2017-12-12T00:33:37.505Z" }, -] - [[package]] name = "redis" version = "7.1.0" @@ -6794,19 +6785,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] -[[package]] -name = "retry" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "decorator" }, - { name = "py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448, upload-time = "2016-05-11T13:58:51.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986, upload-time = "2016-05-11T13:58:39.925Z" }, -] - [[package]] name = "rich" version = "14.3.2"