13 Commits

Author SHA1 Message Date
Jiangzhou ee3515fa57 feat: configurable embedder indexing_params/query_params + curated defaults (#150)
Users can now set `indexing_params` and `query_params` under `embedding:` in
`global_settings.yml` to pass extra kwargs to the embedder separately for
indexing vs. query — supporting asymmetric retrieval models (Cohere v3,
Voyage, Nvidia NIM, Gemini, nomic-ai code/text models, Snowflake arctic,
etc.).

- `ccc init` auto-populates these from a curated table of known models and
  prints the applied defaults; unknown models get a commented-out template
  for the accepted keys (`prompt_name` for sentence-transformers;
  `input_type`, `dimensions` for litellm).
- Daemon validates the effective params at startup; invalid keys fail fast
  with a clear error.
- Backward compat: configs for `nomic-ai/CodeRankEmbed` /
  `nomic-ai/nomic-embed-code` that predate this feature keep the previous
  hardcoded `prompt_name=query` behavior, and a one-time handshake warning
  asks users to make the setting explicit. The warning is suppressible by
  any non-None `query_params` (including `{}`).
- `ccc doctor` now tests indexing and query separately so asymmetric
  misconfigurations surface independently.

Drops the legacy `shared.query_prompt_name` module variable and
`_QUERY_PROMPT_MODELS` set; the new resolution path is centralized in
`embedder_params.resolve_embedder_params` and the curated defaults live in
`embedder_defaults._DEFAULT_PARAMS`.

Also enables `litellm.drop_params = True` so provider-specific kwargs that
a particular model doesn't accept are silently dropped instead of failing.
2026-04-24 16:08:20 -07:00
Jiangzhou da886d5d0d refactor: drop unused shared.embedder global (#149)
The module-level `shared.embedder` global was written by `create_embedder()`
but never read from production code — the embedder flows through the
`EMBEDDER` ContextKey via `context.provide` / `use_context`.

Drop the global along with the two test readers that kept it alive:

- `tests/test_daemon.py` — the `daemon_sock` fixture pre-loaded an embedder
  and monkeypatched `dm.create_embedder` to reuse it. That was only useful
  as a cross-module cache via the now-dead global; with a session-scoped
  fixture, `run_daemon()` loads the embedder once from the saved settings
  regardless.
- `tests/test_chunker_registry.py` — removed a stale
  `monkeypatch.setattr(_shared, "embedder", stub)` whose comment claimed
  CodeChunk.embedding read the global at schema resolution time. It reads
  the EMBEDDER ContextKey instead; the stub is already wired through
  `Project.create(..., stub, ...)`.
2026-04-24 09:51:28 -07:00
Jiangzhou 5b6e3f5538 feat: make sentence-transformers optional; interactive ccc init (#132)
- Move `sentence-transformers` behind `[embeddings-local]` and `[default]`
  extras (via `cocoindex[sentence-transformers]`), so `pip install
  cocoindex-code` is LiteLLM-only. Closes #117.
- `ccc init` is now interactive when global settings don't exist: pick
  provider (sentence-transformers / litellm) and model via a
  questionary TUI. New `--litellm-model MODEL` flag skips prompts and
  is the non-TTY escape hatch for LiteLLM. Closes #70.
- Change the default sentence-transformers model from
  `all-MiniLM-L6-v2` to `Snowflake/snowflake-arctic-embed-xs`
  (lighter, better quality for code).
- Generated `global_settings.yml` now includes a `ccc doctor` reminder
  and commented-out env-var examples (OPENAI_API_KEY, GEMINI_API_KEY,
  ANTHROPIC_API_KEY, VOYAGE_API_KEY).
- Model test during init runs in the daemon via the existing
  `DoctorRequest` path; the daemon loads the model once and stays
  running, so the user's next `ccc index` starts warm.
- Docker image now installs `cocoindex-code[default]` and pre-caches
  the new default model. The `COCOINDEX_CODE_EMBEDDING_MODEL` env var
  is no longer documented for Docker; users mount a
  `global_settings.yml` or pass `--litellm-model`.
- Extract `check_embedding` + `EmbeddingCheckResult` into `shared.py`;
  refactor daemon `_check_model` to delegate. Error messages in doctor
  output now include the exception type name (strictly more
  informative).
- Tests switch to a lighter `paraphrase-MiniLM-L3-v2` model via a new
  `make_test_user_settings()` helper in `conftest.py`, leaving CI
  cache costs unchanged.
2026-04-13 22:25:17 -07:00
Jiangzhou 21f858e840 refactor: extract daemon path helpers to avoid CLI importing cocoindex (#130)
* refactor: extract daemon path helpers to avoid CLI importing cocoindex

Move daemon_dir, daemon_socket_path, daemon_pid_path, daemon_log_path,
and connection_family from daemon.py into a new lightweight _daemon_paths.py
module. This prevents the CLI client from transitively importing cocoindex
and its heavy dependencies (numpy, torch, etc.) when it only needs path
utilities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: mark free-threaded Python CI jobs as continue-on-error

tokenizers lacks pre-built wheels for cp314t (free-threaded ABI),
so uv falls back to compiling from Rust source. This source build
is fragile across macOS runner image updates and broke after the
20260406 image bump. Mark 3.14t jobs as continue-on-error since
free-threaded wheel coverage across the ecosystem is still limited.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 12:38:52 -07:00
Vasilii Alferov f2ecfb5df3 feat: pluggable chunker registry with settings integration (#105)
* chore: enable mypy explicit_package_bases, remove stale type: ignore comments

explicit_package_bases = true resolves module paths from the repo root,
preventing double-discovery of files in tests/ under different module names.
Required for tests/example_toml_chunker.py to be importable as
example_toml_chunker rather than an ambiguous bare module.

Also sets asyncio_mode = auto so pytest-asyncio behaviour is explicit
(default in 1.3.0 is strict).

With explicit_package_bases active, mypy can fully resolve the Response
union in test_daemon.py — the 16 type: ignore[union-attr] and
type: ignore[attr-defined] comments that were suppressing false positives
under the old resolution are now unused and removed.

# Conflicts:
#	tests/test_daemon.py

* feat: pluggable chunker registry with settings integration

Improves retrieval precision by letting users split specific file types at
semantic boundaries (e.g. TOML sections, SQL statements) instead of the
default line-window splitter.

## What

- cocoindex_code/chunking.py: public API module exporting ChunkerFn
  (Callable alias), CHUNKER_REGISTRY context key, and re-exports of
  Chunk/TextPosition from upstream. Single import path for chunker authors.

- tests/example_toml_chunker.py: demo chunker splitting at [section]
  headers; excludes [[array_of_tables]] via negative lookahead. Lives in
  tests/ to signal it belongs to a separate package, not the core library.

- ProjectSettings.chunkers: new list[ChunkerMapping] field, serialised as
  YAML. Each entry maps a file extension to a 'module.path:callable' string.
  Users activate chunkers by editing .cocoindex_code/settings.yml — no code
  changes required.

- daemon.py: _resolve_chunker_registry resolves ChunkerMapping entries via
  importlib at project load time and passes the result to Project.create().
  callable() guard gives a clear error at startup rather than a TypeError
  per file.

- Project.create(chunker_registry=...): new optional parameter. Injected as
  a cocoindex context key (tracked=False) rather than exposed via env internals.
  Empty registry by default — zero behavioural delta for existing users.

- indexer.py: process_file checks the registry per file suffix; falls through
  to RecursiveSplitter unchanged when no chunker is registered.

## Design decisions

- ChunkerFn returns (language_override, chunks): language_override=None keeps
  detect_code_language() result; non-None lets the chunker correct it (e.g.
  .sls files starting with #!py).

- tracked=False is consistent with SQLITE_DB, CODEBASE_DIR, and other
  non-serialisable context keys. Changing a chunker requires a daemon restart,
  which triggers a full re-index anyway.

- _resolve_chunker_registry lives in daemon.py, its only call site, keeping
  settings.py as pure schema/IO and chunking.py as pure type definitions.

# Conflicts:
#	src/cocoindex_code/daemon.py
#	src/cocoindex_code/indexer.py
#	src/cocoindex_code/project.py
#	tests/test_settings.py
2026-03-22 23:03:17 -07:00
Jiangzhou dd5f7b6768 feat: revisit and simplify CLI-daemon connection model (#106)
* feat: revisit and simplify CLI-daemon connection model

- CLI-daemon connections are stateless now.
- Also stop caching project settings in memory. Much easier to pick up
  new version.

* fix: type annotation
2026-03-20 11:49:25 -07:00
Jiangzhou ad5c157f2f fix: make sure search blocks on load-time indexing gracefully (#92) 2026-03-16 20:35:32 -07:00
Jiangzhou a0a5a408ed feat: search pending on load-time indexing (#91) 2026-03-16 19:01:55 -07:00
Jiangzhou 53d808293f fix: make sure daemons are brought down cleanly (#84)
* fix: make sure daemons are brought down cleanly

* chore: be more verbose in pre-commit workflow

* fix: split CI workflow to avoid prek output capture deadlock on Windows

prek --verbose captures subprocess stdout via pipe. On Windows, the 4KB
pipe buffer fills when pytest produces verbose output, causing a deadlock
(pytest blocks writing, prek blocks waiting for exit). Split into two
steps: prek --skip pytest for lint checks, and pytest running directly
for streaming output.

Also:
- Add OSError catch in _pid_alive for Windows edge cases
- Properly shut down daemon thread in test_daemon.py session fixture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: catch SystemError from os.kill on Windows

On Windows, os.kill(pid, 0) raises SystemError when the target process
has exited but its handle state is in transition (WinError 87). This
corrupts CPython C exception state, causing subsequent built-in calls
like time.monotonic() to also raise SystemError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: avoid os.kill on Windows due to CPython C exception state corruption

os.kill(pid, 0) on Windows corrupts CPython C-level exception state
even after the raised OSError is caught. This causes subsequent calls to
C built-ins (time.monotonic, time.sleep) to raise SystemError. Use
ctypes OpenProcess instead, which is the proper Win32 API for checking
process existence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:07:00 -07:00
Jiangzhou 7d9a0dcd3c fix: stablize setting files (#83)
* fix: stablize setting files

* fix: improve daemon readiness detection on Windows

On Windows, os.path.exists() is unreliable for named pipes (\\.\pipe\*).
Replace it with actual connection attempts in _wait_for_daemon and
is_daemon_running. Also increase the default timeout from 10s to 30s
since Windows CI can be slower during model loading.

Additionally, after TerminateProcess on Windows (SIGTERM fallback in
stop_daemon), wait for the old process to fully exit before returning,
so that named pipe handles are released before starting a new daemon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 13:09:58 -07:00
Jiangzhou 0b30f16451 feat: assorted improvements for CLI/daemon to be resilient and ergonomic (#72)
* feat: assorted improvements for CLI/daemon to be resilient and ergonomic

* tests: add more e2e tests

* fix: resolve path comparison and LMDB cleanup issues on Windows/3.14t

Three root causes fixed:

1. Path resolution mismatch on Windows: init and auto_init_project used
   unresolved Path.cwd() but find_parent_with_marker/find_project_root
   resolve internally, causing comparison failures and daemon key mismatches.

2. LMDB not released on remove_project: The daemon ProjectRegistry
   dropped the Project from dicts without closing its SQLite connection or
   forcing GC of the Rust LMDB environment. On free-threaded Python (3.14t)
   and Windows, deferred GC kept the LMDB open, causing environment already
   open errors and PermissionErrors when deleting db files.

3. Silent connection close on streaming errors: When update_index async
   iteration failed in the daemon, the connection was closed without sending
   an ErrorResponse, causing the client to get an unhelpful EOFError.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: break LMDB reference chain explicitly and resolve cwd in path filter

On 3.14t (free-threaded Python), gc.collect() alone does not release the
Rust LMDB environment because deferred reference counting keeps
core.Environment alive through App._core_env_app, ContextProvider._core_env,
and Environment._core_env.  Explicitly null these internal references in
Project.close() before gc.collect() so the Rust object is freed promptly.

Also resolve Path.cwd() in resolve_default_path() — on Windows, the
unresolved cwd did not match the resolved project_root, causing
relative_to() to fail and the subdirectory path filter to be skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use double gc.collect() for layered deferred refcount on 3.14t

Revert fragile internal attribute clearing. On free-threaded Python, the
first gc.collect() frees Python wrappers whose Rust Drop implementations
issue further deferred Py_DECREF calls on core.Environment; a second
gc.collect() flushes those and actually drops the LMDB handle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add sleep in tests for LMDB release on free-threaded Python

On 3.14t, deferred reference counting means the Rust LMDB environment
is not released immediately after remove_project + gc.collect(). Add a
1-second sleep in the two tests that reset and re-index, giving the
runtime time to process pending deferred Py_DECREF calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restart daemon after reset in tests, increase daemon wait timeout

Instead of sleeping, restart the daemon after reset in the two tests
that re-index after removing databases. This reliably releases the LMDB
environment on all platforms including free-threaded Python (3.14t).

Also increase _wait_for_daemon timeout from 5s to 10s — Windows CI
runners occasionally need longer to start the daemon subprocess.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 23:04:00 -07:00
Jiangzhou 86fca84323 feat: show indexing progress gracefully (#71) 2026-03-15 18:28:15 -07:00
Jiangzhou 9000f1df7e feat: preliminary implementation for daemon + CLI (#67) 2026-03-14 15:57:26 -07:00