Spaces:
Sleeping
Sleeping
Edwin Sam Claude Opus 4.8 (1M context) commited on
Commit ·
64a007e
1
Parent(s): 9a58087
Polish for judges: cold-start hint, example prompts, graceful errors, prewarm
Browse files- Cold-start note under the command bar so a 30-60s first request reads as
expected, not broken
- Clickable example prompts (gr.Examples) so a cold visitor reaches the magic
moment without staring at an empty box
- Graceful failure in on_command/on_brief: a model cold-start or timeout shows a
retry hint instead of a raw stack trace, and keeps the typed text in the box
- Best-effort startup prewarm thread so the Space self-warms both Modal models
- Expand README with explicit Build Small track/prize/badge write-up
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README.md
CHANGED
|
@@ -88,4 +88,34 @@ EXTRACT_URL -> the Extractor.web URL (MiniCPM3-4B)
|
|
| 88 |
COACH_URL -> the Coach.web URL (Nemotron-3-Nano-4B)
|
| 89 |
```
|
| 90 |
|
| 91 |
-
Built for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
COACH_URL -> the Coach.web URL (Nemotron-3-Nano-4B)
|
| 89 |
```
|
| 90 |
|
| 91 |
+
## Built for Build Small
|
| 92 |
+
|
| 93 |
+
**Track — Backyard AI.** A practical, problem-solving tool for the daily work of a
|
| 94 |
+
solo health coach: it removes the manual CRM upkeep so they can spend their time
|
| 95 |
+
coaching, not bookkeeping.
|
| 96 |
+
|
| 97 |
+
**How each prize is earned:**
|
| 98 |
+
|
| 99 |
+
- **Modal** — both models run entirely on Modal. `modal_app.py` defines two GPU
|
| 100 |
+
services (L4), each warm-loaded and scaling to zero between calls; the cockpit is
|
| 101 |
+
a thin client that reaches them over HTTP. Modal is the whole runtime, not a side
|
| 102 |
+
call.
|
| 103 |
+
- **OpenBMB (MiniCPM3-4B)** — the write path. One model does routing, structured
|
| 104 |
+
extraction, stage classification, and method harvesting, leaning on MiniCPM3's
|
| 105 |
+
strong format- and JSON-following.
|
| 106 |
+
- **NVIDIA (Nemotron-3-Nano-4B)** — the read path. Compact agentic reasoning over
|
| 107 |
+
the coach's own data to answer questions and write grounded briefs.
|
| 108 |
+
- **Tiny Titan** — every model the app depends on is ≤4B parameters.
|
| 109 |
+
- **Best Agent** — an update flows through a multi-step pipeline: route → extract →
|
| 110 |
+
classify stage → log timeline → harvest reusable methods, then surfaces what needs
|
| 111 |
+
attention. Planning and tool use, not a single prompt.
|
| 112 |
+
- **Off Brand** — a purpose-built CRM cockpit (command bar, pipeline board,
|
| 113 |
+
needs-attention panel, knowledgebase), not the default Gradio look.
|
| 114 |
+
|
| 115 |
+
**Honest small-model fit.** Neither model does medicine. One turns natural language
|
| 116 |
+
into a structured record; the other reasons only over text the coach supplied and is
|
| 117 |
+
told never to introduce outside clinical claims. Every brief ends with "Not medical
|
| 118 |
+
advice. Coach reviews before sending," and nothing is ever sent to a client
|
| 119 |
+
automatically.
|
| 120 |
+
|
| 121 |
+
Models by OpenBMB and NVIDIA, GPUs by Modal.
|
app.py
CHANGED
|
@@ -26,6 +26,7 @@ import json
|
|
| 26 |
import os
|
| 27 |
import re
|
| 28 |
import sqlite3
|
|
|
|
| 29 |
|
| 30 |
import gradio as gr
|
| 31 |
import requests
|
|
@@ -476,13 +477,25 @@ def _refresh(active):
|
|
| 476 |
|
| 477 |
|
| 478 |
def on_command(text, active):
|
| 479 |
-
"""The single command bar. Routes to update or query
|
|
|
|
| 480 |
text = (text or "").strip()
|
| 481 |
if not text:
|
| 482 |
out = "Type an update or a question."
|
| 483 |
return (out, record_html(active), board_html(), needs_html(),
|
| 484 |
gr.update(), gr.update(choices=[], value=[]), [], "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
|
|
|
| 486 |
route = _post(EXTRACT_URL, {"op": "route", "text": text, "clients": list_clients()})
|
| 487 |
intent = route.get("intent", "update")
|
| 488 |
if text.endswith("?"):
|
|
@@ -549,11 +562,15 @@ def on_seed_kb(title, body):
|
|
| 549 |
def on_brief(name, question):
|
| 550 |
if not name:
|
| 551 |
return "Pick a client first."
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
|
| 558 |
|
| 559 |
# ---------------------------------------------------------------------------
|
|
@@ -570,6 +587,7 @@ CSS = """
|
|
| 570 |
#tag { color:var(--muted); margin-top:-.35rem; font-size:.9rem; }
|
| 571 |
.muted { color:var(--muted); }
|
| 572 |
#cmdout { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:6px 14px; min-height:2.2rem; }
|
|
|
|
| 573 |
.record { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:16px 18px; }
|
| 574 |
.rec-head { display:flex; align-items:center; gap:10px; }
|
| 575 |
.rec-name { font-weight:800; font-size:1.15rem; }
|
|
@@ -599,7 +617,24 @@ ul.flags li { color:var(--warn); } ul.follow li { color:#9fd0ff; }
|
|
| 599 |
.gr-button-primary { background:linear-gradient(90deg,#3fcfa0,#4f84ff) !important; border:none !important; }
|
| 600 |
"""
|
| 601 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
db_init()
|
|
|
|
| 603 |
_initial = (list_clients() or [None])[0]
|
| 604 |
|
| 605 |
with gr.Blocks(title="Spruce") as demo:
|
|
@@ -618,6 +653,20 @@ with gr.Blocks(title="Spruce") as demo:
|
|
| 618 |
lines=2, scale=5,
|
| 619 |
)
|
| 620 |
send = gr.Button("Send", variant="primary", scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 621 |
cmd_out = gr.Markdown(elem_id="cmdout")
|
| 622 |
|
| 623 |
with gr.Row():
|
|
|
|
| 26 |
import os
|
| 27 |
import re
|
| 28 |
import sqlite3
|
| 29 |
+
import threading
|
| 30 |
|
| 31 |
import gradio as gr
|
| 32 |
import requests
|
|
|
|
| 477 |
|
| 478 |
|
| 479 |
def on_command(text, active):
|
| 480 |
+
"""The single command bar. Routes to update or query, with graceful failure if a
|
| 481 |
+
model is cold or times out so a live demo never surfaces a raw stack trace."""
|
| 482 |
text = (text or "").strip()
|
| 483 |
if not text:
|
| 484 |
out = "Type an update or a question."
|
| 485 |
return (out, record_html(active), board_html(), needs_html(),
|
| 486 |
gr.update(), gr.update(choices=[], value=[]), [], "")
|
| 487 |
+
try:
|
| 488 |
+
return _handle_command(text, active)
|
| 489 |
+
except Exception:
|
| 490 |
+
out = ("⚠️ The model is waking up or the request timed out. The first request "
|
| 491 |
+
"after idle can take ~30–60s while the models load on Modal — give it a "
|
| 492 |
+
"moment and send again.")
|
| 493 |
+
# Keep the text in the box so the coach can just retry.
|
| 494 |
+
return (out, record_html(active), board_html(), needs_html(),
|
| 495 |
+
gr.update(), gr.update(choices=[], value=[]), [], text)
|
| 496 |
+
|
| 497 |
|
| 498 |
+
def _handle_command(text, active):
|
| 499 |
route = _post(EXTRACT_URL, {"op": "route", "text": text, "clients": list_clients()})
|
| 500 |
intent = route.get("intent", "update")
|
| 501 |
if text.endswith("?"):
|
|
|
|
| 562 |
def on_brief(name, question):
|
| 563 |
if not name:
|
| 564 |
return "Pick a client first."
|
| 565 |
+
try:
|
| 566 |
+
record = get_client(name)["record"]
|
| 567 |
+
kb = retrieve_kb(record)
|
| 568 |
+
out = _strip_think(_post(COACH_URL, {"op": "brief", "record": record, "kb": kb, "question": question})["brief"])
|
| 569 |
+
used = ", ".join(e["title"] for e in kb) if kb else "none yet"
|
| 570 |
+
return f"**Knowledgebase used:** {used}\n\n{out}"
|
| 571 |
+
except Exception:
|
| 572 |
+
return ("⚠️ The coach model is waking up or timed out. The first request after "
|
| 573 |
+
"idle can take ~30–60s on Modal — give it a moment and try again.")
|
| 574 |
|
| 575 |
|
| 576 |
# ---------------------------------------------------------------------------
|
|
|
|
| 587 |
#tag { color:var(--muted); margin-top:-.35rem; font-size:.9rem; }
|
| 588 |
.muted { color:var(--muted); }
|
| 589 |
#cmdout { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:6px 14px; min-height:2.2rem; }
|
| 590 |
+
#coldhint { color:var(--muted); font-size:.78rem; margin:2px 2px 8px; }
|
| 591 |
.record { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:16px 18px; }
|
| 592 |
.rec-head { display:flex; align-items:center; gap:10px; }
|
| 593 |
.rec-name { font-weight:800; font-size:1.15rem; }
|
|
|
|
| 617 |
.gr-button-primary { background:linear-gradient(90deg,#3fcfa0,#4f84ff) !important; border:none !important; }
|
| 618 |
"""
|
| 619 |
|
| 620 |
+
def _prewarm():
|
| 621 |
+
"""Fire one cheap request at each model on boot so the first real interaction is
|
| 622 |
+
warm, not a 30–60s cold start. Best-effort and off the request path: runs in a
|
| 623 |
+
daemon thread, and any failure (endpoint unset, still loading) is ignored."""
|
| 624 |
+
for url, payload in (
|
| 625 |
+
(EXTRACT_URL, {"op": "route", "text": "hello", "clients": []}),
|
| 626 |
+
(COACH_URL, {"op": "answer", "context": "{}", "question": "hello"}),
|
| 627 |
+
):
|
| 628 |
+
if not url:
|
| 629 |
+
continue
|
| 630 |
+
try:
|
| 631 |
+
requests.post(url, json=payload, timeout=180)
|
| 632 |
+
except Exception:
|
| 633 |
+
pass
|
| 634 |
+
|
| 635 |
+
|
| 636 |
db_init()
|
| 637 |
+
threading.Thread(target=_prewarm, daemon=True).start()
|
| 638 |
_initial = (list_clients() or [None])[0]
|
| 639 |
|
| 640 |
with gr.Blocks(title="Spruce") as demo:
|
|
|
|
| 653 |
lines=2, scale=5,
|
| 654 |
)
|
| 655 |
send = gr.Button("Send", variant="primary", scale=1)
|
| 656 |
+
gr.Examples(
|
| 657 |
+
examples=[
|
| 658 |
+
["Had a discovery call with Max, wants to drop 5kg before September and fix afternoon energy crashes. Booked his analysis call Thursday. I always have clients front-load protein at breakfast when energy dips."],
|
| 659 |
+
["Dana reported her 3pm energy crash is gone after the protein change. Moving her into weeks 5-8."],
|
| 660 |
+
["What's the update on Dana?"],
|
| 661 |
+
["Who have I not contacted in a while?"],
|
| 662 |
+
],
|
| 663 |
+
inputs=cmd,
|
| 664 |
+
label="Try one (click to load, then Send):",
|
| 665 |
+
)
|
| 666 |
+
gr.HTML(
|
| 667 |
+
"<div id='coldhint'>First request after idle can take ~30–60s while "
|
| 668 |
+
"MiniCPM3-4B and Nemotron-3-Nano-4B wake on Modal — then it's fast.</div>"
|
| 669 |
+
)
|
| 670 |
cmd_out = gr.Markdown(elem_id="cmdout")
|
| 671 |
|
| 672 |
with gr.Row():
|