JacobLinCool Codex commited on
Commit
1ec322d
·
verified ·
1 Parent(s): 6d9770a

fix: tighten advisor command routing

Browse files

Co-authored-by: Codex <noreply@openai.com>

hackathon_advisor/model_runtime.py CHANGED
@@ -75,13 +75,53 @@ class RuleBasedPlanner:
75
  output = '<function name="list_projects">{"sort":"likes"}</function>'
76
  elif project_id:
77
  output = f'<function name="get_project">{{"id":{_json_string(project_id)}}}</function>'
78
- elif _has_command_term(lower, ("compare", "choose", "rank")):
79
  output = '<function name="compare_ideas">{}</function>'
80
- elif _has_command_term(lower, ("plan", "roadmap", "next step", "milestone")):
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  output = '<function name="make_plan">{}</function>'
82
- elif _has_command_term(lower, ("whitespace", "original", "new", "bolder", "unwritten", "gap")):
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  output = '<function name="find_whitespace">{}</function>'
84
- elif _has_command_term(lower, ("search", "similar", "already", "existing", "overlap", "echo")):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  output = f'<function name="search_projects">{{"query":{_json_string(text)}}}</function>'
86
  else:
87
  title, pitch = idea_from_text(text)
@@ -412,11 +452,8 @@ def _wants_project_list(lower_text: str) -> bool:
412
  return lower_text in exact_phrases or any(lower_text.startswith(prefix) for prefix in command_prefixes)
413
 
414
 
415
- def _has_command_term(lower_text: str, terms: tuple[str, ...]) -> bool:
416
- return any(
417
- re.search(rf"(?<![a-z0-9]){re.escape(term)}(?![a-z0-9])", lower_text)
418
- for term in terms
419
- )
420
 
421
 
422
  def _project_reference_id(text: str) -> str:
 
75
  output = '<function name="list_projects">{"sort":"likes"}</function>'
76
  elif project_id:
77
  output = f'<function name="get_project">{{"id":{_json_string(project_id)}}}</function>'
78
+ elif _matches_command(lower, ("compare", "compare ideas", "choose", "rank", "rank ideas")):
79
  output = '<function name="compare_ideas">{}</function>'
80
+ elif _matches_command(
81
+ lower,
82
+ (
83
+ "plan",
84
+ "make a plan",
85
+ "make a build plan",
86
+ "draft a plan",
87
+ "draft a build plan",
88
+ "build plan",
89
+ "roadmap",
90
+ "next step",
91
+ "milestone",
92
+ ),
93
+ ):
94
  output = '<function name="make_plan">{}</function>'
95
+ elif _matches_command(
96
+ lower,
97
+ (
98
+ "gap",
99
+ "find gap",
100
+ "find a gap",
101
+ "find whitespace",
102
+ "write bolder",
103
+ "bolder",
104
+ "unwritten",
105
+ "make it more original",
106
+ "new direction",
107
+ ),
108
+ ):
109
  output = '<function name="find_whitespace">{}</function>'
110
+ elif _matches_command(
111
+ lower,
112
+ (
113
+ "search",
114
+ "search for",
115
+ "find similar",
116
+ "similar",
117
+ "is this already",
118
+ "already built",
119
+ "check overlap",
120
+ "overlap",
121
+ "show echoes",
122
+ "echo",
123
+ ),
124
+ ):
125
  output = f'<function name="search_projects">{{"query":{_json_string(text)}}}</function>'
126
  else:
127
  title, pitch = idea_from_text(text)
 
452
  return lower_text in exact_phrases or any(lower_text.startswith(prefix) for prefix in command_prefixes)
453
 
454
 
455
+ def _matches_command(lower_text: str, phrases: tuple[str, ...]) -> bool:
456
+ return lower_text in phrases or any(lower_text.startswith(f"{phrase} ") for phrase in phrases)
 
 
 
457
 
458
 
459
  def _project_reference_id(text: str) -> str:
static/app.js CHANGED
@@ -89,6 +89,8 @@ document.querySelectorAll("[data-command]").forEach((button) => {
89
  });
90
  });
91
 
 
 
92
  demoButton.addEventListener("click", async () => {
93
  await loadDemoSession();
94
  });
@@ -1204,6 +1206,37 @@ function clearTurnWatchdog() {
1204
  }
1205
  }
1206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1207
  // Coarse overall completion per stage, so the bar always advances even when token-level
1208
  // progress is unknown (e.g. the rules backend, or the fast tool/writing stages).
1209
  const STAGE_PROGRESS = { planning: 8, running_tool: 85, writing: 95 };
 
89
  });
90
  });
91
 
92
+ setupMenus();
93
+
94
  demoButton.addEventListener("click", async () => {
95
  await loadDemoSession();
96
  });
 
1206
  }
1207
  }
1208
 
1209
+ function setupMenus() {
1210
+ document.querySelectorAll(".menu").forEach((menu) => {
1211
+ const trigger = menu.querySelector(".menu-trigger");
1212
+ const list = menu.querySelector(".menu-list");
1213
+ if (!trigger || !list) return;
1214
+ trigger.addEventListener("click", (event) => {
1215
+ event.stopPropagation();
1216
+ const willOpen = list.hidden;
1217
+ closeAllMenus();
1218
+ if (willOpen) {
1219
+ list.hidden = false;
1220
+ trigger.setAttribute("aria-expanded", "true");
1221
+ }
1222
+ });
1223
+ list.addEventListener("click", () => closeAllMenus()); // selecting an item closes the menu
1224
+ });
1225
+ document.addEventListener("click", () => closeAllMenus());
1226
+ document.addEventListener("keydown", (event) => {
1227
+ if (event.key === "Escape") closeAllMenus();
1228
+ });
1229
+ }
1230
+
1231
+ function closeAllMenus() {
1232
+ document.querySelectorAll(".menu-list").forEach((list) => {
1233
+ list.hidden = true;
1234
+ });
1235
+ document.querySelectorAll(".menu-trigger").forEach((trigger) => {
1236
+ trigger.setAttribute("aria-expanded", "false");
1237
+ });
1238
+ }
1239
+
1240
  // Coarse overall completion per stage, so the bar always advances even when token-level
1241
  // progress is unknown (e.g. the rules backend, or the fast tool/writing stages).
1242
  const STAGE_PROGRESS = { planning: 8, running_tool: 85, writing: 95 };
static/index.html CHANGED
@@ -132,91 +132,76 @@
132
  </div>
133
 
134
  <div class="toolbar command-row" aria-label="Advisor commands">
135
- <button id="submit" class="btn btn-ink" type="submit" title="Score this idea">
136
  <svg class="icon"><use href="#icon-quill"></use></svg>
137
- Ink
138
- </button>
139
- <button
140
- type="button"
141
- class="btn"
142
- data-command="write bolder and find whitespace"
143
- title="Find an under-explored direction"
144
- >
145
- <svg class="icon"><use href="#icon-gap"></use></svg>
146
- Gap
147
- </button>
148
- <button
149
- type="button"
150
- class="btn"
151
- data-command="make a build plan"
152
- title="Draft a build plan for the current idea"
153
- >
154
- <svg class="icon"><use href="#icon-plan"></use></svg>
155
- Plan
156
- </button>
157
- <button
158
- type="button"
159
- class="btn"
160
- data-command="compare ideas"
161
- title="Compare saved ideas"
162
- >
163
- <svg class="icon"><use href="#icon-rank"></use></svg>
164
- Compare
165
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  <span class="spacer"></span>
167
- <button
168
- type="button"
169
- id="load-demo"
170
- class="btn btn-ghost"
171
- title="Load a sample session"
172
- aria-label="Load example idea board"
173
- >
174
- <svg class="icon"><use href="#icon-example"></use></svg>
175
- Example
176
- </button>
177
- <button
178
- type="button"
179
- id="export-notes"
180
- class="btn btn-ghost"
181
- title="Export build notes"
182
- aria-label="Export build notes"
183
- disabled
184
- >
185
- <svg class="icon"><use href="#icon-download"></use></svg>
186
- Notes
187
- </button>
188
- <button
189
- type="button"
190
- id="export-chapter"
191
- class="btn btn-ghost"
192
- title="Export the idea-board chapter"
193
- aria-label="Export idea-board chapter"
194
- disabled
195
- >
196
- <svg class="icon"><use href="#icon-download"></use></svg>
197
- Chapter
198
- </button>
199
- <button
200
- type="button"
201
- id="export-artifact"
202
- class="btn btn-ghost"
203
- title="Export the current page"
204
- aria-label="Export current page as PNG"
205
- disabled
206
- >
207
- <svg class="icon"><use href="#icon-download"></use></svg>
208
- PNG
209
- </button>
210
- <button
211
- type="button"
212
- id="reset-session"
213
- class="btn btn-ghost btn-icon"
214
- title="Reset the session"
215
- aria-label="Reset session"
216
- >
217
- <svg class="icon"><use href="#icon-reset"></use></svg>
218
- <span class="sr-only">Reset</span>
219
- </button>
220
  </div>
221
  </form>
222
 
 
132
  </div>
133
 
134
  <div class="toolbar command-row" aria-label="Advisor commands">
135
+ <button id="submit" class="btn btn-ink" type="submit" title="Score this idea against the map">
136
  <svg class="icon"><use href="#icon-quill"></use></svg>
137
+ Score idea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  </button>
139
+
140
+ <div class="menu" data-menu="actions">
141
+ <button
142
+ type="button"
143
+ id="actions-trigger"
144
+ class="btn menu-trigger"
145
+ aria-haspopup="true"
146
+ aria-expanded="false"
147
+ title="More advisor actions"
148
+ >
149
+ Actions
150
+ <span class="caret" aria-hidden="true">▾</span>
151
+ </button>
152
+ <div class="menu-list" id="actions-menu" role="menu" hidden>
153
+ <button type="button" class="menu-item" role="menuitem" data-command="write bolder and find whitespace">
154
+ <svg class="icon"><use href="#icon-gap"></use></svg>
155
+ <span class="mi-text"><strong>Find a gap</strong><small>An under-explored direction to try</small></span>
156
+ </button>
157
+ <button type="button" class="menu-item" role="menuitem" data-command="make a build plan">
158
+ <svg class="icon"><use href="#icon-plan"></use></svg>
159
+ <span class="mi-text"><strong>Make a plan</strong><small>Draft build steps for the current idea</small></span>
160
+ </button>
161
+ <button type="button" class="menu-item" role="menuitem" data-command="compare ideas">
162
+ <svg class="icon"><use href="#icon-rank"></use></svg>
163
+ <span class="mi-text"><strong>Compare ideas</strong><small>Rank the ideas on your board</small></span>
164
+ </button>
165
+ </div>
166
+ </div>
167
+
168
  <span class="spacer"></span>
169
+
170
+ <div class="menu" data-menu="more">
171
+ <button
172
+ type="button"
173
+ id="more-trigger"
174
+ class="btn btn-ghost menu-trigger btn-icon"
175
+ aria-haspopup="true"
176
+ aria-expanded="false"
177
+ title="Export and session options"
178
+ aria-label="More options"
179
+ >
180
+ <span class="caret-dots" aria-hidden="true">⋯</span>
181
+ </button>
182
+ <div class="menu-list menu-list-right" id="more-menu" role="menu" hidden>
183
+ <button type="button" id="load-demo" class="menu-item" role="menuitem" aria-label="Load example idea board">
184
+ <svg class="icon"><use href="#icon-example"></use></svg>
185
+ Example board
186
+ </button>
187
+ <button type="button" id="export-notes" class="menu-item" role="menuitem" aria-label="Export build notes" disabled>
188
+ <svg class="icon"><use href="#icon-download"></use></svg>
189
+ Build notes
190
+ </button>
191
+ <button type="button" id="export-chapter" class="menu-item" role="menuitem" aria-label="Export idea-board chapter" disabled>
192
+ <svg class="icon"><use href="#icon-download"></use></svg>
193
+ Chapter
194
+ </button>
195
+ <button type="button" id="export-artifact" class="menu-item" role="menuitem" aria-label="Export current page as PNG" disabled>
196
+ <svg class="icon"><use href="#icon-download"></use></svg>
197
+ Page PNG
198
+ </button>
199
+ <button type="button" id="reset-session" class="menu-item menu-item-danger" role="menuitem" aria-label="Reset session">
200
+ <svg class="icon"><use href="#icon-reset"></use></svg>
201
+ Reset session
202
+ </button>
203
+ </div>
204
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  </div>
206
  </form>
207
 
static/styles.css CHANGED
@@ -1402,3 +1402,97 @@ textarea:disabled {
1402
  transform: translateY(0);
1403
  }
1404
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1402
  transform: translateY(0);
1403
  }
1404
  }
1405
+
1406
+ /* Action / overflow menus (consolidate secondary buttons) */
1407
+ .menu {
1408
+ position: relative;
1409
+ display: inline-flex;
1410
+ }
1411
+
1412
+ .menu-trigger .caret {
1413
+ margin-left: 4px;
1414
+ font-size: 0.72em;
1415
+ }
1416
+
1417
+ .menu-trigger[aria-expanded="true"] {
1418
+ border-color: var(--ink-faint);
1419
+ }
1420
+
1421
+ .caret-dots {
1422
+ font-size: 1.05rem;
1423
+ line-height: 1;
1424
+ letter-spacing: 1px;
1425
+ }
1426
+
1427
+ .menu-list {
1428
+ position: absolute;
1429
+ top: calc(100% + 6px);
1430
+ left: 0;
1431
+ z-index: 30;
1432
+ min-width: 210px;
1433
+ display: flex;
1434
+ flex-direction: column;
1435
+ gap: 2px;
1436
+ padding: 5px;
1437
+ background: var(--paper-3);
1438
+ border: 1px solid var(--edge);
1439
+ border-radius: 9px;
1440
+ box-shadow: 0 12px 30px -12px rgba(0, 0, 0, 0.45);
1441
+ }
1442
+
1443
+ .menu-list[hidden] {
1444
+ display: none;
1445
+ }
1446
+
1447
+ .menu-list-right {
1448
+ left: auto;
1449
+ right: 0;
1450
+ }
1451
+
1452
+ .menu-item {
1453
+ display: flex;
1454
+ align-items: center;
1455
+ gap: 9px;
1456
+ width: 100%;
1457
+ padding: 8px 10px;
1458
+ border: 0;
1459
+ border-radius: 6px;
1460
+ background: transparent;
1461
+ color: var(--ink);
1462
+ font: inherit;
1463
+ text-align: left;
1464
+ cursor: pointer;
1465
+ transition: background 0.15s ease;
1466
+ }
1467
+
1468
+ .menu-item:hover:not(:disabled),
1469
+ .menu-item:focus-visible:not(:disabled) {
1470
+ background: rgba(73, 49, 22, 0.09);
1471
+ outline: none;
1472
+ }
1473
+
1474
+ .menu-item:disabled {
1475
+ opacity: 0.42;
1476
+ cursor: default;
1477
+ }
1478
+
1479
+ .menu-item .icon {
1480
+ flex: 0 0 auto;
1481
+ color: var(--ink-soft);
1482
+ }
1483
+
1484
+ .menu-item .mi-text {
1485
+ display: flex;
1486
+ flex-direction: column;
1487
+ line-height: 1.25;
1488
+ }
1489
+
1490
+ .menu-item .mi-text small {
1491
+ color: var(--ink-faint);
1492
+ font-size: 0.72em;
1493
+ }
1494
+
1495
+ .menu-item-danger:hover:not(:disabled) {
1496
+ background: rgba(154, 43, 34, 0.1);
1497
+ color: var(--oxblood);
1498
+ }
tests/test_model_runtime.py CHANGED
@@ -97,13 +97,19 @@ def test_rule_planner_keeps_project_words_inside_ideas() -> None:
97
  def test_rule_planner_does_not_match_commands_inside_idea_words() -> None:
98
  planner = RuleBasedPlanner()
99
 
100
- resolution = planner.plan(
101
  "A neighborhood seed swap archive that reminds gardeners when to plant shared seeds",
102
  {},
103
  )
 
 
 
 
104
 
105
- assert resolution.status == "valid"
106
- assert resolution.call.name == "save_idea"
 
 
107
 
108
 
109
  def test_rule_planner_splits_explicit_idea_pitch() -> None:
 
97
  def test_rule_planner_does_not_match_commands_inside_idea_words() -> None:
98
  planner = RuleBasedPlanner()
99
 
100
+ planting = planner.plan(
101
  "A neighborhood seed swap archive that reminds gardeners when to plant shared seeds",
102
  {},
103
  )
104
+ cooking_plan = planner.plan(
105
+ "A countertop helper that turns pantry leftovers into a weekly cooking plan",
106
+ {},
107
+ )
108
 
109
+ assert planting.status == "valid"
110
+ assert planting.call.name == "save_idea"
111
+ assert cooking_plan.status == "valid"
112
+ assert cooking_plan.call.name == "save_idea"
113
 
114
 
115
  def test_rule_planner_splits_explicit_idea_pitch() -> None: