Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -25,6 +25,7 @@ RECEIPTS_DIR = pathlib.Path("/tmp/receipts"); RECEIPTS_DIR.mkdir(exist_ok=True)
|
|
| 25 |
EVENTS_PATH = "/tmp/events.json"
|
| 26 |
REQS_PATH = "/tmp/requests.jsonl"
|
| 27 |
BUDGET_FILE = "/tmp/nim_calls_today.json"
|
|
|
|
| 28 |
|
| 29 |
# ========= Helpers: canonicalization & hashing =========
|
| 30 |
def canonicalize(text: str) -> str:
|
|
@@ -257,14 +258,12 @@ def completions(request: Request, body: ChatRequest):
|
|
| 257 |
}
|
| 258 |
|
| 259 |
# ========= Session & Merkle helpers =========
|
| 260 |
-
SESSIONS_DIR = pathlib.Path("/tmp/sessions"); SESSIONS_DIR.mkdir(exist_ok=True)
|
| 261 |
-
|
| 262 |
def _session_path(sid: str) -> pathlib.Path:
|
| 263 |
return SESSIONS_DIR / f"{sid}.json"
|
| 264 |
|
| 265 |
def merkle_root_from_leaves(leaves: list[str]) -> str:
|
| 266 |
"""Compute a simple Merkle root from hex-string leaves.
|
| 267 |
-
Parent = sha256_hex(left + right) (deterministic demo)."""
|
| 268 |
if not leaves:
|
| 269 |
return ""
|
| 270 |
level = leaves[:]
|
|
@@ -342,17 +341,83 @@ def api_session_finish(sid: str):
|
|
| 342 |
def api_session_get(sid: str):
|
| 343 |
return session_load(sid)
|
| 344 |
|
| 345 |
-
# =========
|
| 346 |
-
def
|
| 347 |
-
if not
|
| 348 |
-
return
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
def verify_local(prompt_text, response_text, receipt_json, pepper):
|
| 358 |
try:
|
|
@@ -436,6 +501,7 @@ api_ui = gr.Blocks(title="IoM-TR Receipts — Portable Governance")
|
|
| 436 |
with api_ui:
|
| 437 |
gr.Markdown("# IoM-TR Receipts — Portable Governance (NIM + HF Space)")
|
| 438 |
|
|
|
|
| 439 |
with gr.Tab("Ask & Verify (in this Space)"):
|
| 440 |
gr.Markdown("Type a prompt, click **Ask & Get Receipt**. The UI shows the answer, a Receipt ID, the full receipt JSON, and local verification (SHA/HMAC).")
|
| 441 |
p2 = gr.Textbox(label="Prompt", value="In one sentence, what are portable AI governance receipts?")
|
|
@@ -448,11 +514,13 @@ with api_ui:
|
|
| 448 |
link = gr.Markdown()
|
| 449 |
ask.click(ui_chat_with_receipt, inputs=p2, outputs=[ans, st2, rid, rjson, verdict, link])
|
| 450 |
|
|
|
|
| 451 |
with gr.Tab("Dashboard"):
|
| 452 |
refresh = gr.Button("Refresh")
|
| 453 |
grid = gr.Dataframe(interactive=False)
|
| 454 |
refresh.click(lambda: _load_events_df(), outputs=grid)
|
| 455 |
|
|
|
|
| 456 |
with gr.Tab("Manual Verify (paste receipt)"):
|
| 457 |
gr.Markdown("Paste the exact prompt & response you saw, and the receipt JSON from `/receipts/{id}`.")
|
| 458 |
p = gr.Textbox(label="Prompt used")
|
|
@@ -463,6 +531,7 @@ with api_ui:
|
|
| 463 |
verdict2 = gr.Textbox(label="Result")
|
| 464 |
check.click(verify_local, inputs=[p, a, rj, pepper], outputs=verdict2)
|
| 465 |
|
|
|
|
| 466 |
with gr.Tab("Session Root"):
|
| 467 |
gr.Markdown("**Create a session → add last receipt(s) → finish → get one Merkle root + proof JSON.**")
|
| 468 |
|
|
@@ -502,6 +571,56 @@ with api_ui:
|
|
| 502 |
add_btn.click(ui_session_add_last, inputs=[sid_tb, rid], outputs=[sid_tb, sess_status, count_tb, root_tb, proof_code, proof_link])
|
| 503 |
finish_btn.click(ui_session_finish, inputs=[sid_tb], outputs=[sid_tb, sess_status, count_tb, root_tb, proof_code, proof_link])
|
| 504 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
# Expose the ASGI app for Spaces (Docker or Gradio SDK)
|
| 506 |
demo = api_ui
|
| 507 |
app = gr.mount_gradio_app(api, api_ui, path="/")
|
|
|
|
| 25 |
EVENTS_PATH = "/tmp/events.json"
|
| 26 |
REQS_PATH = "/tmp/requests.jsonl"
|
| 27 |
BUDGET_FILE = "/tmp/nim_calls_today.json"
|
| 28 |
+
SESSIONS_DIR = pathlib.Path("/tmp/sessions"); SESSIONS_DIR.mkdir(exist_ok=True)
|
| 29 |
|
| 30 |
# ========= Helpers: canonicalization & hashing =========
|
| 31 |
def canonicalize(text: str) -> str:
|
|
|
|
| 258 |
}
|
| 259 |
|
| 260 |
# ========= Session & Merkle helpers =========
|
|
|
|
|
|
|
| 261 |
def _session_path(sid: str) -> pathlib.Path:
|
| 262 |
return SESSIONS_DIR / f"{sid}.json"
|
| 263 |
|
| 264 |
def merkle_root_from_leaves(leaves: list[str]) -> str:
|
| 265 |
"""Compute a simple Merkle root from hex-string leaves.
|
| 266 |
+
Parent = sha256_hex(left + right) (deterministic demo). Duplicate last if odd per level."""
|
| 267 |
if not leaves:
|
| 268 |
return ""
|
| 269 |
level = leaves[:]
|
|
|
|
| 341 |
def api_session_get(sid: str):
|
| 342 |
return session_load(sid)
|
| 343 |
|
| 344 |
+
# ========= Verify-All helpers (session + per-turn text) =========
|
| 345 |
+
def _list_sessions():
|
| 346 |
+
if not SESSIONS_DIR.exists():
|
| 347 |
+
return []
|
| 348 |
+
files = []
|
| 349 |
+
for p in SESSIONS_DIR.glob("*.json"):
|
| 350 |
+
try:
|
| 351 |
+
files.append((p, p.stat().st_mtime))
|
| 352 |
+
except Exception:
|
| 353 |
+
pass
|
| 354 |
+
files.sort(key=lambda x: x[1], reverse=True)
|
| 355 |
+
return [p.name[:-5] for p, _ in files]
|
| 356 |
+
|
| 357 |
+
def latest_session_id() -> str:
|
| 358 |
+
ids = _list_sessions()
|
| 359 |
+
return ids[0] if ids else ""
|
| 360 |
+
|
| 361 |
+
def recompute_session_root_and_table(sid: str):
|
| 362 |
+
"""Return (computed_root, match_str, rows_for_table, proof_link_md)."""
|
| 363 |
+
doc = session_load(sid)
|
| 364 |
+
# Recompute each leaf from receipts
|
| 365 |
+
rows = []
|
| 366 |
+
leaves = []
|
| 367 |
+
for idx, rid in enumerate(doc.get("receipts", [])):
|
| 368 |
+
rp = RECEIPTS_DIR / f"{rid}.json"
|
| 369 |
+
if not rp.exists():
|
| 370 |
+
rows.append([rid, "(missing receipt)", "", False])
|
| 371 |
+
leaves.append("")
|
| 372 |
+
continue
|
| 373 |
+
rj = json.loads(rp.read_text())
|
| 374 |
+
recomputed_leaf = leaf_from_receipt(rj)
|
| 375 |
+
session_leaf = doc.get("leaf_hashes", [])[idx] if idx < len(doc.get("leaf_hashes", [])) else ""
|
| 376 |
+
ok = (session_leaf == recomputed_leaf) if session_leaf else True
|
| 377 |
+
rows.append([rid, session_leaf, recomputed_leaf, ok])
|
| 378 |
+
leaves.append(recomputed_leaf)
|
| 379 |
+
# Recompute Merkle root from recomputed leaves
|
| 380 |
+
comp_root = merkle_root_from_leaves(leaves)
|
| 381 |
+
ok_root = (comp_root == doc.get("merkle_root", ""))
|
| 382 |
+
return comp_root, f"Merkle root match: {ok_root}", rows, f"[Open proof](/session/{sid})"
|
| 383 |
+
|
| 384 |
+
def make_text_rows_for_session(sid: str):
|
| 385 |
+
"""Return rows the user can paste prompts/answers into: [receipt_id, prompt, answer, sha_ok, hmac_ok]."""
|
| 386 |
+
doc = session_load(sid)
|
| 387 |
+
rows = []
|
| 388 |
+
for rid in doc.get("receipts", []):
|
| 389 |
+
rows.append([rid, "", "", False, (True if not AUDIT_PEPPER else False)])
|
| 390 |
+
return rows
|
| 391 |
+
|
| 392 |
+
def verify_text_rows(sid: str, rows: list, pepper: str):
|
| 393 |
+
"""Rows: [receipt_id, prompt, answer, sha_ok, hmac_ok] -> fill sha_ok/hmac_ok."""
|
| 394 |
+
out = []
|
| 395 |
+
for rec in rows or []:
|
| 396 |
+
if len(rec) < 5:
|
| 397 |
+
rec = list(rec) + ["", "", False, False]
|
| 398 |
+
rid, prompt, answer, sha_ok, hmac_ok = rec[:5]
|
| 399 |
+
try:
|
| 400 |
+
rj = json.loads((RECEIPTS_DIR / f"{rid}.json").read_text())
|
| 401 |
+
except Exception:
|
| 402 |
+
out.append([rid, prompt, answer, False, False])
|
| 403 |
+
continue
|
| 404 |
+
|
| 405 |
+
# Canonicalize like the receipt did
|
| 406 |
+
def C(t):
|
| 407 |
+
t = unicodedata.normalize("NFKC", t or "").replace("\r\n", "\n").replace("\r", "\n")
|
| 408 |
+
t = "\n".join([ln.rstrip() for ln in t.split("\n")]); t = t.rstrip() + "\n"; return t
|
| 409 |
+
def s256(t): return hashlib.sha256(C(t).encode()).hexdigest()
|
| 410 |
+
def h256(k,t):
|
| 411 |
+
if not k: return ""
|
| 412 |
+
return hmac.new(k.encode(), C(t).encode(), hashlib.sha256).hexdigest()
|
| 413 |
+
|
| 414 |
+
sha_ok = (s256(prompt) == rj.get("prompt_sha256")) and (s256(answer) == rj.get("response_sha256"))
|
| 415 |
+
if pepper:
|
| 416 |
+
hmac_ok = (h256(pepper, prompt) == rj.get("prompt_hmac","")) and (h256(pepper, answer) == rj.get("response_hmac",""))
|
| 417 |
+
else:
|
| 418 |
+
hmac_ok = True # if no pepper is supplied, treat HMAC check as N/A/True
|
| 419 |
+
out.append([rid, prompt, answer, bool(sha_ok), bool(hmac_ok)])
|
| 420 |
+
return out
|
| 421 |
|
| 422 |
def verify_local(prompt_text, response_text, receipt_json, pepper):
|
| 423 |
try:
|
|
|
|
| 501 |
with api_ui:
|
| 502 |
gr.Markdown("# IoM-TR Receipts — Portable Governance (NIM + HF Space)")
|
| 503 |
|
| 504 |
+
# ---- Ask & Verify ----
|
| 505 |
with gr.Tab("Ask & Verify (in this Space)"):
|
| 506 |
gr.Markdown("Type a prompt, click **Ask & Get Receipt**. The UI shows the answer, a Receipt ID, the full receipt JSON, and local verification (SHA/HMAC).")
|
| 507 |
p2 = gr.Textbox(label="Prompt", value="In one sentence, what are portable AI governance receipts?")
|
|
|
|
| 514 |
link = gr.Markdown()
|
| 515 |
ask.click(ui_chat_with_receipt, inputs=p2, outputs=[ans, st2, rid, rjson, verdict, link])
|
| 516 |
|
| 517 |
+
# ---- Dashboard ----
|
| 518 |
with gr.Tab("Dashboard"):
|
| 519 |
refresh = gr.Button("Refresh")
|
| 520 |
grid = gr.Dataframe(interactive=False)
|
| 521 |
refresh.click(lambda: _load_events_df(), outputs=grid)
|
| 522 |
|
| 523 |
+
# ---- Manual Verify ----
|
| 524 |
with gr.Tab("Manual Verify (paste receipt)"):
|
| 525 |
gr.Markdown("Paste the exact prompt & response you saw, and the receipt JSON from `/receipts/{id}`.")
|
| 526 |
p = gr.Textbox(label="Prompt used")
|
|
|
|
| 531 |
verdict2 = gr.Textbox(label="Result")
|
| 532 |
check.click(verify_local, inputs=[p, a, rj, pepper], outputs=verdict2)
|
| 533 |
|
| 534 |
+
# ---- Session Root ----
|
| 535 |
with gr.Tab("Session Root"):
|
| 536 |
gr.Markdown("**Create a session → add last receipt(s) → finish → get one Merkle root + proof JSON.**")
|
| 537 |
|
|
|
|
| 571 |
add_btn.click(ui_session_add_last, inputs=[sid_tb, rid], outputs=[sid_tb, sess_status, count_tb, root_tb, proof_code, proof_link])
|
| 572 |
finish_btn.click(ui_session_finish, inputs=[sid_tb], outputs=[sid_tb, sess_status, count_tb, root_tb, proof_code, proof_link])
|
| 573 |
|
| 574 |
+
# ---- Verify All (one place) ----
|
| 575 |
+
with gr.Tab("Verify All (one place)"):
|
| 576 |
+
gr.Markdown(
|
| 577 |
+
"Load a session (or leave blank to use the latest), recompute the Merkle root and leaf hashes, "
|
| 578 |
+
"and optionally paste the exact prompts/answers to verify every receipt’s SHA/HMAC in one click."
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
sid_in = gr.Textbox(label="Session ID (leave blank = latest)")
|
| 582 |
+
load_btn = gr.Button("Load & Recompute")
|
| 583 |
+
comp_root = gr.Textbox(label="Recomputed Merkle root", interactive=False)
|
| 584 |
+
root_match = gr.Textbox(label="Root match", interactive=False)
|
| 585 |
+
table = gr.Dataframe(headers=["receipt_id", "session_leaf", "recomputed_leaf", "match"],
|
| 586 |
+
interactive=False, wrap=True)
|
| 587 |
+
|
| 588 |
+
proof_link2 = gr.Markdown()
|
| 589 |
+
|
| 590 |
+
def ui_load_and_recompute(sid_text: str):
|
| 591 |
+
sid = sid_text.strip() or latest_session_id()
|
| 592 |
+
if not sid:
|
| 593 |
+
return "", "No sessions found", [], ""
|
| 594 |
+
comp, msg, rows, link = recompute_session_root_and_table(sid)
|
| 595 |
+
return comp, msg, rows, link
|
| 596 |
+
|
| 597 |
+
load_btn.click(ui_load_and_recompute, inputs=sid_in, outputs=[comp_root, root_match, table, proof_link2])
|
| 598 |
+
|
| 599 |
+
gr.Markdown("### Per-turn text verification (paste the exact prompts & answers)")
|
| 600 |
+
pepper_tb = gr.Textbox(label="AUDIT_PEPPER (optional; if set in Space secrets, paste the same value)")
|
| 601 |
+
prep_btn = gr.Button("Prepare rows for this session")
|
| 602 |
+
rows_df = gr.Dataframe(headers=["receipt_id","prompt","answer","sha_ok","hmac_ok"],
|
| 603 |
+
row_count=(0, "dynamic"), datatype=["str","str","str","bool","bool"])
|
| 604 |
+
|
| 605 |
+
verify_btn = gr.Button("Verify pasted texts for all receipts")
|
| 606 |
+
rows_out = gr.Dataframe(headers=["receipt_id","prompt","answer","sha_ok","hmac_ok"],
|
| 607 |
+
row_count=(0, "dynamic"), datatype=["str","str","str","bool","bool"])
|
| 608 |
+
|
| 609 |
+
def ui_prep_rows(sid_text: str):
|
| 610 |
+
sid = sid_text.strip() or latest_session_id()
|
| 611 |
+
if not sid:
|
| 612 |
+
return []
|
| 613 |
+
return make_text_rows_for_session(sid)
|
| 614 |
+
|
| 615 |
+
def ui_verify_rows(sid_text: str, rows, pepper):
|
| 616 |
+
sid = sid_text.strip() or latest_session_id()
|
| 617 |
+
if not sid:
|
| 618 |
+
return rows or []
|
| 619 |
+
return verify_text_rows(sid, rows, pepper or "")
|
| 620 |
+
|
| 621 |
+
prep_btn.click(ui_prep_rows, inputs=sid_in, outputs=rows_df)
|
| 622 |
+
verify_btn.click(ui_verify_rows, inputs=[sid_in, rows_df, pepper_tb], outputs=rows_out)
|
| 623 |
+
|
| 624 |
# Expose the ASGI app for Spaces (Docker or Gradio SDK)
|
| 625 |
demo = api_ui
|
| 626 |
app = gr.mount_gradio_app(api, api_ui, path="/")
|