""" CLI client — retrieve + answer in one command, no server. Useful for: - quick smoke-testing the bot before standing up the API - testing one question without keeping a server alive - sanity-checking that retrieval is finding the right passages Usage: python -m app.ask "Can a non-Irish company apply for funding?" python -m app.ask --no-llm "..." # just show top-k passages python -m app.ask --top-k 8 "..." """ from __future__ import annotations import argparse import asyncio import sys from app.config import SETTINGS from app.prompt import IDK_MESSAGE, SYSTEM_PROMPT, format_user_message from app.providers.factory import build_llm from app.retrieve import retrieve async def _ask(question: str, top_k: int, no_llm: bool) -> int: print(f"\nQ: {question}\n", flush=True) result = retrieve(question, top_k=top_k) print(f"-- top-{top_k} passages (best_score={result.best_score:.3f}, " f"threshold={SETTINGS.relevance_threshold}, " f"gate_{'PASS' if result.gate_passed else 'FAIL'}) --", flush=True) for i, p in enumerate(result.passages, 1): page = f" p.{p.page}" if p.page else "" head = (p.heading_path or "").replace("\n", " ")[:70] print(f" [{i}] {p.score:.3f} | {p.category:8s} | {head} | {p.source_url}{page}", flush=True) if no_llm: return 0 if result.gate_passed else 2 if not result.gate_passed: print(f"\nA: {IDK_MESSAGE}\n", flush=True) return 2 try: llm = build_llm(SETTINGS) except Exception as e: print(f"\n[error] could not build LLM provider: {e}", file=sys.stderr) return 3 print(f"\nA ({llm.name}): ", end="", flush=True) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": format_user_message(question, result.passages)}, ] try: async for tok in llm.stream(messages): print(tok, end="", flush=True) except Exception as e: print(f"\n[error] LLM stream failed: {e}", file=sys.stderr) return 4 print("\n", flush=True) # Show deduplicated source URLs seen, sources = set(), [] for p in result.passages: if p.source_url in seen: continue seen.add(p.source_url) sources.append(p) print("Sources:", flush=True) for s in sources: page = f" (page {s.page})" if s.page else "" print(f" - {s.title}{page}\n {s.source_url}", flush=True) return 0 def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("question", help="Your question (in quotes)") parser.add_argument("--top-k", type=int, default=SETTINGS.top_k) parser.add_argument("--no-llm", action="store_true", help="Skip LLM call; only print retrieved passages.") args = parser.parse_args() return asyncio.run(_ask(args.question, args.top_k, args.no_llm)) if __name__ == "__main__": raise SystemExit(main())