mz_chatbot / scripts /check_grok.py
Shurem's picture
initial commit
ff3c2e9
Raw History Blame Contribute Delete
10.8 kB
"""
check_grok.py β€” does grounded discovery actually work, and is it actually grounded?
The whole point of this provider is that the model must LOOK THINGS UP instead of
answering from memory. So this script does not just check that a reply comes
back; it counts tool calls and re-checks the websites the agent returned.
uv run python scripts/check_grok.py
uv run python scripts/check_grok.py --provider groq
uv run python scripts/check_grok.py --list-models
uv run python scripts/check_grok.py --icp kitchens --max 3
uv run python scripts/check_grok.py --icp "your own free-text ICP here"
Costs real tokens on whichever provider runs. No database is touched.
A run that returns companies but made ZERO tool calls is a FAILURE, not a pass β€”
that is the old ungrounded behaviour wearing a new coat.
"""
from __future__ import annotations
import argparse
import asyncio
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import requests # noqa: E402
from app.services import providers # noqa: E402
from app.services.providers import tools as provider_tools # noqa: E402
# Named ICPs to test against. Pass a name (--icp golf) or free text.
#
# "golf" is the sharpest test of grounding: it names a narrow vertical AND a
# checkable property of the website, so a fabricated company is obvious and
# fetch_page can confirm or refute the "static photos, no configurator" claim.
ICP_PRESETS = {
"golf": (
"US custom golf cart and luxury utility vehicle builders doing $1M-$10M revenue "
"whose websites still show static photo galleries instead of an interactive 3D "
"configurator"
),
"btier": (
"US companies that are B Tier companies doing around 1M to 10M in profits and would be "
"interested in Interactive 3D Configurators"
),
"kitchens": (
"Luxury bespoke kitchen cabinet manufacturers in the UAE or UK without a 3D configurator"
),
"yachts": (
"Superyacht and custom boat builders in Europe selling highly configurable vessels "
"with only static photography on their websites"
),
}
DEFAULT_ICP = ICP_PRESETS["golf"]
def _wrap_tools_with_counter() -> dict:
"""Count tool invocations by wrapping each tool's underlying callable."""
counts: dict[str, int] = {}
for tool in provider_tools.DISCOVERY_TOOLS:
name = getattr(tool, "name", None) or getattr(tool, "__name__", "tool")
counts[name] = 0
inner = getattr(tool, "on_invoke_tool", None)
if inner is None:
continue
def make(orig, key):
async def wrapped(*a, **kw):
counts[key] += 1
return await orig(*a, **kw)
return wrapped
tool.on_invoke_tool = make(inner, name) # type: ignore[attr-defined]
return counts
def _list_models(provider) -> list[str]:
"""Ask the provider which models it will actually serve this key."""
try:
r = requests.get(
f"{provider._base_url.rstrip('/')}/models",
headers={"Authorization": f"Bearer {provider._api_key}"},
timeout=10,
)
if r.status_code != 200:
return []
data = r.json()
rows = data.get("data") if isinstance(data, dict) else data
return sorted(str(m.get("id")) for m in rows if isinstance(m, dict) and m.get("id"))
except Exception:
return []
def _diagnose(provider, error: str) -> None:
"""Turn an opaque provider error into something actionable."""
low = error.lower()
key = provider._api_key or ""
if "incorrect api key" in low or "invalid_api_key" in low or "401" in low:
print("\n The endpoint rejected the key.")
if key.startswith("gsk_") and "x.ai" in provider._base_url:
print(" That key starts with 'gsk_', which is a GROQ key (groq.com).")
print(" You are calling api.x.ai, which is xAI's Grok β€” a different company.")
print(" Fix: set LLM_PROVIDER=groq, or put an xai- key in XAI_API_KEY.")
elif key.startswith("xai-") and "groq.com" in provider._base_url:
print(" That key starts with 'xai-', which is an xAI key, but this is Groq.")
print(" Fix: set LLM_PROVIDER=grok.")
else:
print(f" Key prefix is {key[:4]!r}. Check it was copied in full and is still active.")
return
if "model" in low and ("not found" in low or "does not exist" in low or "decommission" in low):
print(f"\n The model {provider._model!r} is not available to this key.")
names = _list_models(provider)
if names:
print(" Models this key CAN use:")
for n in names[:25]:
print(f" {n}")
print(f"\n Set one of those in .env, e.g. GROQ_MODEL=... or XAI_MODEL=...")
else:
print(" Could not list available models either β€” check the key first.")
return
if "tool" in low and "support" in low:
print(f"\n {provider._model!r} appears not to support tool calling, which this")
print(" agent requires. Pick a tool-capable model.")
# A site that refuses an automated request is NOT a dead site. 403/406/429 and
# friends come from bot protection and say nothing about whether the business is
# real -- the same distinction scraper.looks_blocked() and site_matches_lead()
# already make. Only 404/410 and a failure to connect mean the lead is bad.
BLOCKED_STATUSES = {401, 403, 405, 406, 409, 418, 429, 503}
GONE_STATUSES = {404, 410}
# A bare "Mozilla/5.0" is itself enough to trigger some WAFs. Sending what a real
# browser sends removes most false 406s.
_BROWSER_HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
}
def _site_is_live(url: str) -> tuple[str, bool]:
"""Return (human description, counts_as_dead)."""
if not url:
return "no website returned", True
try:
r = requests.head(url, timeout=8, allow_redirects=True, headers=_BROWSER_HEADERS)
# Some servers reject HEAD specifically; retry properly before judging.
if r.status_code >= 400:
r = requests.get(url, timeout=10, allow_redirects=True,
headers=_BROWSER_HEADERS, stream=True)
code = r.status_code
except requests.RequestException as exc:
return f"unreachable ({type(exc).__name__}) <-- DEAD", True
if code < 400:
return f"HTTP {code}", False
if code in BLOCKED_STATUSES:
return f"HTTP {code} β€” bot-blocked, site is up (not a mark against the lead)", False
if code in GONE_STATUSES:
return f"HTTP {code} <-- DEAD", True
return f"HTTP {code} <-- DEAD", True
async def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--provider", default="", help="grok | groq | gemini (default: auto)")
ap.add_argument("--list-models", action="store_true",
help="list the models this key can use, then exit")
ap.add_argument("--icp", default=DEFAULT_ICP,
help="a preset name (" + " | ".join(ICP_PRESETS) + ") or free text")
ap.add_argument("--max", type=int, default=3)
args = ap.parse_args()
icp = ICP_PRESETS.get(args.icp.strip().lower(), args.icp)
status = providers.provider_status()
print("provider credentials:")
for name, ok in status.items():
print(f" {name:8} {'configured' if ok else 'NOT configured'}")
provider = providers.get_provider(args.provider)
print(f"\nusing: {provider.name} (model {provider._model})")
if not provider.available():
print(f"\n{provider.name} has no API key. Set XAI_API_KEY (or GEMINI_API_KEY) in .env.")
return 1
if args.list_models:
names = _list_models(provider)
if not names:
print("\nCould not list models β€” the key may be wrong for this endpoint.")
return 1
print(f"\nmodels available to this key ({len(names)}):")
for n in names:
print(f" {n}")
return 0
counts = _wrap_tools_with_counter()
print(f"\nICP: {icp}\nasking for {args.max} companies...\n")
result = await provider.discover(icp, args.max)
if result.error:
print(f"FAILED: {result.error}")
_diagnose(provider, result.error)
return 1
print(f"tool calls made: {sum(counts.values())}")
for name, n in counts.items():
print(f" {name:22} {n}")
print(f"\ncandidates returned: {len(result.candidates)}")
dead = 0
for c in result.candidates:
print(f"\n {c.company_name} [{c.country or '??'}] {c.industry_tag}")
print(f" website : {c.company_website}")
live, is_dead = _site_is_live(c.company_website)
print(f" live? : {live}")
dead += is_dead
print(f" why fit : {(c.why_fit or '')[:140]}")
print(f" cites : {len(c.citations)} url(s)")
for u in c.citations[:3]:
print(f" {u}")
# ---- the assertions that matter -------------------------------------
print("\n" + "=" * 60)
failures = []
if sum(counts.values()) == 0:
failures.append(
"the agent made NO tool calls β€” it answered from memory, which is exactly "
"the ungrounded behaviour this provider exists to replace"
)
if not result.candidates:
failures.append("no candidates returned")
if dead:
failures.append(
f"{dead} of {len(result.candidates)} websites are genuinely dead "
f"(404/410/unreachable β€” bot-blocks do not count)"
)
uncited = [c.company_name for c in result.candidates if not c.citations]
if uncited:
failures.append(f"no citations for: {', '.join(uncited)}")
# A model must never volunteer contact details -- Candidate has no field for
# them, but check the prose too in case it smuggled one into why_fit.
leaked = [c.company_name for c in result.candidates if "@" in (c.why_fit or "")]
if leaked:
failures.append(f"an email address appeared in why_fit for: {', '.join(leaked)}")
if failures:
print("FAILED:")
for f in failures:
print(f" - {f}")
return 1
print(f"PASS β€” {len(result.candidates)} grounded candidates, "
f"{sum(counts.values())} tool calls, all websites live, all cited.")
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))