Spaces:
Sleeping
Sleeping
| """Interactive CLI for BIS Compass. | |
| Usage: | |
| python -m src.cli # interactive REPL | |
| python -m src.cli "Portland slag cement" # one-shot | |
| Hits are printed as a tidy ranked table with confidence colours. Useful for | |
| quick smoke-testing without spinning up the front-end. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| import time | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from src.offline_guard import enforce_offline_if_cached # noqa: E402 | |
| enforce_offline_if_cached() | |
| # ANSI colour codes (degrade gracefully on unsupported terminals). | |
| def _supports_colour() -> bool: | |
| return sys.stdout.isatty() and sys.platform != "emscripten" | |
| _C = { | |
| "reset": "\x1b[0m", | |
| "dim": "\x1b[2m", | |
| "bold": "\x1b[1m", | |
| "accent": "\x1b[38;2;250;145;90m", # orange | |
| "rose": "\x1b[38;2;220;80;130m", # accent-2 | |
| "green": "\x1b[38;2;52;211;153m", | |
| "amber": "\x1b[38;2;252;211;77m", | |
| "muted": "\x1b[38;2;154;154;176m", | |
| } if True else {} | |
| def c(key: str, s: str) -> str: | |
| if not _supports_colour(): | |
| return s | |
| return f"{_C.get(key, '')}{s}{_C['reset']}" | |
| def _confidence_label(score: float) -> str: | |
| if score >= 0.65: | |
| return c("green", "HIGH ") | |
| if score >= 0.35: | |
| return c("amber", "MEDIUM") | |
| return c("rose", "LOW ") | |
| def render_hits(query: str, hits, latency: float): | |
| print() | |
| print(c("dim", f'> "{query}"')) | |
| print(c("dim", f" {latency * 1000:.0f}ms · top {len(hits)}")) | |
| print() | |
| print( | |
| c("muted", " ## ") + | |
| c("muted", " IS code".ljust(30)) + | |
| c("muted", " confidence".ljust(12)) + | |
| c("muted", " title") | |
| ) | |
| print(c("muted", " " + "-" * 78)) | |
| for h in hits: | |
| rank = c("bold", f" #{h.rank}".ljust(5)) | |
| code = c("accent", f" {h.is_code}".ljust(30)) | |
| conf = f" {_confidence_label(h.rerank_score)}".ljust(12 + 5) # +5 for ANSI | |
| title = h.title[:55] | |
| print(f"{rank} {code} {conf} {title}") | |
| if h.scope: | |
| scope = h.scope[:90].rstrip() | |
| print(c("dim", f" {scope}{'…' if len(h.scope) > 90 else ''}")) | |
| print() | |
| def main(): | |
| ap = argparse.ArgumentParser( | |
| description="BIS Compass — interactive CLI", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| ) | |
| ap.add_argument("query", nargs="?", help="One-shot query. If omitted, enters REPL.") | |
| ap.add_argument("--top-k", type=int, default=5) | |
| args = ap.parse_args() | |
| print(c("bold", "BIS Compass"), c("dim", "· loading retriever (this is a one-time cost)...")) | |
| t0 = time.perf_counter() | |
| from src.retrieval.retriever import Retriever | |
| R = Retriever(final_k=args.top_k) | |
| print(c("dim", f" ready in {time.perf_counter() - t0:.1f}s")) | |
| if args.query: | |
| t = time.perf_counter() | |
| hits = R.search(args.query) | |
| render_hits(args.query, hits, time.perf_counter() - t) | |
| return | |
| # REPL | |
| print(c("muted", " Type a product description and hit Enter. Ctrl+C / Ctrl+D / 'exit' to quit.")) | |
| while True: | |
| try: | |
| q = input(c("accent", "▶ ")).strip() | |
| except (EOFError, KeyboardInterrupt): | |
| print() | |
| return | |
| if not q: | |
| continue | |
| if q.lower() in {"exit", "quit", ":q"}: | |
| return | |
| t = time.perf_counter() | |
| try: | |
| hits = R.search(q) | |
| except Exception as e: | |
| print(c("rose", f" error: {e}")) | |
| continue | |
| render_hits(q, hits, time.perf_counter() - t) | |
| if __name__ == "__main__": | |
| main() | |