Spaces:
Sleeping
Sleeping
| """ | |
| 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()) | |