eolas / app /ask.py
rahulraj1406's picture
Initial commit — Stage 1 POC complete (1A–1E)
9345109
Raw
History Blame Contribute Delete
3.02 kB
"""
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())