Skip to content

OLS-3310 Add OKP Solr hybrid RAG via tool-based retrieval#2926

Open
blublinsky wants to merge 1 commit into
openshift:mainfrom
blublinsky:okp-rag
Open

OLS-3310 Add OKP Solr hybrid RAG via tool-based retrieval#2926
blublinsky wants to merge 1 commit into
openshift:mainfrom
blublinsky:okp-rag

Conversation

@blublinsky

@blublinsky blublinsky commented May 12, 2026

Copy link
Copy Markdown
Contributor

Description

Summary

Adds optional Solr hybrid retrieval against the OKP portal-rag hybrid-search
endpoint, exposed as a search_openshift_documentation LangChain tool.
When ols_config.solr_hybrid is present in the config, the model can invoke
the tool to search published Red Hat product documentation. Solr failures
degrade to empty passages instead of failing the request.

Embedding models (sentence-transformers/all-mpnet-base-v2 for BYOK/tools/skills
RAG, ibm-granite/granite-embedding-30m-english for OKP Solr) are bundled in
the service image via Cachi2 and loaded offline — no runtime downloads.

BYOK FAISS RAG is preserved and coexists with the Solr tool path.

Key changes

  • SolrHybridSettings config model (solr_http_base, max_results,
    hybrid_pool_docs, hybrid_vector_boost, hybrid_score_threshold,
    chunk_filter_query, hybrid_solr_timeout_s)
  • SolrHybridSearch async client with reused httpx.AsyncClient for
    connection pooling
  • search_openshift_documentation async StructuredTool for agent tool loop
  • Prompt supplement for ASK-mode when the Solr docs tool is active
  • Embedding models pre-populated in HuggingFace cache at build time
  • Containerfile + artifacts.lock.yaml updated for both embedding models

Type of change

  • New feature

Related Tickets & Documents

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

Unit tests (make test-unit) and integration tests (make test-integration)
pass. make verify clean (only pre-existing woke finding on origin/main).

Local validation:

  1. Set OLS_CONFIG_FILE with solr_hybrid.solr_http_base pointing at an
    OKP portal-rag instance
  2. Ask an OpenShift question — the model invokes
    search_openshift_documentation and cites docs_url from results
  3. Without solr_hybrid section — BYOK FAISS RAG works as before

Summary by CodeRabbit

  • New Features
    • Added optional Solr-hybrid documentation search (solr_hybrid) with solr_http_base, retrieval/reranking controls, and a fixed Granite embedding model for vector consistency.
    • Enabled dedicated prompt/tool guidance for the OpenShift docs Solr tool when active.
  • Bug Fixes
    • Improved configuration validation (URL scheme enforcement, BYOK-only local index requirement when Solr hybrid is enabled).
    • Updated response generation and readiness behavior to correctly handle missing reference content/RAG.
  • Documentation
    • Documented solr_hybrid YAML structure and example/migration guidance.
  • Tests / Chores
    • Expanded unit tests for Solr-hybrid parsing/validation/tool behavior; updated container/lockfile for the bundled embedding model.

@openshift-ci openshift-ci Bot requested review from bparees and joshuawilson May 12, 2026 14:38
@openshift-ci

openshift-ci Bot commented May 12, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign raptorsun for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@blublinsky blublinsky force-pushed the okp-rag branch 2 times, most recently from 4c89880 to 99d82f9 Compare May 19, 2026 14:57
@blublinsky blublinsky force-pushed the okp-rag branch 3 times, most recently from 914faae to 37a7b67 Compare May 20, 2026 12:30
Comment thread ols/src/prompts/prompts.py Outdated

SOLR_DOCS_TOOL_AGENT_SUPPLEMENT = """
Solr docs tool:
* ``search_openshift_documentation`` searches published Red Hat product docs (not the cluster). Prefer one short query before answering from memory on product behavior, APIs, operators, upgrades, or whether docs exist.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"(not the cluster)" seems not clear: not the documents about this specific cluster? tool does not search inside the cluster for related objects?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread docs/ai/config.md Outdated

Omit the key to leave Solr hybrid RAG off (no client, no tool). When present, values map to `SolrHybridSettings`; `enabled: false` disables the feature without removing the block. `solr_http_base` must be a valid `http` or `https` URL with a host when `enabled` is true (checked in `validate_yaml()`).

When ``solr_hybrid.enabled`` is true, every entry under ``reference_content.indexes``

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

solr_hybrid in config does not have the property enabled.
we can change the phrase to "when solr_hybrid" is defined in the config" corresponding to current code in PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

"No docs above hybrid_score_threshold=%s for: %s",
cfg.hybrid_score_threshold,
query,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be added here the fallback to "/select" endpoint?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hybrid_score_threshold defaults to 0.0 (no filtering) and the operator does not expose this parameter — it always uses the default. When someone explicitly sets a threshold, they're choosing "no context over bad context." Falling back to /select would return the same or weaker documents that the hybrid path already rejected, undermining that intent.

if retrieved_nodes:
logger.info(
"Retrieved %d document nodes for RAG context", len(retrieved_nodes)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it be useful to sort the documents from multiple sources using the same standard to avoid those at the beggining of the list are most weighted to the LLM's reasoning?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern, but in practice:

FAISS and Solr are never used together for OpenShift docs — once we switch to Solr, FAISS-based product documentation goes away.
They can coexist for OpenShift docs (Solr) + BYOK. In that case there are two options: BYOK as direct RAG + Solr as a tool, or both BYOK and Solr as tools (PR for this is coming).
So the mixed-source scoring problem doesn't arise in the direct RAG path — it's either one source or the other, never both unsorted.

@blublinsky

Copy link
Copy Markdown
Contributor Author

/retest e2e-ols-cluster

@blublinsky

Copy link
Copy Markdown
Contributor Author

/retest

@raptorsun

Copy link
Copy Markdown
Contributor

the new code looks good :)
there is some tests about tool calling failing, worth our attention, like test_offloaded_tool_output_triggers_retrieval.

@blublinsky

Copy link
Copy Markdown
Contributor Author

/retest

1 similar comment
@blublinsky

Copy link
Copy Markdown
Contributor Author

/retest

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough
📝 Walkthrough
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding OKP Solr hybrid RAG through tool-based retrieval.
Docstring Coverage ✅ Passed Docstring coverage is 86.55% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (6)
ols/src/query_helpers/docs_summarizer.py (1)

203-274: 📐 Maintainability & Code Quality | 🔵 Trivial

Keep audit instrumentation on the Solr-only direct-RAG path.

The new elif retrieved_nodes: branch returns Solr passages without entering the request.rag span or calling audit_ctx.logger.rag_retrieved(...), so solr_direct_rag=True requests lose retrieval telemetry whenever no local retriever is configured. Consider sharing the truncate/log/audit block between both branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/src/query_helpers/docs_summarizer.py` around lines 203 - 274, The
Solr-only direct-RAG path in the document retrieval flow is skipping audit
telemetry because the shared logging and span updates only happen inside the
rag_retriever branch. Update the retrieval logic in docs_summarizer.py around
the retrieval block in the main query helper so both the rag_retriever path and
the retrieved_nodes-only Solr path reuse the same truncate/context logging/audit
handling, including request.rag span attributes and
audit_ctx.logger.rag_retrieved calls, using the existing retrieved_nodes,
rag_chunks, and span symbols.
tests/unit/utils/test_config.py (1)

1218-1218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add -> None to the new test function.

The added test omits a return annotation. As per coding guidelines, "Use type hints for all function signatures."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/utils/test_config.py` at line 1218, The new test function
test_solr_hybrid_search_uses_fixed_portal_rag_embedding_model is missing a
return type annotation. Update the function signature to include -> None so it
matches the project’s typing guidelines for all test functions.

Source: Coding guidelines

tests/unit/prompts/test_prompt_generator.py (1)

394-437: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add -> None to the new test functions.

The added tests in this block omit return annotations. As per coding guidelines, "Use type hints for all function signatures."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/prompts/test_prompt_generator.py` around lines 394 - 437, The new
test functions in test_prompt_generator.py are missing explicit return type
annotations, which violates the function signature typing guideline. Update each
added test (for example, the solr_docs_tool_guidance_* tests around
GeneratePrompt) to include a -> None return annotation while keeping the
existing assertions and setup unchanged.

Source: Coding guidelines

tests/unit/app/models/test_config.py (1)

2262-2406: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add -> None to the new test functions.

The added tests in this block omit return annotations. As per coding guidelines, "Use type hints for all function signatures."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/app/models/test_config.py` around lines 2262 - 2406, The new test
functions in test_config.py are missing explicit return type annotations, which
violates the function signature typing guideline. Add -> None to each added test
definition in this block, including the
test_ols_config_solr_hybrid_parses_from_yaml_dict,
test_config_reserves_tool_budget_for_solr_tool_only_without_mcp,
test_ols_config_solr_hybrid_validate_yaml_rejects_bad_url,
test_ols_config_validate_yaml_solr_hybrid_rejects_invalid_base_url,
test_ols_config_validate_yaml_rejects_solr_with_non_byok_indexes, and
test_ols_config_validate_yaml_allows_solr_with_byok_indexes functions.

Source: Coding guidelines

tests/unit/query_helpers/test_docs_summarizer.py (1)

78-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add -> None to the new test functions.

The added tests in these blocks omit return annotations. As per coding guidelines, "Use type hints for all function signatures."

Also applies to: 218-297

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/query_helpers/test_docs_summarizer.py` around lines 78 - 99, The
new test functions in test_docs_summarizer should explicitly declare a None
return type to match the type-hinting guideline. Update the signatures of
test_tool_calling_enabled_when_solr_docs_tool_active_without_mcp and
test_tool_calling_disabled_without_mcp_and_without_solr_docs_tool, and also any
similar newly added tests in the same DocsSummarizer test block, to include ->
None. Keep the bodies unchanged; this is just a signature annotation fix.

Source: Coding guidelines

pyproject.toml (1)

138-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep a single opentelemetry-api requirement.

project.dependencies already has opentelemetry-api>=1.20.0 on Line 144, so adding another entry here makes the intended minimum version ambiguous and easy to drift later.

Also applies to: 144-144

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` at line 138, Remove the duplicate opentelemetry-api
dependency so project.dependencies keeps only one requirement entry. Update the
dependencies list in pyproject.toml to keep the single canonical declaration
(the existing opentelemetry-api spec) and delete the extra one, ensuring the
minimum version is defined in only one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/olsconfig.yaml`:
- Around line 78-90: The sample config under reference_content.indexes does not
satisfy OLSConfig.validate_yaml() when solr_hybrid is enabled because every
index must be marked byok_index: true. Update the example so each index entry in
the indexes list explicitly includes byok_index: true, and make sure the
commented guidance matches the actual required shape in
OLSConfig.validate_yaml() and the solr_hybrid/reference_content section.

In `@ols/app/models/config.py`:
- Line 1010: Avoid truthiness coercion when setting OLSConfig.byok_index, since
bool(data.get("byok_index", False)) will treat non-empty strings like "false" as
true and can bypass _validate_solr_hybrid_vs_reference_indexes(). Update the
byok_index assignment in OLSConfig to preserve a real boolean from the config
data or explicitly parse/validate the input type before storing it, so string
values cannot accidentally enable BYOK indexing.
- Around line 1148-1154: The validate_yaml method currently allows
solr_http_base values that already contain a /solr path, which later breaks
endpoint construction in Solr support. Update validate_yaml in Config to reject
URLs whose path is already /solr (or ends with that segment) while keeping the
existing http/https host validation, so callers of solr_support’s hybrid-search
URL building do not produce a duplicated /solr/solr path at runtime.

In `@ols/src/rag_index/solr_support.py`:
- Around line 543-574: The search_openshift_documentation tool description and
return contract are inconsistent with _search_openshift_documentation, since
client.search() already swallows Solr/embedding/HTTP failures and returns an
empty list. Update _search_openshift_documentation and the
StructuredTool.from_function description to match the actual behavior: either
let exceptions propagate so the {"error": ...} path is reachable, or remove the
dead exception handling and document that the tool returns an array (including
[]) rather than an error object.

In `@ols/src/rag/stop_words.py`:
- Around line 74-76: The stop-word list currently includes negation terms, which
causes query normalization and tokenization to strip meaning from phrases like
“pod not ready.” Update the stop-word set in stop_words.py to keep “not”, “no”,
and “nor” out of the list so normalize_solr_hybrid_query() and _tokenize()
preserve negation intent.

In `@ols/utils/config.py`:
- Around line 325-330: Fail fast instead of returning None when Solr hybrid
embedding resolution fails in Config._solr_hybrid_embed_model /
_compute_tool_budgets path. The current exception handler silently disables the
Solr docs tool after tool tokens were already reserved, so change the fallback
to raise for tool-only Solr mode (or otherwise propagate the error) and keep the
existing logger.exception context so DocsSummarizer does not continue with an
unusable smaller budget.

In `@tests/unit/app/endpoints/test_health.py`:
- Around line 174-183: Restore the shared singleton state in the readiness tests
after each scenario by saving and resetting the original values of
config._rag_index_loader and config.ols_config.reference_content around the
assertions in test_health.py; update the affected cases in the readiness probe
tests so they clean up the temporary Mock loader and reference content instead
of leaving mutated config behind.

In `@tests/unit/utils/test_config.py`:
- Around line 1272-1284: The test in test_config.py is leaking cached AppConfig
state, making the HuggingFaceEmbedding assertion order-dependent. Before
accessing solr_hybrid_search, clear the actual private cache used by
AppConfig._solr_hybrid_embed_model() (including _cached_solr_embed_model, and
any related private loader cache like _rag_index_loader), not just entries in
config.__dict__. This ensures the patched constructor is always exercised and
the constructor call assertion is reliable.

---

Nitpick comments:
In `@ols/src/query_helpers/docs_summarizer.py`:
- Around line 203-274: The Solr-only direct-RAG path in the document retrieval
flow is skipping audit telemetry because the shared logging and span updates
only happen inside the rag_retriever branch. Update the retrieval logic in
docs_summarizer.py around the retrieval block in the main query helper so both
the rag_retriever path and the retrieved_nodes-only Solr path reuse the same
truncate/context logging/audit handling, including request.rag span attributes
and audit_ctx.logger.rag_retrieved calls, using the existing retrieved_nodes,
rag_chunks, and span symbols.

In `@pyproject.toml`:
- Line 138: Remove the duplicate opentelemetry-api dependency so
project.dependencies keeps only one requirement entry. Update the dependencies
list in pyproject.toml to keep the single canonical declaration (the existing
opentelemetry-api spec) and delete the extra one, ensuring the minimum version
is defined in only one place.

In `@tests/unit/app/models/test_config.py`:
- Around line 2262-2406: The new test functions in test_config.py are missing
explicit return type annotations, which violates the function signature typing
guideline. Add -> None to each added test definition in this block, including
the test_ols_config_solr_hybrid_parses_from_yaml_dict,
test_config_reserves_tool_budget_for_solr_tool_only_without_mcp,
test_ols_config_solr_hybrid_validate_yaml_rejects_bad_url,
test_ols_config_validate_yaml_solr_hybrid_rejects_invalid_base_url,
test_ols_config_validate_yaml_rejects_solr_with_non_byok_indexes, and
test_ols_config_validate_yaml_allows_solr_with_byok_indexes functions.

In `@tests/unit/prompts/test_prompt_generator.py`:
- Around line 394-437: The new test functions in test_prompt_generator.py are
missing explicit return type annotations, which violates the function signature
typing guideline. Update each added test (for example, the
solr_docs_tool_guidance_* tests around GeneratePrompt) to include a -> None
return annotation while keeping the existing assertions and setup unchanged.

In `@tests/unit/query_helpers/test_docs_summarizer.py`:
- Around line 78-99: The new test functions in test_docs_summarizer should
explicitly declare a None return type to match the type-hinting guideline.
Update the signatures of
test_tool_calling_enabled_when_solr_docs_tool_active_without_mcp and
test_tool_calling_disabled_without_mcp_and_without_solr_docs_tool, and also any
similar newly added tests in the same DocsSummarizer test block, to include ->
None. Keep the bodies unchanged; this is just a signature annotation fix.

In `@tests/unit/utils/test_config.py`:
- Line 1218: The new test function
test_solr_hybrid_search_uses_fixed_portal_rag_embedding_model is missing a
return type annotation. Update the function signature to include -> None so it
matches the project’s typing guidelines for all test functions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: af5959a1-3ceb-4f37-a9b3-688e1d9f7132

📥 Commits

Reviewing files that changed from the base of the PR and between 93b3d43 and 5dad137.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (19)
  • docs/ai/config.md
  • examples/olsconfig.yaml
  • ols/app/endpoints/ols.py
  • ols/app/models/config.py
  • ols/constants.py
  • ols/src/prompts/prompt_generator.py
  • ols/src/prompts/prompts.py
  • ols/src/query_helpers/docs_summarizer.py
  • ols/src/rag/hybrid_rag.py
  • ols/src/rag/stop_words.py
  • ols/src/rag_index/solr_support.py
  • ols/utils/config.py
  • pyproject.toml
  • tests/unit/app/endpoints/test_health.py
  • tests/unit/app/models/test_config.py
  • tests/unit/prompts/test_prompt_generator.py
  • tests/unit/query_helpers/test_docs_summarizer.py
  • tests/unit/rag_index/test_solr_support.py
  • tests/unit/utils/test_config.py

Comment thread examples/olsconfig.yaml Outdated
Comment thread ols/app/models/config.py
self.product_docs_index_path = data.get("product_docs_index_path", None)
self.product_docs_index_id = data.get("product_docs_index_id", None)
self.product_docs_origin = data.get("product_docs_origin", None)
self.byok_index = bool(data.get("byok_index", False))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid truthiness coercion for byok_index.

bool(data.get("byok_index", False)) turns non-empty strings like "false" into True, so a quoted/string config value can accidentally bypass the Solr-vs-local-index check in OLSConfig._validate_solr_hybrid_vs_reference_indexes().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/app/models/config.py` at line 1010, Avoid truthiness coercion when
setting OLSConfig.byok_index, since bool(data.get("byok_index", False)) will
treat non-empty strings like "false" as true and can bypass
_validate_solr_hybrid_vs_reference_indexes(). Update the byok_index assignment
in OLSConfig to preserve a real boolean from the config data or explicitly
parse/validate the input type before storing it, so string values cannot
accidentally enable BYOK indexing.

Comment thread ols/app/models/config.py
Comment on lines +1148 to +1154
def validate_yaml(self) -> None:
"""Validate Solr hybrid settings."""
if not checks.is_valid_http_url(self.solr_http_base):
raise checks.InvalidConfigurationError(
"solr_hybrid.solr_http_base must be a valid http or https URL with a "
f"host; got {self.solr_http_base!r}"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject solr_http_base values that already include /solr.

This validator accepts https://host/solr, but ols/src/rag_index/solr_support.py then appends /solr/{collection}/hybrid-search on Lines 483-495, producing a broken .../solr/solr/... endpoint at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/app/models/config.py` around lines 1148 - 1154, The validate_yaml method
currently allows solr_http_base values that already contain a /solr path, which
later breaks endpoint construction in Solr support. Update validate_yaml in
Config to reject URLs whose path is already /solr (or ends with that segment)
while keeping the existing http/https host validation, so callers of
solr_support’s hybrid-search URL building do not produce a duplicated /solr/solr
path at runtime.

Comment thread ols/src/rag_index/solr_support.py Outdated
Comment thread ols/src/rag/stop_words.py
Comment on lines +74 to +76
"not",
"no",
"nor",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep negation terms out of the stop-word list.

normalize_solr_hybrid_query() and _tokenize() both consume this set, so dropping not/no/nor turns queries like pod not ready into pod ready and can invert retrieval intent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/src/rag/stop_words.py` around lines 74 - 76, The stop-word list currently
includes negation terms, which causes query normalization and tokenization to
strip meaning from phrases like “pod not ready.” Update the stop-word set in
stop_words.py to keep “not”, “no”, and “nor” out of the list so
normalize_solr_hybrid_query() and _tokenize() preserve negation intent.

Comment thread ols/utils/config.py
Comment on lines +325 to +330
try:
embed_model = self._solr_hybrid_embed_model()
encode_fn = embed_model.get_text_embedding
except Exception:
logger.exception("Failed to resolve embedding model for Solr hybrid RAG")
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't silently disable configured Solr tool-only mode.

When this returns None, DocsSummarizer disables the built-in Solr docs tool, but Config._compute_tool_budgets() has already reserved tool tokens for solr_direct_rag=False. The result is a smaller prompt budget with no usable tool. This should fail fast, at least for tool-only Solr mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/utils/config.py` around lines 325 - 330, Fail fast instead of returning
None when Solr hybrid embedding resolution fails in
Config._solr_hybrid_embed_model / _compute_tool_budgets path. The current
exception handler silently disables the Solr docs tool after tool tokens were
already reserved, so change the fallback to raise for tool-only Solr mode (or
otherwise propagate the error) and keep the existing logger.exception context so
DocsSummarizer does not continue with an unusable smaller budget.

Comment thread tests/unit/app/endpoints/test_health.py
Comment on lines +1272 to +1284
config.reload_empty()
config.config = config._load_config_from_yaml_stream(
io.StringIO(yaml_stream), ignore_missing_certs=True
)
for key in (
"solr_hybrid_search",
"rag_index_loader",
"mcp_servers_dict",
"tools_rag",
"skills_rag",
):
config.__dict__.pop(key, None)
client = config.solr_hybrid_search

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clear the actual Solr embedding cache before asserting constructor calls.

AppConfig._solr_hybrid_embed_model() short-circuits on config._cached_solr_embed_model, and reload_empty() only replaces config.config. If a previous test already primed that cache, this test never reaches the patched HuggingFaceEmbedding, so assert_called_once_with(...) becomes order-dependent. Also, pop("rag_index_loader", ...) does not clear the private _rag_index_loader field.

Suggested fix
         mock_hf.return_value = _StubEmbed()
         config.reload_empty()
+        config._cached_solr_embed_model = None
+        config._rag_index_loader = None
         config.config = config._load_config_from_yaml_stream(
             io.StringIO(yaml_stream), ignore_missing_certs=True
         )
         for key in (
             "solr_hybrid_search",
-            "rag_index_loader",
             "mcp_servers_dict",
             "tools_rag",
             "skills_rag",
         ):
             config.__dict__.pop(key, None)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
config.reload_empty()
config.config = config._load_config_from_yaml_stream(
io.StringIO(yaml_stream), ignore_missing_certs=True
)
for key in (
"solr_hybrid_search",
"rag_index_loader",
"mcp_servers_dict",
"tools_rag",
"skills_rag",
):
config.__dict__.pop(key, None)
client = config.solr_hybrid_search
mock_hf.return_value = _StubEmbed()
config.reload_empty()
config._cached_solr_embed_model = None
config._rag_index_loader = None
config.config = config._load_config_from_yaml_stream(
io.StringIO(yaml_stream), ignore_missing_certs=True
)
for key in (
"solr_hybrid_search",
"mcp_servers_dict",
"tools_rag",
"skills_rag",
):
config.__dict__.pop(key, None)
client = config.solr_hybrid_search
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/utils/test_config.py` around lines 1272 - 1284, The test in
test_config.py is leaking cached AppConfig state, making the
HuggingFaceEmbedding assertion order-dependent. Before accessing
solr_hybrid_search, clear the actual private cache used by
AppConfig._solr_hybrid_embed_model() (including _cached_solr_embed_model, and
any related private loader cache like _rag_index_loader), not just entries in
config.__dict__. This ensures the patched constructor is always exercised and
the constructor call assertion is reliable.

@blublinsky blublinsky changed the title Adding OKP for RAG support OLS-3310 Add OKP Solr hybrid RAG via tool-based retrieval Jul 1, 2026
@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 1, 2026
@blublinsky blublinsky force-pushed the okp-rag branch 2 times, most recently from 2fd2994 to 29d62eb Compare July 1, 2026 10:57
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/rag_index/test_index_loader.py (1)

32-40: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Patch HuggingFaceEmbedding here instead of constructing the real model.

This unit test now exercises the actual embedding loader, and Line 34 even clears TRANSFORMERS_OFFLINE, so it can depend on local model state or network access. That makes the test non-hermetic and prone to CI flakes. Stub the constructor like tests/unit/utils/test_config.py does and assert the requested model_name on the mock instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/rag_index/test_index_loader.py` around lines 32 - 40, This unit
test is still constructing the real HuggingFaceEmbedding through IndexLoader,
making it non-hermetic and dependent on local or network model availability.
Update the test around IndexLoader.vector_indexes to patch HuggingFaceEmbedding
the same way as in tests/unit/utils/test_config.py, then assert the mock was
called with model_name set to all-mpnet-base-v2 instead of inspecting a real
_embed_model instance.
♻️ Duplicate comments (1)
examples/olsconfig.yaml (1)

83-89: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Sample config still fails validate_yaml().

With solr_hybrid enabled, _validate_solr_hybrid_vs_reference_indexes rejects any index without byok_index: true. The first index (Lines 85-86) omits the flag entirely, and the second (Lines 87-89) only shows it commented out — same issue flagged in a prior review, still unresolved.

Suggested fix
   reference_content:
     indexes:
-      - product_docs_index_path: "./vector_db/ocp_product_docs/4.17"
-        product_docs_index_id: ocp-product-docs-4_17
       - product_docs_index_path: "./vector_db/user_application_docs/version_1"
         product_docs_index_id: user-application-docs-version_1
-        # byok_index: true
+        byok_index: true
🧹 Nitpick comments (1)
ols/utils/config.py (1)

181-187: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse one cached BYOK embedding instance.

tools_rag and skills_rag both instantiate the same HuggingFaceEmbedding model separately. If both features are enabled, the process pays the model load cost twice and keeps duplicate weights in memory. Pull this behind one shared cached helper/property and reuse it from both paths.

Also applies to: 224-230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/utils/config.py` around lines 181 - 187, The BYOK embedding setup
currently creates separate HuggingFaceEmbedding instances in both the tools_rag
and skills_rag paths, duplicating load time and memory. Move the model creation
behind one shared cached helper/property in config.py, and have both paths reuse
that same cached instance instead of instantiating HuggingFaceEmbedding
directly. Use the existing embed_model setup and
constants.EMBEDDINGS_MODEL_BYOK_SUBDIR as the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Containerfile`:
- Around line 89-95: The Cachi2 pre-copy step in the Containerfile is too
brittle because the Granite model copy can fail and stop the entire RUN before
the fallback download loop executes. Update the RUN block that copies into
embeddings_model/all-mpnet-base-v2 and
embeddings_model/granite-embedding-30m-english so each source file is checked
before copying, or gate each cp independently inside the existing
/cachi2/output/deps/generic path. Keep the later for f in loop intact so missing
models can still be downloaded.

In `@ols/src/query_helpers/docs_summarizer.py`:
- Around line 137-141: The docs_summarizer tool assembly is adding a duplicate
tool name when `get_openshift_docs_tool` is appended after `get_mcp_tools`.
Update the `get_mcp_tools`/`get_openshift_docs_tool` flow in `DocsSummarizer` so
`search_openshift_documentation` is only added if no existing tool with that
name is already present, or otherwise skip the local tool when MCP already
provides it. Use the unique symbols `get_mcp_tools`, `get_openshift_docs_tool`,
and `tools.append(...)` to locate and guard this merge logic.

---

Outside diff comments:
In `@tests/unit/rag_index/test_index_loader.py`:
- Around line 32-40: This unit test is still constructing the real
HuggingFaceEmbedding through IndexLoader, making it non-hermetic and dependent
on local or network model availability. Update the test around
IndexLoader.vector_indexes to patch HuggingFaceEmbedding the same way as in
tests/unit/utils/test_config.py, then assert the mock was called with model_name
set to all-mpnet-base-v2 instead of inspecting a real _embed_model instance.

---

Nitpick comments:
In `@ols/utils/config.py`:
- Around line 181-187: The BYOK embedding setup currently creates separate
HuggingFaceEmbedding instances in both the tools_rag and skills_rag paths,
duplicating load time and memory. Move the model creation behind one shared
cached helper/property in config.py, and have both paths reuse that same cached
instance instead of instantiating HuggingFaceEmbedding directly. Use the
existing embed_model setup and constants.EMBEDDINGS_MODEL_BYOK_SUBDIR as the
single source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 655c8f9e-ab29-47ef-84d3-cf15fb669eca

📥 Commits

Reviewing files that changed from the base of the PR and between 5dad137 and 2fd2994.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (28)
  • Containerfile
  • artifacts.lock.yaml
  • docs/ai/config.md
  • examples/olsconfig.yaml
  • ols/app/endpoints/ols.py
  • ols/app/models/config.py
  • ols/constants.py
  • ols/src/prompts/prompt_generator.py
  • ols/src/prompts/prompts.py
  • ols/src/query_helpers/docs_summarizer.py
  • ols/src/rag/hybrid_rag.py
  • ols/src/rag/stop_words.py
  • ols/src/rag_index/index_loader.py
  • ols/src/rag_index/solr_support.py
  • ols/utils/config.py
  • ols/utils/environments.py
  • pyproject.toml
  • runner.py
  • tests/e2e/utils/ols_installer.py
  • tests/unit/app/endpoints/test_health.py
  • tests/unit/app/models/test_config.py
  • tests/unit/config_status/test_config_status.py
  • tests/unit/prompts/test_prompt_generator.py
  • tests/unit/query_helpers/test_docs_summarizer.py
  • tests/unit/rag_index/test_index_loader.py
  • tests/unit/rag_index/test_solr_support.py
  • tests/unit/utils/test_config.py
  • tests/unit/utils/test_environments.py
💤 Files with no reviewable changes (2)
  • tests/e2e/utils/ols_installer.py
  • tests/unit/config_status/test_config_status.py
✅ Files skipped from review due to trivial changes (1)
  • docs/ai/config.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • pyproject.toml
  • tests/unit/prompts/test_prompt_generator.py
  • ols/app/endpoints/ols.py
  • ols/src/rag/stop_words.py
  • ols/src/prompts/prompts.py
  • ols/constants.py
  • ols/src/rag/hybrid_rag.py
  • tests/unit/app/endpoints/test_health.py
  • ols/src/prompts/prompt_generator.py
  • ols/app/models/config.py

Comment thread Containerfile
Comment on lines +137 to +141
tools = await get_mcp_tools(
mcp_tools_query, self.user_token, self.client_headers
)
if self._solr_hybrid is not None and self._solr_client is not None:
tools.append(get_openshift_docs_tool(self._solr_client))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid adding a second tool with the same name.

get_mcp_tools(..., deduplicate=True) only dedupes MCP results. If an MCP server already exposes search_openshift_documentation, this appends a second tool with the same name and makes name-based tool resolution ambiguous.

Suggested fix
     tools = await get_mcp_tools(
         mcp_tools_query, self.user_token, self.client_headers
     )
     if self._solr_hybrid is not None and self._solr_client is not None:
-        tools.append(get_openshift_docs_tool(self._solr_client))
+        if all(tool.name != "search_openshift_documentation" for tool in tools):
+            tools.append(get_openshift_docs_tool(self._solr_client))
     return tools
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tools = await get_mcp_tools(
mcp_tools_query, self.user_token, self.client_headers
)
if self._solr_hybrid is not None and self._solr_client is not None:
tools.append(get_openshift_docs_tool(self._solr_client))
tools = await get_mcp_tools(
mcp_tools_query, self.user_token, self.client_headers
)
if self._solr_hybrid is not None and self._solr_client is not None:
if all(tool.name != "search_openshift_documentation" for tool in tools):
tools.append(get_openshift_docs_tool(self._solr_client))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/src/query_helpers/docs_summarizer.py` around lines 137 - 141, The
docs_summarizer tool assembly is adding a duplicate tool name when
`get_openshift_docs_tool` is appended after `get_mcp_tools`. Update the
`get_mcp_tools`/`get_openshift_docs_tool` flow in `DocsSummarizer` so
`search_openshift_documentation` is only added if no existing tool with that
name is already present, or otherwise skip the local tool when MCP already
provides it. Use the unique symbols `get_mcp_tools`, `get_openshift_docs_tool`,
and `tools.append(...)` to locate and guard this merge logic.

@blublinsky blublinsky force-pushed the okp-rag branch 2 times, most recently from dfb71a2 to e09d4fa Compare July 1, 2026 11:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/unit/rag_index/test_index_loader.py (1)

31-44: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the embed-model constructor args, not just identity.

The test verifies _embed_model is mock_embed but never checks that HuggingFaceEmbedding was invoked with the expected model_name (EMBEDDINGS_MODEL_BYOK_SUBDIR). A regression that changes the model path passed in IndexLoader._get_embed_model would go undetected.

♻️ Suggested addition
     with (
         patch(
             "ols.src.rag_index.index_loader.HuggingFaceEmbedding",
             return_value=mock_embed,
-        ),
+        ) as mock_hf_embedding,
         patch.object(il.IndexLoader, "_load_index"),
     ):
         index_loader_obj = il.IndexLoader(config.ols_config.reference_content)
         indexes = index_loader_obj.vector_indexes

         assert index_loader_obj._embed_model is mock_embed
+        mock_hf_embedding.assert_called_once_with(model_name=EMBEDDINGS_MODEL_BYOK_SUBDIR)
         assert indexes is None

As per coding guidelines, "Write tests that assert specific values and behaviors rather than only checking that code runs without error."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/rag_index/test_index_loader.py` around lines 31 - 44, The test in
test_index_loader should verify the HuggingFaceEmbedding constructor arguments,
not only that _embed_model matches the mock. Update the assertions around
IndexLoader._get_embed_model / IndexLoader.__init__ to check the patched
HuggingFaceEmbedding call received the expected model_name value
(EMBEDDINGS_MODEL_BYOK_SUBDIR), while keeping the existing identity check if
useful. Use the mocked HuggingFaceEmbedding symbol and IndexLoader to locate the
behavior under test.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ols/src/rag_index/solr_support.py`:
- Around line 381-388: The exception logging in the Solr search paths is
exposing raw user queries, so update the logging in the relevant search methods
(including the hybrid search flow and the other matching search helper) to avoid
printing the full query text. Replace the query argument in the logger.exception
call with either no query content or a non-reversible fingerprint/identifier,
keeping the existing error context and using the surrounding methods in
solr_support.py to locate both affected blocks.
- Line 373: The SolrHybridSearch-owned AsyncClient is created in the constructor
without any shutdown path, so add lifecycle management for the client created in
SolrHybridSearch. Expose an async cleanup method like aclose() and/or make
SolrHybridSearch usable as an async context manager so the owned
httpx.AsyncClient can be closed deterministically. If the client is injected
instead, ensure ownership is clear and the app closes it through the
SolrHybridSearch/solver integration path rather than relying on config reloads.

---

Nitpick comments:
In `@tests/unit/rag_index/test_index_loader.py`:
- Around line 31-44: The test in test_index_loader should verify the
HuggingFaceEmbedding constructor arguments, not only that _embed_model matches
the mock. Update the assertions around IndexLoader._get_embed_model /
IndexLoader.__init__ to check the patched HuggingFaceEmbedding call received the
expected model_name value (EMBEDDINGS_MODEL_BYOK_SUBDIR), while keeping the
existing identity check if useful. Use the mocked HuggingFaceEmbedding symbol
and IndexLoader to locate the behavior under test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 071a1650-91f9-4059-9fe7-9bc8a23d5b42

📥 Commits

Reviewing files that changed from the base of the PR and between 2fd2994 and d81cced.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (28)
  • Containerfile
  • artifacts.lock.yaml
  • docs/ai/config.md
  • examples/olsconfig.yaml
  • ols/app/endpoints/ols.py
  • ols/app/models/config.py
  • ols/constants.py
  • ols/src/prompts/prompt_generator.py
  • ols/src/prompts/prompts.py
  • ols/src/query_helpers/docs_summarizer.py
  • ols/src/rag/hybrid_rag.py
  • ols/src/rag/stop_words.py
  • ols/src/rag_index/index_loader.py
  • ols/src/rag_index/solr_support.py
  • ols/utils/config.py
  • ols/utils/environments.py
  • pyproject.toml
  • runner.py
  • tests/e2e/utils/ols_installer.py
  • tests/unit/app/endpoints/test_health.py
  • tests/unit/app/models/test_config.py
  • tests/unit/config_status/test_config_status.py
  • tests/unit/prompts/test_prompt_generator.py
  • tests/unit/query_helpers/test_docs_summarizer.py
  • tests/unit/rag_index/test_index_loader.py
  • tests/unit/rag_index/test_solr_support.py
  • tests/unit/utils/test_config.py
  • tests/unit/utils/test_environments.py
💤 Files with no reviewable changes (2)
  • tests/unit/config_status/test_config_status.py
  • tests/e2e/utils/ols_installer.py
✅ Files skipped from review due to trivial changes (1)
  • docs/ai/config.md
🚧 Files skipped from review as they are similar to previous changes (21)
  • pyproject.toml
  • artifacts.lock.yaml
  • ols/app/endpoints/ols.py
  • ols/src/rag/hybrid_rag.py
  • ols/src/prompts/prompt_generator.py
  • ols/src/rag/stop_words.py
  • ols/src/prompts/prompts.py
  • Containerfile
  • runner.py
  • ols/constants.py
  • tests/unit/app/endpoints/test_health.py
  • ols/src/query_helpers/docs_summarizer.py
  • tests/unit/utils/test_environments.py
  • tests/unit/utils/test_config.py
  • tests/unit/prompts/test_prompt_generator.py
  • ols/utils/config.py
  • ols/utils/environments.py
  • tests/unit/query_helpers/test_docs_summarizer.py
  • ols/src/rag_index/index_loader.py
  • tests/unit/app/models/test_config.py
  • ols/app/models/config.py

Comment thread ols/src/rag_index/solr_support.py Outdated
Comment on lines +381 to +388
try:
return await self._search_impl(query)
except Exception:
logger.exception(
"Solr hybrid search failed for query: %.200s",
query,
)
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid logging raw user queries.

These paths log the user’s full search text, which can contain support-case details, identifiers, or pasted secrets. Prefer omitting it or logging a non-reversible fingerprint.

Proposed fix
         except Exception:
-            logger.exception(
-                "Solr hybrid search failed for query: %.200s",
-                query,
-            )
+            logger.exception("Solr hybrid search failed")
             return []
@@
         if not hybrid_docs:
-            logger.warning("No results (hybrid-search) for: %s", query)
+            logger.warning("No results (hybrid-search)")
         elif cfg.hybrid_score_threshold > 0:
             logger.warning(
-                "No docs above hybrid_score_threshold=%s for: %s",
+                "No docs above hybrid_score_threshold=%s",
                 cfg.hybrid_score_threshold,
-                query,
             )

Also applies to: 422-429

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/src/rag_index/solr_support.py` around lines 381 - 388, The exception
logging in the Solr search paths is exposing raw user queries, so update the
logging in the relevant search methods (including the hybrid search flow and the
other matching search helper) to avoid printing the full query text. Replace the
query argument in the logger.exception call with either no query content or a
non-reversible fingerprint/identifier, keeping the existing error context and
using the surrounding methods in solr_support.py to locate both affected blocks.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
ols/utils/config.py (1)

259-263: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore logger levels after suppressing embedding-load noise.

setLevel(logging.ERROR) permanently overrides the configured logging behavior for sentence_transformers and transformers after the Solr client is initialized. If the intent is only to quiet model creation, restore the previous levels afterward.

Proposed refactor
-        for name in ("sentence_transformers", "transformers"):
-            logging.getLogger(name).setLevel(logging.ERROR)
-        self._cached_solr_embed_model = HuggingFaceEmbedding(
-            model_name=constants.SOLR_HYBRID_EMBEDDING_MODEL_ID
-        )
+        logger_names = ("sentence_transformers", "transformers")
+        previous_levels = {
+            name: logging.getLogger(name).level for name in logger_names
+        }
+        try:
+            for name in logger_names:
+                logging.getLogger(name).setLevel(logging.ERROR)
+            self._cached_solr_embed_model = HuggingFaceEmbedding(
+                model_name=constants.SOLR_HYBRID_EMBEDDING_MODEL_ID
+            )
+        finally:
+            for name, level in previous_levels.items():
+                logging.getLogger(name).setLevel(level)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/utils/config.py` around lines 259 - 263, The Solr embedding
initialization in config.py permanently changes the logger levels for
sentence_transformers and transformers; keep the quieting scoped to
HuggingFaceEmbedding creation in the Solr client path. Update the logic around
_cached_solr_embed_model so it saves each logger’s previous level before
setLevel(logging.ERROR), creates the model, then restores the original levels
afterward, preserving the rest of the app’s logging configuration.
ols/src/rag_index/solr_support.py (1)

359-373: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Reuse the HTTP client across searches, with an explicit close path.
_search_impl() creates a new httpx.AsyncClient on every call; keep one on SolrHybridSearch to preserve connection pooling, and add aclose()/lifecycle wiring so the shared client is closed cleanly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ols/src/rag_index/solr_support.py` around lines 359 - 373, `SolrHybridSearch`
currently creates a fresh `httpx.AsyncClient` inside `_search_impl()`, which
prevents connection pooling. Add a shared client as an instance member
initialized in `SolrHybridSearch.__init__`, have `_search_impl()` reuse that
client for all searches, and make sure there is an explicit `aclose()` on
`SolrHybridSearch` to shut it down cleanly. Wire the lifecycle so any caller
that owns the search object closes it when finished.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/rag_index/test_solr_support.py`:
- Around line 4-5: Update the helper functions `_patch_httpx_client` and `_ctx`
in `test_solr_support.py` so their full signatures include explicit type hints
for all parameters and return types, matching the repo’s Python typing
conventions. Also rename the affected test at the current
`test_solr_hybrid_search_*` location to follow the `test_<function>_<scenario>`
pattern used by surrounding tests, keeping the name aligned with the
`test_solr_hybrid_search_...` convention.

---

Nitpick comments:
In `@ols/src/rag_index/solr_support.py`:
- Around line 359-373: `SolrHybridSearch` currently creates a fresh
`httpx.AsyncClient` inside `_search_impl()`, which prevents connection pooling.
Add a shared client as an instance member initialized in
`SolrHybridSearch.__init__`, have `_search_impl()` reuse that client for all
searches, and make sure there is an explicit `aclose()` on `SolrHybridSearch` to
shut it down cleanly. Wire the lifecycle so any caller that owns the search
object closes it when finished.

In `@ols/utils/config.py`:
- Around line 259-263: The Solr embedding initialization in config.py
permanently changes the logger levels for sentence_transformers and
transformers; keep the quieting scoped to HuggingFaceEmbedding creation in the
Solr client path. Update the logic around _cached_solr_embed_model so it saves
each logger’s previous level before setLevel(logging.ERROR), creates the model,
then restores the original levels afterward, preserving the rest of the app’s
logging configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 97e3a38e-3a40-4e0f-af45-1062f9c47e00

📥 Commits

Reviewing files that changed from the base of the PR and between d81cced and 1b3bd39.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !uv.lock
📒 Files selected for processing (28)
  • Containerfile
  • artifacts.lock.yaml
  • docs/ai/config.md
  • examples/olsconfig.yaml
  • ols/app/endpoints/ols.py
  • ols/app/models/config.py
  • ols/constants.py
  • ols/src/prompts/prompt_generator.py
  • ols/src/prompts/prompts.py
  • ols/src/query_helpers/docs_summarizer.py
  • ols/src/rag/hybrid_rag.py
  • ols/src/rag/stop_words.py
  • ols/src/rag_index/index_loader.py
  • ols/src/rag_index/solr_support.py
  • ols/utils/config.py
  • ols/utils/environments.py
  • pyproject.toml
  • runner.py
  • tests/e2e/utils/ols_installer.py
  • tests/unit/app/endpoints/test_health.py
  • tests/unit/app/models/test_config.py
  • tests/unit/config_status/test_config_status.py
  • tests/unit/prompts/test_prompt_generator.py
  • tests/unit/query_helpers/test_docs_summarizer.py
  • tests/unit/rag_index/test_index_loader.py
  • tests/unit/rag_index/test_solr_support.py
  • tests/unit/utils/test_config.py
  • tests/unit/utils/test_environments.py
💤 Files with no reviewable changes (2)
  • tests/unit/config_status/test_config_status.py
  • tests/e2e/utils/ols_installer.py
✅ Files skipped from review due to trivial changes (2)
  • docs/ai/config.md
  • artifacts.lock.yaml
🚧 Files skipped from review as they are similar to previous changes (21)
  • ols/src/rag/stop_words.py
  • ols/src/rag/hybrid_rag.py
  • pyproject.toml
  • runner.py
  • ols/src/prompts/prompt_generator.py
  • ols/src/prompts/prompts.py
  • ols/app/endpoints/ols.py
  • ols/constants.py
  • tests/unit/rag_index/test_index_loader.py
  • tests/unit/app/endpoints/test_health.py
  • tests/unit/prompts/test_prompt_generator.py
  • Containerfile
  • ols/src/rag_index/index_loader.py
  • examples/olsconfig.yaml
  • ols/utils/environments.py
  • ols/src/query_helpers/docs_summarizer.py
  • tests/unit/utils/test_config.py
  • tests/unit/utils/test_environments.py
  • tests/unit/query_helpers/test_docs_summarizer.py
  • tests/unit/app/models/test_config.py
  • ols/app/models/config.py

Comment on lines +4 to +5
from contextlib import asynccontextmanager
from typing import Any

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the helper signatures and test name with repo conventions.

_patch_httpx_client/_ctx are missing full signature type hints, and Line 285 should follow the surrounding test_solr_hybrid_search_<scenario> naming pattern.

Proposed cleanup
+from collections.abc import AsyncIterator, Awaitable, Callable
 from contextlib import asynccontextmanager
 from typing import Any
@@
-def _patch_httpx_client(fake_post):
+def _patch_httpx_client(fake_post: Callable[..., Awaitable[Any]]) -> Any:
     """Patch ``httpx.AsyncClient`` so the context manager yields a mock with *fake_post*."""
     mock_client = AsyncMock()
     mock_client.post = fake_post
 
     `@asynccontextmanager`
-    async def _ctx(*args, **kwargs):
+    async def _ctx(*args: Any, **kwargs: Any) -> AsyncIterator[AsyncMock]:
         yield mock_client
@@
-async def test_solr_search_returns_empty_when_embedding_raises() -> None:
+async def test_solr_hybrid_search_returns_empty_when_embedding_raises() -> None:

As per coding guidelines, **/*.py: “Use type hints for all function signatures,” and tests/**/*.py: “Follow the test naming convention test_<function>_<scenario>.”

Also applies to: 32-39, 284-293

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/rag_index/test_solr_support.py` around lines 4 - 5, Update the
helper functions `_patch_httpx_client` and `_ctx` in `test_solr_support.py` so
their full signatures include explicit type hints for all parameters and return
types, matching the repo’s Python typing conventions. Also rename the affected
test at the current `test_solr_hybrid_search_*` location to follow the
`test_<function>_<scenario>` pattern used by surrounding tests, keeping the name
aligned with the `test_solr_hybrid_search_...` convention.

Source: Coding guidelines

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 2, 2026
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 2, 2026
@blublinsky

Copy link
Copy Markdown
Contributor Author

/retest

@vimalk78 vimalk78 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — OLS-3310 (jira mode, round 1)

Score: 84/100 · 4/4 Jira ACs pass · 3 code quality findings (1 must-fix, 1 should-fix, 1 nice-to-have)

Adherence

All four acceptance criteria from OLS-3310 are met:

  1. OCP docs retrieval only via tool — Solr hybrid retrieval is exposed exclusively through search_openshift_documentation StructuredTool.
  2. Direct-RAG code path removed — no solr_direct_rag config or code path exists.
  3. BYOK FAISS unchangedReferenceContent.indexes, IndexLoader, and the BYOK retrieval path are preserved.
  4. Unit tests updated — 491 new lines in test_solr_support.py, plus updates to config, summarizer, prompt, index loader, health, and environment tests.

Code quality issues below as inline comments.

Comment thread ols/src/rag_index/solr_support.py Outdated
doc.get("chunk_index", "?"),
1,
len(ordered),
len(family) if per_chunk_budget > 0 else 0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (must-fix): NameError when max_expansion_neighbors=0 and token_budget>0.

The debug log guards len(family) with per_chunk_budget > 0, but family is only assigned inside the if per_chunk_budget > 0 and cfg.max_expansion_neighbors > 0 block. When a user sets max_expansion_neighbors: 0 (valid — documented as "disables expansion") and the tool loop provides a token budget, the first iteration hits NameError: name 'family' is not defined. On later iterations it silently uses stale data from the previous doc.

Fix: either initialize family = [] before the loop, or change the guard to match the assignment condition:

len(family) if (per_chunk_budget > 0 and cfg.max_expansion_neighbors > 0) else 0,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99ba1e1 — the debug guard now matches the assignment condition (per_chunk_budget > 0 and cfg.max_expansion_neighbors > 0), and a unit test covers max_expansion_neighbors=0 with a non-zero token budget.

Comment thread ols/src/rag_index/solr_support.py Outdated
query_embedding = await asyncio.to_thread(_encode)
vector_str = "[" + ",".join(str(v) for v in query_embedding) + "]"
form = self._build_hybrid_form(cleaned=cleaned, vector_str=vector_str)
async with httpx.AsyncClient(timeout=cfg.hybrid_solr_timeout_s) as client:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should-fix: httpx.AsyncClient is created fresh per search() call — no connection pooling across invocations.

The PR description says "reused httpx.AsyncClient for connection pooling" but only intra-call reuse (hybrid POST + family GETs within one search) happens. Each tool invocation gets a new TCP connection to Solr. For production traffic this causes unnecessary connection churn.

Consider storing a persistent httpx.AsyncClient on SolrHybridSearch (with proper lifecycle via an aclose() hook or startup/shutdown events).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99ba1e1SolrHybridSearch now holds a persistent httpx.AsyncClient via _get_http_client(), with aclose() / close_http_client_sync() called on config reload. Unit test confirms the client is reused across search() calls. Verified locally with 5 concurrent /v1/query requests against RHOKP.

Comment thread pyproject.toml Outdated
"rank-bm25>=0.2.2", # For ToolsRAG sparse retrieval
"python-frontmatter>=1.1.0", # For parsing skill YAML frontmatter
"opentelemetry-api>=1.20.0", # For audit tracing
"opentelemetry-api>=1.30.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice-to-have: Unrelated dependency change — opentelemetry-api floor bumped from >=1.20.0 to >=1.30.0. Keeping changes scoped to the feature helps reviewers and bisection. Consider splitting this into its own commit or PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99ba1e1 — reverted opentelemetry-api floor back to >=1.20.0 and regenerated uv.lock.

@blublinsky

Copy link
Copy Markdown
Contributor Author

@vimalk78 All review items addressed in 99ba1e1:

  1. Must-fix (family NameError) — debug guard aligned with assignment condition; unit test added.
  2. Should-fix (connection pooling) — persistent httpx.AsyncClient on SolrHybridSearch with cleanup on config reload; reuse covered by unit test; concurrent sync queries verified locally.
  3. Nice-to-have (OTel bump) — opentelemetry-api reverted to >=1.20.0, uv.lock updated.

make verify (black, ruff, pylint, mypy) plus unit (1137) and integration (124) tests all pass locally. PTAL when you have a chance.

@blublinsky blublinsky force-pushed the okp-rag branch 2 times, most recently from 66aad53 to f31a029 Compare July 3, 2026 11:58
@vimalk78

vimalk78 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 3, 2026
@openshift-ci openshift-ci Bot removed the lgtm Indicates that a PR is ready to be merged. label Jul 3, 2026
@openshift-ci

openshift-ci Bot commented Jul 3, 2026

Copy link
Copy Markdown

@blublinsky: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/e2e-ols-cluster bf981cb link true /test e2e-ols-cluster
ci/prow/ols-evaluation bf981cb link true /test ols-evaluation

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@vimalk78

vimalk78 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

/lgtm

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm Indicates that a PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants