Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ env/
.env
.env.local
.env.*.local

# Monocle traces
.monocle/
# Logs
*.log
logs/
Expand Down
129 changes: 129 additions & 0 deletions configs/config_openai_cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# This is the default configuration for the CLI mode.
# It has the following features:
# - Human-in-the-loop clarification and plan approval before deep research starts
# - Web search and Paper search tools by default
# - There is no knowledge retrieval

general:
telemetry:
logging:
console:
_type: console
level: INFO
# tracing:
# langsmith: # Optional: LangSmith tracing - requires langsmith API key. Set using `export LANGSMITH_API_KEY=<your-langsmith-api-key>`
# _type: langsmith
# project: nvidia-aiq

llms:
nemotron_llm_intent:
_type: openai
model_name: gpt-4o
api_key: ${OPENAI_API_KEY}
temperature: 0.5
top_p: 0.9
max_tokens: 4096
num_retries: 5

nemotron_super_llm:
_type: openai
model_name: gpt-4o
api_key: ${OPENAI_API_KEY}
temperature: 0.7
top_p: 0.7
max_tokens: 16384
num_retries: 5

functions:
# =========================================================================
# Data Source Registry
# =========================================================================
# Central registry that controls:
# 1. UI toggles — each source appears as an on/off switch in the frontend
# 2. Per-message filtering — users can select active sources per request
# 3. Tool auto-inheritance — agents with no explicit `tools` list receive
# every tool listed here (use `exclude_tools` on agents to specialize)
#
# Source entry fields:
# id, name, description, tools, requires_auth (default: false),
# default_enabled (default: true)
#
# See docs/source/customization/tools-and-sources.md for full details.
# =========================================================================
data_sources:
_type: data_source_registry
sources:
- id: web_search
name: "Web Search"
description: "Search the web for real-time information."
tools:
- web_search_tool
- advanced_web_search_tool
# - id: paper_search
# name: "Academic Papers"
# description: "Search academic papers and scientific publications."
# tools:
# - paper_search_tool

web_search_tool:
_type: exa_web_search
max_results: 5
max_content_length: 1000

advanced_web_search_tool:
_type: exa_web_search
max_results: 2

# Paper Search (optional - requires SERPER_API_KEY)
# Uncomment the block below and set SERPER_API_KEY to enable academic paper search.
# paper_search_tool:
# _type: paper_search
# max_results: 5
# serper_api_key: ${SERPER_API_KEY}

# =========================================================================
# Agents — inherit all registry tools; use exclude_tools to specialize
# =========================================================================
intent_classifier:
_type: intent_classifier
llm: nemotron_llm_intent
# tools: omitted -> inherits all from data_source_registry
# exclude_tools: []
# llm_timeout: 90 # optional; seconds for intent LLM call (default 90)

clarifier_agent:
_type: clarifier_agent
llm: nemotron_super_llm
planner_llm: nemotron_super_llm
# tools: omitted -> inherits all from data_source_registry
# exclude_tools: []
max_turns: 3
enable_plan_approval: true
log_response_max_chars: 2000
verbose: true

shallow_research_agent:
_type: shallow_research_agent
llm: nemotron_super_llm
# tools: omitted -> inherits all from data_source_registry
exclude_tools:
- advanced_web_search_tool
max_llm_turns: 10
max_tool_iterations: 5

deep_research_agent:
_type: deep_research_agent
enable_citation_verification: true
orchestrator_llm: nemotron_super_llm
source_router_llm: nemotron_super_llm
researcher_llm: nemotron_super_llm
planner_llm: nemotron_super_llm
writer_llm: nemotron_super_llm
exclude_tools:
- web_search_tool

workflow:
_type: chat_deepresearcher_agent
enable_escalation: true
enable_clarifier: true
checkpoint_db: ${AIQ_CHECKPOINT_DB:-./checkpoints.db}
73 changes: 73 additions & 0 deletions tests/monocle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AI-Q behavioural tests (Monocle Test Tools)

Trace-based tests that lock in the AI-Q research agent's behaviour. AI-Q runs on
the NeMo Agent Toolkit (NAT); Monocle records each run as a structured trace --
the agent invocation, every tool call, and timings -- and each test asserts
against that trace: which agent ran, which tools it called, what it was asked,
what it produced, and its duration cost. A later prompt, model, or tool change
that regresses the behaviour fails here.

## Layout

- `test_nvidiaaiq.py` — the suite: two offline tests + one live test
- `conftest.py` — Monocle setup, `.env` loading, and `run_nvidiaaiq()`
- `traces/` — recorded good-trace fixtures the offline tests replay
- `requirements.txt` — dependencies

## Tests

| Test | Scenario | What it shows |
|---|---|---|
| `test_capabilities_intro` | "Hi, what can you do?" | direct answer, a `does_not_call_tool` negative, budget |
| `test_nvda_stock_lookup` | Current NVIDIA stock price | `web_search_tool` call, input/output, budget |
| `test_nvda_stock_lookup_live` | The stock question, run live | live run, web-search path, structure + budget |

The offline tests replay recorded traces with duration budgets measured from
those runs (rounded up with headroom). The live test drives the agent
end-to-end and asserts structure and budget only, since the output legitimately
varies run to run.

Note on budgets: NAT traces do not carry token metadata -- the `inference.*`
spans record only finish reasons, no token counts -- so `under_token_limit(...)`
would sum 0 and always pass. This suite omits it and budgets on
`under_duration(..., span_type="workflow")` instead. The agent name asserted
(`LangGraph`) is the real `entity.1.name` on the `agentic.invocation` spans (NAT
runs its research agents on a LangGraph runtime); the web tool is
`web_search_tool`.

## Run

```bash
pip install -r requirements.txt
pytest tests/monocle/ -k "not live" # offline, no network, no keys
```

The live test is opt-in (`RUN_LIVE_NVIDIAAIQ=1`) and skipped by default.
NAT binds its async singletons to the first event loop, so only one in-process
live run works per process, and NAT leaves non-daemon threads that keep the
interpreter from exiting cleanly. So run the live test in its own process, with
keys in `deploy/.env` (`OPENAI_API_KEY` plus a search key, `EXA_API_KEY`/`SERPER_API_KEY`):
Comment on lines +45 to +49

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

Document the key required by the configured live workflow.

configs/config_openai_cli.yml enables _type: exa_web_search; SERPER_API_KEY is only associated with the commented paper-search configuration. Listing it as an alternative can lead users to run the live test without the required Exa credential. Document EXA_API_KEY only, or change the workflow to use Serper.

As per path instructions, config changes must preserve documentation parity.

🤖 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/monocle/README.md` around lines 45 - 49, Update the live-test
documentation near the `RUN_LIVE_NVIDIAAIQ` instructions to require
`EXA_API_KEY` only, matching the active `exa_web_search` configuration in
`configs/config_openai_cli.yml`; remove `SERPER_API_KEY` as an alternative, or
instead change the workflow and maintain documentation parity for the configured
search provider.

Source: Path instructions


```bash
RUN_LIVE_NVIDIAAIQ=1 pytest tests/monocle/ -k nvda_stock_lookup_live -s
```

They drive the workflow via `configs/config_openai_cli.yml` with an
auto-approving `user_input_callback`, so a human-in-the-loop clarification or
plan-approval interrupt is answered automatically instead of blocking the run.

## Add your own test

1. Run AI-Q under Monocle and capture a trace of a run you're happy with
(Monocle writes trace JSON to `.monocle/` by default).
2. Move it into `traces/` and load it with
`monocle_trace_asserter.with_trace_source("file", trace_path=path)`.
3. Assert with the fluent API — `called_agent(...)`, `called_tool(...)`,
`contains_input/output(...)`, `under_duration(..., span_type="workflow")` —
then add it alongside the others.

## Evaluations (optional)

Each test carries a commented-out `check_eval("hallucination", ...)` chain.
Monocle can run evaluation checks against a trace; set `OKAHU_API_KEY` and
uncomment to enable.
57 changes: 57 additions & 0 deletions tests/monocle/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Pytest scaffold for the AI-Q (NeMo Agent Toolkit) Monocle test suite.

Enables Monocle tracing, loads the repo `deploy/.env`, and exposes
``run_nvidiaaiq`` -- the single entry the live tests use to drive the agent
under instrumentation and return its final answer text.
"""
import asyncio
import os
import uuid
from pathlib import Path

from dotenv import load_dotenv
from monocle_apptrace import setup_monocle_telemetry

HERE = Path(__file__).resolve().parent
TRACES = HERE / "traces"
REPO_ROOT = HERE.parent.parent

# This repo runs pytest with `--import-mode=importlib`, so a test's directory is
# not auto-added to sys.path and `import conftest` would load a *second* copy of
# this module -- double-instrumenting the tracer. Guard the one-time setup.
if not os.environ.get("_NVIDIAAIQ_MONOCLE_READY"):
setup_monocle_telemetry(workflow_name="nvidia-aiq")
load_dotenv(REPO_ROOT / "deploy" / ".env")
os.environ["_NVIDIAAIQ_MONOCLE_READY"] = "1"

# The CLI-mode workflow used for live runs. It exposes the web/paper search
# tools and a human-in-the-loop clarification + plan-approval step.
LIVE_CONFIG = REPO_ROOT / "configs" / "config_openai_cli.yml"


def run_nvidiaaiq(message: str) -> str:
"""Run the AI-Q (NAT) workflow once and return its final response text.

NAT's CLI workflow can raise a human-in-the-loop interrupt (clarification or
plan approval). We pass an auto-approving ``user_input_callback`` so the run
never blocks waiting on a human -- a short affirmative satisfies both the
clarifier and plan approval.
"""
from nat.builder.context import ContextState
from nat.data_models.interactive import HumanResponseText
from nat.runtime.loader import load_workflow

async def _auto_approve(prompt):
return HumanResponseText(text="Yes, proceed with the research plan.")

async def _run() -> str:
async with load_workflow(str(LIVE_CONFIG)) as session_manager:
try:
ContextState.get().conversation_id.set(str(uuid.uuid4()))
except Exception:
pass
Comment on lines +49 to +52

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== File context =="
sed -n '1,140p' tests/monocle/conftest.py

echo
echo "== Search for ContextState usages =="
rg -n "ContextState|get\(\)\.conversation_id|conversation_id\.set|conversation_id" tests src sources -g '!**/.venv/**' -g '!**/node_modules/**' || true

echo
echo "== Search for monocle fixtures/tests =="
rg -n "monocle|run_nvidiaaiq|conversation_id" tests -g '!**/.venv/**' || true

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 23860


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Monocle README =="
sed -n '1,140p' tests/monocle/README.md

echo
echo "== Monocle test file =="
sed -n '1,180p' tests/monocle/test_nvidiaaiq.py

echo
echo "== Search for conversation-id docs in repo =="
rg -n "conversation-id|conversation_id|ContextState|checkpoint|state leakage|thread ID" tests/monocle docs/source src sources -g '!**/.venv/**' -g '!**/node_modules/**' || true

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 20851


🌐 Web query:

NeMo Agent Toolkit ContextState.get optional-context exception conversation_id set documented failure mode

💡 Result:

In the NVIDIA NeMo Agent Toolkit, ContextState is implemented as a singleton that manages context variables using contextvars [1][2]. The method ContextState.get() is used to retrieve the global ContextState instance [3][2][4]. There is no documented "failure mode" specifically for ContextState.get() regarding an "optional-context" exception or conversation_id. Based on the toolkit's architecture: 1. Context Management: ContextState provides thread-local or asynchronous-task-local storage for metadata such as conversation_id, user_id, and workflow_run_id [5][3][1]. 2. Property Access: The conversation_id property typically calls self._context_state.conversation_id.get() [5]. If the conversation_id has not been set in the current execution context, contextvars.ContextVar.get() will return its default value (which is None unless otherwise specified) [5][1]. It does not inherently raise an exception [5]. 3. Error Handling: While there are no exceptions specifically tied to accessing an unset conversation_id, the toolkit has implemented rigorous error propagation patterns in other areas (such as agent registration and runner result logging) to avoid "swallowing" exceptions [6]. If you are encountering an unexpected failure, it may be related to how downstream logic expects a non-null conversation_id or how ContextState is initialized within your specific runtime environment (e.g., inside an async with session(...) block) [3][1]. If you are experiencing an error, verify that the conversation_id has been explicitly set using context_state.conversation_id.set("your-id") within your session scope [3][1]. If you are working with custom components, ensure your usage conforms to the standard ContextState access patterns found in the nat.builder.context module [7][5][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Live workflow config =="
sed -n '1,220p' configs/config_openai_cli.yml

echo
echo "== Search for checkpoint / conversation wiring in configs =="
rg -n "checkpoint|conversation-id|conversation_id|thread_id|ContextState" configs src tests -g '!**/.venv/**' -g '!**/node_modules/**' || true

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 21651


Don't silence context setup failures here. If ContextState.get().conversation_id.set(...) raises, let the fixture fail or log it explicitly so the live trace doesn't silently lose its per-run conversation ID.

🤖 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/monocle/conftest.py` around lines 49 - 52, Remove the broad exception
suppression around ContextState.get().conversation_id.set(...) in the fixture
setup; allow failures to propagate so the fixture fails visibly, or explicitly
log the exception before handling it.

async with session_manager.session(user_input_callback=_auto_approve) as session:
async with session.run(message) as runner:
return await runner.result(to_type=str)

return asyncio.run(_run())
5 changes: 5 additions & 0 deletions tests/monocle/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Installing monocle_test_tools pulls in everything this suite needs
# (pytest, pytest-asyncio, and monocle_apptrace come transitively).
monocle_test_tools
# Auto-loads the repo deploy/.env for the live tests.
python-dotenv
Comment on lines +1 to +5

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tests/monocle/requirements.txt ==\n'
cat -n tests/monocle/requirements.txt

printf '\n== Nearby monocle files ==\n'
git ls-files 'tests/monocle/*' | sed 's#^`#-` #'

printf '\n== Search for monocle_test_tools / monocle_apptrace / python-dotenv / constraints ==\n'
rg -n --hidden --glob '!**/.git/**' \
  'monocle_test_tools|monocle_apptrace|python-dotenv|requirements\.txt|constraints|lockfile|uv\.lock|poetry\.lock|pip-tools|pip compile' \
  tests sources src docs configs .agents skills frontends deploy .

printf '\n== Dependency files that may constrain tests/monocle ==\n'
fd -H -a 'requirements.*|uv\.lock|pyproject\.toml|constraints.*|lock.*' tests . | sed 's#^`#-` #'

printf '\n== tests/monocle tree ==\n'
find tests/monocle -maxdepth 2 -type f | sort | sed 's#^`#-` #'

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 29858


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tests/monocle/README.md ==\n'
cat -n tests/monocle/README.md

printf '\n== tests/monocle/conftest.py ==\n'
cat -n tests/monocle/conftest.py

printf '\n== tests/monocle/test_nvidiaaiq.py ==\n'
cat -n tests/monocle/test_nvidiaaiq.py

printf '\n== monocle-related entries in uv.lock ==\n'
python3 - <<'PY'
from pathlib import Path
text = Path("uv.lock").read_text()
for needle in ["monocle_test_tools", "monocle_apptrace", "python-dotenv"]:
    print(f"\n-- {needle} --")
    for i, line in enumerate(text.splitlines(), 1):
        if needle in line:
            print(f"{i}: {line}")
PY

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 13807


Pin the Monocle test deps. requirements.txt is installed directly with pip install -r, so monocle_test_tools can drift and pull a different monocle_apptrace, breaking the recorded trace fixtures. Add version pins or a constraints file for this suite.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[HIGH] 1-1: gitpython 3.1.45: GitPython reference APIs has a path traversal vulnerability that allows arbitrary file write and delete outside the repository

(GHSA-7545-fcxq-7j24)


[HIGH] 1-1: gitpython 3.1.45: GitPython: Newline injection in config_writer() section parameter bypasses CVE-2026-42215 patch, enabling RCE via core.hooksPath

(GHSA-mv93-w799-cj2w)


[HIGH] 1-1: gitpython 3.1.45: GitPython has Command Injection via Git options bypass

(GHSA-rpm5-65cw-6hj4)


[HIGH] 1-1: gitpython 3.1.45: GitPython: Newline injection in config_writer().set_value() enables RCE via core.hooksPath

(GHSA-v87r-6q3f-2j67)


[HIGH] 1-1: gitpython 3.1.45: GitPython: Unsafe option check validates multi_options before shlex.split transformation

(GHSA-x2qx-6953-8485)


[HIGH] 1-1: torch 2.9.1: undefined

(PYSEC-2026-139)


[HIGH] 1-1: torch 2.9.1: PyTorch is vulnerable to memory corruption through its torch.lstm_cell function

(GHSA-qfhq-4f3w-5fph)


[HIGH] 1-1: torch 2.9.1: PyTorch is vulnerable to memory corruption through its torch.jit.script function

(GHSA-rrmf-rvhw-rf47)


[HIGH] 1-1: transformers 4.57.6: undefined

(PYSEC-2025-217)


[HIGH] 1-1: transformers 4.57.6: HuggingFace transformers vulnerable to remote code execution

(GHSA-29pf-2h5f-8g72)


[HIGH] 1-1: transformers 4.57.6: HuggingFace Transformers allows for arbitrary code execution in the Trainer class

(GHSA-69w3-r845-3855)

🤖 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/monocle/requirements.txt` around lines 1 - 5, Pin monocle_test_tools
and its monocle_apptrace dependency in tests/monocle/requirements.txt, either
with compatible explicit versions or a constraints file referenced by the
requirements. Preserve the existing pytest and environment-loading dependencies
while ensuring repeated installs use versions compatible with the recorded trace
fixtures.

Source: Linters/SAST tools

109 changes: 109 additions & 0 deletions tests/monocle/test_nvidiaaiq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Trace-based behavioural tests for AI-Q (NeMo Agent Toolkit), using Monocle
Test Tools.

Each test asserts against the Monocle trace a run emits -- which agent ran,
which tools it called, what it was asked, what it produced, and its duration
cost. Two offline tests replay recorded good traces (fast, no keys); one live
test runs the agent end-to-end.

pytest tests/monocle/ -k "not live" # offline, no keys
RUN_LIVE_NVIDIAAIQ=1 pytest tests/monocle/ -k live -s # live (needs OPENAI + a search key)

NOTE ON TOKENS: no `under_token_limit` here. NAT calls the OpenAI models in
streaming mode without `stream_options.include_usage`, so the response carries
no usage and the `inference.*` spans record only {finish_reason, finish_type}.
This is upstream (NAT), not a Monocle gap -- Monocle captures token counts
whenever the response includes usage. So the suite budgets on
`under_duration(..., span_type="workflow")` instead. The agent name is
"LangGraph" (NAT runs on a LangGraph runtime); the web tool is `web_search_tool`.
"""
import os
import sys
from pathlib import Path

import pytest
from monocle_test_tools import TraceAssertion

# This repo uses `--import-mode=importlib`, which does not put a test's own
# directory on sys.path; make the sibling conftest importable by name.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from conftest import TRACES, run_nvidiaaiq # noqa: E402

# Recorded good traces (captured from this repo under monocle_apptrace 0.8.8).
# Q: "Hi, what can you do?" -- meta/capabilities, answered directly (no tools).
TRACE_INTRO = str(TRACES / "monocle_trace_nvidia-aiq_4a94876861e3ade549dc74fb785e94d9_2026-07-09_13.17.46.json")
# Q: "What is the current stock price of NVIDIA today?" -- live-fact lookup that
# calls web_search_tool once.
TRACE_STOCK = str(TRACES / "monocle_trace_nvidia-aiq_e2961458302a780a27f91a0d86b290dd_2026-07-09_13.25.14.json")


# --- Offline: replay recorded good traces ---------------------------------

def test_capabilities_intro(monocle_trace_asserter: TraceAssertion):
"""Meta/capabilities question. The orchestrator answers directly (routes it
as a meta_response) -- no tool is called. Real trace: 5 spans, ~2.0s."""
monocle_trace_asserter.with_trace_source("file", trace_path=TRACE_INTRO)

monocle_trace_asserter.called_agent("LangGraph").contains_input("what can you do")
monocle_trace_asserter.contains_output("AI Research Assistant")
monocle_trace_asserter.does_not_call_tool("web_search_tool", "LangGraph")
monocle_trace_asserter.under_duration(10, span_type="workflow")

# Eval layer (deferred -- set OKAHU_API_KEY and uncomment to enable):
# monocle_trace_asserter.with_evaluation("okahu").check_eval("hallucination", "no_hallucination") \
# .check_eval("contextual_precision", "high_precision") \
# .check_eval("sentiment", "positive") \
# .check_eval("bias", "unbiased")


def test_nvda_stock_lookup(monocle_trace_asserter: TraceAssertion):
"""Live-fact lookup. The agent calls web_search_tool once and reports the
price it found ($202.78, the captured value). Real trace: 11 spans, ~7.7s."""
monocle_trace_asserter.with_trace_source("file", trace_path=TRACE_STOCK)

monocle_trace_asserter.called_agent("LangGraph").contains_input("stock price of NVIDIA")
monocle_trace_asserter.contains_output("202.78")
monocle_trace_asserter.called_tool("web_search_tool", "LangGraph")
monocle_trace_asserter.under_duration(15, span_type="workflow")

# monocle_trace_asserter.with_evaluation("okahu").check_eval("hallucination", "no_hallucination") \
# .check_eval("contextual_precision", "high_precision") \
# .check_eval("sentiment", "positive") \
# .check_eval("bias", "unbiased")


# --- Live: run the agent end-to-end ---------------------------------------
# The web-search fact lookup (web_search_tool over the configured provider).
# Output text varies run to run, so it asserts structure + budget with
# contains_any_output kept phrasing-robust.
#
# Opt-in (RUN_LIVE_NVIDIAAIQ=1), one run per process: NAT binds its async
# singletons (module-level locks) to the first event loop, so a second
# asyncio.run() in the same process raises "bound to a different event loop",
# and NAT leaves non-daemon worker threads that keep the interpreter from
# exiting cleanly. So a default `pytest tests/monocle/` skips this (offline
# stays green and exits clean); run it explicitly, e.g.:
# RUN_LIVE_NVIDIAAIQ=1 pytest tests/monocle/ -k live -s
_LIVE = pytest.mark.skipif(
os.environ.get("RUN_LIVE_NVIDIAAIQ") != "1",
reason="opt-in live run (set RUN_LIVE_NVIDIAAIQ=1; run one live test per process -- see note above)",
)
Comment on lines +87 to +90

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

Skip the live test when required credentials are unavailable.

The marker checks only RUN_LIVE_NVIDIAAIQ. Enabling it without OPENAI_API_KEY or the configured EXA_API_KEY can make workflow loading or tool execution fail instead of skipping safely. Gate the test on those environment variables without exposing their values; SERPER_API_KEY is not a substitute for this Exa-backed configuration.

As per coding guidelines, missing-secret paths must degrade gracefully (stub/skip), not crash.

🤖 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/monocle/test_nvidiaaiq.py` around lines 87 - 90, The _LIVE skip marker
currently checks only RUN_LIVE_NVIDIAAIQ; update it to also skip unless
OPENAI_API_KEY and the configured EXA_API_KEY environment variables are present,
without logging or exposing secret values. Do not use SERPER_API_KEY as a
substitute, and ensure missing credentials result in a graceful skip rather than
workflow or tool-execution failure.

Source: Coding guidelines



@_LIVE
def test_nvda_stock_lookup_live(monocle_trace_asserter: TraceAssertion):
"""Web-search path: the NVIDIA stock question, run live (web_search_tool)."""
monocle_trace_asserter.validator.test_workflow(
run_nvidiaaiq,
{"test_input": ("What is the current stock price of NVIDIA today?",)},
)

monocle_trace_asserter.called_agent("LangGraph")
monocle_trace_asserter.contains_any_output("NVIDIA", "NVDA", "stock", "price")
monocle_trace_asserter.called_tool("web_search_tool", "LangGraph")
Comment on lines +101 to +103

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

Make the live assertion reject error responses.

contains_any_output("NVIDIA", "NVDA", "stock", "price") can pass for an error such as “I couldn’t retrieve the stock price.” Assert stable success markers, such as a citation section plus a currency/amount pattern, while keeping the exact market value unconstrained.

🤖 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/monocle/test_nvidiaaiq.py` around lines 101 - 103, The output assertion
in the NVIDIA test is too broad and can accept error messages. Update the
assertion near monocle_trace_asserter.called_agent("LangGraph") to require
stable success indicators, including a citation section and a currency/amount
pattern, while leaving the exact market value unconstrained; retain the existing
agent and tool assertions.

monocle_trace_asserter.under_duration(120, span_type="workflow")

# monocle_trace_asserter.with_evaluation("okahu").check_eval("hallucination", "no_hallucination") \
# .check_eval("contextual_precision", "high_precision") \
# .check_eval("sentiment", "positive") \
# .check_eval("bias", "unbiased")
Loading