Merge pull request #91 from ggozad/feat/auto-vacuming

Auto-vacuuming versions older than VACUUM_RETENTION_SECONDS (default=60sec)
This commit is contained in:
Yiorgis Gozadinos 2025-10-07 12:34:32 +03:00 committed by GitHub
commit d87cafdea4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 199 additions and 112 deletions

BIN
docs/.DS_Store vendored

Binary file not shown.

View file

@ -173,6 +173,8 @@ Reduce disk usage by optimizing and pruning old table versions across all tables
haiku-rag vacuum haiku-rag vacuum
``` ```
**Automatic Cleanup:** Vacuum runs automatically in the background after document operations. By default, it removes versions older than 60 seconds (`VACUUM_RETENTION_SECONDS`), preserving recent versions for concurrent connections. Manual vacuum can be useful for cleanup after bulk operations or to free disk space immediately.
### Rebuild Database ### Rebuild Database
Rebuild the database by deleting all chunks & embeddings and re-indexing all documents. This is useful Rebuild the database by deleting all chunks & embeddings and re-indexing all documents. This is useful

View file

@ -232,6 +232,12 @@ CHUNK_SIZE=256
# When expanded chunks overlap or are adjacent, they are automatically merged # When expanded chunks overlap or are adjacent, they are automatically merged
# into single chunks with continuous content to eliminate duplication # into single chunks with continuous content to eliminate duplication
CONTEXT_CHUNK_RADIUS=0 CONTEXT_CHUNK_RADIUS=0
# Vacuum retention threshold (seconds) for automatic cleanup
# When documents are added/updated, old table versions older than this are removed
# Default: 60 seconds (safe for concurrent connections)
# Set to 0 for aggressive cleanup (removes all old versions immediately)
VACUUM_RETENTION_SECONDS=60
``` ```
#### Markdown Preprocessor #### Markdown Preprocessor

BIN
docs/img/.DS_Store vendored

Binary file not shown.

View file

@ -23,10 +23,10 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"docling>=2.52.0", "docling>=2.55.1",
"fastmcp>=2.12.3", "fastmcp>=2.12.3",
"httpx>=0.28.1", "httpx>=0.28.1",
"lancedb>=0.25.0", "lancedb>=0.25.1",
"pydantic>=2.11.9", "pydantic>=2.11.9",
"pydantic-ai>=1.0.8", "pydantic-ai>=1.0.8",
"pydantic-graph>=1.0.8", "pydantic-graph>=1.0.8",
@ -62,7 +62,7 @@ dev = [
"mkdocs-material>=9.6.14", "mkdocs-material>=9.6.14",
"pydantic-evals>=1.0.8", "pydantic-evals>=1.0.8",
"pre-commit>=4.2.0", "pre-commit>=4.2.0",
"pyright>=1.1.405", "pyright>=1.1.406",
"pytest>=8.4.2", "pytest>=8.4.2",
"pytest-asyncio>=1.2.0", "pytest-asyncio>=1.2.0",
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",

View file

@ -61,7 +61,6 @@ async def populate_db(spec: DatasetSpec) -> None:
metadata=payload.metadata, metadata=payload.metadata,
) )
progress.advance(task) progress.advance(task)
rag.store.vacuum()
def _is_relevant_match(retrieved_uri: str | None, sample: RetrievalSample) -> bool: def _is_relevant_match(retrieved_uri: str | None, sample: RetrievalSample) -> bool:

View file

@ -46,6 +46,9 @@ class HaikuRAG:
async def __aexit__(self, exc_type, exc_val, exc_tb): # noqa: ARG002 async def __aexit__(self, exc_type, exc_val, exc_tb): # noqa: ARG002
"""Async context manager exit.""" """Async context manager exit."""
# Wait for any pending vacuum to complete before closing
async with self.store._vacuum_lock:
pass
self.close() self.close()
return False return False
@ -617,13 +620,13 @@ class HaikuRAG:
# Final maintenance: centralized vacuum to curb disk usage # Final maintenance: centralized vacuum to curb disk usage
try: try:
self.store.vacuum() await self.store.vacuum()
except Exception: except Exception:
pass pass
async def vacuum(self) -> None: async def vacuum(self) -> None:
"""Optimize and clean up old versions across all tables.""" """Optimize and clean up old versions across all tables."""
self.store.vacuum() await self.store.vacuum()
def close(self): def close(self):
"""Close the underlying store connection.""" """Close the underlying store connection."""

View file

@ -57,6 +57,11 @@ class AppConfig(BaseModel):
# and error out when the database does not already exist. # and error out when the database does not already exist.
DISABLE_DB_AUTOCREATE: bool = False DISABLE_DB_AUTOCREATE: bool = False
# Vacuum retention threshold in seconds. Only versions older than this
# threshold will be removed during vacuum operations. Default is 60 seconds
# to allow concurrent connections to safely use recent versions.
VACUUM_RETENTION_SECONDS: int = 60
@field_validator("MONITOR_DIRECTORIES", mode="before") @field_validator("MONITOR_DIRECTORIES", mode="before")
@classmethod @classmethod
def parse_monitor_directories(cls, v): def parse_monitor_directories(cls, v):

View file

@ -27,7 +27,7 @@ class SQLiteToLanceDBMigrator:
self.lancedb_path = lancedb_path self.lancedb_path = lancedb_path
self.console = Console() self.console = Console()
def migrate(self) -> bool: async def migrate(self) -> bool:
"""Perform the migration.""" """Perform the migration."""
try: try:
self.console.print( self.console.print(
@ -94,7 +94,7 @@ class SQLiteToLanceDBMigrator:
# Optimize and cleanup using centralized vacuum # Optimize and cleanup using centralized vacuum
self.console.print("[cyan]Optimizing LanceDB...[/cyan]") self.console.print("[cyan]Optimizing LanceDB...[/cyan]")
try: try:
lance_store.vacuum() await lance_store.vacuum()
self.console.print("[green]✅ Optimization completed[/green]") self.console.print("[green]✅ Optimization completed[/green]")
except Exception as e: except Exception as e:
self.console.print( self.console.print(
@ -313,4 +313,4 @@ async def migrate_sqlite_to_lancedb(
lancedb_path = sqlite_path.parent / (sqlite_path.stem + ".lancedb") lancedb_path = sqlite_path.parent / (sqlite_path.stem + ".lancedb")
migrator = SQLiteToLanceDBMigrator(sqlite_path, lancedb_path) migrator = SQLiteToLanceDBMigrator(sqlite_path, lancedb_path)
return migrator.migrate() return await migrator.migrate()

View file

@ -1,3 +1,4 @@
import asyncio
import json import json
import logging import logging
from datetime import timedelta from datetime import timedelta
@ -51,6 +52,7 @@ class Store:
def __init__(self, db_path: Path, skip_validation: bool = False): def __init__(self, db_path: Path, skip_validation: bool = False):
self.db_path: Path = db_path self.db_path: Path = db_path
self.embedder = get_embedder() self.embedder = get_embedder()
self._vacuum_lock = asyncio.Lock()
# Create the ChunkRecord model with the correct vector dimension # Create the ChunkRecord model with the correct vector dimension
self.ChunkRecord = create_chunk_model(self.embedder._vector_dim) self.ChunkRecord = create_chunk_model(self.embedder._vector_dim)
@ -78,14 +80,40 @@ class Store:
if not skip_validation: if not skip_validation:
self._validate_configuration() self._validate_configuration()
def vacuum(self) -> None: async def vacuum(self, retention_seconds: int | None = None) -> None:
"""Optimize and clean up old versions across all tables to reduce disk usage.""" """Optimize and clean up old versions across all tables to reduce disk usage.
Args:
retention_seconds: Retention threshold in seconds. Only versions older
than this will be removed. If None, uses Config.VACUUM_RETENTION_SECONDS.
Note:
If vacuum is already running, this method returns immediately without blocking.
Use asyncio.create_task(store.vacuum()) for non-blocking background execution.
"""
if self._has_cloud_config() and str(Config.LANCEDB_URI).startswith("db://"): if self._has_cloud_config() and str(Config.LANCEDB_URI).startswith("db://"):
return return
# Perform maintenance per table using optimize() with cleanup_older_than 0 # Skip if already running (non-blocking)
for table in [self.documents_table, self.chunks_table, self.settings_table]: if self._vacuum_lock.locked():
table.optimize(cleanup_older_than=timedelta(0)) return
async with self._vacuum_lock:
try:
# Evaluate config at runtime to allow dynamic changes
if retention_seconds is None:
retention_seconds = Config.VACUUM_RETENTION_SECONDS
# Perform maintenance per table using optimize() with configurable retention
retention = timedelta(seconds=retention_seconds)
for table in [
self.documents_table,
self.chunks_table,
self.settings_table,
]:
table.optimize(cleanup_older_than=retention)
except (RuntimeError, OSError) as e:
# Handle resource errors gracefully
logger.debug(f"Vacuum skipped due to resource constraints: {e}")
def _connect_to_lancedb(self, db_path: Path): def _connect_to_lancedb(self, db_path: Path):
"""Establish connection to LanceDB (local, cloud, or object storage).""" """Establish connection to LanceDB (local, cloud, or object storage)."""

View file

@ -1,4 +1,3 @@
import asyncio
import inspect import inspect
import json import json
import logging import logging
@ -23,7 +22,6 @@ class ChunkRepository:
def __init__(self, store: Store) -> None: def __init__(self, store: Store) -> None:
self.store = store self.store = store
self.embedder = get_embedder() self.embedder = get_embedder()
self._optimize_lock = asyncio.Lock()
def _ensure_fts_index(self) -> None: def _ensure_fts_index(self) -> None:
"""Ensure FTS index exists on the content column.""" """Ensure FTS index exists on the content column."""
@ -35,21 +33,6 @@ class ChunkRepository:
# Log the error but don't fail - FTS might already exist # Log the error but don't fail - FTS might already exist
logger.debug(f"FTS index creation skipped: {e}") logger.debug(f"FTS index creation skipped: {e}")
async def _optimize(self) -> None:
"""Optimize the chunks table to refresh indexes."""
# Skip optimization for LanceDB Cloud as it handles this automatically
if Config.LANCEDB_URI and Config.LANCEDB_URI.startswith("db://"):
return
async with self._optimize_lock:
try:
self.store.chunks_table.optimize()
except (RuntimeError, OSError) as e:
# Handle "too many open files" and other resource errors gracefully
logger.debug(
f"Table optimization skipped due to resource constraints: {e}"
)
async def create(self, entity: Chunk) -> Chunk: async def create(self, entity: Chunk) -> Chunk:
"""Create a chunk in the database.""" """Create a chunk in the database."""
assert entity.document_id, "Chunk must have a document_id to be created" assert entity.document_id, "Chunk must have a document_id to be created"
@ -77,11 +60,6 @@ class ChunkRepository:
self.store.chunks_table.add([chunk_record]) self.store.chunks_table.add([chunk_record])
entity.id = chunk_id entity.id = chunk_id
# Try to optimize if not currently locked (non-blocking)
if not self._optimize_lock.locked():
asyncio.create_task(self._optimize())
return entity return entity
async def get_by_id(self, entity_id: str) -> Chunk | None: async def get_by_id(self, entity_id: str) -> Chunk | None:
@ -125,10 +103,6 @@ class ChunkRepository:
"vector": embedding, "vector": embedding,
}, },
) )
# Try to optimize if not currently locked (non-blocking)
if not self._optimize_lock.locked():
asyncio.create_task(self._optimize())
return entity return entity
async def delete(self, entity_id: str) -> bool: async def delete(self, entity_id: str) -> bool:
@ -227,8 +201,6 @@ class ChunkRepository:
if chunk_records: if chunk_records:
self.store.chunks_table.add(chunk_records) self.store.chunks_table.add(chunk_records)
# Force optimization once at the end for bulk operations
await self._optimize()
return created_chunks return created_chunks
async def delete_all(self) -> None: async def delete_all(self) -> None:

View file

@ -1,3 +1,4 @@
import asyncio
import json import json
from datetime import datetime from datetime import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -200,6 +201,9 @@ class DocumentRepository:
chunk.order = order chunk.order = order
await self.chunk_repository.create(chunk) await self.chunk_repository.create(chunk)
# Vacuum old versions in background (non-blocking)
asyncio.create_task(self.store.vacuum())
return created_doc return created_doc
except Exception: except Exception:
# Roll back to the captured versions and re-raise # Roll back to the captured versions and re-raise
@ -230,6 +234,9 @@ class DocumentRepository:
updated_doc.id, docling_document updated_doc.id, docling_document
) )
# Vacuum old versions in background (non-blocking)
asyncio.create_task(self.store.vacuum())
return updated_doc return updated_doc
except Exception: except Exception:
# Roll back to the captured versions and re-raise # Roll back to the captured versions and re-raise

View file

@ -4,43 +4,24 @@ import pytest
from haiku.rag.config import Config from haiku.rag.config import Config
from haiku.rag.store.engine import Store from haiku.rag.store.engine import Store
from haiku.rag.store.models.chunk import Chunk
from haiku.rag.store.models.document import Document
from haiku.rag.store.repositories.chunk import ChunkRepository
from haiku.rag.store.repositories.document import DocumentRepository
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_lancedb_cloud_skips_optimization(temp_db_path): async def test_lancedb_cloud_skips_optimization(temp_db_path):
"""Test that optimization is skipped when using LanceDB Cloud (db:// URI).""" """Test that vacuum is skipped when using LanceDB Cloud (db:// URI)."""
# Create a store # Create a store
store = Store(temp_db_path) store = Store(temp_db_path)
chunk_repo = ChunkRepository(store)
doc_repo = DocumentRepository(store)
# Create a document # Mock all cloud config to simulate LanceDB Cloud usage
document = Document(content="Test document content", metadata={}) with (
created_document = await doc_repo.create(document) patch.object(Config, "LANCEDB_URI", "db://test-database"),
document_id = created_document.id patch.object(Config, "LANCEDB_API_KEY", "test-api-key"),
patch.object(Config, "LANCEDB_REGION", "us-east-1"),
# Mock LANCEDB_URI to simulate LanceDB Cloud usage ):
with patch.object(Config, "LANCEDB_URI", "db://test-database"):
# Mock the optimize method to track if it's called # Mock the optimize method to track if it's called
with patch.object(store.chunks_table, "optimize") as mock_optimize: with patch.object(store.chunks_table, "optimize") as mock_optimize:
# Create a chunk - this should trigger optimization logic # Call vacuum - this should skip optimization for LanceDB Cloud
chunk = Chunk( await store.vacuum()
document_id=document_id,
content="Test chunk content",
metadata={"test": "value"},
)
created_chunk = await chunk_repo.create(chunk)
assert created_chunk.id is not None
# Wait a moment to ensure any async optimization would complete
import asyncio
await asyncio.sleep(0.1)
# The optimize method should NOT have been called for LanceDB Cloud # The optimize method should NOT have been called for LanceDB Cloud
mock_optimize.assert_not_called() mock_optimize.assert_not_called()
@ -50,35 +31,16 @@ async def test_lancedb_cloud_skips_optimization(temp_db_path):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_local_storage_calls_optimization(temp_db_path): async def test_local_storage_calls_optimization(temp_db_path):
"""Test that optimization is called for local storage.""" """Test that vacuum calls optimization for local storage."""
# Create a store # Create a store
store = Store(temp_db_path) store = Store(temp_db_path)
chunk_repo = ChunkRepository(store)
doc_repo = DocumentRepository(store)
# Create a document
document = Document(content="Test document content", metadata={})
created_document = await doc_repo.create(document)
document_id = created_document.id
# Ensure LANCEDB_URI is empty (local storage) # Ensure LANCEDB_URI is empty (local storage)
with patch.object(Config, "LANCEDB_URI", ""): with patch.object(Config, "LANCEDB_URI", ""):
# Mock the optimize method to track if it's called # Mock the optimize method to track if it's called
with patch.object(store.chunks_table, "optimize") as mock_optimize: with patch.object(store.chunks_table, "optimize") as mock_optimize:
# Create a chunk - this should trigger optimization logic # Call vacuum - this should optimize all tables for local storage
chunk = Chunk( await store.vacuum()
document_id=document_id,
content="Test chunk content",
metadata={"test": "value"},
)
created_chunk = await chunk_repo.create(chunk)
assert created_chunk.id is not None
# Wait a moment to ensure async optimization completes
import asyncio
await asyncio.sleep(0.1)
# The optimize method SHOULD have been called for local storage # The optimize method SHOULD have been called for local storage
mock_optimize.assert_called() mock_optimize.assert_called()

View file

@ -122,3 +122,106 @@ def test_existing_database_runs_upgrades(monkeypatch, temp_db_path):
Store(temp_db_path) Store(temp_db_path)
assert called["value"] assert called["value"]
@pytest.mark.asyncio
async def test_vacuum_with_retention_threshold(temp_db_path):
store = Store(temp_db_path)
repo = DocumentRepository(store)
# Stub embeddings to avoid network
dim = repo.chunk_repository.embedder._vector_dim
async def fake_embed(x): # type: ignore[no-redef]
if isinstance(x, list):
return [[0.0] * dim for _ in x]
return [0.0] * dim
repo.chunk_repository.embedder.embed = fake_embed # type: ignore[assignment]
# Create first document
doc1 = Document(content="First document")
dl_doc1 = text_to_docling_document("First document", name="doc1.md")
await repo._create_with_docling(doc1, dl_doc1)
# Create second document
doc2 = Document(content="Second document")
dl_doc2 = text_to_docling_document("Second document", name="doc2.md")
await repo._create_with_docling(doc2, dl_doc2)
# Get initial version counts (should have multiple versions from creates)
initial_doc_versions = len(list(store.documents_table.list_versions()))
initial_chunk_versions = len(list(store.chunks_table.list_versions()))
assert initial_doc_versions > 1, "Should have multiple document table versions"
assert initial_chunk_versions > 1, "Should have multiple chunk table versions"
# Vacuum with default threshold (60 seconds) - should keep recent versions
# Note: vacuum may create new versions even when not cleaning up old ones
await store.vacuum()
after_default_doc_versions = len(list(store.documents_table.list_versions()))
after_default_chunk_versions = len(list(store.chunks_table.list_versions()))
# After vacuum with retention, version count should stay the same or increase
# (optimize may create new versions) but not decrease
assert after_default_doc_versions >= initial_doc_versions, (
"Default vacuum should not remove recent versions"
)
assert after_default_chunk_versions >= initial_chunk_versions, (
"Default vacuum should not remove recent versions"
)
# Vacuum with 0 threshold - should significantly reduce versions
await store.vacuum(retention_seconds=0)
after_zero_doc_versions = len(list(store.documents_table.list_versions()))
after_zero_chunk_versions = len(list(store.chunks_table.list_versions()))
# After aggressive vacuum, should have minimal versions (1-2)
# Note: optimize operation may create a version after cleanup
assert after_zero_doc_versions <= 2, (
f"Should have minimal document versions after vacuum(0), got {after_zero_doc_versions}"
)
assert after_zero_chunk_versions <= 2, (
f"Should have minimal chunk versions after vacuum(0), got {after_zero_chunk_versions}"
)
# And it should be significantly fewer than before
assert after_zero_doc_versions < initial_doc_versions, (
"Should have fewer versions after vacuum(0)"
)
assert after_zero_chunk_versions < initial_chunk_versions, (
"Should have fewer versions after vacuum(0)"
)
@pytest.mark.asyncio
async def test_vacuum_completes_before_context_exit(temp_db_path, monkeypatch):
"""Test that background vacuum completes when context manager exits."""
from haiku.rag.client import HaikuRAG
from haiku.rag.config import Config
from haiku.rag.utils import text_to_docling_document
# Set aggressive vacuum retention for this test
monkeypatch.setattr(Config, "VACUUM_RETENTION_SECONDS", 0)
async with HaikuRAG(db_path=temp_db_path) as client:
# Create multiple documents - each creation triggers automatic vacuum with retention=0
# This aggressively cleans up old versions between operations
for i in range(3):
doc = Document(content=f"Test document {i}")
dl_doc = text_to_docling_document(f"Test document {i}", name=f"test{i}.md")
await client.document_repository._create_with_docling(doc, dl_doc)
# After context exit, automatic vacuum should have kept versions minimal
store = Store(temp_db_path)
final_versions = len(list(store.documents_table.list_versions()))
# With retention_seconds=0, vacuum aggressively cleans up between operations
# Should have very few versions remaining (1-2)
assert final_versions <= 2, (
f"Aggressive vacuum should keep minimal versions, got {final_versions}"
)
assert final_versions >= 1, "Should have at least one version remaining"
store.close()

40
uv.lock
View file

@ -645,7 +645,7 @@ wheels = [
[[package]] [[package]]
name = "docling" name = "docling"
version = "2.52.0" version = "2.55.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "accelerate" }, { name = "accelerate" },
@ -676,14 +676,14 @@ dependencies = [
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typer" }, { name = "typer" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c9/40/e6b25533f99eb48dae6f21b226ee7ebe6c9796d42ecf4ecfce525414f5fa/docling-2.52.0.tar.gz", hash = "sha256:e6c6b4db5ed583e899528f76a9de71679c75f25a0585afcc7fc3ccff6c791d41", size = 196658, upload-time = "2025-09-11T16:12:40.951Z" } sdist = { url = "https://files.pythonhosted.org/packages/81/8c/baa24f0d64a36a87c66eef91dcf169ac346776739c4fb8065e59c31b1291/docling-2.55.1.tar.gz", hash = "sha256:e60a5612b2b993efd8a0b5464aff1b9868e3cab5c2e239c863709e6b780f3c57", size = 212483, upload-time = "2025-10-03T10:27:46.907Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/28/6a/ff3e65868f409438f2957ad4601852db51d7f6e5eb866825fa306177edc7/docling-2.52.0-py3-none-any.whl", hash = "sha256:85927fde42cd6b597c314c7a73241a7d28c817f8666b3c00c045c2905a769d8b", size = 224855, upload-time = "2025-09-11T16:12:39.319Z" }, { url = "https://files.pythonhosted.org/packages/e2/a3/2a2801cb909981b57326da2a9736cd11514d0393dc37771e200615b8b44f/docling-2.55.1-py3-none-any.whl", hash = "sha256:895aba282c6cca9ca1f6b9ff57c2002e4f581f722c608aa671d68382d4d61e07", size = 239394, upload-time = "2025-10-03T10:27:45.157Z" },
] ]
[[package]] [[package]]
name = "docling-core" name = "docling-core"
version = "2.48.1" version = "2.48.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jsonref" }, { name = "jsonref" },
@ -697,9 +697,9 @@ dependencies = [
{ name = "typer" }, { name = "typer" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/f9/0c/dce7f80e99e56570d143885fc40536107e8a39ef4de2888959e055b39607/docling_core-2.48.1.tar.gz", hash = "sha256:48cb77575dfd020a51413957e96b165e45f6d1027c641710fddb389dcb9b189c", size = 161311, upload-time = "2025-09-11T12:33:22.46Z" } sdist = { url = "https://files.pythonhosted.org/packages/38/d8/f0c8034f87d6151eb955e56975b9f2374a54d57af2b56b1682d7c8ff5c71/docling_core-2.48.4.tar.gz", hash = "sha256:d87ce3021cdae3d073ce7572a2396b69be3cde82ebf9a74d4bad1e1cdfdfd524", size = 161377, upload-time = "2025-10-01T09:10:08.614Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/90/fe/1b96120c9d94c97016716ccf46ad2708a2e76157e52dfcca4101db70fc21/docling_core-2.48.1-py3-none-any.whl", hash = "sha256:a3985999ac2067e15e589ef0f11ccde264deacaea403c0f94049242f10a6189a", size = 164330, upload-time = "2025-09-11T12:33:20.935Z" }, { url = "https://files.pythonhosted.org/packages/c8/2a/06e5f9d3083f830de8bef86f91acda994965f88d8b945ce3b257ea83e780/docling_core-2.48.4-py3-none-any.whl", hash = "sha256:367675c1165d0934ae498fa57ca2d27ef0468aad74dc44a5ab061f5d87882ea1", size = 164374, upload-time = "2025-10-01T09:10:06.034Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -1153,10 +1153,10 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "docling", specifier = ">=2.52.0" }, { name = "docling", specifier = ">=2.55.1" },
{ name = "fastmcp", specifier = ">=2.12.3" }, { name = "fastmcp", specifier = ">=2.12.3" },
{ name = "httpx", specifier = ">=0.28.1" }, { name = "httpx", specifier = ">=0.28.1" },
{ name = "lancedb", specifier = ">=0.25.0" }, { name = "lancedb", specifier = ">=0.25.1" },
{ name = "mxbai-rerank", marker = "extra == 'mxbai'", specifier = ">=0.1.6" }, { name = "mxbai-rerank", marker = "extra == 'mxbai'", specifier = ">=0.1.6" },
{ name = "pydantic", specifier = ">=2.11.9" }, { name = "pydantic", specifier = ">=2.11.9" },
{ name = "pydantic-ai", specifier = ">=1.0.8" }, { name = "pydantic-ai", specifier = ">=1.0.8" },
@ -1178,7 +1178,7 @@ dev = [
{ name = "mkdocs-material", specifier = ">=9.6.14" }, { name = "mkdocs-material", specifier = ">=9.6.14" },
{ name = "pre-commit", specifier = ">=4.2.0" }, { name = "pre-commit", specifier = ">=4.2.0" },
{ name = "pydantic-evals", specifier = ">=1.0.8" }, { name = "pydantic-evals", specifier = ">=1.0.8" },
{ name = "pyright", specifier = ">=1.1.405" }, { name = "pyright", specifier = ">=1.1.406" },
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" }, { name = "pytest-asyncio", specifier = ">=1.2.0" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
@ -1516,7 +1516,7 @@ wheels = [
[[package]] [[package]]
name = "lancedb" name = "lancedb"
version = "0.25.0" version = "0.25.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "deprecation" }, { name = "deprecation" },
@ -1529,13 +1529,13 @@ dependencies = [
{ name = "tqdm" }, { name = "tqdm" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/e7/10953deea89b06ae5bc568169d5ae888ff6df314decb92b9b3e453f53f0b/lancedb-0.25.0-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:ae2e80b7b3be3fa4d92fc8d500f47549dd1f8d28ca5092f1c898b92d0cfd4393", size = 34171227, upload-time = "2025-09-04T11:05:31.327Z" }, { url = "https://files.pythonhosted.org/packages/ad/2b/ed9870288506d8ca61cddf7b1dbb03c68f95b8797feb49467b33ef185477/lancedb-0.25.1-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:ec0a1cab435a5307054b84ffb798a4d828253f23698848788bfe31930e343c6c", size = 34985432, upload-time = "2025-09-23T23:15:56.558Z" },
{ url = "https://files.pythonhosted.org/packages/55/7f/2874a3709f1b8c487e707e171c9004a9240af3af0fd7a247b9187bb6e0f7/lancedb-0.25.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a9d67ea9edffa596c6f190151fdd535da8e355a4fd1979c1dc19d540a5665916", size = 31552856, upload-time = "2025-09-04T09:46:50.788Z" }, { url = "https://files.pythonhosted.org/packages/58/75/320f9142918b646b4b6d0277676c2466d2e0ce2a22aca320d0113b3ef035/lancedb-0.25.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:69e1f8343f6a4ff6985ea13f5c5cdf6d07435d04f8279c4fc6e623a34ceadda0", size = 31993179, upload-time = "2025-09-23T22:20:23.039Z" },
{ url = "https://files.pythonhosted.org/packages/e3/e9/faab70ad918576ed3bb7cb936474137ac265ac3026d3e16e30cd4d3daac2/lancedb-0.25.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8fe20079ed86b1ab75c65dcfc920a9646c835e9c40ef825cadd148c11b0001e", size = 32487962, upload-time = "2025-09-04T08:51:35.358Z" }, { url = "https://files.pythonhosted.org/packages/fd/44/d223cb64c9feb78dfa3857690d743e961f76e065935c8c4304cb64659882/lancedb-0.25.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9432134155474e73907fc5e1f8a4310433b9234a0c5f964c21b4c39aca50dde6", size = 32872519, upload-time = "2025-09-23T22:29:03.5Z" },
{ url = "https://files.pythonhosted.org/packages/ce/40/5471bc8115f287040b5afdf9d7a20c4685ec16cddb4a7da79e7c1f63914e/lancedb-0.25.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37bc402d85c83e454d9f2e79480b31acc5904bb159a4fc715032c7560494157", size = 35726794, upload-time = "2025-09-04T08:57:30.554Z" }, { url = "https://files.pythonhosted.org/packages/61/a6/e6d88d8076fa8c40b7b6f96a37f21c75ce3518ccbf64a351d26ae983461a/lancedb-0.25.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955c6e1aa4e249be7456ea7f7c42ba119be5a5c2c51f4d78efeb6c4f3cc2dbdf", size = 36325984, upload-time = "2025-09-23T22:31:46.118Z" },
{ url = "https://files.pythonhosted.org/packages/47/5e/aa3d9d2c7a834a9aa539b2b1c731ab860f7e32e2c87b9086ad233ecb13cd/lancedb-0.25.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9bbc20bd1e64be359ca11c90428c00b0062d26b0291bddf32ab5471a3525c76", size = 32492508, upload-time = "2025-09-04T08:53:54.661Z" }, { url = "https://files.pythonhosted.org/packages/97/84/14d4f0c3a98a324fcb401161e25fb1699c69ba1cd2928983fb283bd8b04f/lancedb-0.25.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d584bdfb96372c03a209bb8f010eb7358135e4adddb903ae1385450af39e1187", size = 32883704, upload-time = "2025-09-23T22:27:41.393Z" },
{ url = "https://files.pythonhosted.org/packages/fa/37/75f4e3ed7fa00a2cd5d321e8bf13441cdb61a83fbbcd0fa0f1a7241affe1/lancedb-0.25.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1306be9c08e208a5bcb5188275f47f962c2eda96369fad5949a3ddaf592afc6d", size = 35776383, upload-time = "2025-09-04T08:57:18.737Z" }, { url = "https://files.pythonhosted.org/packages/68/10/3e8ae8bf9880b2fed10122cef5e535bd67f0df0a874cc3122220d47ca255/lancedb-0.25.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c495da53d3dfa105364f202710d0bb2f031fe54a077b9c2ac9d098d02bd20bb2", size = 36369514, upload-time = "2025-09-23T22:30:53.605Z" },
{ url = "https://files.pythonhosted.org/packages/b5/af/eb217ea1daab5c28ce4c764d2f672f4e3a5bcd3d4faf7921a8ee28c6cb5b/lancedb-0.25.0-cp39-abi3-win_amd64.whl", hash = "sha256:f66283e5d63c99c2bfbd4eaa134d9a5c5b0145eb26a972648214f8ba87777e24", size = 37826272, upload-time = "2025-09-04T09:15:23.729Z" }, { url = "https://files.pythonhosted.org/packages/0d/fb/dce4757f257cb4e11e13b71ce502dc5d1caf51f1e5cccfdae85bf23960a0/lancedb-0.25.1-cp39-abi3-win_amd64.whl", hash = "sha256:2c6effc10c8263ea84261f49d5ff1957c18814ed7e3eaa5094d71b1aa0573871", size = 38390878, upload-time = "2025-09-23T22:55:24.687Z" },
] ]
[[package]] [[package]]
@ -3212,15 +3212,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b
[[package]] [[package]]
name = "pyright" name = "pyright"
version = "1.1.405" version = "1.1.406"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nodeenv" }, { name = "nodeenv" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" } sdist = { url = "https://files.pythonhosted.org/packages/f7/16/6b4fbdd1fef59a0292cbb99f790b44983e390321eccbc5921b4d161da5d1/pyright-1.1.406.tar.gz", hash = "sha256:c4872bc58c9643dac09e8a2e74d472c62036910b3bd37a32813989ef7576ea2c", size = 4113151, upload-time = "2025-10-02T01:04:45.488Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" }, { url = "https://files.pythonhosted.org/packages/f6/a2/e309afbb459f50507103793aaef85ca4348b66814c86bc73908bdeb66d12/pyright-1.1.406-py3-none-any.whl", hash = "sha256:1d81fb43c2407bf566e97e57abb01c811973fdb21b2df8df59f870f688bdca71", size = 5980982, upload-time = "2025-10-02T01:04:43.137Z" },
] ]
[[package]] [[package]]