Spaces:
Sleeping
Sleeping
File size: 3,611 Bytes
62f3c0d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | """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()
|