Spaces:
Running on Zero
Running on Zero
Add robust comparison whiteboard template
#1
by ProCreations - opened
- board_model_prompt.py +1 -1
- board_quality.py +94 -2
- engine.py +4 -0
- mock_engine.py +16 -7
- test_board_quality.py +24 -0
board_model_prompt.py
CHANGED
|
@@ -28,7 +28,7 @@ Ops:
|
|
| 28 |
{"op":"highlight","at":[x,y],"w":W}
|
| 29 |
Colors: ink, blue, red, green, orange, purple, gray. Use ^ for exponents in text.
|
| 30 |
|
| 31 |
-
Draw the step's REAL content (actual names, values, shapes) with 2-5 compact ops, unless it is only a check-in. The board should teach visually about as much as the voice does: if the narration names a part, value, result, or relationship, add a mark for it. Use compact boxes/ellipses, usually under 18 wide and 10 high; main diagrams should leave room for notes, formulas, and callouts. Use callout to circle important labels like c and explain them with a short leader line. Keep related items adjacent instead of drawing long arrows. Every unlabeled arrow must connect two visible items; do not point into empty space. Build on what is already on the board — never redraw it, never collide with it."""
|
| 32 |
|
| 33 |
|
| 34 |
def board_context(prior_ops):
|
|
|
|
| 28 |
{"op":"highlight","at":[x,y],"w":W}
|
| 29 |
Colors: ink, blue, red, green, orange, purple, gray. Use ^ for exponents in text.
|
| 30 |
|
| 31 |
+
Draw the step's REAL content (actual names, values, shapes) with 2-5 compact ops, unless it is only a check-in. The board should teach visually about as much as the voice does: if the narration names a part, value, result, or relationship, add a mark for it. For "X vs Y", "compare X and Y", or "difference between X and Y", use a two-column comparison layout with one side for each named thing; never stack the two options vertically with unrelated arrows. Use compact boxes/ellipses, usually under 18 wide and 10 high; main diagrams should leave room for notes, formulas, and callouts. Use callout to circle important labels like c and explain them with a short leader line. Keep related items adjacent instead of drawing long arrows. Every unlabeled arrow must connect two visible items; do not point into empty space. Build on what is already on the board — never redraw it, never collide with it."""
|
| 32 |
|
| 33 |
|
| 34 |
def board_context(prior_ops):
|
board_quality.py
CHANGED
|
@@ -17,6 +17,8 @@ def _blob(*parts):
|
|
| 17 |
|
| 18 |
def diagram_family(question, say=""):
|
| 19 |
text = _blob(question, say)
|
|
|
|
|
|
|
| 20 |
if ("rocket" in text or "spacecraft" in text or "launch" in text) and (
|
| 21 |
"orbit" in text or "gravity turn" in text or "orbital" in text
|
| 22 |
):
|
|
@@ -64,14 +66,15 @@ def improve_step_board(question, step_index, say, board):
|
|
| 64 |
board = [dict(op) for op in (board or []) if isinstance(op, dict)]
|
| 65 |
family = diagram_family(question, say)
|
| 66 |
if family:
|
| 67 |
-
templated = _template(family, step_index, question)
|
| 68 |
if templated is not None:
|
| 69 |
return templated
|
| 70 |
return _repair_generic(question, step_index, say, board)
|
| 71 |
|
| 72 |
|
| 73 |
-
def _template(family, step_index, question=""):
|
| 74 |
templates = {
|
|
|
|
| 75 |
"rocket_orbit": _rocket_orbit,
|
| 76 |
"model_access_directive": _model_access_directive,
|
| 77 |
"no_code_platform": _no_code_platform,
|
|
@@ -87,9 +90,98 @@ def _template(family, step_index, question=""):
|
|
| 87 |
}
|
| 88 |
if family == "no_code_platform":
|
| 89 |
return templates[family](step_index, question)
|
|
|
|
|
|
|
| 90 |
return templates[family](step_index)
|
| 91 |
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
def _topic_title(question, fallback="How It Works"):
|
| 94 |
text = re.sub(r"[?!.\"]", "", str(question or "")).strip()
|
| 95 |
text = re.sub(r"^(explain|teach me|show me|tell me about)\s+", "", text, flags=re.I)
|
|
|
|
| 17 |
|
| 18 |
def diagram_family(question, say=""):
|
| 19 |
text = _blob(question, say)
|
| 20 |
+
if _comparison_terms(question, say):
|
| 21 |
+
return "comparison"
|
| 22 |
if ("rocket" in text or "spacecraft" in text or "launch" in text) and (
|
| 23 |
"orbit" in text or "gravity turn" in text or "orbital" in text
|
| 24 |
):
|
|
|
|
| 66 |
board = [dict(op) for op in (board or []) if isinstance(op, dict)]
|
| 67 |
family = diagram_family(question, say)
|
| 68 |
if family:
|
| 69 |
+
templated = _template(family, step_index, question, say)
|
| 70 |
if templated is not None:
|
| 71 |
return templated
|
| 72 |
return _repair_generic(question, step_index, say, board)
|
| 73 |
|
| 74 |
|
| 75 |
+
def _template(family, step_index, question="", say=""):
|
| 76 |
templates = {
|
| 77 |
+
"comparison": _comparison,
|
| 78 |
"rocket_orbit": _rocket_orbit,
|
| 79 |
"model_access_directive": _model_access_directive,
|
| 80 |
"no_code_platform": _no_code_platform,
|
|
|
|
| 90 |
}
|
| 91 |
if family == "no_code_platform":
|
| 92 |
return templates[family](step_index, question)
|
| 93 |
+
if family == "comparison":
|
| 94 |
+
return templates[family](step_index, question, say)
|
| 95 |
return templates[family](step_index)
|
| 96 |
|
| 97 |
|
| 98 |
+
def _clean_term(text):
|
| 99 |
+
text = re.sub(r"[?!.\"]", "", str(text or "")).strip(" :,-")
|
| 100 |
+
text = re.sub(
|
| 101 |
+
r"^(explain|teach me|show me|tell me about|what is|what's|whats|how is|how are|compare)\s+",
|
| 102 |
+
"", text, flags=re.I,
|
| 103 |
+
)
|
| 104 |
+
text = re.split(
|
| 105 |
+
r"\s+(?:for|if|when|because|in order to|so that)\s+",
|
| 106 |
+
text, maxsplit=1, flags=re.I,
|
| 107 |
+
)[0]
|
| 108 |
+
words = text.split()[:4]
|
| 109 |
+
text = " ".join(words).strip(" :,-")
|
| 110 |
+
known = {"linkedin": "LinkedIn", "linkme": "Linkme"}
|
| 111 |
+
return known.get(text.lower(), text[:24] or "Option")
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def _comparison_terms(question, say=""):
|
| 115 |
+
src = str(question or "") or str(say or "")
|
| 116 |
+
patterns = [
|
| 117 |
+
r"(.+?)\s+(?:vs\.?|versus)\s+(.+?)(?:[?.,]|$)",
|
| 118 |
+
r"(?:difference between|compare)\s+(.+?)\s+(?:and|with|to)\s+(.+?)(?:[?.,]|$)",
|
| 119 |
+
]
|
| 120 |
+
for pat in patterns:
|
| 121 |
+
m = re.search(pat, src, flags=re.I)
|
| 122 |
+
if not m:
|
| 123 |
+
continue
|
| 124 |
+
left, right = _clean_term(m.group(1)), _clean_term(m.group(2))
|
| 125 |
+
if left and right and left.lower() != right.lower():
|
| 126 |
+
return left, right
|
| 127 |
+
return None
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _comparison_lines(name, question, say):
|
| 131 |
+
low = name.lower()
|
| 132 |
+
if low == "linkme":
|
| 133 |
+
return ["one shareable URL", "bio / portfolio links", "send people outward"]
|
| 134 |
+
if low == "linkedin":
|
| 135 |
+
return ["career profile", "jobs + recruiters", "professional network"]
|
| 136 |
+
|
| 137 |
+
text = str(say or question or "")
|
| 138 |
+
for sent in re.split(r"[.;]\s*", text):
|
| 139 |
+
if low not in sent.lower():
|
| 140 |
+
continue
|
| 141 |
+
lines = _note_lines(sent)
|
| 142 |
+
if lines:
|
| 143 |
+
return lines[:3]
|
| 144 |
+
return ["main purpose", "best-fit use", "when to choose it"]
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _comparison(i, question="", say=""):
|
| 148 |
+
left, right = _comparison_terms(question, say) or ("Option A", "Option B")
|
| 149 |
+
left_lines = _comparison_lines(left, question, say)
|
| 150 |
+
right_lines = _comparison_lines(right, question, say)
|
| 151 |
+
if {left.lower(), right.lower()} == {"linkme", "linkedin"}:
|
| 152 |
+
linkme = left if left.lower() == "linkme" else right
|
| 153 |
+
linkedin = left if left.lower() == "linkedin" else right
|
| 154 |
+
decision = f"{linkme}: links; {linkedin}: jobs"
|
| 155 |
+
else:
|
| 156 |
+
decision = "Choose by goal, not name"
|
| 157 |
+
steps = [
|
| 158 |
+
[
|
| 159 |
+
{"op": "title", "text": f"{left} vs {right}"},
|
| 160 |
+
{"op": "box", "at": [8, 25], "w": 30, "h": 10,
|
| 161 |
+
"label": left, "color": "blue"},
|
| 162 |
+
{"op": "box", "at": [62, 25], "w": 30, "h": 10,
|
| 163 |
+
"label": right, "color": "purple"},
|
| 164 |
+
{"op": "line", "from": [50, 22], "to": [50, 68],
|
| 165 |
+
"color": "gray", "dash": True},
|
| 166 |
+
],
|
| 167 |
+
[
|
| 168 |
+
{"op": "notes", "title": f"{left} fits",
|
| 169 |
+
"lines": left_lines, "at": [8, 42], "color": "blue",
|
| 170 |
+
"compact": True},
|
| 171 |
+
],
|
| 172 |
+
[
|
| 173 |
+
{"op": "notes", "title": f"{right} fits",
|
| 174 |
+
"lines": right_lines, "at": [62, 42], "color": "purple",
|
| 175 |
+
"compact": True},
|
| 176 |
+
],
|
| 177 |
+
[
|
| 178 |
+
{"op": "text", "text": decision, "at": [35, 19],
|
| 179 |
+
"size": "s", "color": "green", "fit_w": 42},
|
| 180 |
+
],
|
| 181 |
+
]
|
| 182 |
+
return steps[i] if i < len(steps) else None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
def _topic_title(question, fallback="How It Works"):
|
| 186 |
text = re.sub(r"[?!.\"]", "", str(question or "")).strip()
|
| 187 |
text = re.sub(r"^(explain|teach me|show me|tell me about)\s+", "", text, flags=re.I)
|
engine.py
CHANGED
|
@@ -233,6 +233,10 @@ Rules for "steps":
|
|
| 233 |
- For "how X works" lessons, do not stop at two boxes. Draw the working system:
|
| 234 |
3 to 5 topic-specific parts, causal arrows with short labels, and one compact
|
| 235 |
note block that explains what the diagram means.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
- Values like "a = 3" are lines inside the notes block, NEVER standalone text ops.
|
| 237 |
- Each step draws ONLY NEW marks. Never redraw the title, the diagram, or anything
|
| 238 |
already on the board — duplicates are discarded.
|
|
|
|
| 233 |
- For "how X works" lessons, do not stop at two boxes. Draw the working system:
|
| 234 |
3 to 5 topic-specific parts, causal arrows with short labels, and one compact
|
| 235 |
note block that explains what the diagram means.
|
| 236 |
+
- For comparison lessons ("X vs Y", "compare X and Y", "difference between X and Y"),
|
| 237 |
+
use a two-column comparison board with one side for each named thing, short feature
|
| 238 |
+
notes under each side, and a compact takeaway. Do not stack the two options
|
| 239 |
+
vertically or connect them with arrows unless there is a real causal relationship.
|
| 240 |
- Values like "a = 3" are lines inside the notes block, NEVER standalone text ops.
|
| 241 |
- Each step draws ONLY NEW marks. Never redraw the title, the diagram, or anything
|
| 242 |
already on the board — duplicates are discarded.
|
mock_engine.py
CHANGED
|
@@ -44,14 +44,23 @@ def _step(say, board):
|
|
| 44 |
|
| 45 |
|
| 46 |
def _templated_lesson(topic):
|
| 47 |
-
|
|
|
|
| 48 |
return None
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
out = []
|
| 56 |
for i, say in enumerate(says):
|
| 57 |
board = improve_step_board(topic, i, say, [])
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def _templated_lesson(topic):
|
| 47 |
+
family = diagram_family(topic)
|
| 48 |
+
if family not in {"pythagorean", "comparison"}:
|
| 49 |
return None
|
| 50 |
+
if family == "pythagorean":
|
| 51 |
+
says = [
|
| 52 |
+
"Let's identify the right triangle first: the two legs make the L shape, and c is the hypotenuse across from it.",
|
| 53 |
+
"The theorem says the square on c equals the two leg squares added together.",
|
| 54 |
+
"For a concrete example, set a to three and b to four, then substitute those values into the formula.",
|
| 55 |
+
"That gives twenty five, so c is five. Notice five is the longest side, which matches the drawing.",
|
| 56 |
+
]
|
| 57 |
+
else:
|
| 58 |
+
says = [
|
| 59 |
+
f"Let's compare {topic} by purpose instead of drawing a generic flowchart.",
|
| 60 |
+
"The left side is for the first option's best-fit job and the context where it makes sense.",
|
| 61 |
+
"The right side is for the second option's job, so the differences stay parallel and easy to scan.",
|
| 62 |
+
"The takeaway is to choose based on what you are trying to accomplish, not which name sounds broader.",
|
| 63 |
+
]
|
| 64 |
out = []
|
| 65 |
for i, say in enumerate(says):
|
| 66 |
board = improve_step_board(topic, i, say, [])
|
test_board_quality.py
CHANGED
|
@@ -80,6 +80,27 @@ class BoardQualityTests(unittest.TestCase):
|
|
| 80 |
xs = [p[0] for p in tri["points"]]
|
| 81 |
self.assertLessEqual(max(xs) - min(xs), 40)
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
def test_fable_mythos_access_template_matches_directive(self):
|
| 84 |
self.assertEqual(
|
| 85 |
diagram_family("Anthropic Fable 5 Mythos 5 government ban access"),
|
|
@@ -104,6 +125,7 @@ class BoardQualityTests(unittest.TestCase):
|
|
| 104 |
self.assertEqual(diagram_family("How do neural networks learn?"), "neural_network")
|
| 105 |
self.assertEqual(diagram_family("Explain supply and demand"), "supply_demand")
|
| 106 |
self.assertEqual(diagram_family("How does Unqork work?"), "no_code_platform")
|
|
|
|
| 107 |
self.assertEqual(diagram_family("What happened in space exploration this month?"), None)
|
| 108 |
|
| 109 |
def test_checkin_steps_stay_empty_when_model_drew_nothing(self):
|
|
@@ -124,6 +146,8 @@ class BoardQualityTests(unittest.TestCase):
|
|
| 124 |
"How does photosynthesis work?",
|
| 125 |
"How do rainbows form?",
|
| 126 |
"Explain the water cycle",
|
|
|
|
|
|
|
| 127 |
]
|
| 128 |
for question in cases:
|
| 129 |
with self.subTest(question=question):
|
|
|
|
| 80 |
xs = [p[0] for p in tri["points"]]
|
| 81 |
self.assertLessEqual(max(xs) - min(xs), 40)
|
| 82 |
|
| 83 |
+
def test_unique_comparisons_get_structured_two_column_board(self):
|
| 84 |
+
self.assertEqual(diagram_family("Linkme vs LinkedIn"), "comparison")
|
| 85 |
+
ns = load_engine_ns()
|
| 86 |
+
question = "Linkme vs LinkedIn"
|
| 87 |
+
violations, ops = simulate_lesson(ns, _template_steps(question))
|
| 88 |
+
self.assertEqual(violations, [])
|
| 89 |
+
parts = []
|
| 90 |
+
for op in ops:
|
| 91 |
+
parts.extend(str(op.get(k, "")) for k in ("text", "label", "title"))
|
| 92 |
+
parts.extend(str(line) for line in op.get("lines", []) or [])
|
| 93 |
+
text = " ".join(parts)
|
| 94 |
+
self.assertIn("Linkme", text)
|
| 95 |
+
self.assertIn("LinkedIn", text)
|
| 96 |
+
self.assertIn("one shareable URL", text)
|
| 97 |
+
self.assertIn("jobs + recruiters", text)
|
| 98 |
+
boxes = [op for op in ops if op.get("op") == "box"]
|
| 99 |
+
self.assertEqual(len(boxes), 2)
|
| 100 |
+
self.assertLess(boxes[0]["at"][0], 40)
|
| 101 |
+
self.assertGreater(boxes[1]["at"][0], 55)
|
| 102 |
+
self.assertFalse(any(op.get("op") == "arrow" for op in ops))
|
| 103 |
+
|
| 104 |
def test_fable_mythos_access_template_matches_directive(self):
|
| 105 |
self.assertEqual(
|
| 106 |
diagram_family("Anthropic Fable 5 Mythos 5 government ban access"),
|
|
|
|
| 125 |
self.assertEqual(diagram_family("How do neural networks learn?"), "neural_network")
|
| 126 |
self.assertEqual(diagram_family("Explain supply and demand"), "supply_demand")
|
| 127 |
self.assertEqual(diagram_family("How does Unqork work?"), "no_code_platform")
|
| 128 |
+
self.assertEqual(diagram_family("Compare Notion and Airtable"), "comparison")
|
| 129 |
self.assertEqual(diagram_family("What happened in space exploration this month?"), None)
|
| 130 |
|
| 131 |
def test_checkin_steps_stay_empty_when_model_drew_nothing(self):
|
|
|
|
| 146 |
"How does photosynthesis work?",
|
| 147 |
"How do rainbows form?",
|
| 148 |
"Explain the water cycle",
|
| 149 |
+
"Linkme vs LinkedIn",
|
| 150 |
+
"Compare Notion and Airtable",
|
| 151 |
]
|
| 152 |
for question in cases:
|
| 153 |
with self.subTest(question=question):
|