cloud ask: human-rendered output by default, JSON=1 to switch

Mirrors `make query` ergonomics:
  - default = pretty terminal layout (audit_label, sources w/ roles,
    bucket stats footer)
  - JSON=1 (or --json) = full machine-readable record

_render_cloud_ask_human shares the audit-label primitive
(_render_audit_label) with the local query renderer so the
HYBRID/STRICT/UNGROUNDED tokens map to the same four-rung ladder
labels in lattice modes (POINTER-LINKED / ANCHOR-WARRANTED / ...).
Bucket-direct path doesn't emit warrant-tail / run-DAG /
retrieval-purity so those sections are trimmed.

Output footer adds 'bucket: N HTTP requests · KB · endpoint / model'
so the operator can see network cost + LLM identity inline.
This commit is contained in:
russell@unturf.com 2026-05-30 11:06:41 -04:00
parent 462f639163
commit cb8c1deff2
No known key found for this signature in database
2 changed files with 70 additions and 6 deletions

View file

@ -1572,13 +1572,14 @@ cloud-fetch-chunk: bootstrap ## fetch + hash-verify one chunk from a bucket [LEA
@test -n "$(BLOB_BASE)" || { echo 'BLOB_BASE required (per-chunk blobs/<hash> base URL)'; exit 2; }
@$(ARBORIST) cloud fetch-chunk "$(LEAF_HASH)" --blob-base "$(BLOB_BASE)"
cloud-ask: bootstrap ## bucket-direct end-to-end: FTS → LLM → verify [Q="..." SHARD_URL=... TOP_K=N MAX_CONTEXT=N CACHE_MB=N]
@test -n "$(Q)" || { echo 'usage: make cloud-ask Q="your question" [SHARD_URL=...] [TOP_K=4] [MAX_CONTEXT=24000] [CACHE_MB=64]'; exit 2; }
cloud-ask: bootstrap ## bucket-direct end-to-end: FTS → LLM → verify [Q="..." JSON=1 SHARD_URL=... TOP_K=N MAX_CONTEXT=N CACHE_MB=N]
@test -n "$(Q)" || { echo 'usage: make cloud-ask Q="your question" [JSON=1] [SHARD_URL=...] [TOP_K=4] [MAX_CONTEXT=24000] [CACHE_MB=64]'; exit 2; }
@echo "# shard: $(SHARD_URL)" >&2
@$(ARBORIST) cloud ask '$(Q)' --shard-url "$(SHARD_URL)" \
--top-k $(or $(TOP_K),4) \
--max-context-chars $(or $(MAX_CONTEXT),24000) \
--cache-mb $(or $(CACHE_MB),64)
--cache-mb $(or $(CACHE_MB),64) \
$(if $(JSON),--json,)
# ---------------------------------------------------------------------------
# Head-to-head: local query (BURN=1) vs cloud-ask, same question, same data.

View file

@ -7290,6 +7290,11 @@ def build_parser() -> argparse.ArgumentParser:
"--model", default=None,
help="LLM model id (default: $ARBORIST_LLM_MODEL or Hermes-3-8B).",
)
cloud_ask.add_argument(
"--json", action="store_true",
help="emit the full result as JSON. Default: human-rendered "
"(matches `arborist query` output).",
)
cloud_ask.set_defaults(func=_cmd_cloud_ask)
return p
@ -7550,7 +7555,8 @@ def _cmd_cloud_ask(args: argparse.Namespace) -> int:
finally:
client.close()
print(json.dumps({
elapsed = _time.time() - t0
result = {
"answer_text": answer,
"audit_mode": verdict["audit_mode"],
"verifier_method": verdict["verifier_method"],
@ -7559,13 +7565,70 @@ def _cmd_cloud_ask(args: argparse.Namespace) -> int:
"unverified_quotes": verdict.get("unverified_quotes", []),
"sources": sources,
"stats": client.stats(),
"timing_s": round(_time.time() - t0, 3),
"timing_s": round(elapsed, 3),
"endpoint": endpoint,
"model": model,
}, indent=2, ensure_ascii=False))
}
if getattr(args, "json", False):
print(json.dumps(result, indent=2, ensure_ascii=False))
else:
print(_render_cloud_ask_human(result, args.question))
return 0
def _render_cloud_ask_human(result: dict, question: str) -> str:
"""Pretty-print a cloud-ask result. Mirrors `_render_query_human`'s
layout but trims sections that the bucket-direct path doesn't
produce (warrant tails, run-DAG, retrieval-purity)."""
audit = result.get("audit_mode") or "UNGROUNDED"
method = result.get("verifier_method") or "?"
n_quotes = result.get("n_quotes") or 0
n_verified = result.get("n_verified") or 0
elapsed_s = result.get("timing_s") or 0.0
label = _render_audit_label(audit, method, [])
stats = result.get("stats") or {}
cache_stats = stats.get("cache") or {}
http_reqs = stats.get("http_requests")
bytes_fetched = cache_stats.get("bytes_fetched") or 0
lines: list[str] = []
lines.append(question)
lines.append(
f" {label} {n_verified}/{n_quotes} {elapsed_s:.2f}s (bucket-direct)"
)
lines.append("")
lines.append(result.get("answer_text") or "")
lines.append("")
sources = result.get("sources") or []
if sources:
lines.append(f"sources ({len(sources)}):")
for i, s in enumerate(sources, start=1):
uri = s.get("document_uri", "")
title = (s.get("title") or "").strip() or _short_path(uri)
role = s.get("source_role")
role_part = f"{role}" if role else ""
lines.append(
f" [{i}] {title}{role_part}{_strip_scheme(uri)}"
)
lines.append("")
unverified = result.get("unverified_quotes") or []
if unverified:
lines.append(f"unverified ({len(unverified)}):")
for q in unverified[:5]:
lines.append(f' - "{q[:140]}{"" if len(q) > 140 else ""}"')
lines.append("")
bytes_kb = bytes_fetched / 1024.0
lines.append(
f"bucket: {http_reqs} HTTP requests · {bytes_kb:.1f} KB · "
f"{result.get('endpoint','')} / {result.get('model','')}"
)
lines.append(" <run with --json for full record>")
return "\n".join(lines)
def _cmd_cloud_fetch_chunk(args: argparse.Namespace) -> int:
from arborist.merkle import hash_leaf
client = _make_bucket_client(args)