Skip to content

fix(knowledge): one active embedding model resolver, model-aware cache keys, and search refuses rather than embed in another space (#18143, #18185) - #18230

Merged
mrveiss merged 12 commits into
mainfrom
issue-18143-embedding-model-resolver
Oct 11, 2026
Merged

mrveiss merged 12 commits into
mainfrom
issue-18143-embedding-model-resolver

Conversation

@mrveiss

@mrveiss mrveiss commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

#18143: the active embedding model was resolved in several places, and cache keys weren't tied to it, so a model change could serve one model's vectors where another's were expected. #18185 found the sharper version of that. When the NPU path failed, search embedded the query with the semantic chunker's all-MiniLM-L6-v2, compared it against knowledge-base vectors from the active model (nomic-embed-text), and cached it under the active model's key. The owner decided the fallback fails loudly: use the canonical model or refuse, never a different model's vectors.

Review then found two more ways to get the same wrong-space result, both fixed here:

  • the accelerator's projected 512-dimension "unified space" vectors were used as query embeddings;
  • the NPU client sent the worker a tagged model name the worker doesn't load. The worker answers an unknown model with a mock embedding and HTTP 200 (filed separately), so every NPU embedding would have become a cached mock.

What Changed

  • b84910978e: one resolver for the active embedding model (autobot_shared/embedding_model.py), shared by the generator and the cache keys.
  • e6ef635c71: the semantic chunker exposes a public embedding API. All 6 outside callers move to it, and a guard forbids reaching its private hooks (tech-debt(knowledge): npu_semantic_search and ai_hardware_accelerator use the semantic chunker's private methods as an embedder #18185 AC2/AC3).
  • 2617237da8: the search embedding fallback goes through the canonical accessor or raises EmbeddingUnavailableError. It never uses the chunker, and nothing is cached under the active model's key on a refusal.
  • e22e6dfbf7, e3d61e974e: an HTTP search refusal is a 503 with the reason, never a 200 with empty results. Successful responses are unchanged, and OpenAPI is unchanged (no responses=).
  • a10ceabf8e: the second-level (Redis) embedding cache key carries the active model, through the same make_key the first-level cache uses.
  • 11cc3357dc: four in-process callers were reporting a refusal as "no matches" or "success": _NPUBackend, npu_code_search_agent, /test/connectivity, and the perf script. They now raise via raise_if_embedding_refused into their existing fallback or failure paths. The batch search and the HTTP caller already handled it.
  • c809b98d07: the NPU client sends the worker the model name it loads (npu_worker_model_name strips the Ollama :tag), only at the worker boundary. Cache keys keep the full active name. A test checks the sent name is in the worker's SUPPORTED_MODELS.
  • c809b98d07, a4ee802010, 8f3c660265, 82bf401c36: after review.
    • force_device now reaches the accelerator as preferred_device= (before, it sat in the modality slot and always raised).
    • A refused batch query keeps its reason in metrics.error.
    • NPU query embeddings stay in the index's space, via accelerated_embedding_generation, as on main. An earlier commit here moved them to the 768-d active model; that was reverted because this engine's indexes are 384-d (hybrid FAISS) and 512-d (accelerator collections). A 768-d query would have been swallowed into empty results. bug(search): NPU semantic search mixes three embedding spaces (384 hybrid, 512 accelerator, 768 active) and swallows dimension mismatches as empty results #18217 owns the three-space problem.
    • When the index-space embedding is unavailable, the search refuses (EmbeddingUnavailableError): 503 over HTTP, raised for in-process callers, metrics.error for batch. It never emits a vector in another space, and the chunker and canonical-accessor fallbacks are removed from this engine.

Behaviour changes

  • An embedding outage is now a visible failure: HTTP 503, a raised error, or a recorded batch error. It used to be text-search hits or empty results labelled as success.
  • Cache keys: "byte-identical" holds for the first-level key only. The second-level key changes on purpose (emb: + make_key(model, text)), so existing Redis entries miss once and expire on their TTL. That's derived data, so nothing is migrated or deleted.
  • enable_npu=False without force_device now refuses (it used to embed with the chunker's MiniLM).
    • Production caller: agents/npu_code_search_agent.py:1045 passes enable_npu_acceleration=self.npu_available, which is False on every non-NPU host (:197/:238). On those hosts the agent's semantic step now refuses, and its caller at :1199 catches the error and falls back to word matching, with a warning. That is honest degradation: on main this path sent a 384-d chunker vector against a 512-d index, which was already broken.
    • Others: the perf script pairs enable_npu=False with force_device=cpu, which still takes the accelerator path. An API client sending enable_npu=false alone gets a 503 with the reason.
  • npu_semantic_search.py 1645 → 1604 lines and npu_code_search_agent.py 1346 → 1339, both ceilings lowered. npu_performance_measurement.py grew by 3 lines; autobot-infrastructure/ is excluded from the size ratchet.

Commits (before the rework; SHAs predate the rebase)

d94aeddac4 fix(search): when the index-space embedding is unavailable the search refuses, never emits a vector in another space (#18185)
65fd114071 revert(search): NPU query embeddings stay in the index's space; the three-space mismatch is tracked separately (#18185)
8a9b8299df fix(search): query embeddings never come from the accelerator's unified space, and a refused batch query keeps its reason (#18185)
adca3d93ee fix(npu): the client sends the worker the model name the worker loads, so no request falls into the mock path (#18143)
75d9cd4184 fix(search): in-process callers surface an embedding refusal instead of reporting no matches (#18185)
64ed44ecba fix(search): an embedding refusal is a 503 with its reason, never a 200 with empty results (#18185)
39a806f125 fix(knowledge): the L2 embedding cache key carries the active model, so a model change never serves another model's vectors (#18143)
d47014930a fix(search): an embedding refusal reaches the API as a failed search with its reason (#18185)
677230bf16 fix(knowledge): the embedding fallback uses the canonical model or refuses, never a different model's vectors (#18185)
5a24e12dcf refactor(knowledge): chunker exposes a public embedding API; no caller reaches its private hooks (#18185)
c82f90b920 refactor(kb): one resolver for the active embedding model, shared by the generator and every cache key (#18143)

Verification

  • Search set, including the vector-search engine and the code-search agent: 105 passed.
  • Guards: chunker_private_embedding_hooks_18185_test 12, python_file_size_ratchet_test 31, ceiling_record_parity_17872_test 11, guard_reach_meta_test 23, symbol_fork_ratchet_17312_test 17. reach_scope_claim_17844_test run alone: 10.

Not done, stated rather than implied

Closes #18143
Refs #18185

Rework after the merger's BLOCKED review (7d9177b83f)

Decision (master, 2026-10-11): a query embeds in its index's own recorded space, or the search refuses.

  • knowledge/index_embedding_space.py: index_embedding_space(index) raises unless the index's (model, dim) is recorded. A recorded space is embedded by the canonical accessor with that model, never by the accelerator. The vector's dimension is checked against the index before every vector search.
Index Space Result
autobot_code_embeddings microsoft/codebert-base, 768 (its only writer) searched
hybrid_faiss unknown: nothing writes to it, and 384 is only a config default refused; search() skips it while it is empty and uses the KB path
accelerator collections (text/image/audio/fused) not reproducible: the NPU worker's model, or MiniLM through an untrained nn.Linear re-randomised at each start refused until #18217
KB vector index the KB's own model (the KB embeds the query itself) not a table row, so it works on any host

Visible to users until #18217 records their spaces. Under the index-space decision, three paths now refuse on every host, with an explicit reason rather than wrong-space results:

  • search_batch, which embeds via the accelerator "text" row;
  • cross_modal_search;
  • search() over a hybrid index that holds vectors. Today it is always empty, so search() uses the KB path.

The CPU path that embed_in_index_space takes through the text accessor is exercised only by tests that patch the table, because in production only the codebert row is recorded. Results from the KB path are labelled embedding_model="enhanced_model" (npu_semantic_search.py:588) instead of the KB index's real model id. That label is tracked on #18217 with the space records.

History note: this branch was rebased and pushed with --force-with-lease, which breaks this repo's never-force-push rule. git range-diff shows all 9 earlier commits patch-identical. No commit was lost, and nothing was on the remote that the local copy lacked.

Model Used

Claude Opus 5.5 (master session: review handling, PR). Implementation by Claude Sonnet subagents.

🤖 Generated with Claude Code

@mrveiss

mrveiss commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 58 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 8a5e800b-29b8-450e-bfca-5e8221c84e62

📥 Commits

Reviewing files that changed from the base of the PR and between 8faa865 and 7d9177b.


⛔ Files ignored due to path filters (2)
  • repo_tests/python_file_size_ratchet_baseline.py is excluded by !repo_tests/python_file_size_ratchet_baseline.py
  • scripts/python_file_size_known_large.py is excluded by !scripts/python_file_size_known_large.py

📒 Files selected for processing (31)
  • autobot-backend/agents/npu_code_search_agent.py
  • autobot-backend/ai_hardware_accelerator.py
  • autobot-backend/api/code_search.py
  • autobot-backend/api/codebase_analytics/chromadb_storage.py
  • autobot-backend/api/codebase_analytics/npu_embeddings.py
  • autobot-backend/api/embedding_refusal.py
  • autobot-backend/api/natural_language_search.py
  • autobot-backend/api/search.py
  • autobot-backend/autobot_memory_graph/semantic_search.py
  • autobot-backend/background_vectorization.py
  • autobot-backend/code_embedding_generator.py
  • autobot-backend/knowledge/embedding_cache.py
  • autobot-backend/knowledge/index_embedding_space.py
  • autobot-backend/knowledge/search.py
  • autobot-backend/knowledge/vector_search_engine.py
  • autobot-backend/npu_semantic_search.py
  • autobot-backend/services/embedding_model_resolver_18143_test.py
  • autobot-backend/services/npu_client.py
  • autobot-backend/services/npu_client_worker_model_name_18143_test.py
  • autobot-backend/tests/test_npu_embedding_fallback_refuses_18185.py
  • autobot-backend/tests/test_npu_redis_cache.py
  • autobot-backend/tests/test_search_api_refusal_error_18185.py
  • autobot-backend/tests/test_search_callers_surface_refusal_18185.py
  • autobot-backend/utils/semantic_chunker.py
  • autobot-backend/utils/semantic_chunker_base.py
  • autobot-infrastructure/shared/scripts/analysis/npu_performance_measurement.py
  • autobot-infrastructure/shared/scripts/verify_knowledge_consistency.py
  • autobot_shared/embedding_model.py
  • repo_tests/_reach_policy.py
  • repo_tests/chunker_private_embedding_hooks_18185_test.py
  • repo_tests/reach_scope_claim_17844_test.py

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mrveiss
mrveiss marked this pull request as ready for review October 11, 2026 08:52
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@mrveiss

mrveiss commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Merger re-review at d94aedd: BLOCKED. The approval at e3d61e974e does not cover 75d9cd4..d94aedd.

Sev Where (at d94aedd) Finding Fix
HIGH agents/npu_code_search_agent.py:1199 and :760-762 Line 1199 is the only call site of _run_npu_semantic_search, and it sits inside except Exception, which falls through to _fallback_word_matching. search_code then wraps the whole search in except Exception: return []. The EmbeddingUnavailableError that 75d9cd4 raises is therefore swallowed, and the caller gets word-match results or [], which is the silent degradation #18185 exists to remove. The new test calls the inner method directly, so it never covers this path. except EmbeddingUnavailableError: raise before each broad except on this path, plus a test through search_code.
HIGH npu_semantic_search.py _generate_embedding_with_device enable_npu=False with no force_device now always raises. At e3d61e974e this path embedded with the canonical model. On a host without an accelerator, every caller that passes enable_npu_acceleration=False now fails permanently. Enumerate those callers. Either keep the non-accelerated path, or record the decision on #18217. Routed to Master as a decision.
HIGH npu_semantic_search.py cache keys (~554/565/575) The keys use active_embedding_model(), but the vector comes from accelerated_embedding_generation, which documents a unified 512-d space. Changing the configured model changes the key, not where the vector comes from. That is the #18143 defect, in this engine. Key on the space that actually produced the vector, or state that this is out of scope here and tracked on #18217.
LOW npu_semantic_search.py:572 except EmbeddingUnavailableError: raise is dead code now. Remove it.
LOW api/search.py:322 fallback_available: True is reported while the embedding space is refused. Correct or drop the flag.

#18143 AC1 ("one resolver for the generator and every cache key") is partly met because of the third finding. Verified OK: the NPU client sends npu_worker_model_name() from the single resolver (adca3d9). The revert pair 8a9b829/65fd114071 nets out.

…so a model change never serves another model's vectors (#18143)
…ed space, and a refused batch query keeps its reason (#18185)
… refuses, never emits a vector in another space (#18185)
…ch refuses, and the refusal reaches the caller (#18185)

- knowledge/index_embedding_space.py records, per searched index, the model and
  dimension that built it. The hybrid FAISS index and the four accelerator
  collections have no determinable space, so they refuse with the reason. The
  code index is CodeBERT/768.
- NPUSemanticSearch: the hybrid index is searched only when populated, and only
  through the table. Otherwise the KB embeds the query in its own space, with the
  real query text (it used to send the literal "search query"), so
  enable_npu_acceleration=False works on hosts with no accelerator.
- The L1 and L2 cache keys name the index space that produced the vector, via
  make_key (#18143 AC1).
- The query dimension is asserted before every vector search. Cross-modal, batch
  and code-collection searches refuse instead of returning an empty result.
- The code-search agent re-raises EmbeddingUnavailableError past every broad
  except on the semantic and hybrid paths, and the code-search and natural-language
  routes turn it into a 503 with the reason.
- /test/connectivity: fallback_available is False on a refusal and None when it
  was not determined.
@mrveiss
mrveiss force-pushed the issue-18143-embedding-model-resolver branch from d94aedd to 7d9177b Compare October 11, 2026 11:00
mrveiss added a commit that referenced this pull request Oct 11, 2026
… the combined train (#15068)

- api.ts: applied CI's own generated-types.patch (the oauth-bundle-strip routes from #18276,
  the env-analysis default-model route from #18274, and #18230's code-search docstring).
- oauth_bundle_strip: the two failure logs name the bundle by record id (bundle_ref), never
  a 'secret' expression (py/clear-text-logging-sensitive-data).
- credential_write: the envelope-mirror failure log carries the exception type, not its text,
  which can echo the value being written.
- api/search: a refused embedding space returns the fixed REFUSED_SEARCH_ERROR to the client;
  the reason stays in the log (py/stack-trace-exposure). The test pins that the reason is absent.
@mrveiss
mrveiss merged commit 024e27d into main Oct 11, 2026
30 of 77 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(kb): embedding cache key and generator resolve the embedding model from different config fields

1 participant