Skip to content

Adding AIQ shallow research eval script for RAG datasets#283

Draft
niyatisingal wants to merge 2 commits into
NVIDIA-AI-Blueprints:developfrom
niyatisingal:aiq-shallow-research-eval-script-for-rag-datasets
Draft

Adding AIQ shallow research eval script for RAG datasets#283
niyatisingal wants to merge 2 commits into
NVIDIA-AI-Blueprints:developfrom
niyatisingal:aiq-shallow-research-eval-script-for-rag-datasets

Conversation

@niyatisingal

@niyatisingal niyatisingal commented Jun 24, 2026

Copy link
Copy Markdown

Overview

Validation

  • I ran the relevant local checks or explained why they are not applicable.
  • I added or updated tests for behavior changes.
  • I updated documentation for user-facing or contributor-facing changes.
  • I confirmed this PR does not include secrets, credentials, or internal-only data.
  • I certify this contribution under the Developer Certificate of Origin (DCO) and signed my commits with git commit -s or an equivalent sign-off.

Where should reviewers start?

Related Issues

  • Relates to #

Summary by CodeRabbit

  • New Features

    • Added a new evaluation CLI for running RAG assessments against AI-Q datasets.
    • Added support for dataset validation, optional ingestion, query execution, metrics scoring, and saved output summaries.
    • Added documentation covering setup, dataset layout, command options, and result files.
  • Chores

    • Added project packaging and dependency metadata for the evaluation tool.

@copy-pr-bot

copy-pr-bot Bot commented Jun 24, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 0ce20146-6f59-474a-91b9-21fb11d43c69

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds a new scripts/eval/ package (aiq-rag-eval) containing a 1,200-line evaluation CLI (evaluate_aiq_rag.py), pyproject.toml with pinned dependencies, and a README.md. The script validates RAG dataset roots, optionally ingests documents into an AI-Q collection, runs per-question inference via async jobs or ATIF streaming, extracts contexts and token usage, and scores outputs with RAGAS NVIDIA metrics.

Changes

AI-Q RAG Evaluation CLI

Layer / File(s) Summary
Project setup and data models
scripts/eval/pyproject.toml, scripts/eval/evaluate_aiq_rag.py
Defines package metadata, pinned dependencies, module constants, regex patterns, judge-model env-var selection, and Pydantic models (IngestionMetrics, EvaluationMetrics, QueryTokenUsage, TokenUsageMetrics, RagEvaluationMetrics).
SSE/tool-output parsing helpers
scripts/eval/evaluate_aiq_rag.py
Implements context normalization, knowledge-retrieval tool-output parsing, tool-artifact context extraction, numeric token-field coercion, ATIF step/trajectory token-usage extraction, knowledge-context and answer extraction from ATIF steps, and ATIF SSE + AI-Q job SSE stream parsers with llm.end token aggregation.
AIQRagClient orchestration
scripts/eval/evaluate_aiq_rag.py
Implements AIQRagClient covering health checks, collection management, PDF page counting, batched ingestion with polling, async job submit/poll/artifact retrieval, ATIF workflow streaming, inference-mode routing, concurrent per-question execution with incremental save-lock writes, and full pipeline orchestration.
Dataset validation and RAGAS scoring
scripts/eval/evaluate_aiq_rag.py
Adds validate_dataset_roots for corpus/+train.json checks and evaluate_result that builds EvaluationDataset rows, conditionally selects context metrics, runs ragas.evaluate via ChatNVIDIA+LangchainLLMWrapper, and aggregates token-usage fields.
CLI entrypoint and output writing
scripts/eval/evaluate_aiq_rag.py
Adds main() with full argument parsing, NVIDIA_API_KEY enforcement, per-dataset pipeline execution, and writing of summary/results/metrics JSON files plus an optional combined summary under --output-dir.
README
scripts/eval/README.md
Documents prerequisites, uv-based installation, dataset layout, measured metrics, example invocations, CLI flags table, output artifact layout, and inference-mode operational notes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Title check ❌ Error The title is related to the change, but it does not use the required Conventional Commits format. Rename it to a Conventional Commits title like feat(evaluation): add AIQ shallow research eval script for RAG datasets.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR follows the template headings, but the Overview, review-start, and related-issues sections are still empty placeholders, so the intent is unclear. Fill in the overview, exact validation commands, reviewer starting point, and any related issue links; otherwise the description remains too generic.
✅ Passed checks (2 passed)
Check name Status Explanation
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

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

@niyatisingal niyatisingal marked this pull request as draft June 24, 2026 05:08

@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: 9

🤖 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 `@scripts/eval/evaluate_aiq_rag.py`:
- Around line 849-850: The current skip path in run_pipeline() returns too
early, which stops create_eval_dict() and ties inference-data generation to
RAGAS scoring. Update run_pipeline() and the related scoring flow so
skip_evaluation only disables the scoring step, while the inference-data
generation path still runs; use a separate scoring-only branch/flag in the
evaluate_aiq_rag.py pipeline. Also, where judge credentials are missing, make
the missing-secret path in the RAGAS/judge setup warn and skip scoring
gracefully instead of crashing, using the existing run_pipeline(),
create_eval_dict(), and skip_evaluation logic as the main points to adjust.
- Around line 564-567: Reject duplicate corpus basenames before building the
upload payload in the file upload flow that populates handles and files. Since
the code uses os.path.basename(file_path) and downstream reconciliation also
matches on basenames, add a pre-upload check over file_paths to detect duplicate
basenames and fail fast with a clear error before any open(file_path, "rb") or
files.append(...) work happens. Apply the same guard anywhere the corpus is
prepared for upload so recursive inputs with matching leaf filenames cannot be
skipped or misidentified.
- Around line 754-760: Validate the loaded eval rows in load_eval_data before
any workers start: after json.load and the existing list check, verify each item
is a dict with non-empty question and answer fields, and fail fast with a clear
error if any row is malformed or the dataset is empty. Use load_eval_data as the
single entry point for this validation so downstream worker code and RAGAS
scoring never receive invalid train.json rows.
- Around line 1026-1061: Add CLI validation in the argument parsing flow so
invalid numeric values are rejected before they reach execution. In the
parser/entrypoint around the argparse setup, enforce positive integers for
`--thread`, `--batch-size`, `--timeout`, and `--ingestion-timeout` in
`evaluate_aiq_rag.py`, using the existing argument names and defaults as the
main touchpoints. Make `--thread` and `--batch-size` require values greater than
0, and prevent negative values for the timeout options so downstream uses like
`ThreadPoolExecutor` and ingestion batching cannot fail at runtime.
- Around line 893-907: The context-metric gate in the evaluation flow is too
permissive because has_contexts uses any(...) and turns on ContextRelevance and
ResponseGroundedness for the whole EvaluationDataset even when some samples have
empty generated_contexts. Update the logic around evaluate_aiq_rag.py’s dataset
construction to either require every sample to have retrieved contexts before
adding those metrics, or build a filtered subset containing only samples with
generated_contexts and run the context metrics on that subset. Keep
AnswerAccuracy on the full set, and ensure SingleTurnSample creation and the
metrics list stay aligned with the chosen subset.
- Around line 1105-1110: The dataset loop in evaluate_aiq_rag.py does not guard
against duplicate run labels, so different paths with the same basename can
collide on output_dir, collection_name, and all_summaries. Add validation in the
dataset-processing loop before creating outputs to detect repeated run_label
values (or collisions after deriving collection_name) and fail fast with a clear
error instead of proceeding. Use the existing args.dataset_paths iteration and
the run_label/output_dir/all_summaries logic as the place to enforce uniqueness.
- Around line 74-77: The default judge model in the evaluate_aiq_rag.py module
is set to a different model ID than the documented public default, so unset
environment users may resolve the wrong target. Update the _DEFAULT_JUDGE_MODEL
constant used by JUDGE_MODEL to match the documented public model ID, and keep
the RAG_EVAL_JUDGE_MODEL override behavior unchanged so the environment variable
still takes precedence.

In `@scripts/eval/README.md`:
- Around line 115-116: Clarify the flags table entry for --skip-evaluation in
README so it explicitly says it skips RAGAS scoring only, while inference/output
generation still runs. Update the description alongside --skip-ingestion in the
flags table to remove the ambiguous “inference-only” wording and make the
partial-run behavior clear to operators.
- Around line 53-60: The dataset layout section in README should explicitly
document the required per-row schema for train.json, not just that it is a UTF-8
JSON array. Update the description near evaluate_aiq_rag.py and
validate_dataset_roots() to state that each array item must be an object with
question and answer keys, and make clear that missing either key will cause
evaluation to fail later.
🪄 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: ASSERTIVE

Plan: Enterprise

Run ID: 5987f490-c2f7-48c5-a695-233275248d78

📥 Commits

Reviewing files that changed from the base of the PR and between 3f02d59 and 767ab33.

⛔ Files ignored due to path filters (1)
  • scripts/eval/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • scripts/eval/README.md
  • scripts/eval/evaluate_aiq_rag.py
  • scripts/eval/pyproject.toml
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**

⚙️ CodeRabbit configuration file

**:

AI-Q Agent Guidance

Repository-global instructions for coding agents and for humans reviewing
agent-authored changes. These rules apply to every task in this repository.
Task-specific runbooks live in .agents/skills/ — load the
relevant skill before starting a workflow it covers.

Project overview

AI-Q is an NVIDIA AI Blueprint: an enterprise research agent built on the
NeMo Agent Toolkit (NAT). The deployed product is a research blueprint, not
a general skill runtime. New retrieval sources and tools are NAT functions;
agent behavior is driven by workflow YAML, Jinja2 prompts, and a data-source
registry — not by hard-coded logic.

Primary boundaries:

  • Backend Python package: src/aiq_agent/.
  • Data-source and tool packages: sources/ (each is its own package).
  • Frontends and tooling: frontends/ (web UI in frontends/ui/, eval harnesses
    in frontends/benchmarks/).
  • Configs, deployment, docs: configs/, deploy/, docs/.

Stay inside this repository. If your workspace also contains adjacent repos
(for example a sibling NeMo-Relay checkout), do not edit them as part of an AI-Q
change. Treat sources/* as independent packages: prefer the smallest change
scoped to the package you are touching.

Repository structure

Path Purpose
src/aiq_agent/ Backend agent, FastAPI extensions, auth, observability, knowledge
sources/ Data-source / tool packages (e.g. tavily_web_search, google_scholar_paper_search)
configs/ Workflow YAML configs (e.g. config_cli_default.yml)
frontends/ui/ Next.js / React / TypeScript / Tailwind / KUI web UI
frontends/benchmarks/ Eval harnesses: freshqa, deepsearch_qa, deepresearch_bench
deploy/ Docker Compose and Helm/Kubernetes assets; deploy/.env for secrets
docs/source/ ...

Files:

  • scripts/eval/pyproject.toml
  • scripts/eval/README.md
  • scripts/eval/evaluate_aiq_rag.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run ruff check and ruff format validation for Python code changes

**/*.py: Python code must be linted and formatted with Ruff using line length 120, target Python 3.11, rule sets E,F,W,I,PL,UP, and isort force-single-line configuration
Never commit secrets, tokens, or environment-specific hostnames; use environment variables and SecretStr instead, resolving API keys at runtime
Never print or log secret values, including in tool output or error messages
Missing-secret paths must degrade gracefully (stub/skip), not crash or leak
Do not hand-reformat unrelated code when making changes; match the existing import and formatting style

Files:

  • scripts/eval/evaluate_aiq_rag.py
🪛 ast-grep (0.44.0)
scripts/eval/evaluate_aiq_rag.py

[warning] 471-471: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.get(f"{self.server_url}{path}", timeout=30)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)


[warning] 646-651: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.post(
url,
json=body,
headers=headers,
timeout=self._workflow_request_timeout(),
)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)


[warning] 716-716: Request-controlled URL passed to requests; validate against an allowlist to prevent SSRF.
Context: requests.get(url, timeout=self._workflow_request_timeout(), stream=True)
Note: [CWE-918] Server-Side Request Forgery (SSRF).

(ssrf-requests)


[warning] 523-523: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(file_path, "rb")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 564-564: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(file_path, "rb")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 750-750: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(self._evaluation_data_path(), "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 754-754: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(self.eval_data_path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 814-814: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(output_path, "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 1194-1194: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(summary_path, "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 1198-1198: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(results_path, "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 1202-1202: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(metrics_path, "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[warning] 1207-1207: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(combined_path, "w", encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)


[info] 716-716: no timeout was given on call to external resource
Context: requests.get(url, timeout=self._workflow_request_timeout(), stream=True)
Note: [CWE-1088] Synchronous Access of Remote Resource without Timeout.

(requests-timeout)

🔇 Additional comments (1)
scripts/eval/pyproject.toml (1)

1-17: LGTM!

Comment thread scripts/eval/evaluate_aiq_shallow_research.py Outdated
Comment on lines +564 to +567
for file_path in file_paths:
handle = open(file_path, "rb")
handles.append(handle)
files.append(("files", (os.path.basename(file_path), handle, "application/octet-stream")))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject duplicate corpus basenames before upload.

Uploads strip paths to os.path.basename(file_path), and ingestion reconciliation also compares only basenames. A recursive corpus containing a/doc.pdf and b/doc.pdf can skip or misidentify one document.

Proposed guard
 def collect_files_to_upload(self, ingested_documents: list[str]) -> list[str]:
     ingested = {os.path.basename(name) for name in ingested_documents}
+    seen_basenames: dict[str, str] = {}
     files_to_upload: list[str] = []
     for root, _, files in os.walk(self.dataset_path):
         for filename in files:
+            file_path = os.path.join(root, filename)
+            if filename in seen_basenames:
+                raise ValueError(
+                    f"Duplicate corpus filename {filename!r}: {seen_basenames[filename]} and {file_path}. "
+                    "Use unique basenames because the knowledge API stores file_name without relative paths."
+                )
+            seen_basenames[filename] = file_path
             if filename not in ingested:
-                files_to_upload.append(os.path.join(root, filename))
+                files_to_upload.append(file_path)
     return files_to_upload

Also applies to: 596-603

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 564-564: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(file_path, "rb")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 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 `@scripts/eval/evaluate_aiq_rag.py` around lines 564 - 567, Reject duplicate
corpus basenames before building the upload payload in the file upload flow that
populates handles and files. Since the code uses os.path.basename(file_path) and
downstream reconciliation also matches on basenames, add a pre-upload check over
file_paths to detect duplicate basenames and fail fast with a clear error before
any open(file_path, "rb") or files.append(...) work happens. Apply the same
guard anywhere the corpus is prepared for upload so recursive inputs with
matching leaf filenames cannot be skipped or misidentified.

Comment on lines +754 to +760
def load_eval_data(self) -> list[dict[str, Any]]:
with open(self.eval_data_path, encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, list):
print("Error: train.json must be a JSON array of objects with question/answer fields.")
sys.exit(1)
return data

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate train.json row shape before launching workers.

Later code assumes each row is a dict with a non-empty question and answer; malformed or empty datasets currently fail as worker errors or RAGAS scoring failures after inference has started.

Proposed validation
         if not isinstance(data, list):
             print("Error: train.json must be a JSON array of objects with question/answer fields.")
             sys.exit(1)
+        if not data:
+            print("Error: train.json must contain at least one evaluation row.")
+            sys.exit(1)
+        for index, row in enumerate(data):
+            if (
+                not isinstance(row, dict)
+                or not str(row.get("question", "")).strip()
+                or row.get("answer") in (None, "")
+            ):
+                print(f"Error: train.json row {index} must include non-empty question and answer fields.")
+                sys.exit(1)
         return data
📝 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
def load_eval_data(self) -> list[dict[str, Any]]:
with open(self.eval_data_path, encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, list):
print("Error: train.json must be a JSON array of objects with question/answer fields.")
sys.exit(1)
return data
def load_eval_data(self) -> list[dict[str, Any]]:
with open(self.eval_data_path, encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, list):
print("Error: train.json must be a JSON array of objects with question/answer fields.")
sys.exit(1)
if not data:
print("Error: train.json must contain at least one evaluation row.")
sys.exit(1)
for index, row in enumerate(data):
if (
not isinstance(row, dict)
or not str(row.get("question", "")).strip()
or row.get("answer") in (None, "")
):
print(f"Error: train.json row {index} must include non-empty question and answer fields.")
sys.exit(1)
return data
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 754-754: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(self.eval_data_path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 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 `@scripts/eval/evaluate_aiq_rag.py` around lines 754 - 760, Validate the loaded
eval rows in load_eval_data before any workers start: after json.load and the
existing list check, verify each item is a dict with non-empty question and
answer fields, and fail fast with a clear error if any row is malformed or the
dataset is empty. Use load_eval_data as the single entry point for this
validation so downstream worker code and RAGAS scoring never receive invalid
train.json rows.

Comment on lines +849 to +850
if self.skip_evaluation:
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 | 🏗️ Heavy lift

Separate RAGAS scoring skips from inference-data generation.

--skip-evaluation is documented and suggested as skipping scoring, but run_pipeline() returns before create_eval_dict(). Users without NVIDIA_API_KEY either crash or skip the whole inference-data generation path. Introduce a scoring-only branch/flag and let missing judge credentials warn and skip scoring instead. As per coding guidelines, “Missing-secret paths must degrade gracefully (stub/skip), not crash or leak.”

Also applies to: 1033-1036, 1063-1067

🤖 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 `@scripts/eval/evaluate_aiq_rag.py` around lines 849 - 850, The current skip
path in run_pipeline() returns too early, which stops create_eval_dict() and
ties inference-data generation to RAGAS scoring. Update run_pipeline() and the
related scoring flow so skip_evaluation only disables the scoring step, while
the inference-data generation path still runs; use a separate scoring-only
branch/flag in the evaluate_aiq_rag.py pipeline. Also, where judge credentials
are missing, make the missing-secret path in the RAGAS/judge setup warn and skip
scoring gracefully instead of crashing, using the existing run_pipeline(),
create_eval_dict(), and skip_evaluation logic as the main points to adjust.

Source: Coding guidelines

Comment on lines +893 to +907
has_contexts = any(sample.get("generated_contexts") for sample in eval_data)
if has_contexts:
eval_dataset = EvaluationDataset(
[
SingleTurnSample(
user_input=sample["question"],
reference=sample["answer"],
response=sample["generated_answer"],
reference_contexts=sample.get("contexts", []),
retrieved_contexts=sample["generated_contexts"],
)
for sample in eval_data
]
)
metrics = [AnswerAccuracy(), ContextRelevance(), ResponseGroundedness()]

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

Do not enable context metrics for partially missing retrieved contexts.

any(...) enables context metrics for the entire dataset when only one sample has contexts; rows with empty generated_contexts violate the documented metric precondition and can produce failures or NaNs. Evaluate context metrics on the filtered subset, or require all samples to have contexts before adding those metrics.

Minimal safe gate
-    has_contexts = any(sample.get("generated_contexts") for sample in eval_data)
+    has_contexts = bool(eval_data) and all(sample.get("generated_contexts") for sample in eval_data)
📝 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
has_contexts = any(sample.get("generated_contexts") for sample in eval_data)
if has_contexts:
eval_dataset = EvaluationDataset(
[
SingleTurnSample(
user_input=sample["question"],
reference=sample["answer"],
response=sample["generated_answer"],
reference_contexts=sample.get("contexts", []),
retrieved_contexts=sample["generated_contexts"],
)
for sample in eval_data
]
)
metrics = [AnswerAccuracy(), ContextRelevance(), ResponseGroundedness()]
has_contexts = bool(eval_data) and all(sample.get("generated_contexts") for sample in eval_data)
if has_contexts:
eval_dataset = EvaluationDataset(
[
SingleTurnSample(
user_input=sample["question"],
reference=sample["answer"],
response=sample["generated_answer"],
reference_contexts=sample.get("contexts", []),
retrieved_contexts=sample["generated_contexts"],
)
for sample in eval_data
]
)
metrics = [AnswerAccuracy(), ContextRelevance(), ResponseGroundedness()]
🤖 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 `@scripts/eval/evaluate_aiq_rag.py` around lines 893 - 907, The context-metric
gate in the evaluation flow is too permissive because has_contexts uses any(...)
and turns on ContextRelevance and ResponseGroundedness for the whole
EvaluationDataset even when some samples have empty generated_contexts. Update
the logic around evaluate_aiq_rag.py’s dataset construction to either require
every sample to have retrieved contexts before adding those metrics, or build a
filtered subset containing only samples with generated_contexts and run the
context metrics on that subset. Keep AnswerAccuracy on the full set, and ensure
SingleTurnSample creation and the metrics list stay aligned with the chosen
subset.

Comment on lines +1026 to +1061
"--thread",
default=DEFAULT_MAX_WORKERS,
type=int,
help="Parallel shallow-research jobs (default: 1).",
)
parser.add_argument("--output-dir", default="results", help="Directory for evaluation outputs.")
parser.add_argument("--batch-size", default=DEFAULT_BATCH_SIZE, type=int, help="Ingestion upload batch size.")
parser.add_argument("--collection", default=None, help="Collection name (default: dataset directory basename).")
parser.add_argument("--skip-ingestion", action="store_true", help="Skip corpus ingestion.")
parser.add_argument("--skip-evaluation", action="store_true", help="Skip RAGAS scoring.")
parser.add_argument("--force-ingestion", action="store_true", help="Delete and recreate the collection.")
parser.add_argument(
"--inference-mode",
choices=("async", "atif"),
default=DEFAULT_INFERENCE_MODE,
help=(
"Inference API: 'async' uses /v1/jobs/async/submit (default, faster polling); "
"'atif' uses POST /v1/workflow/atif (slower, full ATIF trajectory)."
),
)
parser.add_argument(
"--timeout",
default=DEFAULT_TIMEOUT,
type=int,
help=(
"Per-query timeout in seconds (default: 1800). For async jobs this is the poll deadline; "
"for ATIF it is the max gap between stream chunks. Use 0 for no read timeout on ATIF."
),
)
parser.add_argument(
"--ingestion-timeout",
default=DEFAULT_INGESTION_TIMEOUT,
type=int,
help=("Max seconds to wait per ingestion batch (default: 3600). Scales with batch size up to this limit."),
)
args = parser.parse_args()

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 | 🟠 Major | ⚡ Quick win

Validate numeric CLI bounds before use.

--thread 0 raises in ThreadPoolExecutor, --batch-size 0 divides by zero, and negative timeouts produce immediate or inconsistent timeout behavior.

Proposed validation
     args = parser.parse_args()
+
+    if args.thread < 1:
+        parser.error("--thread must be >= 1")
+    if args.batch_size < 1:
+        parser.error("--batch-size must be >= 1")
+    if args.timeout < 0:
+        parser.error("--timeout must be >= 0")
+    if args.ingestion_timeout < 1:
+        parser.error("--ingestion-timeout must be >= 1")
📝 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
"--thread",
default=DEFAULT_MAX_WORKERS,
type=int,
help="Parallel shallow-research jobs (default: 1).",
)
parser.add_argument("--output-dir", default="results", help="Directory for evaluation outputs.")
parser.add_argument("--batch-size", default=DEFAULT_BATCH_SIZE, type=int, help="Ingestion upload batch size.")
parser.add_argument("--collection", default=None, help="Collection name (default: dataset directory basename).")
parser.add_argument("--skip-ingestion", action="store_true", help="Skip corpus ingestion.")
parser.add_argument("--skip-evaluation", action="store_true", help="Skip RAGAS scoring.")
parser.add_argument("--force-ingestion", action="store_true", help="Delete and recreate the collection.")
parser.add_argument(
"--inference-mode",
choices=("async", "atif"),
default=DEFAULT_INFERENCE_MODE,
help=(
"Inference API: 'async' uses /v1/jobs/async/submit (default, faster polling); "
"'atif' uses POST /v1/workflow/atif (slower, full ATIF trajectory)."
),
)
parser.add_argument(
"--timeout",
default=DEFAULT_TIMEOUT,
type=int,
help=(
"Per-query timeout in seconds (default: 1800). For async jobs this is the poll deadline; "
"for ATIF it is the max gap between stream chunks. Use 0 for no read timeout on ATIF."
),
)
parser.add_argument(
"--ingestion-timeout",
default=DEFAULT_INGESTION_TIMEOUT,
type=int,
help=("Max seconds to wait per ingestion batch (default: 3600). Scales with batch size up to this limit."),
)
args = parser.parse_args()
"--thread",
default=DEFAULT_MAX_WORKERS,
type=int,
help="Parallel shallow-research jobs (default: 1).",
)
parser.add_argument("--output-dir", default="results", help="Directory for evaluation outputs.")
parser.add_argument("--batch-size", default=DEFAULT_BATCH_SIZE, type=int, help="Ingestion upload batch size.")
parser.add_argument("--collection", default=None, help="Collection name (default: dataset directory basename).")
parser.add_argument("--skip-ingestion", action="store_true", help="Skip corpus ingestion.")
parser.add_argument("--skip-evaluation", action="store_true", help="Skip RAGAS scoring.")
parser.add_argument("--force-ingestion", action="store_true", help="Delete and recreate the collection.")
parser.add_argument(
"--inference-mode",
choices=("async", "atif"),
default=DEFAULT_INFERENCE_MODE,
help=(
"Inference API: 'async' uses /v1/jobs/async/submit (default, faster polling); "
"'atif' uses POST /v1/workflow/atif (slower, full ATIF trajectory)."
),
)
parser.add_argument(
"--timeout",
default=DEFAULT_TIMEOUT,
type=int,
help=(
"Per-query timeout in seconds (default: 1800). For async jobs this is the poll deadline; "
"for ATIF it is the max gap between stream chunks. Use 0 for no read timeout on ATIF."
),
)
parser.add_argument(
"--ingestion-timeout",
default=DEFAULT_INGESTION_TIMEOUT,
type=int,
help=("Max seconds to wait per ingestion batch (default: 3600). Scales with batch size up to this limit."),
)
args = parser.parse_args()
if args.thread < 1:
parser.error("--thread must be >= 1")
if args.batch_size < 1:
parser.error("--batch-size must be >= 1")
if args.timeout < 0:
parser.error("--timeout must be >= 0")
if args.ingestion_timeout < 1:
parser.error("--ingestion-timeout must be >= 1")
🤖 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 `@scripts/eval/evaluate_aiq_rag.py` around lines 1026 - 1061, Add CLI
validation in the argument parsing flow so invalid numeric values are rejected
before they reach execution. In the parser/entrypoint around the argparse setup,
enforce positive integers for `--thread`, `--batch-size`, `--timeout`, and
`--ingestion-timeout` in `evaluate_aiq_rag.py`, using the existing argument
names and defaults as the main touchpoints. Make `--thread` and `--batch-size`
require values greater than 0, and prevent negative values for the timeout
options so downstream uses like `ThreadPoolExecutor` and ingestion batching
cannot fail at runtime.

Comment on lines +1105 to +1110
for dataset_root in args.dataset_paths:
dataset_root = os.path.abspath(dataset_root)
run_label = os.path.basename(dataset_root.rstrip(os.sep)) or "dataset"
collection_name = args.collection or run_label
output_dir = os.path.join(args.output_dir, run_label)
os.makedirs(output_dir, exist_ok=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject duplicate dataset run labels before writing outputs.

run_label is only the dataset basename, so two paths ending in the same directory name share output_dir, collection name, and all_summaries key; the second run can overwrite or mix results.

Proposed validation
     validate_dataset_roots(list(args.dataset_paths))
+    run_labels = [os.path.basename(os.path.abspath(path).rstrip(os.sep)) or "dataset" for path in args.dataset_paths]
+    duplicate_labels = sorted({label for label in run_labels if run_labels.count(label) > 1})
+    if duplicate_labels:
+        parser.error(f"Dataset basenames must be unique for output isolation: {duplicate_labels}")
     data_sources = _parse_data_sources(args.data_sources)
📝 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
for dataset_root in args.dataset_paths:
dataset_root = os.path.abspath(dataset_root)
run_label = os.path.basename(dataset_root.rstrip(os.sep)) or "dataset"
collection_name = args.collection or run_label
output_dir = os.path.join(args.output_dir, run_label)
os.makedirs(output_dir, exist_ok=True)
validate_dataset_roots(list(args.dataset_paths))
run_labels = [os.path.basename(os.path.abspath(path).rstrip(os.sep)) or "dataset" for path in args.dataset_paths]
duplicate_labels = sorted({label for label in run_labels if run_labels.count(label) > 1})
if duplicate_labels:
parser.error(f"Dataset basenames must be unique for output isolation: {duplicate_labels}")
for dataset_root in args.dataset_paths:
dataset_root = os.path.abspath(dataset_root)
run_label = os.path.basename(dataset_root.rstrip(os.sep)) or "dataset"
collection_name = args.collection or run_label
output_dir = os.path.join(args.output_dir, run_label)
os.makedirs(output_dir, exist_ok=True)
🤖 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 `@scripts/eval/evaluate_aiq_rag.py` around lines 1105 - 1110, The dataset loop
in evaluate_aiq_rag.py does not guard against duplicate run labels, so different
paths with the same basename can collide on output_dir, collection_name, and
all_summaries. Add validation in the dataset-processing loop before creating
outputs to detect repeated run_label values (or collisions after deriving
collection_name) and fail fast with a clear error instead of proceeding. Use the
existing args.dataset_paths iteration and the run_label/output_dir/all_summaries
logic as the place to enforce uniqueness.

Comment thread scripts/eval/README.md
Comment on lines +53 to +60
train.json ← required; eval questions and answers
```

`evaluate_aiq_rag.py` validates each root with `validate_dataset_roots()`:

- The path must be a directory.
- `corpus/` must exist (files are discovered recursively).
- `train.json` must be a UTF-8 JSON array.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document required train.json row keys explicitly (question, answer).

The README says train.json is required, but not the exact per-row key contract. The evaluator expects objects with question/answer; missing keys will fail later. Add this explicitly in the dataset layout section.

🤖 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 `@scripts/eval/README.md` around lines 53 - 60, The dataset layout section in
README should explicitly document the required per-row schema for train.json,
not just that it is a UTF-8 JSON array. Update the description near
evaluate_aiq_rag.py and validate_dataset_roots() to state that each array item
must be an object with question and answer keys, and make clear that missing
either key will cause evaluation to fail later.

Comment thread scripts/eval/README.md
Comment on lines +115 to +116
| `--skip-ingestion` / `--skip-evaluation` | Ingest-only or inference-only partial runs |
| `--force-ingestion` | Delete and recreate the collection before upload |

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

Clarify --skip-evaluation semantics in the flags table.

“inference-only” is ambiguous here. --skip-evaluation skips RAGAS scoring; inference/output generation still run. Rewording this avoids operator confusion in partial runs.

🤖 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 `@scripts/eval/README.md` around lines 115 - 116, Clarify the flags table entry
for --skip-evaluation in README so it explicitly says it skips RAGAS scoring
only, while inference/output generation still runs. Update the description
alongside --skip-ingestion in the flags table to remove the ambiguous
“inference-only” wording and make the partial-run behavior clear to operators.

@niyatisingal niyatisingal force-pushed the aiq-shallow-research-eval-script-for-rag-datasets branch from 767ab33 to c411aa3 Compare June 24, 2026 05:19
def run_query(row: dict[str, Any]) -> dict[str, Any] | None:
question = row.get("question", "")
eval_query = (
"Answer using only the documents in the knowledge base. "

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a mistake it will direct agent to answer only using the kb base even when web search is enabled.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done

default=DEFAULT_AGENT_TYPE,
help=f"Async agent type (default: {DEFAULT_AGENT_TYPE}).",
)
parser.add_argument(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Make DEFAULT_DATA_SOURCES sources as empty, so that if nothing is passed, agent decides it. This is more intuitive.

@niyatisingal niyatisingal Jun 28, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added code to pickup all configured data sources if not explicitly provided

@niyatisingal niyatisingal force-pushed the aiq-shallow-research-eval-script-for-rag-datasets branch from c411aa3 to c7be568 Compare June 28, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants