From cb8c1deff2e11fb985ce957e136cec6689148dd0 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Sat, 30 May 2026 11:06:41 -0400 Subject: [PATCH] cloud ask: human-rendered output by default, JSON=1 to switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 7 ++--- arborist/cli.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 69d5421..825a06c 100644 --- a/Makefile +++ b/Makefile @@ -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/ 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. diff --git a/arborist/cli.py b/arborist/cli.py index 5af5ce2..ff042cd 100644 --- a/arborist/cli.py +++ b/arborist/cli.py @@ -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(" ") + return "\n".join(lines) + + def _cmd_cloud_fetch_chunk(args: argparse.Namespace) -> int: from arborist.merkle import hash_leaf client = _make_bucket_client(args)