Skip to content

Commit 6edb5e4

Browse files
committed
fix(playground): validate query params, render metadata, abort stale queries
1 parent 21246bd commit 6edb5e4

2 files changed

Lines changed: 69 additions & 12 deletions

File tree

packages/moss-cli/src/moss_cli/commands/playground.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import asyncio
1111
import inspect
1212
import json
13+
import math
1314
import secrets
1415
import socket
1516
import threading
@@ -122,6 +123,8 @@ class PlaygroundHandler(SimpleHTTPRequestHandler):
122123
_token: str = ""
123124
_server_host: str = ""
124125
_worker: AsyncWorker | None = None
126+
_latest_request_id: int = 0
127+
_request_lock = threading.Lock()
125128

126129
def __init__(self, *args, **kwargs):
127130
super().__init__(*args, directory=str(HERE / "playground"), **kwargs)
@@ -285,22 +288,45 @@ def _handle_post_query(self, data: dict) -> None:
285288
self._send_json(400, {"error": "Missing 'name' or 'query' in request body"})
286289
return
287290

288-
try:
289-
raw_top_k = data.get("topK")
290-
raw_alpha = data.get("alpha")
291-
top_k = int(raw_top_k) if raw_top_k is not None else None
292-
alpha = float(raw_alpha) if raw_alpha is not None else None
293-
except (TypeError, ValueError):
294-
self._send_json(400, {"error": "'topK' must be an integer and 'alpha' must be a number"})
291+
raw_request_id = data.get("requestId")
292+
if isinstance(raw_request_id, bool) or not isinstance(raw_request_id, int):
293+
self._send_json(400, {"error": "Missing or invalid 'requestId'"})
295294
return
296295

296+
raw_top_k = data.get("topK")
297+
if raw_top_k is not None:
298+
if isinstance(raw_top_k, bool) or not isinstance(raw_top_k, int):
299+
self._send_json(400, {"error": "'topK' must be an integer"})
300+
return
301+
top_k = raw_top_k
302+
else:
303+
top_k = None
304+
305+
raw_alpha = data.get("alpha")
306+
if raw_alpha is not None:
307+
if isinstance(raw_alpha, bool) or not isinstance(raw_alpha, (int, float)):
308+
self._send_json(400, {"error": "'alpha' must be a number"})
309+
return
310+
alpha = float(raw_alpha)
311+
if not math.isfinite(alpha):
312+
self._send_json(400, {"error": "'alpha' must be a finite number"})
313+
return
314+
else:
315+
alpha = None
316+
297317
if top_k is not None and (top_k < 1 or top_k > 50):
298318
self._send_json(400, {"error": "topK must be between 1 and 50"})
299319
return
300320
if alpha is not None and (alpha < 0 or alpha > 1):
301321
self._send_json(400, {"error": "alpha must be between 0 and 1"})
302322
return
303323

324+
with PlaygroundHandler._request_lock:
325+
if raw_request_id < PlaygroundHandler._latest_request_id:
326+
self._send_json(200, {"docs": [], "timeTakenMs": 0, "query": query, "superseded": True})
327+
return
328+
PlaygroundHandler._latest_request_id = raw_request_id
329+
304330
try:
305331
def _build_result(r):
306332
return {
@@ -312,6 +338,8 @@ def _build_result(r):
312338
"query": r.query,
313339
}
314340
async def _call():
341+
if raw_request_id < PlaygroundHandler._latest_request_id:
342+
return {"docs": [], "timeTakenMs": 0, "query": query, "superseded": True}
315343
result = client.query(name, query, QueryOptions(top_k=top_k, alpha=alpha))
316344
if inspect.isawaitable(result):
317345
result = await result

packages/moss-cli/src/moss_cli/playground/index.html

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
.empty-state p{font-size:13px}
6868
.hidden{display:none!important}
6969
.result-timestamp{font-size:10px;color:var(--text2);opacity:.5}
70+
.result-metadata{margin-top:8px;border-top:1px solid var(--border);padding-top:8px}
71+
.result-metadata summary{font-size:11px;color:var(--text2);cursor:pointer;user-select:none}
72+
.result-metadata pre{margin-top:6px;font-size:11px;color:var(--text2);background:var(--surface2);padding:8px;border-radius:6px;overflow-x:auto;font-family:var(--mono);line-height:1.5;white-space:pre-wrap;word-break:break-word}
7073
</style>
7174
</head>
7275
<body>
@@ -145,6 +148,7 @@ <h1>Moss <span>Playground</span></h1>
145148
let indexes = [];
146149
let currentIndex = null;
147150
let requestId = 0;
151+
let searchAbort = null;
148152

149153
const $ = id => document.getElementById(id);
150154
const select = $('index-select');
@@ -244,6 +248,8 @@ <h1>Moss <span>Playground</span></h1>
244248

245249
const loadReqId = ++requestId;
246250
if (searchTimeout) clearTimeout(searchTimeout);
251+
if (searchAbort) searchAbort.abort();
252+
searchAbort = null;
247253
select.disabled = true;
248254

249255
loadBtn.disabled = true;
@@ -337,6 +343,18 @@ <h1>Moss <span>Playground</span></h1>
337343
textDiv.textContent = doc.text;
338344
item.appendChild(textDiv);
339345

346+
if (doc.metadata && typeof doc.metadata === 'object' && Object.keys(doc.metadata).length > 0) {
347+
const details = document.createElement('details');
348+
details.className = 'result-metadata';
349+
const summary = document.createElement('summary');
350+
summary.textContent = 'Metadata';
351+
details.appendChild(summary);
352+
const pre = document.createElement('pre');
353+
pre.textContent = JSON.stringify(doc.metadata, null, 2);
354+
details.appendChild(pre);
355+
item.appendChild(details);
356+
}
357+
340358
results.appendChild(item);
341359
});
342360
}
@@ -355,6 +373,10 @@ <h1>Moss <span>Playground</span></h1>
355373

356374
const query = searchInput.value.trim();
357375
if (!query) {
376+
if (searchTimeout) clearTimeout(searchTimeout);
377+
if (searchAbort) searchAbort.abort();
378+
searchAbort = null;
379+
++requestId;
358380
results.innerHTML = '';
359381
const empty = document.createElement('div');
360382
empty.className = 'empty-state';
@@ -363,13 +385,17 @@ <h1>Moss <span>Playground</span></h1>
363385
return;
364386
}
365387

366-
const parsedTopK = parseInt(topkInput.value, 10);
367-
const topK = Number.isNaN(parsedTopK) ? 5 : parsedTopK;
388+
const rawTopK = topkInput.value.trim();
389+
const parsedTopK = Number(rawTopK);
390+
const topK = rawTopK === '' || Number.isNaN(parsedTopK) ? 5 : parsedTopK;
368391
const parsedAlpha = parseFloat(alphaSlider.value);
369392
const alpha = Number.isNaN(parsedAlpha) ? 0.5 : parsedAlpha;
370393

371394
if (searchTimeout) clearTimeout(searchTimeout);
372395
const thisRequestId = ++requestId;
396+
if (searchAbort) searchAbort.abort();
397+
searchAbort = new AbortController();
398+
const thisAbort = searchAbort;
373399
const idx = currentIndex;
374400
searchTimeout = setTimeout(async () => {
375401
const placeholder = results.querySelector('.empty-state');
@@ -379,17 +405,18 @@ <h1>Moss <span>Playground</span></h1>
379405
const res = await apiFetch('/api/query', {
380406
method: 'POST',
381407
headers: { 'Content-Type': 'application/json' },
382-
body: JSON.stringify({ name: idx, query, topK, alpha }),
408+
signal: thisAbort.signal,
409+
body: JSON.stringify({ name: idx, query, topK, alpha, requestId: thisRequestId }),
383410
});
384411
if (!res.ok) {
385412
const err = await res.json();
386413
throw new Error(err.error || 'Query failed');
387414
}
388415
const searchResults = await res.json();
389-
if (thisRequestId !== requestId) return;
416+
if (thisRequestId !== requestId || thisAbort.signal.aborted) return;
390417
renderResults(searchResults);
391418
} catch (e) {
392-
if (thisRequestId !== requestId) return;
419+
if (thisRequestId !== requestId || thisAbort.signal.aborted) return;
393420
console.error('Query failed:', e);
394421
results.innerHTML = `<div class="status-box status-error">Query failed: ${escapeHtml(e.message || e)}</div>`;
395422
}
@@ -399,6 +426,8 @@ <h1>Moss <span>Playground</span></h1>
399426
select.addEventListener('change', () => {
400427
++requestId;
401428
if (searchTimeout) clearTimeout(searchTimeout);
429+
if (searchAbort) searchAbort.abort();
430+
searchAbort = null;
402431
results.innerHTML = '';
403432
const empty = document.createElement('div');
404433
empty.className = 'empty-state';

0 commit comments

Comments
 (0)