diff --git a/pyproject.toml b/pyproject.toml index e13ecf4..2488987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ dependencies = [ "mcp>=1.0.0", - "cocoindex[litellm]==1.0.0a31", + "cocoindex[litellm]==1.0.0a32", "sentence-transformers>=2.2.0", "sqlite-vec>=0.1.0", "pydantic>=2.0.0", diff --git a/src/cocoindex_code/cli.py b/src/cocoindex_code/cli.py index dde76a0..4e25d29 100644 --- a/src/cocoindex_code/cli.py +++ b/src/cocoindex_code/cli.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from .client import DaemonClient -from .protocol import ProjectStatusResponse, SearchResponse +from .protocol import IndexingProgress, ProjectStatusResponse, SearchResponse from .settings import ( default_project_settings, default_user_settings, @@ -80,8 +80,21 @@ def resolve_default_path(project_root: Path) -> str | None: return f"{rel.as_posix()}/*" +def _format_progress(progress: IndexingProgress) -> str: + """Format an IndexingProgress snapshot as a human-readable string.""" + return ( + f"{progress.num_execution_starts} files listed" + f" | {progress.num_adds} added, {progress.num_deletes} deleted," + f" {progress.num_reprocesses} reprocessed," + f" {progress.num_unchanged} unchanged," + f" error: {progress.num_errors}" + ) + + def print_index_stats(status: ProjectStatusResponse) -> None: """Print formatted index statistics.""" + if status.progress is not None: + _typer.echo(f"Indexing in progress: {_format_progress(status.progress)}") _typer.echo("\nIndex stats:") _typer.echo(f" Chunks: {status.total_chunks}") _typer.echo(f" Files: {status.total_files}") @@ -153,13 +166,40 @@ def init( @app.command() def index() -> None: """Create/update index for the codebase.""" + from rich.console import Console as _Console + from rich.live import Live as _Live + from rich.spinner import Spinner as _Spinner + client, project_root = require_daemon_for_project() - _typer.echo("Indexing...") - try: - resp = client.index(project_root) - except RuntimeError as e: - _typer.echo(f"Indexing failed: {e}", err=True) - raise _typer.Exit(code=1) + err_console = _Console(stderr=True) + last_progress_line: str | None = None + + with _Live(_Spinner("dots", "Indexing..."), console=err_console, transient=True) as live: + + def _on_waiting() -> None: + live.update( + _Spinner( + "dots", + "Another indexing is ongoing, waiting for it to finish...", + ) + ) + + def _on_progress(progress: IndexingProgress) -> None: + nonlocal last_progress_line + last_progress_line = f"Indexing: {_format_progress(progress)}" + live.update(_Spinner("dots", last_progress_line)) + + try: + resp = client.index(project_root, on_progress=_on_progress, on_waiting=_on_waiting) + except RuntimeError as e: + live.stop() + _typer.echo(f"Indexing failed: {e}", err=True) + raise _typer.Exit(code=1) + + # Print the final progress line so it remains visible after the spinner clears + if last_progress_line is not None: + _typer.echo(last_progress_line, err=True) + if not resp.success: _typer.echo(f"Indexing failed: {resp.message}", err=True) raise _typer.Exit(code=1) diff --git a/src/cocoindex_code/client.py b/src/cocoindex_code/client.py index e0fdb36..c650caa 100644 --- a/src/cocoindex_code/client.py +++ b/src/cocoindex_code/client.py @@ -8,6 +8,7 @@ import subprocess import sys import time +from collections.abc import Callable from multiprocessing.connection import Client, Connection from pathlib import Path @@ -18,8 +19,11 @@ ErrorResponse, HandshakeRequest, HandshakeResponse, + IndexingProgress, + IndexProgressUpdate, IndexRequest, IndexResponse, + IndexWaitingNotice, ProjectStatusRequest, ProjectStatusResponse, Request, @@ -59,9 +63,30 @@ def handshake(self) -> HandshakeResponse: """Send version handshake.""" return self._send(HandshakeRequest(version=__version__)) # type: ignore[return-value] - def index(self, project_root: str) -> IndexResponse: - """Request indexing. Blocks until complete.""" - return self._send(IndexRequest(project_root=project_root)) # type: ignore[return-value] + def index( + self, + project_root: str, + on_progress: Callable[[IndexingProgress], None] | None = None, + on_waiting: Callable[[], None] | None = None, + ) -> IndexResponse: + """Request indexing with streaming progress. Blocks until complete.""" + self._conn.send_bytes(encode_request(IndexRequest(project_root=project_root))) + while True: + data = self._conn.recv_bytes() + resp = decode_response(data) + if isinstance(resp, ErrorResponse): + raise RuntimeError(f"Daemon error: {resp.message}") + if isinstance(resp, IndexWaitingNotice): + if on_waiting is not None: + on_waiting() + continue + if isinstance(resp, IndexProgressUpdate): + if on_progress is not None: + on_progress(resp.progress) + continue + if isinstance(resp, IndexResponse): + return resp + raise RuntimeError(f"Unexpected response: {type(resp).__name__}") def search( self, diff --git a/src/cocoindex_code/daemon.py b/src/cocoindex_code/daemon.py index 1fb5f7a..582f526 100644 --- a/src/cocoindex_code/daemon.py +++ b/src/cocoindex_code/daemon.py @@ -9,6 +9,7 @@ import sys import threading import time +from collections.abc import AsyncIterator from multiprocessing.connection import Connection, Listener from pathlib import Path from typing import Any @@ -22,8 +23,12 @@ ErrorResponse, HandshakeRequest, HandshakeResponse, + IndexingProgress, + IndexProgressUpdate, IndexRequest, IndexResponse, + IndexStreamResponse, + IndexWaitingNotice, ProjectStatusRequest, ProjectStatusResponse, Request, @@ -113,14 +118,43 @@ async def get_project(self, project_root: str) -> Project: self._indexing[project_root] = False return self._projects[project_root] - async def update_index(self, project_root: str) -> None: - """Update index for project, serialized by per-project lock.""" + async def update_index(self, project_root: str) -> AsyncIterator[IndexStreamResponse]: + """Update index, yielding progress updates and a final IndexResponse.""" project = await self.get_project(project_root) lock = self._index_locks[project_root] + + # If lock is already held, notify the client and block until released + if lock.locked(): + yield IndexWaitingNotice() + async with lock: self._indexing[project_root] = True try: - await project.update_index() + progress_queue: asyncio.Queue[IndexingProgress] = asyncio.Queue() + + def on_progress(progress: IndexingProgress) -> None: + progress_queue.put_nowait(progress) + + update_task = asyncio.create_task(project.update_index(on_progress=on_progress)) + + # Drain the queue until the update completes + while not update_task.done(): + try: + progress = await asyncio.wait_for(progress_queue.get(), timeout=0.1) + yield IndexProgressUpdate(progress=progress) + except TimeoutError: + continue + + # Drain any remaining items + while not progress_queue.empty(): + yield IndexProgressUpdate(progress=progress_queue.get_nowait()) + + # Propagate any exception from the update task + update_task.result() + + yield IndexResponse(success=True) + except Exception as e: + yield IndexResponse(success=False, message=str(e)) finally: self._indexing[project_root] = False @@ -177,11 +211,14 @@ def get_status(self, project_root: str) -> ProjectStatusResponse: " GROUP BY language ORDER BY cnt DESC" ).fetchall() + is_indexing = self._indexing.get(project_root, False) + progress = project.indexing_stats if is_indexing else None return ProjectStatusResponse( - indexing=self._indexing.get(project_root, False), + indexing=is_indexing, total_chunks=total_chunks, total_files=total_files, languages={lang: cnt for lang, cnt in lang_rows}, + progress=progress, ) def list_projects(self) -> list[DaemonProjectInfo]: @@ -246,8 +283,12 @@ def _recv() -> bytes: handshake_done = True continue - resp = await _dispatch(req, registry, start_time, shutdown_event) - conn.send_bytes(encode_response(resp)) + result = await _dispatch(req, registry, start_time, shutdown_event) + if isinstance(result, AsyncIterator): + async for resp in result: + conn.send_bytes(encode_response(resp)) + else: + conn.send_bytes(encode_response(result)) if isinstance(req, StopRequest): break @@ -265,16 +306,21 @@ async def _dispatch( registry: ProjectRegistry, start_time: float, shutdown_event: asyncio.Event, -) -> Response: - """Dispatch a request to the appropriate handler.""" +) -> Response | AsyncIterator[IndexStreamResponse]: + """Dispatch a request to the appropriate handler. + + Returns a single Response for most requests, or an AsyncIterator for + streaming requests (IndexRequest). + """ try: if isinstance(req, IndexRequest): - await registry.update_index(req.project_root) - return IndexResponse(success=True) + return registry.update_index(req.project_root) if isinstance(req, SearchRequest): if req.refresh: - await registry.update_index(req.project_root) + # Consume the index stream silently for refresh + async for _ in registry.update_index(req.project_root): + pass results = await registry.search( project_root=req.project_root, query=req.query, diff --git a/src/cocoindex_code/project.py b/src/cocoindex_code/project.py index faf81c8..7876f7d 100644 --- a/src/cocoindex_code/project.py +++ b/src/cocoindex_code/project.py @@ -3,12 +3,14 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from pathlib import Path import cocoindex as coco from cocoindex.connectors import sqlite from .indexer import indexer_main +from .protocol import IndexingProgress from .settings import PROJECT_SETTINGS, ProjectSettings from .shared import CODEBASE_DIR, EMBEDDER, SQLITE_DB, Embedder @@ -18,14 +20,43 @@ class Project: _app: coco.App[[], None] _index_lock: asyncio.Lock _initial_index_done: bool = False + _indexing_stats: IndexingProgress | None = None - async def update_index(self, *, report_to_stdout: bool = False) -> None: - """Update the index, serializing concurrent calls via lock.""" - async with self._index_lock: - try: - await self._app.update(report_to_stdout=report_to_stdout) - finally: - self._initial_index_done = True + async def update_index( + self, + *, + on_progress: Callable[[IndexingProgress], None] | None = None, + ) -> None: + """Update the index, streaming progress via callback. + + The lock is NOT acquired here — callers (e.g. ProjectRegistry) are + responsible for serialization so they can inspect lock state and + yield one-shot snapshots before blocking. + """ + try: + handle = self._app.update() + async for snapshot in handle.watch(): + file_stats = snapshot.stats.by_processor.get("process_file") + if file_stats is not None: + progress = IndexingProgress( + num_execution_starts=file_stats.num_execution_starts, + num_unchanged=file_stats.num_unchanged, + num_adds=file_stats.num_adds, + num_deletes=file_stats.num_deletes, + num_reprocesses=file_stats.num_reprocesses, + num_errors=file_stats.num_errors, + ) + self._indexing_stats = progress + if on_progress is not None: + on_progress(progress) + await asyncio.sleep(0.1) + finally: + self._indexing_stats = None + self._initial_index_done = True + + @property + def indexing_stats(self) -> IndexingProgress | None: + return self._indexing_stats @property def env(self) -> coco.Environment: diff --git a/src/cocoindex_code/protocol.py b/src/cocoindex_code/protocol.py index 7672f95..5b7c6c5 100644 --- a/src/cocoindex_code/protocol.py +++ b/src/cocoindex_code/protocol.py @@ -63,6 +63,29 @@ class IndexResponse(_msgspec.Struct, tag="index"): message: str | None = None +class IndexingProgress(_msgspec.Struct): + """Indexing stats snapshot, shared between progress updates and status responses.""" + + num_execution_starts: int + num_unchanged: int + num_adds: int + num_deletes: int + num_reprocesses: int + num_errors: int + + +class IndexProgressUpdate(_msgspec.Struct, tag="index_progress"): + """Streamed during indexing — one per stats change, before the final IndexResponse.""" + + progress: IndexingProgress + + +class IndexWaitingNotice(_msgspec.Struct, tag="index_waiting"): + """Sent when another indexing is already in progress and the client must wait.""" + + pass + + class SearchResult(_msgspec.Struct): file_path: str language: str @@ -85,6 +108,7 @@ class ProjectStatusResponse(_msgspec.Struct, tag="project_status"): total_chunks: int total_files: int languages: dict[str, int] + progress: IndexingProgress | None = None class DaemonProjectInfo(_msgspec.Struct): @@ -109,6 +133,8 @@ class ErrorResponse(_msgspec.Struct, tag="error"): Response = ( HandshakeResponse | IndexResponse + | IndexProgressUpdate + | IndexWaitingNotice | SearchResponse | ProjectStatusResponse | DaemonStatusResponse @@ -116,6 +142,8 @@ class ErrorResponse(_msgspec.Struct, tag="error"): | ErrorResponse ) +IndexStreamResponse = IndexProgressUpdate | IndexWaitingNotice | IndexResponse | ErrorResponse + # --------------------------------------------------------------------------- # Encode / decode helpers (msgpack binary) # --------------------------------------------------------------------------- diff --git a/src/cocoindex_code/server.py b/src/cocoindex_code/server.py index b1343a6..914b1d7 100644 --- a/src/cocoindex_code/server.py +++ b/src/cocoindex_code/server.py @@ -21,6 +21,8 @@ if TYPE_CHECKING: from .client import DaemonClient +from .protocol import IndexingProgress + _MCP_INSTRUCTIONS = ( "Code search and codebase understanding tools." "\n" @@ -274,8 +276,38 @@ def main() -> None: # --- Delegate to daemon --- if args.command == "index": + import sys + + from rich.console import Console + from rich.live import Live + from rich.spinner import Spinner + + from .cli import _format_progress + client = ensure_daemon() - resp = client.index(str(project_root)) + err_console = Console(stderr=True) + last_progress_line: str | None = None + + with Live(Spinner("dots", "Indexing..."), console=err_console, transient=True) as live: + + def _on_waiting() -> None: + live.update( + Spinner( + "dots", + "Another indexing is ongoing, waiting for it to finish...", + ) + ) + + def _on_progress(progress: IndexingProgress) -> None: + nonlocal last_progress_line + last_progress_line = f"Indexing: {_format_progress(progress)}" + live.update(Spinner("dots", last_progress_line)) + + resp = client.index(str(project_root), on_progress=_on_progress, on_waiting=_on_waiting) + + if last_progress_line is not None: + print(last_progress_line, file=sys.stderr) + if resp.success: status = client.project_status(str(project_root)) print("\nIndex stats:") diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 294e40c..df3d099 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -21,7 +21,10 @@ from cocoindex_code.protocol import ( DaemonStatusRequest, HandshakeRequest, + IndexProgressUpdate, IndexRequest, + IndexResponse, + IndexWaitingNotice, ProjectStatusRequest, Response, SearchRequest, @@ -95,6 +98,21 @@ def daemon_sock() -> Iterator[str]: os.environ["COCOINDEX_CODE_DIR"] = old_env +def _recv_index_response(conn: Connection) -> tuple[list[IndexProgressUpdate], IndexResponse]: + """Read streaming index responses until the final IndexResponse arrives.""" + progress_updates: list[IndexProgressUpdate] = [] + while True: + resp = decode_response(conn.recv_bytes()) + if isinstance(resp, IndexProgressUpdate): + progress_updates.append(resp) + continue + if isinstance(resp, IndexWaitingNotice): + continue + if isinstance(resp, IndexResponse): + return progress_updates, resp + raise AssertionError(f"Unexpected response during indexing: {type(resp).__name__}") + + @pytest.fixture(scope="session") def daemon_project(daemon_sock: str) -> str: """Create and index a project once for the session. Returns project_root str.""" @@ -106,7 +124,8 @@ def daemon_project(daemon_sock: str) -> str: conn.send_bytes(encode_request(HandshakeRequest(version=__version__))) decode_response(conn.recv_bytes()) conn.send_bytes(encode_request(IndexRequest(project_root=str(project)))) - decode_response(conn.recv_bytes()) + _updates, final = _recv_index_response(conn) + assert final.success is True conn.close() return str(project) @@ -160,3 +179,20 @@ def test_daemon_search_after_index(daemon_sock: str, daemon_project: str) -> Non assert len(resp.results) > 0 # type: ignore[union-attr] assert "main.py" in resp.results[0].file_path # type: ignore[union-attr] conn.close() + + +def test_index_streams_progress(daemon_sock: str) -> None: + """Indexing a new project should stream IndexProgressUpdate before IndexResponse.""" + project = Path(tempfile.mkdtemp(prefix="ccc_strm_")) + save_project_settings(project, default_project_settings()) + (project / "main.py").write_text(SAMPLE_MAIN_PY) + + conn, _ = _connect_and_handshake(daemon_sock) + conn.send_bytes(encode_request(IndexRequest(project_root=str(project)))) + updates, final = _recv_index_response(conn) + conn.close() + + assert final.success is True + assert len(updates) > 0, "Expected at least one IndexProgressUpdate" + for u in updates: + assert u.progress.num_execution_starts >= 0 diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 2094ff6..58f4b63 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -192,6 +192,14 @@ def test_daemon_status(self, e2e_env: Path, monkeypatch: pytest.MonkeyPatch) -> assert result.exit_code == 0, result.output assert "Daemon version:" in result.output + def test_index_shows_stats(self, e2e_env: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Indexing should complete and show final stats.""" + monkeypatch.chdir(e2e_env) + result = runner.invoke(app, ["index"], catch_exceptions=False) + assert result.exit_code == 0, result.output + assert "Chunks:" in result.output + assert "Files:" in result.output + def test_incremental_index_new_file( self, e2e_env: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/test_protocol.py b/tests/test_protocol.py index e2b8e18..08de1cf 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -8,8 +8,11 @@ DaemonStatusResponse, ErrorResponse, HandshakeRequest, + IndexingProgress, + IndexProgressUpdate, IndexRequest, IndexResponse, + IndexWaitingNotice, ProjectStatusRequest, ProjectStatusResponse, Request, @@ -141,9 +144,85 @@ def test_all_request_types_round_trip() -> None: assert type(decoded) is type(req) +def test_encode_decode_index_waiting_notice() -> None: + resp = IndexWaitingNotice() + data = encode_response(resp) + decoded = decode_response(data) + assert isinstance(decoded, IndexWaitingNotice) + + +def test_encode_decode_index_progress_update() -> None: + progress = IndexingProgress( + num_execution_starts=10, + num_unchanged=3, + num_adds=5, + num_deletes=1, + num_reprocesses=0, + num_errors=1, + ) + resp = IndexProgressUpdate(progress=progress) + data = encode_response(resp) + decoded = decode_response(data) + assert isinstance(decoded, IndexProgressUpdate) + assert decoded.progress.num_execution_starts == 10 + assert decoded.progress.num_unchanged == 3 + assert decoded.progress.num_adds == 5 + assert decoded.progress.num_deletes == 1 + assert decoded.progress.num_reprocesses == 0 + assert decoded.progress.num_errors == 1 + + +def test_encode_decode_project_status_with_progress() -> None: + progress = IndexingProgress( + num_execution_starts=7, + num_unchanged=2, + num_adds=4, + num_deletes=0, + num_reprocesses=1, + num_errors=0, + ) + resp = ProjectStatusResponse( + indexing=True, + total_chunks=50, + total_files=10, + languages={"python": 50}, + progress=progress, + ) + data = encode_response(resp) + decoded = decode_response(data) + assert isinstance(decoded, ProjectStatusResponse) + assert decoded.progress is not None + assert decoded.progress.num_execution_starts == 7 + assert decoded.progress.num_adds == 4 + + +def test_encode_decode_project_status_without_progress() -> None: + resp = ProjectStatusResponse( + indexing=False, + total_chunks=50, + total_files=10, + languages={"python": 50}, + ) + data = encode_response(resp) + decoded = decode_response(data) + assert isinstance(decoded, ProjectStatusResponse) + assert decoded.progress is None + + def test_all_response_types_round_trip() -> None: responses: list[Response] = [ IndexResponse(success=True), + IndexProgressUpdate( + progress=IndexingProgress( + num_execution_starts=0, + num_unchanged=0, + num_adds=0, + num_deletes=0, + num_reprocesses=0, + num_errors=0, + ) + ), + IndexWaitingNotice(), SearchResponse(success=True), ProjectStatusResponse(indexing=False, total_chunks=0, total_files=0, languages={}), DaemonStatusResponse(version="1.0.0", uptime_seconds=0.0, projects=[]), diff --git a/uv.lock b/uv.lock index 455158f..2b3f34a 100644 --- a/uv.lock +++ b/uv.lock @@ -339,7 +339,7 @@ wheels = [ [[package]] name = "cocoindex" -version = "1.0.0a31" +version = "1.0.0a32" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -350,17 +350,17 @@ dependencies = [ { name = "typing-extensions" }, { name = "watchfiles" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/20/245ec9ff9d499a9fc699ae1176e523ea671266c99678b93e2ab08802896b/cocoindex-1.0.0a31.tar.gz", hash = "sha256:302515fb8fa6ca7cb4bc7fda7397b321be34e0ab72c2a6cc2c4aa7249a270b44", size = 286158, upload-time = "2026-03-14T21:46:48.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/1e/7869447a51b12ec6e541fb5000f7d32870347d76480627e2947e74a3fa82/cocoindex-1.0.0a32.tar.gz", hash = "sha256:bc9365662ec7364e8a3dd70474c19dea2758f606998e77482b4adc366ebd4f28", size = 290075, upload-time = "2026-03-15T17:54:53.669Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/58/c8db553650d58f49ad6ce729b1d923a57060da09dd0d8a03531e134945aa/cocoindex-1.0.0a31-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:02f4388d21f4ef1374981d4a8e6edd4a3f3d0d85fd63a75eb36cd266a2d3145d", size = 6246835, upload-time = "2026-03-14T21:46:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/6c/18/c87a4f3f9332da8019571e7392508ffbd02de520482f7532604e1ef71b83/cocoindex-1.0.0a31-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b77f6f5b532b14587aef1c69359e3dc7ec443cb02a772f9bd06964d09ccde3f8", size = 6367066, upload-time = "2026-03-14T21:46:43.565Z" }, - { url = "https://files.pythonhosted.org/packages/38/e6/a747115c3d19364e52ac1f6eb4cbbf1ac951dd864e025ec50b12a4fcd04b/cocoindex-1.0.0a31-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c25de6648b9885b06f9c46d2535844e3e16434806ad457f6f67452ef30046b59", size = 6152245, upload-time = "2026-03-14T21:46:37.061Z" }, - { url = "https://files.pythonhosted.org/packages/5b/0f/f847ac9d5d0f08d6685334a6b50e628b165b7857612df2b97ef6948bf086/cocoindex-1.0.0a31-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:01177e30e4d31ea9990a5c2d53b13a4006647b000c333b03e7686bcb039f2c06", size = 6345010, upload-time = "2026-03-14T21:46:40.551Z" }, - { url = "https://files.pythonhosted.org/packages/44/bd/069ecf02de5707aea6a46d9688ad689547df85e21aa6368bb52c4006ba5f/cocoindex-1.0.0a31-cp311-abi3-win_amd64.whl", hash = "sha256:aa2c6db7e41517000d7ef717d5f288a238c1fa0fe23a17e98c32799bb6476943", size = 6532907, upload-time = "2026-03-14T21:46:49.169Z" }, - { url = "https://files.pythonhosted.org/packages/d8/30/97526840f74c9ab676ee6d20bfbe0d9a41590a6451d586fda028f2ec6f60/cocoindex-1.0.0a31-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:81ccf43d3900488029babb30ff292613dd863e5013f5e2737e588382f26f51c6", size = 6363058, upload-time = "2026-03-14T21:46:45.011Z" }, - { url = "https://files.pythonhosted.org/packages/1a/11/4579ff59c7c376a8d2db04cdbdec812ac9f34a5335dc949d8d713d9bb345/cocoindex-1.0.0a31-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9964135f87cdbc604b64ad96c01c9cde0158859bd144a95160853c7891d78a35", size = 6151838, upload-time = "2026-03-14T21:46:38.919Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/f91244554ce51ff1480731e8f3c7d5b53b7087bff7e44956824abd630e02/cocoindex-1.0.0a31-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4142fbbc7c74a3295c907899c16d9a5ad36bb17bae6085f50ab755305c5e03f7", size = 6344002, upload-time = "2026-03-14T21:46:42.132Z" }, - { url = "https://files.pythonhosted.org/packages/82/77/8a45924b732bc83bc7f29171f1f3ef27bd233b98f3d08ff56acd983bcddd/cocoindex-1.0.0a31-cp314-cp314t-win_amd64.whl", hash = "sha256:ff20052618c180becd1d3a863b40d788d4141725dd55930d08cbb24fa941a740", size = 6522233, upload-time = "2026-03-14T21:46:50.806Z" }, + { url = "https://files.pythonhosted.org/packages/92/ca/6d593d32c76b8d9ee213b093330cf5146bb7c7bfedb2bfe3ad781a8c85a5/cocoindex-1.0.0a32-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85ce3859ce746a730a27ea74468bf0d80595463842dfaf45da95612dea042036", size = 6281470, upload-time = "2026-03-15T17:54:52.13Z" }, + { url = "https://files.pythonhosted.org/packages/12/a1/eff80a187be9b02a22d0610114ed2127825db866fd185c3f4eb2e2dc5175/cocoindex-1.0.0a32-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:634d1e3642b2ba9e7be7751e383b2b321dea91061c9fd1d89d29e05db4aa55df", size = 6392457, upload-time = "2026-03-15T17:54:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/aa/94/c189279ec1ab03cdfb7eaee028bb7581068e30903c4096aef8c4e59a8589/cocoindex-1.0.0a32-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c00eeed432278026ce653b3db1210f33d753c71ae40fa74b33beaaf4d5097ed3", size = 6180655, upload-time = "2026-03-15T17:54:39.056Z" }, + { url = "https://files.pythonhosted.org/packages/41/9e/e8f879353596ff2fbe0490f7edc6ddbd88cee91adf8617524211e22bb9c7/cocoindex-1.0.0a32-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e463efae7f9e7812ba46a66440976bd013cedcb6486b22302dd1bf9d7ee01720", size = 6378837, upload-time = "2026-03-15T17:54:43.423Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2e/6b7bea3ffc39b185d4ae2634fba1f22f1d0658ab4d1f81d74f1422c638a2/cocoindex-1.0.0a32-cp311-abi3-win_amd64.whl", hash = "sha256:be35cb542b65bc0180445a11a70df78c63368085ae2bd89765da4155476c80eb", size = 6574523, upload-time = "2026-03-15T17:54:55.451Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/ba3410130a8069d9b969ee1a7af16fe65d1751da8047c3d81a82218724a1/cocoindex-1.0.0a32-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:87035949a40a2e80ebd4ddbf7db0f3edeefb6ff2ef1afd23e88c63e697ae9cb2", size = 6390570, upload-time = "2026-03-15T17:54:50.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/b6/9d1932c6dafb0bedf312043afefd58a968c64361861db3699bbbb077c445/cocoindex-1.0.0a32-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a755f8d7a0528cd4108d906d10af9e70b847805edce07de55fe0ff063ca7f242", size = 6180468, upload-time = "2026-03-15T17:54:41.4Z" }, + { url = "https://files.pythonhosted.org/packages/f9/2c/1b35da289b8871f2ea80b8d1ec9a02cc5f3e477ee5053a99baaf4874bcf8/cocoindex-1.0.0a32-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:97d72744c4583979d0cbfc47a11570b24d966c7f22cbfff2759b25975e32ebb2", size = 6372755, upload-time = "2026-03-15T17:54:45.637Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c8/f3568024de15f79a6b764733dd82e04de407c77efca1d622bcc358333a84/cocoindex-1.0.0a32-cp314-cp314t-win_amd64.whl", hash = "sha256:c5f5afbfa3959c69c15baea209d311592b618b414a115f1930bbf725600533eb", size = 6560211, upload-time = "2026-03-15T17:54:57.435Z" }, ] [package.optional-dependencies] @@ -407,7 +407,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "cocoindex", extras = ["litellm"], specifier = "==1.0.0a31" }, + { name = "cocoindex", extras = ["litellm"], specifier = "==1.0.0a32" }, { name = "einops", specifier = ">=0.8.2" }, { name = "mcp", specifier = ">=1.0.0" }, { name = "msgspec", specifier = ">=0.19.0" },