-
Notifications
You must be signed in to change notification settings - Fork 222
fix: clarifier searches before asking clarification questions #245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cdgamarose-nv
merged 15 commits into
NVIDIA-AI-Blueprints:develop
from
torkian:fix/clarifier-search-before-questions
Jul 7, 2026
Merged
Changes from 3 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
086d203
Force clarifier to search before asking clarification questions
torkian 388149c
Lint: enforce force-single-line imports in e2e reproducer
torkian 8f18f80
Address Greptile review: iteration guard on guidance, stricter e2e gate
torkian 1377209
Simplify clarifier search-before-clarify to an inline retry
torkian 2eccd5b
Fix consecutive assistant messages in forced-search retry
torkian 832a1fa
Fix consecutive assistant messages on the skip-clarification path
torkian 24e6e84
Merge branch 'develop' into fix/clarifier-search-before-questions
cdgamarose-nv 8dda5c0
fix(clarifier): normalize blank skip reply; tighten completion test
torkian 6fef9e3
fix(clarifier): scope search guard and harden exhausted completion
torkian 0836327
Merge branch 'develop' into fix/clarifier-search-before-questions
cdgamarose-nv 82fdab9
fix(clarifier): address review — SystemMessage nudge, trim comments, …
torkian 62151d8
fix(clarifier): don't complete over a pending tool call at exhaustion
torkian 1f6ae72
fix(clarifier): fold retry guidance into the leading system prompt
torkian bd866f4
Merge branch 'develop' into fix/clarifier-search-before-questions
torkian 7dcdeef
Merge branch 'develop' into fix/clarifier-search-before-questions
cdgamarose-nv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| """End-to-end test for issue #234 fix. | ||
|
|
||
| Runs the ClarifierAgent against a real NVIDIA NIM-hosted LLM with a real Tavily | ||
| web search tool, using an obscure query the LLM cannot possibly know from | ||
| training data. Verifies that the agent issues a search call before falling back | ||
| to asking the user for clarification. | ||
|
|
||
| Run with: | ||
| PYTHONPATH=src .venv/bin/python scripts/e2e_clarifier_search_test.py | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import os | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
| # Load deploy/.env if present. | ||
| ENV_FILE = Path(__file__).resolve().parents[1] / "deploy" / ".env" | ||
| if ENV_FILE.exists(): | ||
| for line in ENV_FILE.read_text().splitlines(): | ||
| line = line.strip() | ||
| if not line or line.startswith("#") or "=" not in line: | ||
| continue | ||
| key, _, value = line.partition("=") | ||
| os.environ.setdefault(key.strip(), value.strip()) | ||
|
|
||
| if not os.environ.get("NVIDIA_API_KEY"): | ||
| sys.exit("NVIDIA_API_KEY not set (paste it into deploy/.env)") | ||
| if not os.environ.get("TAVILY_API_KEY"): | ||
| sys.exit("TAVILY_API_KEY not set (paste it into deploy/.env)") | ||
|
|
||
| from langchain_community.tools.tavily_search import TavilySearchResults # noqa: E402 | ||
| from langchain_core.messages import HumanMessage # noqa: E402 | ||
| from langchain_core.messages import ToolMessage # noqa: E402 | ||
| from langchain_nvidia_ai_endpoints import ChatNVIDIA # noqa: E402 | ||
|
|
||
| from aiq_agent.agents.clarifier.agent import ClarifierAgent # noqa: E402 | ||
| from aiq_agent.agents.clarifier.models import ClarifierAgentState # noqa: E402 | ||
| from aiq_agent.common import LLMProvider # noqa: E402 | ||
|
|
||
| try: | ||
| from aiq_agent.agents.clarifier.agent import FORCE_SEARCH_GUIDANCE # noqa: E402 | ||
| except ImportError: | ||
| FORCE_SEARCH_GUIDANCE = None # running against the pre-fix branch | ||
|
|
||
| # Test queries: | ||
| # - "obscure": something no LLM can know without searching. | ||
| # - "knowable": something the LLM thinks it knows; in the failure mode it | ||
| # skips the search and just asks "which aspect?". | ||
| TEST_QUERIES = { | ||
| "obscure": "Tell me about Project Zphyr-77Q at QXR Industries — what does it do and who works on it?", | ||
| "knowable": "Research the latest NVIDIA AI announcements from 2026", | ||
| } | ||
| OBSCURE_QUERY = TEST_QUERIES[os.environ.get("AIQ_TEST_QUERY", "knowable")] | ||
|
|
||
| # Model defaults to a small open model so the test is cheap to run. | ||
| MODEL_NAME = os.environ.get("AIQ_CLARIFIER_TEST_MODEL", "meta/llama-3.3-70b-instruct") | ||
|
|
||
|
|
||
| async def main() -> int: | ||
| llm = ChatNVIDIA(model=MODEL_NAME, temperature=0) | ||
| search = TavilySearchResults(max_results=3, name="web_search_tool") | ||
|
|
||
| provider = LLMProvider() | ||
| provider.set_default(llm) | ||
|
|
||
| # The user_prompt_callback should never be invoked if the search-before-ask | ||
| # behavior is working. If it is invoked, we record it so the test can fail | ||
| # loudly. | ||
| user_prompts: list[str] = [] | ||
|
|
||
| async def user_prompt_callback(question: str) -> str: | ||
| user_prompts.append(question) | ||
| # If reached, pretend the user said "skip" so the run still terminates. | ||
| return "skip" | ||
|
|
||
| agent = ClarifierAgent( | ||
| llm_provider=provider, | ||
| tools=[search], | ||
| user_prompt_callback=user_prompt_callback, | ||
| max_turns=2, | ||
| ) | ||
|
|
||
| state = ClarifierAgentState(messages=[HumanMessage(content=OBSCURE_QUERY)]) | ||
| print(f"Model: {MODEL_NAME}") | ||
| print(f"Query: {OBSCURE_QUERY}\n") | ||
|
|
||
| final_state = await agent.graph.ainvoke(state, config={"callbacks": []}) | ||
|
|
||
| # Pull the final messages out for inspection. | ||
| if isinstance(final_state, dict): | ||
| messages = final_state["messages"] | ||
| force_search_used = final_state.get("force_search_used", False) | ||
| else: | ||
| messages = final_state.messages | ||
| force_search_used = getattr(final_state, "force_search_used", False) | ||
|
|
||
| tool_calls_made = sum(len(getattr(m, "tool_calls", None) or []) for m in messages) | ||
| tool_results = sum(1 for m in messages if isinstance(m, ToolMessage)) | ||
| forced = bool(force_search_used) | ||
|
|
||
| print("=" * 72) | ||
| print("Conversation trace") | ||
| print("=" * 72) | ||
| for i, m in enumerate(messages): | ||
| kind = type(m).__name__ | ||
| content = str(getattr(m, "content", "")).replace("\n", " ")[:160] | ||
| extra = "" | ||
| if getattr(m, "tool_calls", None): | ||
| extra = f" tool_calls={[(tc.get('name'), tc.get('args')) for tc in m.tool_calls]}" | ||
| print(f"{i:>2}. {kind}: {content}{extra}") | ||
|
|
||
| print() | ||
| print("=" * 72) | ||
| print("Verdict") | ||
| print("=" * 72) | ||
| print(f" user_prompt_callback invocations : {len(user_prompts)}") | ||
| print(f" total tool_calls emitted by LLM : {tool_calls_made}") | ||
| print(f" total ToolMessages (results) : {tool_results}") | ||
| print(f" force_search guidance injected : {forced}") | ||
|
|
||
| if tool_calls_made >= 1 and len(user_prompts) == 0: | ||
| print("\nPASS: the LLM issued at least one search before any user prompt.") | ||
| return 0 | ||
| if tool_calls_made == 0: | ||
| print("\nFAIL: the LLM never issued a search.") | ||
| else: | ||
| print("\nFAIL: the LLM issued a search, but the user was also prompted.") | ||
| return 1 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(asyncio.run(main())) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.