From 4530ab130d47456a626d12268f2da2438aafc20b Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 03:03:46 +0530 Subject: [PATCH 01/34] UX v2 Phase 0: unify connection status across the three indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header pill, the home landing line, and the agent dock each computed connection state from their own signal at their own time. home.js read state.connected exactly once at tab init — before the /ws handshake — and never corrected, so the landing showed "Offline — start the agent to connect" while the header pill showed "Online" and state.connected was true. Add a single sticky ConnectionStatus store (status-store.js) holding three distinct signals — gentlyConnected (/ws), microscopeConnected (/api/device-status poll), agentConnected (/ws/agent) — which replays its current snapshot to every new subscriber, so a late subscriber can never miss the initial state (the root of the bug). All three surfaces now read from / write to this store: - websocket.js onopen/onclose -> setGently (via updateGentlyStatus) - app.js fetchDeviceStatus -> setMicroscope; header renders via subscriber - home.js updateStatus reads the store and re-renders on every change - agent-chat.js setConn -> setAgent Verified live: after reload the home line and header pill agree (no more "Offline while Online"); no console errors. Bug #3 (idle event-count inflation) needs no code change: the high-frequency telemetry (DEVICE_STATE_UPDATE/BOTTOM_CAMERA_FRAME) is already excluded from the events table + count at websocket.js, and idle measurement showed the count is calm and dominated by LOG_RECORD. Co-Authored-By: Claude Fable 5 --- gently/ui/web/static/js/agent-chat.js | 2 + gently/ui/web/static/js/app.js | 37 ++++++++++++----- gently/ui/web/static/js/home.js | 14 ++++++- gently/ui/web/static/js/status-store.js | 54 +++++++++++++++++++++++++ gently/ui/web/templates/index.html | 1 + 5 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 gently/ui/web/static/js/status-store.js diff --git a/gently/ui/web/static/js/agent-chat.js b/gently/ui/web/static/js/agent-chat.js index 63339b92..620c9d60 100644 --- a/gently/ui/web/static/js/agent-chat.js +++ b/gently/ui/web/static/js/agent-chat.js @@ -636,6 +636,8 @@ const AgentChat = (() => { conn.classList.toggle('ac-conn-bad', !ok); conn.textContent = label || (ok ? 'Connected' : 'Reconnecting…'); if (toggleDot) toggleDot.classList.toggle('ok', ok); + // Feed the shared connection store (agent /ws/agent liveness). + if (typeof ConnectionStatus !== 'undefined') ConnectionStatus.setAgent(ok); } // ── Transport ───────────────────────────────────────────── diff --git a/gently/ui/web/static/js/app.js b/gently/ui/web/static/js/app.js index d1e75a72..3395c89b 100644 --- a/gently/ui/web/static/js/app.js +++ b/gently/ui/web/static/js/app.js @@ -538,11 +538,13 @@ function fetchDeviceStatus() { .then(r => r.json()) .then(data => { _microscopeConnected = data.microscope; - _setBadge('status-microscope-badge', data.microscope, 'Online', 'Offline'); - updateTopLevelDot(); + ConnectionStatus.setMicroscope(data.microscope); }) .catch(() => { - _setBadge('status-microscope-badge', false, '', '--'); + // Transient poll failure: keep the last-known badge. The next + // successful poll re-renders via the store if the value changed + // (writing '--' here could stick, since the store only re-renders + // on an actual change, not on an unchanged success). }); } @@ -555,23 +557,26 @@ function _setBadge(id, isOn, onText, offText) { } function updateGentlyStatus(connected) { - _setBadge('status-gently-badge', connected, 'Online', 'Offline'); - updateTopLevelDot(); + // Feed the single source of truth; the header re-renders via the + // ConnectionStatus subscriber (renderConnectionUI). + ConnectionStatus.setGently(connected); } -function updateTopLevelDot() { +// Single renderer for the header connection UI, driven by a ConnectionStatus +// snapshot. Subscribed once at startup, so the pill, both popover badges, and +// the dot always reflect the same shared state. +function renderConnectionUI(s) { + _setBadge('status-gently-badge', s.gentlyConnected, 'Online', 'Offline'); + _setBadge('status-microscope-badge', s.microscopeConnected, 'Online', 'Offline'); const dot = document.getElementById('status-dot'); const text = document.getElementById('status-text'); if (!dot || !text) return; - const gentlyUp = state.connected; - const scopeUp = _microscopeConnected; - dot.classList.remove('connected', 'partial'); - if (gentlyUp && scopeUp) { + if (s.gentlyConnected && s.microscopeConnected) { dot.classList.add('connected'); text.textContent = 'Connected'; - } else if (gentlyUp) { + } else if (s.gentlyConnected) { dot.classList.add('partial'); text.textContent = 'Online'; } else { @@ -579,6 +584,11 @@ function updateTopLevelDot() { } } +// Back-compat shim: any legacy caller re-renders from the current snapshot. +function updateTopLevelDot() { + renderConnectionUI(ConnectionStatus.get()); +} + document.addEventListener('DOMContentLoaded', () => { // Initialize presence manager (before WebSocket so ID is ready) PresenceManager.init(); @@ -620,6 +630,11 @@ document.addEventListener('DOMContentLoaded', () => { } }); + // Connection status: one source of truth, three writers (this /ws, the + // device-status poll, and the agent /ws/agent). Subscribe the header + // renderer BEFORE connecting so the first handshake renders correctly. + ConnectionStatus.subscribe(renderConnectionUI); + // Start WebSocket connection connectWebSocket(); diff --git a/gently/ui/web/static/js/home.js b/gently/ui/web/static/js/home.js index 089d7de3..bf5cad17 100644 --- a/gently/ui/web/static/js/home.js +++ b/gently/ui/web/static/js/home.js @@ -136,7 +136,13 @@ const HomeApp = (() => { function updateStatus() { const el = document.getElementById('home-status'); if (!el) return; - const connected = (typeof state !== 'undefined' && state.connected); + // Read the shared ConnectionStatus store, not a one-shot snapshot of + // state.connected — the latter was read once at tab init (before the + // /ws handshake) and never corrected, showing "Offline" while the + // header pill said "Online". + const connected = (typeof ConnectionStatus !== 'undefined') + ? ConnectionStatus.get().gentlyConnected + : (typeof state !== 'undefined' && state.connected); const n = (typeof state !== 'undefined' && Array.isArray(state.embryos)) ? state.embryos.length : 0; el.textContent = connected ? `Connected · ${n} embryo${n === 1 ? '' : 's'} in view` @@ -162,6 +168,12 @@ const HomeApp = (() => { if (AgentChat.runCommand) setTimeout(() => AgentChat.runCommand('/wizard'), 250); } }); + // Re-render the status line on every connection change. subscribe() + // replays the current snapshot immediately, so a late init still + // renders correct state. Registered once (inside the _inited guard). + if (typeof ConnectionStatus !== 'undefined') { + ConnectionStatus.subscribe(() => updateStatus()); + } } refresh(); // re-fetch on every entry to the tab } diff --git a/gently/ui/web/static/js/status-store.js b/gently/ui/web/static/js/status-store.js new file mode 100644 index 00000000..0e50dd30 --- /dev/null +++ b/gently/ui/web/static/js/status-store.js @@ -0,0 +1,54 @@ +/** + * ConnectionStatus — the single source of truth for connection liveness. + * + * Fixes the "three disagreeing indicators" bug where the header pill, the home + * landing line, and the agent dock each computed connection state from their + * own signal at their own time (home.js read state.connected ONCE at tab init, + * before the /ws handshake, and never corrected — showing "Offline" while the + * header showed "Online"). + * + * Three genuinely distinct signals (kept separate, not flattened): + * - gentlyConnected : the main /ws telemetry socket (websocket.js) + * - microscopeConnected : the /api/device-status health poll (app.js) + * - agentConnected : the /ws/agent chat socket (agent-chat.js) + * + * Writers call set*(); readers subscribe(). The store is STICKY: subscribe() + * immediately replays the current snapshot to the new subscriber, so a late + * subscriber can never miss the initial state. Events only fire on real change. + */ +const ConnectionStatus = (() => { + const s = { gentlyConnected: false, microscopeConnected: false, agentConnected: false }; + + function emit() { + if (typeof ClientEventBus !== 'undefined') { + ClientEventBus.emit('CONNECTION_STATUS', { ...s }); + } + } + + function set(key, val) { + val = !!val; + if (s[key] === val) return; // only emit on actual change + s[key] = val; + emit(); + } + + return { + setGently(v) { set('gentlyConnected', v); }, + setMicroscope(v) { set('microscopeConnected', v); }, + setAgent(v) { set('agentConnected', v); }, + get() { return { ...s }; }, + + /** + * Subscribe to status changes AND immediately receive the current + * snapshot (sticky replay). This is the guard against the original bug: + * a subscriber that registers after the first emit still renders from + * the correct current state instead of a stale default. + */ + subscribe(handler) { + if (typeof ClientEventBus !== 'undefined') { + ClientEventBus.on('CONNECTION_STATUS', handler); + } + try { handler({ ...s }); } catch (e) { console.error('ConnectionStatus subscriber error', e); } + } + }; +})(); diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index 85a60345..9aaf279a 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -616,6 +616,7 @@

Properties

+ From 4571a58cba470a1e704c4fd66fa0344a80e69057 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 03:23:23 +0530 Subject: [PATCH 02/34] UX v2 Phase 1 (scaffold): add GENTLY_UX_V2 feature flag Adds UISettings.ux_v2 (env GENTLY_UX_V2, default off) and threads it into the index.html template context from pages.py. This is the coexistence switch for the agent-first UX: the v2 markup/JS will mount only under this flag, so the v1 dashboard stays the default and prod is unaffected while the migration soaks behind the flag. No behaviour change yet (flag off by default; nothing reads it client-side until the Phase 1 dual-render lands). Co-Authored-By: Claude Fable 5 --- gently/settings.py | 11 +++++++++++ gently/ui/web/routes/pages.py | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/gently/settings.py b/gently/settings.py index 68cebd38..8b16f871 100644 --- a/gently/settings.py +++ b/gently/settings.py @@ -120,6 +120,16 @@ class TransferSettings: ) +@dataclass(frozen=True) +class UISettings: + """Web UI feature flags.""" + # New agent-first UX paradigm (welcome→shell unfold, dual-rendered agent + # asks, inference-first plan mode, shared-visibility surface). Off by + # default; flip per-deployment via GENTLY_UX_V2=1 while the migration + # soaks behind the flag. The v1 dashboard stays the default until then. + ux_v2: bool = field(default_factory=lambda: _env("UX_V2", False)) + + @dataclass(frozen=True) class Settings: """Top-level settings container.""" @@ -132,6 +142,7 @@ class Settings: api: ApiSettings = field(default_factory=ApiSettings) ml: MlSettings = field(default_factory=MlSettings) transfer: TransferSettings = field(default_factory=TransferSettings) + ui: UISettings = field(default_factory=UISettings) # Singleton — import this everywhere diff --git a/gently/ui/web/routes/pages.py b/gently/ui/web/routes/pages.py index 3858f4e7..04a1a959 100644 --- a/gently/ui/web/routes/pages.py +++ b/gently/ui/web/routes/pages.py @@ -3,6 +3,8 @@ from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse, RedirectResponse +from gently.settings import settings + def create_router(server) -> APIRouter: router = APIRouter() @@ -16,7 +18,9 @@ async def index(request: Request): chat window's "Sign in" affordance), not a gate on the page itself. """ return server.templates.TemplateResponse( - request, "index.html", {"active_section": "embryos", "is_live": True} + request, + "index.html", + {"active_section": "embryos", "is_live": True, "ux_v2": settings.ui.ux_v2}, ) # Standalone URLs redirect to SPA with hash fragment for tab routing From 17e66cbfb81567dc2b69aee6c2c20ea24f4601dd Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 03:58:13 +0530 Subject: [PATCH 03/34] UX v2 Phase 1: dual-render agent asks (chat transcript + main stage) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent's structured asks (choice_request / choice_response over /ws/agent) already ARE the one-payload protocol. This makes the SAME ask render both in the chat transcript and prominently on a new main-stage surface (#ask-stage), behind GENTLY_UX_V2 — the foundation for the agent-first paradigm. Frontend only; no wire-protocol or backend change. The double-answer and turn-wedge concerns are handled without touching the server: - agent-chat.js renderChoice is factored into a pure buildAskCard(data, {reqId, isWake, hasControl, onPick}) reused by both surfaces; exported alongside answerChoice + a hasControl getter. - A module-level answeredAsks Set keyed by request_id makes answering idempotent across both surfaces (only ONE choice_response is ever sent), so the existing holder-gate + _choice_futures.pop on the server stay correct and never see a duplicate. - The CLEAR signal fires off the CHOICE lifecycle: answerChoice emits ASK_CLEARED{request_id} the instant a response is sent (NOT stream_end, which lands after the answer for in-turn asks and never for a cancelled turn). Both surfaces clear on it; '*' clears all on cancel/error/socket-drop. - Read-only when !hasControl on both surfaces (observers can't answer), matching the server's holder gate — no dismiss-without-answer path, so asend can't wedge. - Adds the free-text "Something else…" escape the web cards lacked (the bridge routes unknown selections to LLM resolution). ask-stage.js (new) renders the current ask into #ask-stage via AgentChat.buildAskCard and clears on ASK_CLEARED; no-ops unless #ask-stage is present (flag off → v1 untouched). Verified: node --check on all touched JS, Jinja parse on index.html. Co-Authored-By: Claude Fable 5 --- gently/ui/web/static/css/ask-stage.css | 42 ++++++++ gently/ui/web/static/js/agent-chat.js | 132 ++++++++++++++++++++----- gently/ui/web/static/js/ask-stage.js | 53 ++++++++++ gently/ui/web/templates/index.html | 7 +- 4 files changed, 211 insertions(+), 23 deletions(-) create mode 100644 gently/ui/web/static/css/ask-stage.css create mode 100644 gently/ui/web/static/js/ask-stage.js diff --git a/gently/ui/web/static/css/ask-stage.css b/gently/ui/web/static/css/ask-stage.css new file mode 100644 index 00000000..d706a05c --- /dev/null +++ b/gently/ui/web/static/css/ask-stage.css @@ -0,0 +1,42 @@ +/* Main-stage ask surface (ux_v2): the agent's current pending ask, rendered + prominently outside the chat transcript. Reuses the .ac-choice card markup + from agent-chat.css; this file frames the stage container and adds the + shared free-text ("Something else…") escape styling. Only #ask-stage is + gated behind the flag, so loading this CSS unconditionally is harmless. */ + +.ask-stage { margin: 14px 16px 0; } +.ask-stage.hidden { display: none; } + +.ask-stage .ac-choice { + border: 1px solid var(--border, #e4e9f0); + border-radius: 14px; + padding: 16px 18px; + background: var(--surface, #fff); + box-shadow: 0 8px 28px rgba(15, 23, 42, .08); +} +.ask-stage .ac-choice-q { + font-size: 1.02rem; + font-weight: 600; + margin-bottom: 12px; +} + +/* Free-text "Something else…" escape — present on ask cards in BOTH surfaces. */ +.ac-choice-otherwrap { margin-top: 6px; } +.ac-choice-other.hidden, +.ac-choice-otherform.hidden { display: none; } +.ac-choice-otherform { display: flex; gap: 6px; align-items: center; margin-top: 4px; } +.ac-choice-otherinput { + flex: 1; min-width: 0; + padding: 8px 10px; + border: 1px solid var(--border, #cbd5e1); + border-radius: 8px; + font: inherit; + background: var(--surface, #fff); + color: inherit; +} +.ac-choice-otherinput:focus { outline: none; border-color: var(--accent, #2f6df6); } +.ac-choice-othergo { + border: 0; cursor: pointer; + background: var(--accent, #2f6df6); color: #fff; + border-radius: 8px; padding: 8px 12px; line-height: 1; +} diff --git a/gently/ui/web/static/js/agent-chat.js b/gently/ui/web/static/js/agent-chat.js index 620c9d60..c032e67a 100644 --- a/gently/ui/web/static/js/agent-chat.js +++ b/gently/ui/web/static/js/agent-chat.js @@ -16,6 +16,7 @@ const AgentChat = (() => { let panelOpen = false; let hasControl = true; // optimistic until the server says otherwise + const answeredAsks = new Set(); // request_ids answered from EITHER surface (transcript / main stage) let holderLabel = null; let streaming = false; let currentAgentEl = null; // the agent content element being streamed into @@ -211,6 +212,8 @@ const AgentChat = (() => { hasControl = !!msg.you_have_control; holderLabel = msg.holder_label || null; renderControl(); + // The main-stage ask renderer re-renders read-only on control loss. + if (typeof ClientEventBus !== 'undefined') ClientEventBus.emit('AGENT_CONTROL', { hasControl }); break; case 'history': @@ -342,6 +345,7 @@ const AgentChat = (() => { hideActivity(); setBusy(false); addSystemLine(msg.error || 'Unknown error', 'error'); + clearPendingAsks(); // a cancelled/errored turn sends no choice_response break; case 'ping': @@ -353,12 +357,19 @@ const AgentChat = (() => { } } - function renderChoice(msg) { - const data = msg.choice_data || {}; - const reqId = msg.request_id || data.request_id || ''; - const isWake = msg.origin === 'wake'; + // Build an ask card from a choice_data payload. Pure: the caller supplies + // hasControl + onPick, so the SAME builder renders in the chat transcript + // and on the main stage (#ask-stage) — one payload, two renderers. + function buildAskCard(data, opts) { + opts = opts || {}; + const reqId = opts.reqId || ''; + const isWake = !!opts.isWake; + const canAct = !!opts.hasControl && !answeredAsks.has(reqId); + const onPick = opts.onPick || function () {}; + const wrap = document.createElement('div'); wrap.className = 'ac-choice' + (isWake ? ' ac-choice-wake' : ''); + wrap.dataset.reqId = reqId; if (isWake) { const tag = document.createElement('div'); tag.className = 'ac-choice-origin'; @@ -373,30 +384,101 @@ const AgentChat = (() => { (data.options || []).forEach(opt => { const btn = document.createElement('button'); btn.className = 'ac-choice-opt'; - btn.disabled = !!opt.disabled || !hasControl; // observers see it read-only + btn.disabled = !!opt.disabled || !canAct; // observers / already-answered → read-only const desc = opt.description ? `${escapeHtml(opt.description)}` : ''; btn.innerHTML = `${escapeHtml(opt.label)}${desc}`; - btn.addEventListener('click', () => { - send({ type: 'choice_response', request_id: reqId, selected: opt.id }); - [...wrap.querySelectorAll('button')].forEach(b => b.disabled = true); - wrap.classList.add('ac-choice-answered'); - btn.classList.add('ac-choice-picked'); - if (streaming) setActivity('Working…'); - if (isWake && pendingSlot) { - setTimeout(() => { pendingSlot.classList.add('hidden'); pendingSlot.innerHTML = ''; }, 700); - } - }); + btn.addEventListener('click', () => onPick(opt.id)); wrap.appendChild(btn); }); - // ASK approvals pin to the sticky slot above the composer so they can't - // scroll out of reach; ordinary choices stay inline in the transcript. + + // Free-text escape — the bridge routes an unknown selection to LLM + // resolution, so the agent's asend always unblocks. (The TUI had this; + // the web ask cards previously did not.) + if (canAct) { + const ow = document.createElement('div'); + ow.className = 'ac-choice-otherwrap'; + const otherBtn = document.createElement('button'); + otherBtn.className = 'ac-choice-opt ac-choice-other'; + otherBtn.innerHTML = 'Something else…'; + const form = document.createElement('div'); + form.className = 'ac-choice-otherform hidden'; + const ti = document.createElement('input'); + ti.type = 'text'; + ti.className = 'ac-choice-otherinput'; + ti.placeholder = 'Type your own answer…'; + const go = document.createElement('button'); + go.className = 'ac-choice-othergo'; + go.textContent = '→'; + const submitOther = () => { const v = ti.value.trim(); if (v) onPick(v); }; + otherBtn.addEventListener('click', () => { otherBtn.classList.add('hidden'); form.classList.remove('hidden'); ti.focus(); }); + go.addEventListener('click', submitOther); + ti.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); submitOther(); } }); + form.appendChild(ti); form.appendChild(go); + ow.appendChild(otherBtn); ow.appendChild(form); + wrap.appendChild(ow); + } + + if (answeredAsks.has(reqId)) wrap.classList.add('ac-choice-answered'); + return wrap; + } + + // Send the answer ONCE (idempotent across both surfaces), then fire the + // clear off the CHOICE lifecycle — NOT stream_end (which lands after the + // answer for in-turn asks, and never for a cancelled turn). + function answerChoice(reqId, selected) { + if (!reqId || answeredAsks.has(reqId)) return; + answeredAsks.add(reqId); + send({ type: 'choice_response', request_id: reqId, selected }); + if (streaming) setActivity('Working…'); + if (typeof ClientEventBus !== 'undefined') ClientEventBus.emit('ASK_CLEARED', { request_id: reqId }); + } + + // Disable + mark-answered any transcript / sticky-slot ask card for this + // request_id ('*' = all). The main stage clears itself via its own handler. + function markAnswered(reqId) { + [log, pendingSlot].forEach(scope => { + if (!scope) return; + scope.querySelectorAll('.ac-choice').forEach(card => { + if (reqId !== '*' && card.dataset.reqId !== reqId) return; + card.querySelectorAll('button').forEach(b => b.disabled = true); + card.classList.add('ac-choice-answered'); + }); + }); + if (pendingSlot) { + const slotCard = pendingSlot.querySelector('.ac-choice'); + if (reqId === '*' || (slotCard && slotCard.dataset.reqId === reqId)) { + setTimeout(() => { pendingSlot.classList.add('hidden'); pendingSlot.innerHTML = ''; }, 700); + } + } + } + + // Retire all pending asks (turn cancelled/errored, or socket dropped). + function clearPendingAsks() { + if (typeof ClientEventBus !== 'undefined') ClientEventBus.emit('ASK_CLEARED', { request_id: '*' }); + else markAnswered('*'); + } + + function renderChoice(msg) { + const data = msg.choice_data || {}; + const reqId = msg.request_id || data.request_id || ''; + const isWake = msg.origin === 'wake'; + const card = buildAskCard(data, { + reqId, isWake, hasControl, + onPick: (sel) => answerChoice(reqId, sel), + }); + // Mirror onto the main stage (ask-stage.js renders it prominently). + if (typeof ClientEventBus !== 'undefined') { + ClientEventBus.emit('AGENT_ASK', { request_id: reqId, choice_data: data, origin: msg.origin }); + } + // ASK approvals pin to the sticky slot above the composer; ordinary + // choices stay inline in the transcript. if (isWake && pendingSlot) { pendingSlot.innerHTML = ''; - pendingSlot.appendChild(wrap); + pendingSlot.appendChild(card); pendingSlot.classList.remove('hidden'); return; } - log.appendChild(wrap); + log.appendChild(card); scrollToBottom(); } @@ -655,6 +737,7 @@ const AgentChat = (() => { setBusy(false); streaming = false; hideActivity(); + clearPendingAsks(); // stale asks: clear the stage on socket drop setTimeout(connect, reconnectDelay); reconnectDelay = Math.min(reconnectDelay * 2, MAX_DELAY); }; @@ -873,6 +956,11 @@ const AgentChat = (() => { if (!panel) return; // markup not present restorePrefs(); + // Dual-render: retire a transcript ask card when its ask is answered + // (from the transcript OR the main stage) or the turn is cancelled. + if (typeof ClientEventBus !== 'undefined') { + ClientEventBus.on('ASK_CLEARED', ({ request_id }) => markAnswered(request_id)); + } if (toggleBtn) toggleBtn.addEventListener('click', () => togglePanel()); closeBtn.addEventListener('click', () => togglePanel(false)); if (pinBtn) pinBtn.addEventListener('click', togglePin); @@ -914,7 +1002,7 @@ const AgentChat = (() => { stopBtn.className = 'ac-stop hidden'; stopBtn.textContent = 'Stop'; stopBtn.title = 'Stop the current turn'; - stopBtn.addEventListener('click', () => { send({ type: 'cancel' }); setBusy(false); }); + stopBtn.addEventListener('click', () => { send({ type: 'cancel' }); setBusy(false); clearPendingAsks(); }); inputWrap.appendChild(stopBtn); // Sticky ASK-approval slot — above the queue + composer, never scrolls away. @@ -951,7 +1039,7 @@ const AgentChat = (() => { // Escape mirrors Stop: cancel a cancellable (user) turn and clear busy // (a cancelled turn emits no stream_end, so clear optimistically). if (e.key === 'Escape' && agentBusy && busySource === 'user') { - e.preventDefault(); send({ type: 'cancel' }); setBusy(false); + e.preventDefault(); send({ type: 'cancel' }); setBusy(false); clearPendingAsks(); } }); } @@ -966,5 +1054,5 @@ const AgentChat = (() => { actuallySend(text); } - return { togglePanel, runCommand }; + return { togglePanel, runCommand, buildAskCard, answerChoice, hasControl: () => hasControl }; })(); diff --git a/gently/ui/web/static/js/ask-stage.js b/gently/ui/web/static/js/ask-stage.js new file mode 100644 index 00000000..b5e63db1 --- /dev/null +++ b/gently/ui/web/static/js/ask-stage.js @@ -0,0 +1,53 @@ +/** + * AskStage (ux_v2) — renders the agent's CURRENT pending ask prominently on the + * main stage, in addition to the chat transcript. One payload, two renderers: + * it reuses AgentChat.buildAskCard so the stage and the transcript can't drift, + * and answering from either surface clears both (via the ASK_CLEARED event that + * AgentChat fires off the CHOICE lifecycle — not stream_end, which arrives only + * after an in-turn answer and never for a cancelled turn). + * + * No-ops unless #ask-stage is present (gated behind GENTLY_UX_V2 in the + * template), so it never affects the v1 dashboard. + */ +const AskStage = (() => { + let stageEl = null; + let current = null; // { reqId, data, isWake } + + function clear() { + current = null; + if (stageEl) { stageEl.innerHTML = ''; stageEl.classList.add('hidden'); } + } + + function render() { + if (!stageEl || !current || typeof AgentChat === 'undefined' || !AgentChat.buildAskCard) return; + const hasControl = AgentChat.hasControl ? AgentChat.hasControl() : true; + const card = AgentChat.buildAskCard(current.data, { + reqId: current.reqId, + isWake: current.isWake, + hasControl, + onPick: (sel) => AgentChat.answerChoice(current.reqId, sel), + }); + stageEl.innerHTML = ''; + stageEl.appendChild(card); + stageEl.classList.remove('hidden'); + } + + function init() { + stageEl = document.getElementById('ask-stage'); + if (!stageEl || typeof ClientEventBus === 'undefined') return; // ux_v2 off → no-op + + ClientEventBus.on('AGENT_ASK', ({ request_id, choice_data, origin }) => { + current = { reqId: request_id, data: choice_data || {}, isWake: origin === 'wake' }; + render(); + }); + ClientEventBus.on('ASK_CLEARED', ({ request_id }) => { + if (!current) return; + if (request_id === '*' || request_id === current.reqId) clear(); + }); + // Re-render read-only / actionable when control changes hands mid-ask. + ClientEventBus.on('AGENT_CONTROL', () => { if (current) render(); }); + } + + document.addEventListener('DOMContentLoaded', init); + return { clear }; +})(); diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index 9aaf279a..b02568d8 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -15,8 +15,9 @@ + - + {% include '_header.html' %} {% include '_navbar.html' %}
@@ -637,5 +641,6 @@

Properties

+ From 52503e706914e03732b51b12240fb0258a524bcc Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 04:15:58 +0530 Subject: [PATCH 04/34] UX v2 Phase 2: grouped rail nav + session-context strip (behind flag) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the flat 8-tab bar with a calm grouped left rail (Now / Library / System) and adds a session-context strip at the top of the main area — the structural transformation toward the prototype's shell. All scoped under body.ux-v2; v1 markup/CSS untouched (no consolidation of the known duplicate .tab rulesets — deferred to the final cleanup phase). - shell.js (new): wires each rail item to switchTab(tabId) — it ROUTES through the single init chokepoint, never reimplements tab activation, so every tab's lazy-init side-effect still fires. Keeps the rail's active state in sync via a new TAB_CHANGED event; populates the strip's status/embryo count from the Phase 0 ConnectionStatus store. Wires the rail's "Talk to Gently" to the existing AgentChat dock. No-ops unless body.ux-v2 is present. - app.js: switchTab now emits TAB_CHANGED(tabName) — additive; v1 has no listener, so no behaviour change. - index.html: the rail (first child of the flex-row .app-shell) + the strip (top of .app-main) + shell.css/shell.js includes, all under {% if ux_v2 %}. - shell.css (new): rail + strip styling and a subtle unfold animation, every rule scoped under body.ux-v2. Deferred to keep this phase low-risk: History-API routing and the session_changed in-place re-hydration (current hash routing + reload still work). Verified: node --check on all touched JS, Jinja parse on index.html. Co-Authored-By: Claude Fable 5 --- gently/ui/web/static/css/shell.css | 79 ++++++++++++++++++++++++++++++ gently/ui/web/static/js/app.js | 2 + gently/ui/web/static/js/shell.js | 58 ++++++++++++++++++++++ gently/ui/web/templates/index.html | 28 +++++++++++ 4 files changed, 167 insertions(+) create mode 100644 gently/ui/web/static/css/shell.css create mode 100644 gently/ui/web/static/js/shell.js diff --git a/gently/ui/web/static/css/shell.css b/gently/ui/web/static/css/shell.css new file mode 100644 index 00000000..7501572f --- /dev/null +++ b/gently/ui/web/static/css/shell.css @@ -0,0 +1,79 @@ +/* ux_v2 shell chrome: grouped left-rail nav + session-context strip. + Everything is scoped under body.ux-v2 so the v1 dashboard is byte-for-byte + untouched — no consolidation of the existing duplicate .tab rulesets here + (that cleanup is deferred to the final phase). */ + +/* Replace the flat 8-tab bar with the rail. */ +body.ux-v2 .tabs { display: none; } + +/* ── Left rail ─────────────────────────────────────────────── */ +body.ux-v2 .v2-rail { + flex: 0 0 212px; + display: flex; + flex-direction: column; + gap: 2px; + padding: 14px 10px; + border-right: 1px solid var(--border, #e4e9f0); + background: var(--bg-card, #fff); + overflow-y: auto; + animation: v2-rise .45s ease backwards; +} +body.ux-v2 .v2-nav-group { margin-bottom: 6px; } +body.ux-v2 .v2-nav-label { + font-size: 10px; letter-spacing: .1em; text-transform: uppercase; + color: var(--text-muted, #94a3b8); padding: 10px 10px 4px; +} +body.ux-v2 .v2-nav-item { + display: flex; align-items: center; gap: 8px; width: 100%; + background: none; border: 0; cursor: pointer; text-align: left; + padding: 8px 10px; border-radius: 8px; + font: inherit; font-size: 13.5px; + color: var(--text-secondary, #475569); + transition: background .15s, color .15s; +} +body.ux-v2 .v2-nav-item:hover { background: var(--bg-hover, #f1f5f9); color: var(--text, #0f172a); } +body.ux-v2 .v2-nav-item.active { + background: var(--accent-soft, #eaf1ff); + color: var(--accent, #2f6df6); + font-weight: 600; +} +body.ux-v2 .v2-rail-chat { + margin-top: auto; + display: flex; align-items: center; gap: 9px; + background: none; border: 1px solid var(--border, #e4e9f0); border-radius: 10px; + padding: 9px 12px; cursor: pointer; + font: inherit; font-size: 13px; + color: var(--text-secondary, #475569); + transition: border-color .15s, color .15s; +} +body.ux-v2 .v2-rail-chat:hover { border-color: var(--accent, #2f6df6); color: var(--accent, #2f6df6); } +body.ux-v2 .v2-rail-orb { + width: 18px; height: 18px; border-radius: 50%; flex: none; + background: radial-gradient(closest-side at 38% 34%, #fff, #bcd3ff 42%, var(--accent, #2f6df6) 100%); +} + +/* ── Session-context strip (top of main) ───────────────────── */ +body.ux-v2 .v2-strip { + flex: none; + display: flex; align-items: center; gap: 12px; + padding: 9px 16px; + border-bottom: 1px solid var(--border, #e4e9f0); + background: var(--bg-card, #fff); + font-size: 12.5px; color: var(--text-muted, #94a3b8); + animation: v2-rise .45s ease backwards .05s; +} +body.ux-v2 .v2-strip-live { + display: inline-flex; align-items: center; gap: 6px; + font-size: 10.5px; font-weight: 700; letter-spacing: .08em; color: #ef4444; +} +body.ux-v2 .v2-strip-dot { + width: 8px; height: 8px; border-radius: 50%; background: #ef4444; +} +body.ux-v2 .v2-strip-status { margin-left: auto; font-variant-numeric: tabular-nums; } + +body.ux-v2 .app-main { animation: v2-rise .5s ease backwards .1s; } + +@keyframes v2-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } +@media (prefers-reduced-motion: reduce) { + body.ux-v2 .v2-rail, body.ux-v2 .v2-strip, body.ux-v2 .app-main { animation: none; } +} diff --git a/gently/ui/web/static/js/app.js b/gently/ui/web/static/js/app.js index 3395c89b..a8c6450c 100644 --- a/gently/ui/web/static/js/app.js +++ b/gently/ui/web/static/js/app.js @@ -60,6 +60,8 @@ function updateCalibrationCount() { function switchTab(tabName) { if (!tabName) return; state.tab = tabName; + // ux_v2 grouped rail mirrors the active tab off this single chokepoint. + if (typeof ClientEventBus !== 'undefined') ClientEventBus.emit('TAB_CHANGED', tabName); // Update tab styling document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); diff --git a/gently/ui/web/static/js/shell.js b/gently/ui/web/static/js/shell.js new file mode 100644 index 00000000..c85b2474 --- /dev/null +++ b/gently/ui/web/static/js/shell.js @@ -0,0 +1,58 @@ +/** + * Shell (ux_v2): the grouped left-rail nav (Now / Library / System) + the + * session-context strip that replace the flat 8-tab bar. + * + * CRITICAL: the rail ROUTES THROUGH switchTab(tabId) for every reveal — it + * never reimplements tab activation, so each tab's lazy-init side-effect + * (HomeApp.init, EmbryosManager.clearDetectionBadge, CampaignsApp.init, …) + * still fires. switchTab emits TAB_CHANGED, which keeps the rail's active + * state in sync no matter who switched (rail, keyboard shortcut, home card, + * hash route). No-ops unless body.ux-v2 is present (flag off → v1 untouched). + */ +const Shell = (() => { + let railItems = []; + + function setActive(tabName) { + railItems.forEach(b => b.classList.toggle('active', b.dataset.tab === tabName)); + } + + function currentTab() { + const active = document.querySelector('.tab.active'); + return (active && active.dataset.tab) || + (typeof state !== 'undefined' && state.tab) || 'home'; + } + + function renderStrip(status) { + const el = document.getElementById('v2-strip-status'); + if (!el) return; + const s = status || (typeof ConnectionStatus !== 'undefined' ? ConnectionStatus.get() : {}); + const n = (typeof state !== 'undefined' && Array.isArray(state.embryos)) ? state.embryos.length : 0; + const conn = s.gentlyConnected ? (s.microscopeConnected ? 'Connected' : 'Online') : 'Offline'; + el.textContent = `${n} embryo${n === 1 ? '' : 's'} · ${conn}`; + } + + function init() { + if (!document.body.classList.contains('ux-v2')) return; // flag off → no-op + + railItems = Array.from(document.querySelectorAll('.v2-nav-item')); + railItems.forEach(btn => btn.addEventListener('click', () => { + if (typeof switchTab === 'function') switchTab(btn.dataset.tab); + })); + setActive(currentTab()); + + if (typeof ClientEventBus !== 'undefined') { + ClientEventBus.on('TAB_CHANGED', (tabName) => setActive(tabName)); + ClientEventBus.on('CONNECTION_STATUS', (s) => renderStrip(s)); + } + + const chatBtn = document.getElementById('v2-rail-chat'); + if (chatBtn) chatBtn.addEventListener('click', () => { + if (typeof AgentChat !== 'undefined' && AgentChat.togglePanel) AgentChat.togglePanel(true); + }); + + renderStrip(); + } + + document.addEventListener('DOMContentLoaded', init); + return {}; +})(); diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index b02568d8..4bcf99c0 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -16,6 +16,7 @@ + {% include '_header.html' %} @@ -23,8 +24,34 @@
+ {% if ux_v2 %} + + {% endif %}
+ {% if ux_v2 %}
LIVE
{% endif %} + {# ux_v2: the agent's current pending ask, dual-rendered here + in the chat. #} {% if ux_v2 %}{% endif %} @@ -642,5 +669,6 @@

Properties

+ From 288f97b8e6caf3e18e5f1add65104442cb40cf73 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 04:33:43 +0530 Subject: [PATCH 05/34] UX v2 Phase 3a: inference-first plan mode (model-driven, with provenance) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flips plan mode from ASK-FIRST to INFERENCE-FIRST: the agent arrives with a draft instead of interrogating the researcher. Per Keshu's call, the genotype -> imaging-channel inference is done by the MODEL (it reads the reporters and knows fluorophore spectra) — NOT a hardcoded heuristic table, which would only cover a fraction of real fluorophores/dyes and force needless "asks". - prompt.py: the stance now says infer what you can (channels from the strain genotype via your own fluorophore knowledge, organism defaults, lab/campaign context), record each inferred value's source + confidence in the spec's provenance, state a wavelength only when confident (else mark low-confidence and confirm via ask_user_choice — never guess a number), and ask ONLY for genuine gaps / low-confidence / consequential choices. - model.py: ImagingSpec gains a `provenance` map (field -> {source, confidence}). It's a valid dataclass field, so it flows end-to-end with no extra plumbing: the model passes it in create_plan_item(spec=...), the store rebuilds it via ImagingSpec(**valid-fields), and it round-trips through serialization. Backend only; needs a server restart to load the new prompt. The actual inference behaviour is validated live in plan mode (see handoff). Co-Authored-By: Claude Fable 5 --- gently/harness/memory/model.py | 5 +++++ gently/harness/plan_mode/prompt.py | 21 +++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/gently/harness/memory/model.py b/gently/harness/memory/model.py index 2a164176..58c8ee84 100644 --- a/gently/harness/memory/model.py +++ b/gently/harness/memory/model.py @@ -240,6 +240,11 @@ class ImagingSpec: success_criteria: str | None = None comparison_to: str | None = None # "Compare to WT session 1" + # Per-field provenance for INFERRED values — field name -> {source, confidence}. + # e.g. {"laser_wavelength_nm": {"source": "inferred:genotype", "confidence": "medium"}} + # Lets the UI tag each value with where it came from and what to confirm. + provenance: Dict[str, Dict[str, str]] = field(default_factory=dict) + @dataclass class BenchSpec: diff --git a/gently/harness/plan_mode/prompt.py b/gently/harness/plan_mode/prompt.py index 4228249c..62df77d7 100644 --- a/gently/harness/plan_mode/prompt.py +++ b/gently/harness/plan_mode/prompt.py @@ -21,8 +21,17 @@ 6. Challenge assumptions — suggest controls the researcher might not have thought of 7. Suggest experiments outside of imaging where appropriate (bench assays, genetics, analysis) -DO NOT rush to a plan. Gather information first. Ask questions. Search the literature. -Understand the researcher's goals and constraints before proposing. +Work INFERENCE-FIRST: arrive with a draft, don't interrogate. Infer what you +reasonably can — read the reporters in the strain's genotype and set the +excitation wavelengths from your knowledge of fluorophore spectra (e.g. +TagRFP/mCherry ≈ 561 nm, GFP/GCaMP ≈ 488 nm), let the organism set sensible +defaults, and let lab/campaign context fill the rest. Record each inferred +value's source and confidence in the imaging spec's ``provenance``. State a +wavelength only when you're confident; if a reporter is unfamiliar or ambiguous, +mark it low-confidence and confirm via ask_user_choice rather than guessing a +number. Then surface the draft for review, asking ONLY for genuine gaps, +low-confidence guesses, or consequential choices. Search the literature to +confirm, not to stall. ## How to Design an Experimental Plan @@ -115,8 +124,12 @@ PLAN_MODE_GUIDELINES = """\ # Behavior in Plan Mode -1. **Ask before assuming**: Don't assume the researcher's constraints. Ask about - available strains, timeline, equipment access, collaborators. +1. **Infer, then confirm — don't interrogate**: Fill what you can from the strain + genotype, organism defaults, and lab/campaign context, and record where each + value came from (database citation, or your own fluorophore/biology knowledge) + in the spec's ``provenance``. Ask — via ask_user_choice — only for genuine + gaps, low-confidence guesses, or consequential choices, not for things you can + derive or look up. 2. **Think about the full story**: What would reviewers want to see? What controls would strengthen the claims? 3. **Be realistic about timelines**: Genetic crosses take weeks. Behavioral assays From e9475060eaddf149628ba69774c029ab3dc606ac Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 04:46:29 +0530 Subject: [PATCH 06/34] UX v2 Phase 3b: surface inferred imaging spec + per-field provenance in the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes Phase 3a's inference visible. renderSpec now shows the channel (laser_wavelength_nm) and reporter/genotype rows — which it omitted before — and tags each value with its provenance ("561 nm · inferred · medium") read from spec.provenance, so the researcher can see what was inferred vs. cited and what to confirm. - agent-chat.js renderSpec: keyed rows (label, value, fieldKey) with a small source/confidence tag per row when spec.provenance carries that field; adds Genotype/Reporter/Channel rows. - bridge.py: the spec payload builder now includes genotype/reporter/ laser_wavelength_nm and the provenance map (was a curated subset that dropped the channel), so the UI has the data to render. - ask-stage.css: styling for the .ac-spec-src provenance tag. Frontend + a contained backend payload enrichment; node --check + bridge import verified. Note: this surfaces provenance wherever the spec panel is shown and in the plan document (which already serializes provenance); threading it through every spec-emission path (e.g. the apply_plan_acquisition_spec stash) and a full plan_confirm ask with inline edit/confirm is the remaining 3b polish. Co-Authored-By: Claude Fable 5 --- gently/harness/bridge.py | 8 ++++++ gently/ui/web/static/css/ask-stage.css | 14 +++++++++++ gently/ui/web/static/js/agent-chat.js | 34 +++++++++++++++++++------- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/gently/harness/bridge.py b/gently/harness/bridge.py index ddbfa9ca..fdd21040 100644 --- a/gently/harness/bridge.py +++ b/gently/harness/bridge.py @@ -251,9 +251,12 @@ def _candidate_to_option(self, item, spec, campaign) -> dict: spec_dict: dict[str, Any] = {} for field in ( "strain", + "genotype", + "reporter", "temperature_c", "num_slices", "exposure_ms", + "laser_wavelength_nm", "interval_s", "stop_condition", "success_criteria", @@ -261,6 +264,11 @@ def _candidate_to_option(self, item, spec, campaign) -> dict: val = getattr(spec, field, None) if val is not None: spec_dict[field] = val + # Carry per-field provenance so the UI can tag inferred values + # (e.g. "561 nm · inferred · medium") and show what to confirm. + prov = getattr(spec, "provenance", None) + if prov: + spec_dict["provenance"] = prov if spec_dict: meta["spec"] = spec_dict diff --git a/gently/ui/web/static/css/ask-stage.css b/gently/ui/web/static/css/ask-stage.css index d706a05c..dc15c623 100644 --- a/gently/ui/web/static/css/ask-stage.css +++ b/gently/ui/web/static/css/ask-stage.css @@ -40,3 +40,17 @@ background: var(--accent, #2f6df6); color: #fff; border-radius: 8px; padding: 8px 12px; line-height: 1; } + +/* Per-field provenance tag on imaging-spec rows (Phase 3b): shows where an + inferred value came from, e.g. "inferred · medium". */ +.ac-spec-src { + margin-left: 6px; + font-size: 10px; + letter-spacing: .02em; + color: var(--text-muted, #94a3b8); + background: var(--bg-hover, #f1f5f9); + border-radius: 999px; + padding: 1px 7px; + white-space: nowrap; + vertical-align: middle; +} diff --git a/gently/ui/web/static/js/agent-chat.js b/gently/ui/web/static/js/agent-chat.js index c032e67a..c55c358a 100644 --- a/gently/ui/web/static/js/agent-chat.js +++ b/gently/ui/web/static/js/agent-chat.js @@ -483,19 +483,35 @@ const AgentChat = (() => { } function renderSpec(spec) { + const prov = spec.provenance || {}; const rows = []; - const add = (k, v) => { if (v !== undefined && v !== null && v !== '') rows.push([k, v]); }; - add('Strain', spec.strain); - add('Temperature', spec.temperature_c != null ? `${spec.temperature_c} °C` : null); - add('Slices', spec.num_slices); - add('Exposure', spec.exposure_ms != null ? `${spec.exposure_ms} ms` : null); - add('Interval', spec.interval_s != null ? `${spec.interval_s} s` : null); - add('Stop at', spec.stop_condition); + // (label, value, fieldKey) — fieldKey ties a row to its provenance entry. + const add = (label, value, key) => { + if (value === undefined || value === null || value === '') return; + rows.push({ label, value, src: key ? prov[key] : null }); + }; + add('Strain', spec.strain, 'strain'); + add('Genotype', spec.genotype, 'genotype'); + add('Reporter', spec.reporter, 'reporter'); + add('Channel', spec.laser_wavelength_nm != null ? `${spec.laser_wavelength_nm} nm` : null, 'laser_wavelength_nm'); + add('Temperature', spec.temperature_c != null ? `${spec.temperature_c} °C` : null, 'temperature_c'); + add('Slices', spec.num_slices, 'num_slices'); + add('Exposure', spec.exposure_ms != null ? `${spec.exposure_ms} ms` : null, 'exposure_ms'); + add('Interval', spec.interval_s != null ? `${spec.interval_s} s` : null, 'interval_s'); + add('Stop at', spec.stop_condition, 'stop_condition'); if (!rows.length) return; + // A small "where did this come from" tag for inferred values. + const srcTag = (src) => { + if (!src || !src.source) return ''; + const where = String(src.source).split(':')[0]; + const conf = src.confidence ? ` · ${src.confidence}` : ''; + const title = escapeHtml(String(src.source) + (src.confidence ? ` (confidence: ${src.confidence})` : '')); + return ` ${escapeHtml(where + conf)}`; + }; const el = document.createElement('div'); el.className = 'ac-spec'; - el.innerHTML = '
Imaging spec applied
' + - rows.map(([k, v]) => `
${escapeHtml(k)}${escapeHtml(v)}
`).join(''); + el.innerHTML = '
Imaging spec
' + + rows.map(r => `
${escapeHtml(r.label)}${escapeHtml(r.value)}${srcTag(r.src)}
`).join(''); log.appendChild(el); scrollToBottom(); } From 2387225d30614a70954fc5ae47594bff74ef9042 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 05:28:05 +0530 Subject: [PATCH 07/34] UX v2 Phase 4: co-editable shared-visibility surface (the agent's mind, live) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders the agent's expectations (beliefs), watchpoints (attention), and open questions (uncertainty) as a calm panel on the landing, updating live and resolvable by the control holder. The "store has no event bus" blocker is solved via the EXISTING global bus rather than dependency injection (matches agent.py's `emit` usage, so no __init__/launch_gently changes): - core/event_bus.py: new EventType.CONTEXT_UPDATED. - file_store.py: FileContextStore._notify_context_change() emits it (lazy import, best-effort) from add/resolve of expectations, watchpoints, and questions. The server already broadcasts ALL bus events to /ws (subscribe_async("*")), and websocket.js re-emits them on ClientEventBus — so the surface refreshes live with zero new transport. Verified: the emit fires on the bus (unit check). - routes/context.py (new, registered): GET /api/context (read the 3 lenses, defensive on cold start) + POST .../{id}/resolve for questions/watchpoints/ expectations, each gated by Depends(require_control) (data.py pattern, NOT the mesh-scoped campaigns auth) so viewers can't mutate the agent's mind. Read side reuses campaigns._serialize. - context-surface.js (new, ux_v2 only): fetches /api/context, renders the three lenses, re-fetches on CONTEXT_UPDATED + AGENT_CONTROL, and lets the holder answer a question (inline input — no native prompt), resolve a watchpoint, or confirm an expectation. shell.css: scoped styling. index.html: panel mounted at the top of the home landing under {% if ux_v2 %}. Backend needs a server restart to load; live push + render is validated in-app. Proactive #ask-stage cards from watchpoint creation are the remaining Phase-4 polish (noted). Co-Authored-By: Claude Fable 5 --- gently/core/event_bus.py | 4 + gently/harness/memory/file_store.py | 15 +++ gently/ui/web/routes/__init__.py | 2 + gently/ui/web/routes/context.py | 71 ++++++++++++++ gently/ui/web/static/css/shell.css | 25 +++++ gently/ui/web/static/js/context-surface.js | 105 +++++++++++++++++++++ gently/ui/web/templates/index.html | 2 + 7 files changed, 224 insertions(+) create mode 100644 gently/ui/web/routes/context.py create mode 100644 gently/ui/web/static/js/context-surface.js diff --git a/gently/core/event_bus.py b/gently/core/event_bus.py index 3b98e616..09edd389 100644 --- a/gently/core/event_bus.py +++ b/gently/core/event_bus.py @@ -92,6 +92,10 @@ class EventType(Enum): # gently/core/log_bridge.py — opt-in handler. LOG_RECORD = auto() + # Agent context/mind updates (expectations / watchpoints / questions) — + # drives the shared-visibility surface in the v2 UI. + CONTEXT_UPDATED = auto() + # Operator-action events. Distinct from EMBRYOS_UPDATE because they # carry intent ("a human did this") rather than just state delta. # Candidate orchestrators can subscribe and reason about what the diff --git a/gently/harness/memory/file_store.py b/gently/harness/memory/file_store.py index 0531dcce..3672ba88 100644 --- a/gently/harness/memory/file_store.py +++ b/gently/harness/memory/file_store.py @@ -1908,6 +1908,15 @@ def get_observations_for_embryo(self, embryo_id: str, limit: int = 20) -> list[O # Expectations # ================================================================== + def _notify_context_change(self, kind: str = "context") -> None: + """Emit CONTEXT_UPDATED on the global bus so the shared-visibility + surface refreshes live. Best-effort — a bus failure never breaks a write.""" + try: + from gently.core.event_bus import emit, EventType + emit(EventType.CONTEXT_UPDATED, {"kind": kind}, source="context_store") + except Exception: + pass + def add_expectation(self, exp: Expectation): path = self.agent_dir / "active" / "expectations.yaml" items = self._read_yaml(path) or [] @@ -1925,6 +1934,7 @@ def add_expectation(self, exp: Expectation): } ) self._write_yaml(path, items) + self._notify_context_change("expectation") def get_pending_expectations(self) -> list[Expectation]: path = self.agent_dir / "active" / "expectations.yaml" @@ -1956,6 +1966,7 @@ def resolve_expectation(self, exp_id: str, status: ExpectationStatus): item["resolved_at"] = now break self._write_yaml(path, items) + self._notify_context_change("expectation") # ================================================================== # Watchpoints @@ -1975,6 +1986,7 @@ def add_watchpoint(self, wp: Watchpoint): } ) self._write_yaml(path, items) + self._notify_context_change("watchpoint") def get_active_watchpoints(self) -> list[Watchpoint]: path = self.agent_dir / "active" / "watchpoints.yaml" @@ -2002,6 +2014,7 @@ def resolve_watchpoint(self, wp_id: str): item["status"] = "resolved" break self._write_yaml(path, items) + self._notify_context_change("watchpoint") # ================================================================== # Questions @@ -2021,6 +2034,7 @@ def add_question(self, q: Question): } ) self._write_yaml(path, items) + self._notify_context_change("question") def get_open_questions(self) -> list[Question]: path = self.agent_dir / "active" / "questions.yaml" @@ -2042,6 +2056,7 @@ def resolve_question(self, q_id: str, resolution: str): item["resolved_at"] = now break self._write_yaml(path, items) + self._notify_context_change("question") # ================================================================== # Learnings diff --git a/gently/ui/web/routes/__init__.py b/gently/ui/web/routes/__init__.py index ebd90770..bdbf3db7 100644 --- a/gently/ui/web/routes/__init__.py +++ b/gently/ui/web/routes/__init__.py @@ -10,6 +10,7 @@ from .auth_routes import create_router as create_auth_router from .campaigns import create_router as create_campaigns_router from .chat import create_router as create_chat_router +from .context import create_router as create_context_router from .data import create_router as create_data_router from .experiments import create_router as create_experiments_router from .images import create_router as create_images_router @@ -33,6 +34,7 @@ def register_all_routes(server): create_websocket_router, create_agent_ws_router, create_chat_router, + create_context_router, ): router = factory(server) server.app.include_router(router) diff --git a/gently/ui/web/routes/context.py b/gently/ui/web/routes/context.py new file mode 100644 index 00000000..2d7c39df --- /dev/null +++ b/gently/ui/web/routes/context.py @@ -0,0 +1,71 @@ +"""Context (shared-visibility) routes. + +Exposes the agent's "mind" — its open questions (uncertainty), active +watchpoints (attention), and pending expectations (beliefs) — read by anyone, +resolvable only by the control holder. Live updates ride the CONTEXT_UPDATED +event the FileContextStore emits on the global bus, which the server already +broadcasts to /ws; the client just re-fetches /api/context on it (no polling). +""" + +from fastapi import APIRouter, Body, Depends + +from gently.ui.web.auth import require_control +from .campaigns import _serialize + + +def create_router(server) -> APIRouter: + router = APIRouter() + + def _store(): + # Defensive: the store is wired after construction; tolerate cold start. + return getattr(server, "context_store", None) + + @router.get("/api/context") + async def get_context(): + cs = _store() + empty = {"available": False, "expectations": [], "watchpoints": [], "questions": []} + if cs is None: + return empty + try: + return { + "available": True, + "questions": [_serialize(q) for q in cs.get_open_questions()], + "watchpoints": [_serialize(w) for w in cs.get_active_watchpoints()], + "expectations": [_serialize(e) for e in cs.get_pending_expectations()], + } + except Exception: + return empty + + @router.post("/api/context/questions/{q_id}/resolve", + dependencies=[Depends(require_control)]) + async def resolve_question(q_id: str, resolution: str = Body("", embed=True)): + cs = _store() + if cs is None: + return {"ok": False, "error": "context store unavailable"} + cs.resolve_question(q_id, resolution or "") + return {"ok": True} + + @router.post("/api/context/watchpoints/{wp_id}/resolve", + dependencies=[Depends(require_control)]) + async def resolve_watchpoint(wp_id: str): + cs = _store() + if cs is None: + return {"ok": False, "error": "context store unavailable"} + cs.resolve_watchpoint(wp_id) + return {"ok": True} + + @router.post("/api/context/expectations/{exp_id}/resolve", + dependencies=[Depends(require_control)]) + async def resolve_expectation(exp_id: str, status: str = Body("confirmed", embed=True)): + cs = _store() + if cs is None: + return {"ok": False, "error": "context store unavailable"} + from gently.harness.memory.model import ExpectationStatus + try: + st = ExpectationStatus(status) + except ValueError: + st = ExpectationStatus.CONFIRMED + cs.resolve_expectation(exp_id, st) + return {"ok": True} + + return router diff --git a/gently/ui/web/static/css/shell.css b/gently/ui/web/static/css/shell.css index 7501572f..211be553 100644 --- a/gently/ui/web/static/css/shell.css +++ b/gently/ui/web/static/css/shell.css @@ -77,3 +77,28 @@ body.ux-v2 .app-main { animation: v2-rise .5s ease backwards .1s; } @media (prefers-reduced-motion: reduce) { body.ux-v2 .v2-rail, body.ux-v2 .v2-strip, body.ux-v2 .app-main { animation: none; } } + +/* ── Shared-visibility surface (the agent's view) ──────────── */ +body.ux-v2 .cx-surface { + margin: 0 0 16px; + border: 1px solid var(--border, #e4e9f0); + border-radius: 14px; + background: var(--bg-card, #fff); + padding: 14px 16px; +} +body.ux-v2 .cx-surface.hidden { display: none; } +body.ux-v2 .cx-title { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted, #94a3b8); margin-bottom: 8px; } +body.ux-v2 .cx-lens { margin-bottom: 10px; } +body.ux-v2 .cx-lens-h { font-size: 11px; font-weight: 600; color: var(--text-secondary, #475569); margin: 6px 0 4px; } +body.ux-v2 .cx-item { display: flex; align-items: center; gap: 9px; padding: 5px 0; flex-wrap: wrap; } +body.ux-v2 .cx-text { flex: 1; min-width: 0; font-size: 13px; color: var(--text, #0f172a); } +body.ux-v2 .cx-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; } +body.ux-v2 .cx-dot.cx-q { background: #d97706; } +body.ux-v2 .cx-dot.cx-w { background: var(--accent, #2f6df6); } +body.ux-v2 .cx-dot.cx-e { background: var(--accent-green, #16a34a); } +body.ux-v2 .cx-act { flex: none; border: 1px solid var(--border, #e4e9f0); background: none; color: var(--text-secondary, #475569); border-radius: 8px; padding: 3px 10px; font: inherit; font-size: 12px; cursor: pointer; } +body.ux-v2 .cx-act:hover { border-color: var(--accent, #2f6df6); color: var(--accent, #2f6df6); } +body.ux-v2 .cx-answer { display: flex; gap: 6px; align-items: center; flex: 1 0 100%; margin-top: 4px; } +body.ux-v2 .cx-answer.hidden { display: none; } +body.ux-v2 .cx-answer-input { flex: 1; min-width: 0; border: 1px solid var(--border, #cbd5e1); border-radius: 8px; padding: 6px 9px; font: inherit; font-size: 12px; } +body.ux-v2 .cx-answer-go { border: 0; background: var(--accent, #2f6df6); color: #fff; border-radius: 8px; padding: 6px 10px; cursor: pointer; } diff --git a/gently/ui/web/static/js/context-surface.js b/gently/ui/web/static/js/context-surface.js new file mode 100644 index 00000000..d8c2d0de --- /dev/null +++ b/gently/ui/web/static/js/context-surface.js @@ -0,0 +1,105 @@ +/** + * ContextSurface (ux_v2): renders the agent's "mind" as a calm, always-visible + * panel — open questions (uncertainty), watchpoints (attention), expectations + * (beliefs) — read from /api/context and refreshed live on the CONTEXT_UPDATED + * event (the store emits it; the server broadcasts it to /ws; no polling). + * + * The control holder can resolve items inline (answer a question, resolve a + * watchpoint, confirm an expectation); observers see it read-only. No-ops + * unless #context-surface is present (flag off → v1 untouched). + */ +const ContextSurface = (() => { + let el = null, loading = false; + + const esc = (s) => (typeof escapeHtml === 'function') + ? escapeHtml(String(s == null ? '' : s)) : String(s == null ? '' : s); + const hasControl = () => + (typeof AgentChat !== 'undefined' && AgentChat.hasControl) ? AgentChat.hasControl() : true; + + async function fetchAndRender() { + if (!el || loading) return; + loading = true; + try { render(await (await fetch('/api/context')).json()); } + catch (e) { /* keep last render */ } + finally { loading = false; } + } + + function section(label, html) { return html ? `
${label}
${html}
` : ''; } + + function render(data) { + if (!el) return; + const hc = hasControl(); + const questions = data.questions || [], watchpoints = data.watchpoints || [], expectations = data.expectations || []; + if (!questions.length && !watchpoints.length && !expectations.length) { + el.innerHTML = ''; el.classList.add('hidden'); return; + } + el.classList.remove('hidden'); + + const qHtml = questions.map(it => ` +
+ + ${esc(it.content)} + ${hc ? '' : ''} + ${hc ? '' : ''} +
`).join(''); + const wHtml = watchpoints.map(it => ` +
+ + ${esc(it.target)}${it.condition ? ' — ' + esc(it.condition) : ''} + ${hc ? '' : ''} +
`).join(''); + const eHtml = expectations.map(it => ` +
+ + ${esc(it.target)}${it.prediction ? ': ' + esc(it.prediction) : ''} + ${hc ? '' : ''} +
`).join(''); + + el.innerHTML = '
Agent’s view
' + + section('Open questions', qHtml) + section('Watching', wHtml) + section('Expectations', eHtml); + wire(); + } + + function wire() { + el.querySelectorAll('.cx-item').forEach(item => { + const kind = item.dataset.kind, id = item.dataset.id; + const actBtn = item.querySelector('.cx-act'); + if (!actBtn) return; + const act = actBtn.dataset.act; + if (act === 'answer') { + const box = item.querySelector('.cx-answer'); + const input = item.querySelector('.cx-answer-input'); + const submit = () => resolve(kind, id, { resolution: input.value.trim() }); + actBtn.addEventListener('click', () => { box.classList.toggle('hidden'); if (!box.classList.contains('hidden')) input.focus(); }); + item.querySelector('.cx-answer-go').addEventListener('click', submit); + input.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); submit(); } }); + } else if (act === 'resolve') { + actBtn.addEventListener('click', () => resolve(kind, id, {})); + } else if (act === 'confirm') { + actBtn.addEventListener('click', () => resolve(kind, id, { status: 'confirmed' })); + } + }); + } + + async function resolve(kind, id, body) { + try { + await fetch(`/api/context/${kind}/${encodeURIComponent(id)}/resolve`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}), + }); + } catch (e) { /* ignore; surface stays as-is */ } + fetchAndRender(); // CONTEXT_UPDATED also re-fetches for every client + } + + function init() { + el = document.getElementById('context-surface'); + if (!el) return; // ux_v2 off → no-op + if (typeof ClientEventBus !== 'undefined') { + ClientEventBus.on('CONTEXT_UPDATED', () => fetchAndRender()); + ClientEventBus.on('AGENT_CONTROL', () => fetchAndRender()); // re-render with/without resolve controls + } + fetchAndRender(); + } + + document.addEventListener('DOMContentLoaded', init); + return { refresh: fetchAndRender }; +})(); diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index 4bcf99c0..9a4ff9dd 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -59,6 +59,7 @@
+ {% if ux_v2 %}{% endif %}

Welcome to Gently

@@ -670,5 +671,6 @@

Properties

+ From e8ce2bfd0498a1188f1d1034025c43b9a658e5fc Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 05:33:42 +0530 Subject: [PATCH 08/34] UX v2 Phase 4 follow-up: show an empty-state for the agent's-view panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The context surface hid itself entirely when the agent had no expectations/ watchpoints/questions — so on a fresh session it was invisible and read as "missing". Render a calm "nothing yet" empty-state instead, so the surface is discoverable before the agent has formed any beliefs. Static JS/CSS only — hard-reload to pick it up, no restart. Co-Authored-By: Claude Fable 5 --- gently/ui/web/static/css/shell.css | 1 + gently/ui/web/static/js/context-surface.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/gently/ui/web/static/css/shell.css b/gently/ui/web/static/css/shell.css index 211be553..c38ae89a 100644 --- a/gently/ui/web/static/css/shell.css +++ b/gently/ui/web/static/css/shell.css @@ -102,3 +102,4 @@ body.ux-v2 .cx-answer { display: flex; gap: 6px; align-items: center; flex: 1 0 body.ux-v2 .cx-answer.hidden { display: none; } body.ux-v2 .cx-answer-input { flex: 1; min-width: 0; border: 1px solid var(--border, #cbd5e1); border-radius: 8px; padding: 6px 9px; font: inherit; font-size: 12px; } body.ux-v2 .cx-answer-go { border: 0; background: var(--accent, #2f6df6); color: #fff; border-radius: 8px; padding: 6px 10px; cursor: pointer; } +body.ux-v2 .cx-empty { font-size: 12.5px; color: var(--text-muted, #94a3b8); font-style: italic; padding: 2px 0 4px; } diff --git a/gently/ui/web/static/js/context-surface.js b/gently/ui/web/static/js/context-surface.js index d8c2d0de..2e10000d 100644 --- a/gently/ui/web/static/js/context-surface.js +++ b/gently/ui/web/static/js/context-surface.js @@ -30,10 +30,14 @@ const ContextSurface = (() => { if (!el) return; const hc = hasControl(); const questions = data.questions || [], watchpoints = data.watchpoints || [], expectations = data.expectations || []; + el.classList.remove('hidden'); if (!questions.length && !watchpoints.length && !expectations.length) { - el.innerHTML = ''; el.classList.add('hidden'); return; + // Show an empty-state rather than vanishing, so the surface is + // discoverable before the agent has formed any beliefs. + el.innerHTML = '
Agent’s view
' + + '
Nothing yet — the agent’s expectations, watchpoints, and open questions appear here as it works.
'; + return; } - el.classList.remove('hidden'); const qHtml = questions.map(it => `
From c98a33fd9ad980d0760908e6ece34a752a728b9e Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 05:38:44 +0530 Subject: [PATCH 09/34] Put the free-the-port command in the "port in use" error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the viz server can't bind its port, the error now tells you how to free it (fuser -k /tcp, or lsof -ti | xargs kill) instead of just "close it first" — uses self.port so it's always the right port. (Standalone DX fix.) Co-Authored-By: Claude Fable 5 --- gently/ui/web/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gently/ui/web/server.py b/gently/ui/web/server.py index c16c548d..faf2a2d8 100644 --- a/gently/ui/web/server.py +++ b/gently/ui/web/server.py @@ -788,9 +788,9 @@ async def on_start(self): sock.bind((self.host, self.port)) except OSError: raise OSError( - f"Port {self.port} is already in use. " - "Is another instance of the agent running? " - "Close it first and try again." + f"Port {self.port} is already in use — another instance may be running. " + f"Free it with: fuser -k {self.port}/tcp " + f"(or: lsof -ti:{self.port} | xargs -r kill), then try again." ) from None finally: sock.close() From 927900ada57471306cd8d970de7f95f4c5bbd11e Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 05:47:24 +0530 Subject: [PATCH 10/34] =?UTF-8?q?UX=20v2=20Phase=205:=20Experiment=20view?= =?UTF-8?q?=20shows=20real=20tactics=20only=20=E2=80=94=20drop=20stubbed?= =?UTF-8?q?=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Experiment view's job is to show the live experimental TACTIC patterns (cadence: base/fast/burst/cooldown + reactive-monitoring rules). It was falling back to a ~130-line STUB_STRATEGY (and a "mockup · stubbed data" badge) whenever there was no active experiment or the fetch wasn't ready — i.e. production could render fake tactics. - Removed the STUB_STRATEGY const entirely. - loadStrategy() now returns null on non-OK / error (no stub fallback). - render(null) shows a calm empty state ("No active experiment — the imaging tactics will appear here once a run is live"), never fabricated data. - Removed both "mockup · stubbed data" badges; the header just shows "live". Affects v1 and v2 (the Experiment tab isn't flag-gated) — removing fake data is correct for both; it only changes the no-active-experiment case (stub → empty state), real live runs render as before. node --check clean. NOT done (deliberately): the large carve of a new per-embryo renderer out of the 4,556-line embryos.js — the existing ExperimentOverview already renders the tactic patterns from /api/experiments/current/strategy, and the detailed contents are yours to define. The reconcileWithServerState/clearAllState contract in embryos.js is left untouched. Co-Authored-By: Claude Fable 5 --- .../ui/web/static/js/experiment-overview.js | 176 ++---------------- 1 file changed, 19 insertions(+), 157 deletions(-) diff --git a/gently/ui/web/static/js/experiment-overview.js b/gently/ui/web/static/js/experiment-overview.js index 33032a40..9e7fc221 100644 --- a/gently/ui/web/static/js/experiment-overview.js +++ b/gently/ui/web/static/js/experiment-overview.js @@ -1,147 +1,12 @@ /** - * Experiment Overview Tab — vector-graphics view of the planned timelapse. + * Experiment Overview Tab — vector-graphics view of the live imaging tactics + * (cadence patterns + reactive-monitoring rules) for the running experiment. * - * Data source priority: - * 1. GET /api/experiments/current/strategy — live snapshot from FileStore. - * 2. STUB_STRATEGY below — used when the live fetch - * fails or no session exists. - * - * The render path is data-shape-driven and doesn't care which source the - * snapshot came from — only ``ExperimentOverview.isLive`` differs so the - * header badge can say "live" or "mockup · stubbed data". + * Data source: GET /api/experiments/current/strategy — the live snapshot from + * FileStore. When there is no active experiment (or the fetch isn't ready), the + * view shows a calm empty state; it never renders stubbed/mock data. */ -const STUB_STRATEGY = { - session_id: "20260522_1430_dopaminergic_demo_a3f8e1c2", - session_name: "dopaminergic-reporter demo", - started_at: "2026-05-22T14:30:00", - now_offset_s: 8100, // 2h 15min into the run - horizon_s: 14400, // 4h total view window (past + projected) - base_interval_s: 120, - dose_budget_base_ms: 50000, - per_timepoint_ms: 500, // 50 slices × 10ms - monitoring_modes: [ - { - name: "expression_monitoring", - description: "Anticipating fluorescent-reporter onset on Test embryos: accelerate to 60s on signal, ramp 488 down on saturation.", - applies_to_roles: ["test"], - params: { - fast_interval: 60, - rampdown_step_pct: 1.0, - rampdown_floor_pct: 2.0, - rampdown_ceiling_pct: 6.0 - } - }, - { - name: "pre_terminal_monitoring", - description: "Anticipating organism pre-terminal stage (pretzel): accelerate to 30s on detection.", - applies_to_roles: ["test"], - params: { fast_interval: 30 } - } - ], - triggers: [ - { id: "t1", kind: "interval_rule", label: "signal onset", - when_text: "dopaminergic ≥ WEAK", then_text: "120s → 60s", - applies_to: ["test"], one_time: true }, - { id: "t2", kind: "power_rule", label: "488 ramp down", - when_text: "intensity = SATURATING (×3)", then_text: "488 ↓ 1%/step, floor 2%", - applies_to: ["test"] }, - { id: "t3", kind: "burst", label: "structure-triggered burst", - when_text: "structure_quality = GOOD", then_text: "burst 200 frames @ 20 Hz", - applies_to: ["test"] }, - { id: "t4", kind: "interval_rule", label: "pre-terminal speedup", - when_text: "stage = pretzel", then_text: "60s → 30s", - applies_to: ["test"], one_time: true } - ], - embryos: [ - { - id: "E1", role: "test", color: "#ff66cc", icon: "★", - dose_used_ms: 12500, dose_budget_ms: 50000, - tp_acquired: 25, - stop_condition: "hatching+3 OR 24h duration", - stop_kind: "bounded", - laser_488_pct_now: 3.0, - phases: [ - { mode: "base", start: 0, end: 1800, cadence_s: 120 }, - { mode: "fast", start: 1800, end: 3600, cadence_s: 60 }, - { mode: "burst", start: 3600, end: 3610, frames: 200, hz: 20 }, - { mode: "cooldown", start: 3610, end: 3640, cadence_s: 60 }, - { mode: "fast", start: 3640, end: 8100, cadence_s: 60 } - ], - trigger_events: [ - { trigger_id: "t1", at: 1800 }, - { trigger_id: "t3", at: 3600 }, - { trigger_id: "t2", at: 5400, count: 3 } - ], - power_history_488: [ - { at: 0, pct: 5.0 }, - { at: 5400, pct: 4.0 }, - { at: 5460, pct: 3.0 }, - { at: 8100, pct: 3.0 } - ], - // Future projection at current cadence (60s, fast). Hatching not - // deterministic so projected_end_s is null — render fades to ∞. - projected_cadence_s: 60, - projected_end_s: null - }, - { - id: "E2", role: "test", color: "#ff66cc", icon: "★", - dose_used_ms: 6500, dose_budget_ms: 50000, - tp_acquired: 13, - stop_condition: "hatching+3 OR 24h duration", - stop_kind: "bounded", - laser_488_pct_now: 5.0, - phases: [ - { mode: "base", start: 0, end: 8100, cadence_s: 120 } - ], - trigger_events: [], - power_history_488: [ - { at: 0, pct: 5.0 }, - { at: 8100, pct: 5.0 } - ], - projected_cadence_s: 120, - projected_end_s: null - }, - { - id: "E3", role: "test", color: "#ff66cc", icon: "★", - dose_used_ms: 38000, dose_budget_ms: 50000, - tp_acquired: 76, - stop_condition: "manual", - stop_kind: "open_ended", - laser_488_pct_now: 5.0, - phases: [ - { mode: "base", start: 0, end: 8100, cadence_s: 120 } - ], - trigger_events: [], - power_history_488: [ - { at: 0, pct: 5.0 }, - { at: 8100, pct: 5.0 } - ], - // Projected dose-exhaust horizon = 4.0h from now (warning condition) - projected_cadence_s: 120, - projected_end_s: null, - dose_exhaust_at_s: 12000 // budget will run out at this elapsed time - }, - { - id: "C1", role: "calibration", color: "#22d3ee", icon: "◆", - dose_used_ms: 33500, dose_budget_ms: 500000, // 10× multiplier - tp_acquired: 67, - stop_condition: "manual", - stop_kind: "open_ended", - laser_488_pct_now: 5.0, - phases: [ - { mode: "base", start: 0, end: 8100, cadence_s: 120 } - ], - trigger_events: [], - power_history_488: [ - { at: 0, pct: 5.0 }, - { at: 8100, pct: 5.0 } - ], - projected_cadence_s: 120, - projected_end_s: null - } - ] -}; const ExperimentOverview = { initialized: false, @@ -164,23 +29,19 @@ const ExperimentOverview = { cache: 'no-store' }); if (!resp.ok) { - console.warn( - '[ExperimentOverview] strategy fetch returned', - resp.status, '- falling back to stub' - ); + // No active experiment / not ready yet — show the empty state, + // never stubbed data. + console.warn('[ExperimentOverview] strategy fetch returned', resp.status); this.isLive = false; - return STUB_STRATEGY; + return null; } const data = await resp.json(); this.isLive = true; return data; } catch (e) { - console.warn( - '[ExperimentOverview] strategy fetch error - falling back to stub:', - e - ); + console.warn('[ExperimentOverview] strategy fetch error:', e); this.isLive = false; - return STUB_STRATEGY; + return null; } }, @@ -193,7 +54,7 @@ const ExperimentOverview = { }); // Re-render against the last fetched strategy (no re-fetch on tab // switch — refresh happens on tab activation in the bootstrap). - this.render(this.activeStrategy || STUB_STRATEGY); + this.render(this.activeStrategy); }, render(s) { @@ -204,6 +65,12 @@ const ExperimentOverview = { } // Tear down any prior ticker before we blow away the SVG it pointed at. this._stopNowTicker(); + if (!s) { + // No active experiment — a calm empty state, never stubbed data. + root.innerHTML = '
' + + 'No active experiment — the imaging tactics (cadence, reactive rules) will appear here once a run is live.
'; + return; + } try { root.innerHTML = ''; if (this.activeView === 'rules') { @@ -301,7 +168,6 @@ const ExperimentOverview = { const metaRow = el('div', 'expov-header-row expov-header-row-meta'); metaRow.appendChild(elText('span', 'expov-session-name', s.session_name)); metaRow.appendChild(elText('span', 'expov-session-id', s.session_id)); - metaRow.appendChild(elText('span', 'expov-mockup-badge', 'mockup · stubbed data')); header.appendChild(metaRow); root.appendChild(header); @@ -330,11 +196,7 @@ const ExperimentOverview = { if (s.session_name && s.session_name !== s.session_id) { metaRow.appendChild(elText('span', 'expov-session-name', s.session_name)); } - if (this.isLive) { - metaRow.appendChild(elText('span', 'expov-live-badge', 'live')); - } else { - metaRow.appendChild(elText('span', 'expov-mockup-badge', 'mockup · stubbed data')); - } + metaRow.appendChild(elText('span', 'expov-live-badge', 'live')); wrap.appendChild(metaRow); // Compact key-metric strip From f69bc4e881da74910ac63ea06a5104fd0eb01bad Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 05:54:17 +0530 Subject: [PATCH 11/34] UX v2 Phase 6 (step 1): flip GENTLY_UX_V2 default ON, keep v1 fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v2 is now the default UI — no env var needed. The v1 dashboard stays reachable as a fallback via GENTLY_UX_V2=0 (and its markup is NOT deleted yet). This is the reversible "flip → soak" step; the irreversible v1 markup/CSS deletion is deferred until v2 has run as the default and is confirmed good. Co-Authored-By: Claude Fable 5 --- gently/settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gently/settings.py b/gently/settings.py index 8b16f871..d197bc0d 100644 --- a/gently/settings.py +++ b/gently/settings.py @@ -124,10 +124,10 @@ class TransferSettings: class UISettings: """Web UI feature flags.""" # New agent-first UX paradigm (welcome→shell unfold, dual-rendered agent - # asks, inference-first plan mode, shared-visibility surface). Off by - # default; flip per-deployment via GENTLY_UX_V2=1 while the migration - # soaks behind the flag. The v1 dashboard stays the default until then. - ux_v2: bool = field(default_factory=lambda: _env("UX_V2", False)) + # asks, inference-first plan mode, shared-visibility surface). Now ON by + # default; the v1 dashboard remains available as a fallback via + # GENTLY_UX_V2=0 until the v1 markup is removed in a later cleanup step. + ux_v2: bool = field(default_factory=lambda: _env("UX_V2", True)) @dataclass(frozen=True) From e4de3a117449b9214cd2399c9c7b6295b78a01a4 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 07:39:10 +0530 Subject: [PATCH 12/34] =?UTF-8?q?UX=20v2:=20agent-first=20landing=20?= =?UTF-8?q?=E2=86=92=20in-page=20plan=20wizard=20(flag-gated)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the entry landing the prototype sketched — agent orb, time-aware greeting, and choice cards (Plan / quick look / free-text escape), behind GENTLY_UX_V2 and receding into the workspace. Crucially, the plan dialogue renders IN the landing, not the chat REPL: 'Plan an experiment' switches to an in-place plan-wizard screen, enters /plan, and renders the agent's ask_user_choice questions as button cards there (reusing AgentChat.buildAskCard), with the plan assembling from each pick. agent-chat.js: runCommand is now connection-aware (connect + queue/flush on open) so the page drives the agent without opening the chat panel. Co-Authored-By: Claude Fable 5 --- gently/ui/web/static/css/landing.css | 227 ++++++++++++++++++++++++++ gently/ui/web/static/js/agent-chat.js | 18 +- gently/ui/web/static/js/landing.js | 214 ++++++++++++++++++++++++ gently/ui/web/templates/index.html | 82 ++++++++++ 4 files changed, 539 insertions(+), 2 deletions(-) create mode 100644 gently/ui/web/static/css/landing.css create mode 100644 gently/ui/web/static/js/landing.js diff --git a/gently/ui/web/static/css/landing.css b/gently/ui/web/static/css/landing.css new file mode 100644 index 00000000..c5152cf5 --- /dev/null +++ b/gently/ui/web/static/css/landing.css @@ -0,0 +1,227 @@ +/* ux_v2 landing — the agent-first welcome that the prototype sketched, ported + into production. A full-bleed overlay shown on first entry that recedes into + the workspace once the user picks a path. Everything is scoped under + body.ux-v2 and the #v2-landing node only renders when the flag is on, so v1 + is byte-for-byte untouched. Visual language mirrors ux-prototype/landing.html + but reuses production's CSS variables (with the prototype hexes as fallback) + so it tracks the app theme. */ + +.v2-landing { + position: fixed; + inset: 0; + z-index: 200; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + overflow: hidden; + background: + radial-gradient(1100px 700px at 78% -8%, #eaf1ff 0%, transparent 55%), + radial-gradient(900px 600px at 8% 108%, #eafaf0 0%, transparent 55%), + var(--bg, #f6f8fb); + transition: opacity .5s cubic-bezier(.22,1,.36,1), transform .5s cubic-bezier(.22,1,.36,1), visibility .5s; +} +/* The calm screen "unfolds" into the workspace: fade + slight scale-up, then + the node is pulled from the layout (display:none set by JS after the + transition) so it never traps clicks. */ +.v2-landing.dismissed { + opacity: 0; + visibility: hidden; + transform: scale(1.015); + pointer-events: none; +} +.v2-landing::before { + content: ""; + position: absolute; + inset: -20vmax; + background: radial-gradient(closest-side, rgba(47,109,246,.10), transparent 70%); + filter: blur(30px); + animation: v2land-drift 26s cubic-bezier(.22,1,.36,1) infinite alternate; + pointer-events: none; +} +@keyframes v2land-drift { + 0% { transform: translate(-6vw,-4vh) scale(1); } + 100% { transform: translate(8vw,6vh) scale(1.15); } +} + +.v2-landing-inner { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + max-width: 760px; + width: 100%; +} +.v2-landing-rise { animation: v2land-rise .6s cubic-bezier(.22,1,.36,1) backwards; } +.v2-landing-rise[data-i="1"] { animation-delay: .07s; } +.v2-landing-rise[data-i="2"] { animation-delay: .14s; } +.v2-landing-rise[data-i="3"] { animation-delay: .21s; } +@keyframes v2land-rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } } + +/* agent presence */ +.v2-landing-agent { display: flex; flex-direction: column; align-items: center; gap: 16px; } +.v2-landing-orb { + width: 52px; height: 52px; border-radius: 50%; + background: radial-gradient(closest-side at 38% 34%, #ffffff, #bcd3ff 40%, var(--accent, #2f6df6) 100%); + box-shadow: 0 6px 22px rgba(47,109,246,.45), inset 0 0 12px rgba(255,255,255,.6); + animation: v2land-breathe 4s ease-in-out infinite; +} +@keyframes v2land-breathe { 0%,100% { transform: scale(1); } 50% { transform: scale(1.06); } } +.v2-landing-say { + font-size: clamp(20px, 3vw, 28px); font-weight: 600; letter-spacing: -.02em; + text-align: center; max-width: 22ch; line-height: 1.25; color: var(--text, #0f172a); +} +.v2-landing-say .dim { color: var(--text-muted, #94a3b8); font-weight: 500; } + +/* choice cards */ +.v2-landing-choices { + display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 18px; + margin-top: 34px; width: min(720px, 92vw); +} +@media (max-width: 620px) { .v2-landing-choices { grid-template-columns: 1fr; } } +.v2-choice { + text-align: left; cursor: pointer; position: relative; overflow: hidden; + border: 1px solid var(--border, #e4e9f0); background: var(--bg-card, #fff); + border-radius: 18px; padding: 22px 22px 20px; + box-shadow: 0 1px 2px rgba(15,23,42,.04), 0 8px 28px rgba(15,23,42,.06); + font: inherit; color: var(--text, #0f172a); + transition: transform .26s cubic-bezier(.22,1,.36,1), box-shadow .26s cubic-bezier(.22,1,.36,1), border-color .26s; +} +.v2-choice:hover { + transform: translateY(-4px); border-color: #cfdcf5; + box-shadow: 0 2px 6px rgba(15,23,42,.06), 0 18px 50px rgba(47,109,246,.14); +} +.v2-choice:active { transform: translateY(-1px) scale(.995); } +.v2-choice-ic { + width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; + background: var(--accent-soft, #eaf1ff); color: var(--accent, #2f6df6); margin-bottom: 14px; +} +.v2-choice.alt .v2-choice-ic { background: var(--accent-green-soft, #e7f6ec); color: var(--accent-green, #16a34a); } +.v2-choice h3 { margin: 0 0 6px; font-size: 17px; letter-spacing: -.01em; } +.v2-choice p { margin: 0; color: var(--text-secondary, #475569); font-size: 13.5px; line-height: 1.5; } +.v2-choice-tag { + position: absolute; top: 16px; right: 16px; + font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted, #94a3b8); + border: 1px solid var(--border, #e4e9f0); border-radius: 999px; padding: 3px 9px; +} +.v2-choice-go { + margin-top: 16px; display: flex; align-items: center; gap: 6px; + color: var(--accent, #2f6df6); font-size: 13px; font-weight: 600; + opacity: 0; transform: translateX(-4px); transition: .26s cubic-bezier(.22,1,.36,1); +} +.v2-choice.alt .v2-choice-go { color: var(--accent-green, #16a34a); } +.v2-choice:hover .v2-choice-go { opacity: 1; transform: none; } + +/* escape hatch — chat is the last resort, an obvious pill */ +.v2-escape { margin-top: 26px; display: flex; flex-direction: column; align-items: center; } +.v2-escape-toggle { + display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font: inherit; font-size: 13px; + background: var(--bg-card, #fff); border: 1px solid var(--border, #e4e9f0); color: var(--text-secondary, #475569); + padding: 8px 15px; border-radius: 999px; + box-shadow: 0 1px 2px rgba(15,23,42,.04), 0 8px 28px rgba(15,23,42,.06); + transition: color .2s, border-color .2s, transform .2s cubic-bezier(.22,1,.36,1); +} +.v2-escape-toggle:hover { color: var(--text, #0f172a); border-color: #cfd9e6; transform: translateY(-1px); } +.v2-escape-toggle .v2-escape-caret { display: inline-block; transition: transform .3s cubic-bezier(.22,1,.36,1); opacity: .55; } +.v2-escape.open .v2-escape-toggle .v2-escape-caret { transform: rotate(180deg); } +.v2-escape-field { + display: flex; align-items: center; gap: 8px; width: min(520px, 90vw); + max-height: 0; opacity: 0; overflow: hidden; + transition: max-height .4s cubic-bezier(.22,1,.36,1), opacity .4s cubic-bezier(.22,1,.36,1), margin .4s cubic-bezier(.22,1,.36,1); +} +.v2-escape.open .v2-escape-field { max-height: 64px; opacity: 1; margin-top: 12px; } +.v2-escape-field input { + flex: 1; min-width: 0; border: 1px solid var(--border, #e4e9f0); background: var(--bg-card, #fff); + border-radius: 12px; padding: 12px 14px; font: inherit; font-size: 14px; color: var(--text, #0f172a); + outline: none; box-shadow: 0 1px 2px rgba(15,23,42,.04); + transition: border-color .2s, box-shadow .2s; +} +.v2-escape-field input:focus { border-color: var(--accent, #2f6df6); box-shadow: 0 0 0 4px rgba(47,109,246,.12); } +.v2-escape-send { + appearance: none; border: 0; cursor: pointer; flex: none; width: 42px; height: 42px; border-radius: 12px; + background: var(--accent, #2f6df6); color: #fff; display: grid; place-items: center; + box-shadow: 0 6px 16px rgba(47,109,246,.35); transition: transform .2s cubic-bezier(.22,1,.36,1), filter .2s; +} +.v2-escape-send:hover { transform: translateY(-1px); filter: brightness(1.05); } + +/* one-way skip into the workspace (e.g. a reload mid-session) */ +.v2-landing-skip { + margin-top: 28px; background: none; border: 0; cursor: pointer; font: inherit; font-size: 12.5px; + color: var(--text-muted, #94a3b8); padding: 6px 10px; border-radius: 8px; + transition: color .2s; +} +.v2-landing-skip:hover { color: var(--text-secondary, #475569); } + +/* Under ux_v2 the landing IS the welcome moment, so the legacy home hero + (static "Welcome to Gently" + start button) would be a duplicate behind it — + hide it. The recent-* cards and the context surface stay. */ +body.ux-v2 .home-hero { display: none; } + +/* ── Two-screen system: welcome ↔ in-place plan wizard ───────── */ +.v2-landing-inner { max-width: 980px; } /* widen for the plan layout */ +.v2-landing .v2-screen { display: none; width: 100%; } +.v2-landing[data-screen="welcome"] .v2-screen-welcome { + display: flex; flex-direction: column; align-items: center; + max-width: 760px; margin: 0 auto; +} +.v2-landing[data-screen="plan"] .v2-screen-plan { + display: flex; flex-direction: column; + animation: v2land-rise .45s cubic-bezier(.22,1,.36,1) backwards; +} + +/* plan header */ +.v2-plan-head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; } +.v2-plan-orb { width: 34px; height: 34px; } +.v2-plan-who { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #94a3b8); } +.v2-plan-title { font-size: 18px; font-weight: 600; letter-spacing: -.01em; color: var(--text, #0f172a); } +.v2-plan-back { + margin-left: auto; background: none; border: 0; cursor: pointer; font: inherit; font-size: 13px; + color: var(--text-muted, #94a3b8); padding: 6px 10px; border-radius: 8px; transition: color .2s, background .2s; +} +.v2-plan-back:hover { color: var(--text, #0f172a); background: rgba(15,23,42,.05); } + +/* plan body: ask stage + assembling plan */ +.v2-plan-wrap { display: grid; grid-template-columns: 1.35fr .9fr; gap: 20px; align-items: start; } +@media (max-width: 720px) { .v2-plan-wrap { grid-template-columns: 1fr; } } +.v2-plan-main { min-height: 220px; } +.v2-plan-ask:empty { display: none; } +.v2-plan-thinking { display: flex; align-items: center; gap: 9px; color: var(--text-muted, #94a3b8); font-size: 13.5px; padding: 22px 4px; } +.v2-plan-thinking.hidden { display: none; } +.v2-typing { display: inline-flex; gap: 5px; align-items: center; } +.v2-typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #2f6df6); opacity: .4; animation: v2-blink 1.1s infinite; } +.v2-typing i:nth-child(2) { animation-delay: .15s; } +.v2-typing i:nth-child(3) { animation-delay: .3s; } +@keyframes v2-blink { 0%,100% { opacity: .25; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } } + +.v2-plan-side { + background: var(--bg-card, #fff); border: 1px solid var(--border, #e4e9f0); border-radius: 14px; padding: 16px 18px; + box-shadow: 0 1px 2px rgba(15,23,42,.04), 0 8px 28px rgba(15,23,42,.06); +} +.v2-plan-side-h { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted, #94a3b8); margin-bottom: 12px; } +.v2-plan-side-empty { color: var(--text-muted, #94a3b8); font-size: 13px; font-style: italic; } +.v2-plan-row { display: flex; flex-direction: column; gap: 2px; padding: 9px 0; border-top: 1px dashed var(--border, #e4e9f0); } +.v2-plan-row:first-child { border-top: 0; } +.v2-plan-row .k { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #94a3b8); } +.v2-plan-row .v { font-size: 14px; color: var(--text, #0f172a); font-weight: 550; } + +/* plan footer */ +.v2-plan-foot { display: flex; align-items: center; gap: 10px; margin-top: 22px; } +.v2-plan-chat { + margin-right: auto; background: none; border: 1px solid var(--border, #e4e9f0); color: var(--text-secondary, #475569); + border-radius: 999px; padding: 8px 14px; font: inherit; font-size: 13px; cursor: pointer; transition: border-color .2s, color .2s; +} +.v2-plan-chat:hover { border-color: #cfd9e6; color: var(--text, #0f172a); } +.v2-plan-continue { + background: var(--accent, #2f6df6); color: #fff; border: 0; border-radius: 10px; padding: 10px 16px; + font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer; transition: transform .2s cubic-bezier(.22,1,.36,1), filter .2s; +} +.v2-plan-continue:hover { transform: translateY(-1px); filter: brightness(1.05); } + +@media (prefers-reduced-motion: reduce) { + .v2-landing, .v2-landing::before, .v2-landing-rise, .v2-landing-orb, + .v2-landing[data-screen="plan"] .v2-screen-plan, .v2-typing i { + animation: none !important; + transition-duration: .12s !important; + } +} diff --git a/gently/ui/web/static/js/agent-chat.js b/gently/ui/web/static/js/agent-chat.js index c55c358a..33dcf2b3 100644 --- a/gently/ui/web/static/js/agent-chat.js +++ b/gently/ui/web/static/js/agent-chat.js @@ -12,6 +12,9 @@ const AgentChat = (() => { let ws = null; let reconnectDelay = 1000; + // Commands/messages requested before the socket is open (e.g. from the + // landing, with the chat panel closed) — flushed in order on ws.onopen. + let pendingProgrammatic = []; const MAX_DELAY = 30000; let panelOpen = false; @@ -743,11 +746,17 @@ const AgentChat = (() => { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj)); } + // Flush programmatic sends queued before the socket opened (see runCommand). + function flushProgrammatic() { + if (!pendingProgrammatic.length) return; + pendingProgrammatic.splice(0).forEach(t => actuallySend(t)); + } + function connect() { const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; setConn(false, 'Connecting…'); ws = new WebSocket(`${proto}//${location.host}/ws/agent`); - ws.onopen = () => { reconnectDelay = 1000; setConn(true); }; + ws.onopen = () => { reconnectDelay = 1000; setConn(true); flushProgrammatic(); }; ws.onclose = () => { setConn(false, 'Reconnecting…'); setBusy(false); @@ -1067,7 +1076,12 @@ const AgentChat = (() => { function runCommand(text) { if (!text) return; if (!hasControl) { renderControl(); return; } - actuallySend(text); + // Works whether or not the chat panel is open, so the landing can drive + // the agent (enter plan mode) without foregrounding the chat REPL. If the + // socket isn't up yet, queue and connect — it flushes on open. + if (ws && ws.readyState === WebSocket.OPEN) { actuallySend(text); return; } + pendingProgrammatic.push(text); + if (!ws) connect(); } return { togglePanel, runCommand, buildAskCard, answerChoice, hasControl: () => hasControl }; diff --git a/gently/ui/web/static/js/landing.js b/gently/ui/web/static/js/landing.js new file mode 100644 index 00000000..35eaf1fa --- /dev/null +++ b/gently/ui/web/static/js/landing.js @@ -0,0 +1,214 @@ +/** + * V2Landing (ux_v2): the agent-first welcome AND the in-place plan wizard. + * + * The key paradigm fix: the plan dialogue renders IN THE LANDING, not the chat + * REPL. Clicking "Plan an experiment" doesn't recede to the dashboard — it + * switches the landing to a plan screen, enters plan mode, and renders the + * agent's ask_user_choice questions as button cards right there (#v2-plan-ask), + * reusing AgentChat.buildAskCard so it's the same card/answer path as elsewhere. + * The plan assembles on the right from each pick. Chat is never the surface + * (it's one click away via "Open conversation" as the last resort). + * + * Plan an experiment → switch to plan screen; /plan + kickoff; asks render here + * Take a quick look → scope (Devices view) + * "just tell me…" → free-text into the agent loop (then chat as transcript) + * + * No-ops unless #v2-landing is present (flag off → v1 untouched, overlay absent). + */ +const V2Landing = (() => { + let el = null; + let current = null; // { reqId, data, isWake } — the ask showing in the plan stage + + const $ = (id) => document.getElementById(id); + + function greet() { + const g = $('v2-landing-greeting'); + if (!g) return; + const h = new Date().getHours(); + const t = h < 5 ? 'Still here.' : h < 12 ? 'Good morning.' + : h < 18 ? 'Good afternoon.' : 'Good evening.'; + g.innerHTML = t + '
What are we doing today?'; + } + + function setScreen(name) { if (el) el.dataset.screen = name; } + function planActive() { + return !!el && el.dataset.screen === 'plan' && !el.classList.contains('dismissed') + && el.style.display !== 'none'; + } + + function dismiss() { + if (!el || el.classList.contains('dismissed')) return; + el.classList.add('dismissed'); + let done = false; + const finish = () => { + if (done) return; + done = true; + el.style.display = 'none'; + el.setAttribute('aria-hidden', 'true'); + }; + el.addEventListener('transitionend', finish, { once: true }); + setTimeout(finish, 650); + } + + // ── plan stage helpers ──────────────────────────────────────────── + function showThinking(on) { + const t = $('v2-plan-thinking'); + if (t) t.classList.toggle('hidden', !on); + } + function clearAsk() { const m = $('v2-plan-ask'); if (m) m.innerHTML = ''; } + function resetSummary() { + const list = $('v2-plan-summary'); + if (list) list.innerHTML = '
Assembling from your choices…
'; + } + function labelFor(data, sel) { + const opts = (data && data.options) || []; + const one = (s) => { + const o = opts.find(o => o && (o.id === s || o.value === s || o.label === s)); + return o ? o.label : String(s); + }; + if (Array.isArray(sel)) return sel.map(one).join(', '); + return one(sel); + } + function recordPick(data, sel) { + const list = $('v2-plan-summary'); + if (!list) return; + const empty = list.querySelector('.v2-plan-side-empty'); + if (empty) empty.remove(); + const row = document.createElement('div'); + row.className = 'v2-plan-row'; + row.innerHTML = ''; + row.querySelector('.k').textContent = (data && data.question) || 'Choice'; + row.querySelector('.v').textContent = labelFor(data, sel); + list.appendChild(row); + } + function renderAsk() { + const mount = $('v2-plan-ask'); + if (!mount || !current || typeof AgentChat === 'undefined' || !AgentChat.buildAskCard) return; + showThinking(false); + const data = current.data, reqId = current.reqId; + const hasControl = AgentChat.hasControl ? AgentChat.hasControl() : true; + const card = AgentChat.buildAskCard(data, { + reqId, isWake: current.isWake, hasControl, + onPick: (sel) => { + recordPick(data, sel); + AgentChat.answerChoice(reqId, sel); + current = null; + clearAsk(); + showThinking(true); // agent computing the next step + }, + }); + mount.innerHTML = ''; + mount.appendChild(card); + } + + function startPlan() { + setScreen('plan'); // stay on the overlay; swap welcome → plan wizard + resetSummary(); + clearAsk(); + current = null; + showThinking(true); + // /plan deterministically enters plan mode; the kickoff draws out the + // first question, which the agent asks via ask_user_choice (its prompt + // mandates it) → renders here, not in chat. runCommand connects on its + // own and flushes both in order without opening the chat panel. + if (typeof AgentChat !== 'undefined' && AgentChat.runCommand) { + AgentChat.runCommand('/plan'); + AgentChat.runCommand("Let's design this run — what should it capture?"); + } + } + + function openScope() { + dismiss(); + if (typeof switchTab === 'function') switchTab('devices'); + } + + function sendFreeform(text) { + const v = (text || '').trim(); + dismiss(); // free-text is the chat last-resort → recede, then open chat + if (typeof AgentChat !== 'undefined' && AgentChat.togglePanel) { + AgentChat.togglePanel(true); + if (v && AgentChat.runCommand) setTimeout(() => AgentChat.runCommand(v), 300); + } + } + + function init() { + el = $('v2-landing'); + if (!el || typeof ClientEventBus === 'undefined') return; // flag off → no-op + greet(); + + // welcome choices + el.querySelectorAll('[data-landing]').forEach(btn => btn.addEventListener('click', () => { + const kind = btn.dataset.landing; + if (kind === 'plan') startPlan(); + else if (kind === 'standalone') openScope(); + })); + + // welcome escape field + const esc = $('v2-escape'), escToggle = $('v2-escape-toggle'), + escInput = $('v2-escape-input'), escSend = $('v2-escape-send'); + if (escToggle && esc && escInput) { + escToggle.addEventListener('click', () => { + const open = esc.classList.toggle('open'); + if (open) setTimeout(() => escInput.focus(), 120); + }); + const submit = () => sendFreeform(escInput.value); + if (escSend) escSend.addEventListener('click', submit); + escInput.addEventListener('keydown', e => { + if (e.key === 'Enter') { e.preventDefault(); submit(); } + else if (e.key === 'Escape') { e.stopPropagation(); esc.classList.remove('open'); } + }); + } + + const skip = $('v2-landing-skip'); + if (skip) skip.addEventListener('click', dismiss); + + // plan-screen controls + const back = $('v2-plan-back'); + if (back) back.addEventListener('click', () => setScreen('welcome')); + const planChat = $('v2-plan-chat'); + if (planChat) planChat.addEventListener('click', () => { + dismiss(); // chat panel lives under the overlay → recede first + if (typeof AgentChat !== 'undefined' && AgentChat.togglePanel) { + setTimeout(() => AgentChat.togglePanel(true), 300); + } + }); + const cont = $('v2-plan-continue'); + if (cont) cont.addEventListener('click', dismiss); + + // The agent's questions render in the plan stage while it's active; once + // we've receded into the workspace, AskStage (#ask-stage) takes over. + ClientEventBus.on('AGENT_ASK', ({ request_id, choice_data, origin }) => { + if (!planActive()) return; + current = { reqId: request_id, data: choice_data || {}, isWake: origin === 'wake' }; + renderAsk(); + }); + ClientEventBus.on('ASK_CLEARED', ({ request_id }) => { + if (request_id === '*' || (current && request_id === current.reqId)) { + current = null; + clearAsk(); + if (planActive()) showThinking(true); + } + }); + ClientEventBus.on('AGENT_CONTROL', () => { if (current && planActive()) renderAsk(); }); + + document.addEventListener('keydown', e => { + if (e.key !== 'Escape' || !el || el.classList.contains('dismissed')) return; + if (el.dataset.screen === 'plan') setScreen('welcome'); // step back, don't bail + else dismiss(); + }); + } + + document.addEventListener('DOMContentLoaded', init); + + return { + dismiss, + show: () => { + if (!el) return; + el.style.display = ''; + el.removeAttribute('aria-hidden'); + el.classList.remove('dismissed'); + setScreen('welcome'); + greet(); + }, + }; +})(); diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index 9a4ff9dd..c972dc3d 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -17,8 +17,89 @@ + + {% if ux_v2 %} + {# Agent-first welcome — shown on entry, recedes into the workspace once the + user picks a path. Chat is the last resort (the escape pill), not the + first thing. Only rendered when the flag is on. #} +
+
+ + {# ── Screen 1: welcome ── #} +
+
+
+
Hello.
What are we doing today?
+
+ +
+ + + +
+ +
+ +
+ + +
+
+ + +
+ + {# ── Screen 2: the plan wizard, hosted IN the landing. The agent's + ask_user_choice questions render here as button cards (#v2-plan-ask), + NOT in the chat panel. The plan assembles on the right as you pick. #} +
+
+ +
+
Gently · planning
+
Let's design your run
+
+ +
+
+
+
+
thinking through the next step…
+
+ +
+
+ + +
+
+ +
+
+ {% endif %} {% include '_header.html' %} {% include '_navbar.html' %} +
+
+
+
Good evening.
What are we doing today?
+
+ +
+ + + +
+ +
+ +
+ + +
+
+
+ + +
+
+
+
+
+ Gently + +
+
+
+
+
+ + +
+
+
+
+
+ +
+

The plan

+

assembling from your choices

+
+
Nothing yet — pick above and watch it fill in.
+
+
+
+
+
+ + +
+
+
+

Quick look

+

I'll take one careful volume right where the stage is now — nothing scheduled, nothing committed.
(We'll design this surface next — it's a stub for now.)

+
+
+
+ + +
+
+
+ LIVE + run + + 0h 12m elapsed · next 1:43 +
+ + + +
+
+
⚠ Needs you
+
Embryo 3 has been quiet for 40 min — past its expected division window. Keep waiting, or flag it for you?
+
+ + + +
+
+ +

Embryos · 3 tracked

representative — the embryo-wise tactical view is yours to define
+
+
Embryo 1on track
4-cell · imaging normally
+
Embryo 2dividing
sped up to every 30 s
+
Embryo 3stalled
40 min quiet · flagged above
+
+
+
+
+ +
+
+ + + + From 6c6bc7095ab51719521ce6ff29c3be35d9e23344 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 07:39:28 +0530 Subject: [PATCH 14/34] Detectors: forced-tool structured output for hatching + verifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert hatching.py and the verifier's four challenger strategies from JSON-in-prose / startswith-scraping with silent defaults to forced tool_choice — the verdict arrives as a validated dict on the tool_use block. Deletes the regex/parse layer; downstream vote-tally/consensus is untouched. Also drop self-rated confidence from these schemas (a heuristics-era artifact — the boolean/categorical judgment is the signal); the ensemble's derived agreement ratio stays. docs/HEURISTICS-AUDIT.md ranks the remaining candidates and the keep-deterministic boundary. Co-Authored-By: Claude Fable 5 --- docs/HEURISTICS-AUDIT.md | 112 ++++ gently/app/detectors/hatching.py | 124 +++-- gently/harness/detection/verifier.py | 758 +++++++++++---------------- 3 files changed, 488 insertions(+), 506 deletions(-) create mode 100644 docs/HEURISTICS-AUDIT.md diff --git a/docs/HEURISTICS-AUDIT.md b/docs/HEURISTICS-AUDIT.md new file mode 100644 index 00000000..249b27cc --- /dev/null +++ b/docs/HEURISTICS-AUDIT.md @@ -0,0 +1,112 @@ +# Heuristics audit — where to use the model (as a typed-output function) instead + +Codebase sweep (5 parallel scanners + synthesis) for heuristics that **fake +judgment** an LLM would do better — in the spirit of the genotype→channel +refactor (drop the lookup table, let the model infer, keep a typed provenance +record + confirm-when-unsure). The flip side — logic that **must stay +deterministic** (safety, math, calibration, transport) — is listed at the end so +we don't mistakenly LLM-ify it. + +The unifying move for every candidate: **LLM with a typed structured-output +schema + provenance + a confirm/UNCERTAIN escape**, never free-text-then-parse. + +## Model candidates (ranked) + +### High value + +1. **Hatching / time-to-stage prediction** — `organisms/celegans/developmental_tracker.py` + *(the closest twin of genotype→channel; medium effort)* + Three hardcoded 20 °C lookup tables (`STAGE_TIMING_20C`, `TIME_TO_HATCHING`, + `TIMING_VARIABILITY`) plus magic `{HIGH:1.0, MEDIUM:1.5, LOW:2.0}` uncertainty + fudge factors. Structurally **can't use the rig's actual temperature** (we run + a TEC), the strain, or the embryo's observed progression rate. Let the model + produce a calibrated, explained interval; **keep the literature table as a + deterministic sanity bracket** and flag when the estimate falls outside it. + → `{ predicted_minutes_to_hatching, low, high, basis, assumptions{temperature_c,strain,used_observed_rate}, confidence, reasoning }` + +2. **Citation → PubMed query** — `harness/plan_mode/tools/research.py` (`_search_pmid`) + A regex that only handles "Surname et al YEAR …" + six hand-rolled query- + relaxation strategies + a stopword/word-position ladder that drops load-bearing + nouns. The model parses the sloppy citation and proposes relaxed queries; **code + keeps the deterministic esearch call and never fabricates a PMID.** + → `{ author_last, year, journal, topic_keywords[], organism, pubmed_query, alt_queries[], confidence }` + +3. **Lab-history retrieval** — `harness/plan_mode/tools/lab_context.py`, `harness/memory/interface.py` + Semantic recall faked by substring-OR over query tokens (matches "we"/"before", + misses every paraphrase). Feed the model the candidate records and have it + **rank/select from provided ids only** (no fabrication). Read-only, no + acquisition risk. + → `{ matches:[{kind,id,summary,relevance,why_relevant}], answer }` + +4. **Stage-label parse via 22-entry synonym dict** — `developmental_tracker.py` (`_parse_stage_name`) + *(small effort, pure robustness win)* The Vision call already classifies; the + brittleness is a plain-text `STAGE:/CONFIDENCE:` block scraped line-by-line, with + off-vocabulary phrasings silently collapsing to `UNKNOWN` (which kills the + downstream hatching prediction). Constrained-enum structured output deletes the + parser + synonym table. + → `{ stage: enum(...), confidence: enum(high|medium|low), is_transitional, reasoning }` + +### Medium value (mostly small — fix the output contract, not the judgment) + +5. **Calibration Vision calls** — `hardware/dispim/claude_client.py` + Four Vision calls return positional free text recovered by `'yes' in first_line` + / `re.search(r'\d+')` / first-valid-letter, with silent defaults (so "no, this is + not yes…" reads as *yes*). Typed output deletes the parse + silent-default layer. + +6. **ML architecture ranking** — `ml/architectures.py` (`get_suitable_architectures`) + Hard feasibility gates (VRAM / dataset) are correct **and stay**; the `+2/+1/+1` + point-score ranking that follows discards the per-arch prose. Let the model rank + the *pre-filtered feasible set* (ids constrained to that set). + +7. **Training label normalization** — `ml/data_loader.py` (`build_labels_from_store`) + Class space built by exact-string identity over free-text human annotations — + "1.5-fold" and "1.5 fold" become different classes. Model normalizes to the + canonical staging vocabulary, flags novel/ambiguous ones. + +### Lower value + +8. **"Plan has a control?"** — `plan_mode/tools/validation.py` — substring scan of a + 6-word keyword set; a scientific judgment over the whole plan. Non-blocking + warning → safe for the model. +9. **CGC HTML scraping** — `research.py` (`_cgc_search`) — positional multi-group + regex over fetched HTML; structured extraction the model does better (HTTP GET + stays code; **mark strain names low-confidence to avoid sending someone to order + a hallucinated strain**). + +### Cross-cutting batch (small each): typed output for the detector/verifier cluster +`harness/detection/verifier.py`, `app/detectors/hatching.py`, +`app/detectors/dopaminergic_signal.py`, `hardware/dispim/sam_detection.py` — all +already make the right model call but reconstruct the verdict via +`startswith`/regex-JSON-scraping with silent defaults. A batch move to native +structured output **strictly reduces parse-induced false negatives** without +touching the deterministic vote-tally/consensus/enum-dispatch downstream. + +**Reference implementations already in the repo (imitate, don't change):** +`dopaminergic_signal`'s perceiver→classifier rubric (typed enums, UNCERTAIN +escape, conservative-on-tie) and onboarding's `_extract_with_llm` (typed +extraction, degrade-to-verbatim fallback). + +## Keep deterministic (do NOT LLM-ify) +Safety, math, calibration, and transport — where a hallucinated value is unsafe +or breaks reproducibility: +- Laser-power safety limits + wavelength→MM-property map (`hardware/dispim/devices/optical.py`) +- SPIM trigger-timing arithmetic, piezo–galvo calibration, MM framing (`dispim/config.py`) +- Calibration prior EMA + R²≥0.75 slope-lock gate (`dispim/calibration.py`) +- SwitchBot GATT byte commands / status decoding (`hardware/switchbot.py`) +- Temperature setpoint bound [0,99.9] °C + stabilization I/O (`hardware/temperature.py`) +- Autofocus signal-processing, curve fitting, adaptive-sweep stop rules (`analysis/core.py`, `analysis/focus.py`) +- Classical-CV ROI detection + pixel→stage coordinate transforms (`detection.py`, `sam_detection.py` geometry) +- Timelapse rule dispatch + `confirm_timepoints` debounce + monotonic power ramp (`app/orchestration/timelapse.py`) +- Volume→b64 dark/flat calibration + fixed brightness scaling (`dopaminergic_signal._volume_to_b64` — deliberately non-adaptive) +- Wake-router debounce/throttle/stage-transition gate (`app/wake_router.py`) +- Plan hardware limits, detector-preset membership, dependency-cycle DFS, stage-order normalization (`plan_mode/tools/validation.py`) +- Ensemble vote tally + 0.70 quorum / unanimity consensus (`detection/verifier.py`) +- ML metric/aggregation math: confusion matrix, F1, federated averaging (`ml/evaluation.py`, `federated.py`) +- Core imaging geometry (max-projection, crop bounds, Euler rotations) + UI event reduction/routing/security (`core/imaging.py`, `ui/web/*`) +- Device-state SSE watchdog/staleness timers (`app/device_state_monitor.py`) +- Reference-type dispatch (PMID/DOI/URL by canonical syntax), `os.path.isfile` checks (`research.py`) + +## Note +`gap_assessment.conversation_weight` (the 0.25/0.1/0.05 readiness scalar) is now +largely **vestigial** — it only returns 'heavy' (lab onboarding) or 'none' — so +it's not worth an API call. Left off the candidate list. diff --git a/gently/app/detectors/hatching.py b/gently/app/detectors/hatching.py index 62dd31cb..ca63acd3 100644 --- a/gently/app/detectors/hatching.py +++ b/gently/app/detectors/hatching.py @@ -5,12 +5,16 @@ pipeline trains on. The dopaminergic-signal detector already returns ``has_hatched`` as part of its richer schema; this is a lighter-weight yes/no for use cases where structure / intensity assessment isn't needed. + +The verdict comes back as a forced tool call (``tool_choice`` pins the model +to ``record_hatching``), so the structured fields arrive already parsed as +``block.input`` — no JSON-from-prose scraping, no silent-default parse layer. """ import asyncio import logging import time -from typing import Any +from typing import Any, Dict, Optional import numpy as np @@ -20,8 +24,7 @@ logger = logging.getLogger(__name__) -_HATCHING_PROMPT = """You are observing a C. elegans embryo on a microscope. Decide whether -the embryo has HATCHED. +_HATCHING_PROMPT = """You are observing a C. elegans embryo on a microscope. Decide whether the embryo has HATCHED, then record your decision with the record_hatching tool. A HATCHED embryo: - Has visibly broken out of the eggshell @@ -32,39 +35,53 @@ - Is still contained within an intact eggshell - May be at any pre-hatching stage (bean, comma, 1.5-fold, 2-fold, pretzel) -Respond with ONLY a JSON object exactly matching this schema: +Default to has_hatched=false unless you are confident. Don't over-call hatching. +""" -{ - "has_hatched": true|false, - "confidence": "LOW|MEDIUM|HIGH", - "reasoning": "..." -} -Default to false unless you are confident. Don't over-call hatching. -""" +# Forced tool schema — the model is pinned to this via tool_choice, so the +# fields come back as a validated dict on the tool_use block. The conservative +# "default to false" guidance lives in the prompt. We deliberately do NOT ask +# the model to self-rate confidence — that's a heuristics-era artifact; the +# has_hatched judgment is the signal. +_HATCHING_TOOL = { + "name": "record_hatching", + "description": "Record whether the C. elegans embryo has hatched, with brief reasoning.", + "input_schema": { + "type": "object", + "properties": { + "has_hatched": { + "type": "boolean", + "description": "True only if the embryo has visibly broken out of the eggshell.", + }, + "reasoning": { + "type": "string", + "description": "One short sentence citing the visual evidence for the call.", + }, + }, + "required": ["has_hatched", "reasoning"], + }, +} class HatchingDetector(Detector): - """Claude-vision hatching yes/no, with confidence.""" + """Claude-vision hatching yes/no.""" name = "hatching" - def __init__(self, claude_client=None, model: str | None = None): + def __init__(self, claude_client=None, model: Optional[str] = None): self._claude = claude_client self._model = model async def run( self, volume: np.ndarray, - context: dict[str, Any], + context: Dict[str, Any], ) -> DetectorResult: + from gently.settings import settings import json - import re - import anthropic - from gently.settings import settings - embryo_id = context.get("embryo_id", "?") timepoint = int(context.get("timepoint", 0)) start = time.time() @@ -84,7 +101,7 @@ async def run( detector_name=self.name, embryo_id=embryo_id, timepoint=timepoint, - findings={"has_hatched": False, "confidence": "LOW"}, + findings={"has_hatched": False}, reasoning="Empty / unreadable volume", elapsed_ms=(time.time() - start) * 1000, ) @@ -93,41 +110,40 @@ async def run( response = await asyncio.to_thread( claude.messages.create, model=self._model or settings.models.fast, - max_tokens=200, - messages=[ - { - "role": "user", - "content": [ - {"type": "text", "text": _HATCHING_PROMPT}, - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": b64_image, - }, - }, - ], - } - ], + max_tokens=256, + tools=[_HATCHING_TOOL], + tool_choice={"type": "tool", "name": _HATCHING_TOOL["name"]}, + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": _HATCHING_PROMPT}, + {"type": "image", "source": { + "type": "base64", + "media_type": "image/png", + "data": b64_image, + }}, + ], + }], + ) + + # Forced tool_choice guarantees a tool_use block; read its parsed + # input directly. No regex, no JSON-from-prose fallback. + tool_input = next( + (b.input for b in response.content + if getattr(b, "type", None) == "tool_use"), + None, ) - raw = response.content[0].text if response.content else "" - findings = {"has_hatched": False, "confidence": "LOW"} + findings = {"has_hatched": False} reasoning = None err = None - try: - m = re.search(r"\{.*?\}", raw, re.DOTALL) - blob = m.group(0) if m else raw.strip() - parsed = json.loads(blob) - findings["has_hatched"] = bool(parsed.get("has_hatched", False)) - confidence = str(parsed.get("confidence", "LOW")).upper() - if confidence not in {"LOW", "MEDIUM", "HIGH"}: - confidence = "LOW" - findings["confidence"] = confidence - reasoning = parsed.get("reasoning") - except (json.JSONDecodeError, AttributeError) as e: - err = f"parse error: {e}" + if isinstance(tool_input, dict): + findings["has_hatched"] = bool(tool_input.get("has_hatched", False)) + reasoning = tool_input.get("reasoning") + else: + # Shouldn't happen with forced tool_choice — keep the + # conservative default and record why. + err = "no tool_use block in response" return DetectorResult( detector_name=self.name, @@ -135,16 +151,12 @@ async def run( timepoint=timepoint, findings=findings, reasoning=reasoning, - raw_response=raw, + raw_response=json.dumps(tool_input) if isinstance(tool_input, dict) else None, elapsed_ms=(time.time() - start) * 1000, error=err, ) - except ( - anthropic.APIConnectionError, - anthropic.RateLimitError, - anthropic.APIStatusError, - ) as e: + except (anthropic.APIConnectionError, anthropic.RateLimitError, anthropic.APIStatusError) as e: logger.error("[%s] Claude API error for %s: %s", self.name, embryo_id, e) return DetectorResult( detector_name=self.name, diff --git a/gently/harness/detection/verifier.py b/gently/harness/detection/verifier.py index 0370abb7..849fbdbf 100644 --- a/gently/harness/detection/verifier.py +++ b/gently/harness/detection/verifier.py @@ -14,35 +14,102 @@ import logging from dataclasses import dataclass, field from datetime import datetime -from typing import Any +from typing import Any, Callable, Dict, List, Optional import anthropic -from gently.core import EventType, get_event_bus from gently.settings import settings - +from .detector import Detector, DetectionResult, ConfidenceLevel from ..state import EmbryoState -from .detector import ConfidenceLevel, DetectionResult, Detector +from gently.core import EventType, get_event_bus logger = logging.getLogger(__name__) +# Each verification strategy is pinned to its tool via tool_choice, so the +# verdict arrives as a validated dict on the tool_use block — no +# startswith()-scraping of a "FIELD: VALUE" plain-text format, no silent +# defaults from a missed line. Downstream vote-tally / consensus logic is +# untouched: these helpers still produce the same strategy dataclasses. +# +# We deliberately don't ask the model to self-rate confidence (a heuristics-era +# artifact) — the boolean verdict is the signal, and the only confidence-like +# measure we keep is the ensemble's agreement ratio, which is *derived* from +# many independent votes rather than introspected by one call. +_ADVERSARIAL_TOOL = { + "name": "record_adversarial_review", + "description": "Record the critical review verdict: whether counter-evidence against the detection was found.", + "input_schema": { + "type": "object", + "properties": { + "found_counter_evidence": {"type": "boolean", "description": "True only if there is real evidence the detection is wrong."}, + "concerns": {"type": "array", "items": {"type": "string"}, "description": "Specific doubts or alternative explanations; empty list if none."}, + }, + "required": ["found_counter_evidence", "concerns"], + }, +} + +_INDEPENDENT_TOOL = { + "name": "record_independent_assessment", + "description": "Record an unbiased fresh assessment of whether the event occurred in this image.", + "input_schema": { + "type": "object", + "properties": { + "detected": {"type": "boolean", "description": "True if the event is observed in this image."}, + "key_evidence": {"type": "string", "description": "What specifically supports the conclusion."}, + }, + "required": ["detected", "key_evidence"], + }, +} + +_TEMPORAL_TOOL = { + "name": "record_temporal_comparison", + "description": "Record whether a real change consistent with the event occurred between the previous and current frames.", + "input_schema": { + "type": "object", + "properties": { + "change_detected": {"type": "boolean", "description": "True if a clear change consistent with the event is visible across frames."}, + "description": {"type": "string", "description": "The specific change observed between previous and current frames."}, + }, + "required": ["change_detected", "description"], + }, +} + +_HARDWARE_CONTEXT_TOOL = { + "name": "record_hardware_context", + "description": "Record whether hardware errors could have caused a false-positive detection.", + "input_schema": { + "type": "object", + "properties": { + "suspicious": {"type": "boolean", "description": "True if hardware errors could have affected image quality or positioning for this embryo."}, + "concerns": {"type": "array", "items": {"type": "string"}, "description": "Specific hardware concerns; empty list if none."}, + "reasoning": {"type": "string", "description": "Brief explanation of the analysis."}, + }, + "required": ["suspicious", "concerns", "reasoning"], + }, +} + + +def _tool_input(response) -> Optional[Dict[str, Any]]: + """Return the parsed input of the first tool_use block, or None.""" + for block in getattr(response, "content", None) or []: + if getattr(block, "type", None) == "tool_use": + return block.input + return None + + @dataclass class AdversarialResult: """Result of adversarial verification strategy""" - found_counter_evidence: bool - concerns: list[str] - confidence_in_original: ConfidenceLevel | None + concerns: List[str] raw_response: str @dataclass class IndependentResult: """Result of independent verification strategy""" - detected: bool - confidence: ConfidenceLevel | None key_evidence: str raw_response: str @@ -50,31 +117,27 @@ class IndependentResult: @dataclass class TemporalResult: """Result of temporal comparison strategy""" - change_detected: bool description: str - confidence: ConfidenceLevel | None raw_response: str @dataclass class EnsembleResult: """Result of ensemble voting strategy""" - votes_yes: int votes_no: int total_votes: int agreement_ratio: float # votes_yes / total_votes if detected, votes_no / total_votes if not consensus_detected: bool # True if >70% agree on YES - raw_responses: list[str] = field(default_factory=list) + raw_responses: List[str] = field(default_factory=list) @dataclass class HardwareContextResult: """Result of hardware context analysis strategy""" - suspicious: bool # True if hardware errors could have caused false positive - concerns: list[str] # Specific concerns identified + concerns: List[str] # Specific concerns identified reasoning: str raw_response: str @@ -82,16 +145,15 @@ class HardwareContextResult: @dataclass class VerificationResult: """Combined result of all verification strategies""" - original_detected: bool - original_confidence: ConfidenceLevel | None + original_confidence: Optional[ConfidenceLevel] # Strategy results adversarial: AdversarialResult independent: IndependentResult temporal: TemporalResult - ensemble: EnsembleResult | None = None # Only for hatching detection - hardware_context: HardwareContextResult | None = None # Only when errors present + ensemble: Optional[EnsembleResult] = None # Only for hatching detection + hardware_context: Optional[HardwareContextResult] = None # Only when errors present # Consensus consensus: bool = False @@ -101,50 +163,41 @@ class VerificationResult: timestamp: datetime = field(default_factory=datetime.now) verification_duration_seconds: float = 0.0 - def to_dict(self) -> dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: """Serialize to dictionary""" result = { - "original_detected": self.original_detected, - "original_confidence": self.original_confidence.value - if self.original_confidence - else None, - "adversarial": { - "found_counter_evidence": self.adversarial.found_counter_evidence, - "concerns": self.adversarial.concerns, - "confidence_in_original": self.adversarial.confidence_in_original.value - if self.adversarial.confidence_in_original - else None, + 'original_detected': self.original_detected, + 'original_confidence': self.original_confidence.value if self.original_confidence else None, + 'adversarial': { + 'found_counter_evidence': self.adversarial.found_counter_evidence, + 'concerns': self.adversarial.concerns, }, - "independent": { - "detected": self.independent.detected, - "confidence": self.independent.confidence.value - if self.independent.confidence - else None, - "key_evidence": self.independent.key_evidence, + 'independent': { + 'detected': self.independent.detected, + 'key_evidence': self.independent.key_evidence, }, - "temporal": { - "change_detected": self.temporal.change_detected, - "description": self.temporal.description, - "confidence": self.temporal.confidence.value if self.temporal.confidence else None, + 'temporal': { + 'change_detected': self.temporal.change_detected, + 'description': self.temporal.description, }, - "consensus": self.consensus, - "consensus_reasoning": self.consensus_reasoning, - "timestamp": self.timestamp.isoformat(), - "verification_duration_seconds": self.verification_duration_seconds, + 'consensus': self.consensus, + 'consensus_reasoning': self.consensus_reasoning, + 'timestamp': self.timestamp.isoformat(), + 'verification_duration_seconds': self.verification_duration_seconds, } if self.ensemble: - result["ensemble"] = { - "votes_yes": self.ensemble.votes_yes, - "votes_no": self.ensemble.votes_no, - "total_votes": self.ensemble.total_votes, - "agreement_ratio": self.ensemble.agreement_ratio, - "consensus_detected": self.ensemble.consensus_detected, + result['ensemble'] = { + 'votes_yes': self.ensemble.votes_yes, + 'votes_no': self.ensemble.votes_no, + 'total_votes': self.ensemble.total_votes, + 'agreement_ratio': self.ensemble.agreement_ratio, + 'consensus_detected': self.ensemble.consensus_detected, } if self.hardware_context: - result["hardware_context"] = { - "suspicious": self.hardware_context.suspicious, - "concerns": self.hardware_context.concerns, - "reasoning": self.hardware_context.reasoning, + result['hardware_context'] = { + 'suspicious': self.hardware_context.suspicious, + 'concerns': self.hardware_context.concerns, + 'reasoning': self.hardware_context.reasoning, } return result @@ -189,7 +242,7 @@ def __init__( self.ensemble_threshold = ensemble_threshold self._event_bus = event_bus or get_event_bus() - def _emit_event(self, event_type: EventType, data: dict): + def _emit_event(self, event_type: EventType, data: Dict): """Emit event to viz server""" if self._event_bus: self._event_bus.publish(event_type, data) @@ -223,13 +276,19 @@ async def verify( start_time = datetime.now() # Run all strategies in parallel for speed - adversarial_task = self._run_adversarial(detector, embryo_state, original_result, timepoint) - independent_task = self._run_independent(detector, embryo_state, timepoint) - temporal_task = self._run_temporal_check(detector, embryo_state, timepoint) + adversarial_task = self._run_adversarial( + detector, embryo_state, original_result, timepoint + ) + independent_task = self._run_independent( + detector, embryo_state, timepoint + ) + temporal_task = self._run_temporal_check( + detector, embryo_state, timepoint + ) # For hatching detection, also run ensemble voting ensemble_result = None - if detector.name == "hatching": + if detector.name == 'hatching': ensemble_task = self._run_ensemble_hatching(embryo_state) adversarial, independent, temporal, ensemble_result = await asyncio.gather( adversarial_task, independent_task, temporal_task, ensemble_task @@ -261,11 +320,7 @@ async def verify( logger.info( f"Verification complete for {detector.name}: " f"consensus={consensus}, duration={duration:.2f}s" - + ( - f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" - if ensemble_result - else "" - ) + + (f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" if ensemble_result else "") ) return result @@ -304,15 +359,21 @@ async def verify_with_context( start_time = datetime.now() # Run all strategies in parallel for speed - adversarial_task = self._run_adversarial(detector, embryo_state, original_result, timepoint) - independent_task = self._run_independent(detector, embryo_state, timepoint) - temporal_task = self._run_temporal_check(detector, embryo_state, timepoint) + adversarial_task = self._run_adversarial( + detector, embryo_state, original_result, timepoint + ) + independent_task = self._run_independent( + detector, embryo_state, timepoint + ) + temporal_task = self._run_temporal_check( + detector, embryo_state, timepoint + ) # For hatching detection, also run ensemble voting ensemble_result = None hardware_result = None - if detector.name == "hatching": + if detector.name == 'hatching': ensemble_task = self._run_ensemble_hatching(embryo_state) # Run hardware context analysis if there are errors @@ -320,26 +381,11 @@ async def verify_with_context( hardware_task = self._run_hardware_context_analysis( global_error_context, embryo_state.id ) - ( - adversarial, - independent, - temporal, - ensemble_result, - hardware_result, - ) = await asyncio.gather( - adversarial_task, - independent_task, - temporal_task, - ensemble_task, - hardware_task, + adversarial, independent, temporal, ensemble_result, hardware_result = await asyncio.gather( + adversarial_task, independent_task, temporal_task, ensemble_task, hardware_task ) else: - ( - adversarial, - independent, - temporal, - ensemble_result, - ) = await asyncio.gather( + adversarial, independent, temporal, ensemble_result = await asyncio.gather( adversarial_task, independent_task, temporal_task, ensemble_task ) else: @@ -354,151 +400,88 @@ async def verify_with_context( # Adversarial result strategies_complete += 1 - self._emit_event( - EventType.VERIFICATION_STRATEGY, - { - "embryo_id": embryo_id, - "detector_name": detector.name, - "strategy": "adversarial", - "passed": not adversarial.found_counter_evidence, - "summary": ( - "Counter-evidence: " - + ( - "YES - " + ", ".join(adversarial.concerns) - if adversarial.found_counter_evidence - else "None found" - ) - ), - "confidence": adversarial.confidence_in_original.value - if adversarial.confidence_in_original - else None, - }, - ) - self._emit_event( - EventType.VERIFICATION_PROGRESS, - { - "embryo_id": embryo_id, - "strategies_complete": strategies_complete, - "total_strategies": total_strategies, - }, - ) + self._emit_event(EventType.VERIFICATION_STRATEGY, { + 'embryo_id': embryo_id, + 'detector_name': detector.name, + 'strategy': 'adversarial', + 'passed': not adversarial.found_counter_evidence, + 'summary': f"Counter-evidence: {'YES - ' + ', '.join(adversarial.concerns) if adversarial.found_counter_evidence else 'None found'}", + }) + self._emit_event(EventType.VERIFICATION_PROGRESS, { + 'embryo_id': embryo_id, + 'strategies_complete': strategies_complete, + 'total_strategies': total_strategies, + }) # Independent result strategies_complete += 1 - self._emit_event( - EventType.VERIFICATION_STRATEGY, - { - "embryo_id": embryo_id, - "detector_name": detector.name, - "strategy": "independent", - "passed": independent.detected, - "summary": ( - f"Independent detection: {'YES' if independent.detected else 'NO'}" - f" - {independent.key_evidence}" - ), - "confidence": independent.confidence.value if independent.confidence else None, - }, - ) - self._emit_event( - EventType.VERIFICATION_PROGRESS, - { - "embryo_id": embryo_id, - "strategies_complete": strategies_complete, - "total_strategies": total_strategies, - }, - ) + self._emit_event(EventType.VERIFICATION_STRATEGY, { + 'embryo_id': embryo_id, + 'detector_name': detector.name, + 'strategy': 'independent', + 'passed': independent.detected, + 'summary': f"Independent detection: {'YES' if independent.detected else 'NO'} - {independent.key_evidence}", + }) + self._emit_event(EventType.VERIFICATION_PROGRESS, { + 'embryo_id': embryo_id, + 'strategies_complete': strategies_complete, + 'total_strategies': total_strategies, + }) # Temporal result strategies_complete += 1 - self._emit_event( - EventType.VERIFICATION_STRATEGY, - { - "embryo_id": embryo_id, - "detector_name": detector.name, - "strategy": "temporal", - "passed": temporal.change_detected, - "summary": ( - f"Change detected: {'YES' if temporal.change_detected else 'NO'}" - f" - {temporal.description}" - ), - "confidence": temporal.confidence.value if temporal.confidence else None, - }, - ) - self._emit_event( - EventType.VERIFICATION_PROGRESS, - { - "embryo_id": embryo_id, - "strategies_complete": strategies_complete, - "total_strategies": total_strategies, - }, - ) + self._emit_event(EventType.VERIFICATION_STRATEGY, { + 'embryo_id': embryo_id, + 'detector_name': detector.name, + 'strategy': 'temporal', + 'passed': temporal.change_detected, + 'summary': f"Change detected: {'YES' if temporal.change_detected else 'NO'} - {temporal.description}", + }) + self._emit_event(EventType.VERIFICATION_PROGRESS, { + 'embryo_id': embryo_id, + 'strategies_complete': strategies_complete, + 'total_strategies': total_strategies, + }) # Ensemble result (if applicable) if ensemble_result: strategies_complete += 1 - self._emit_event( - EventType.VERIFICATION_STRATEGY, - { - "embryo_id": embryo_id, - "detector_name": detector.name, - "strategy": "ensemble", - "passed": ensemble_result.consensus_detected, - "summary": ( - f"Ensemble vote: {ensemble_result.votes_yes}/{ensemble_result.total_votes}" - f" YES ({ensemble_result.agreement_ratio * 100:.0f}%)" - ), - "votes_yes": ensemble_result.votes_yes, - "votes_no": ensemble_result.votes_no, - "total_votes": ensemble_result.total_votes, - }, - ) - self._emit_event( - EventType.VERIFICATION_PROGRESS, - { - "embryo_id": embryo_id, - "strategies_complete": strategies_complete, - "total_strategies": total_strategies, - }, - ) + self._emit_event(EventType.VERIFICATION_STRATEGY, { + 'embryo_id': embryo_id, + 'detector_name': detector.name, + 'strategy': 'ensemble', + 'passed': ensemble_result.consensus_detected, + 'summary': f"Ensemble vote: {ensemble_result.votes_yes}/{ensemble_result.total_votes} YES ({ensemble_result.agreement_ratio*100:.0f}%)", + 'votes_yes': ensemble_result.votes_yes, + 'votes_no': ensemble_result.votes_no, + 'total_votes': ensemble_result.total_votes, + }) + self._emit_event(EventType.VERIFICATION_PROGRESS, { + 'embryo_id': embryo_id, + 'strategies_complete': strategies_complete, + 'total_strategies': total_strategies, + }) # Hardware context result (if applicable) if hardware_result: strategies_complete += 1 - self._emit_event( - EventType.VERIFICATION_STRATEGY, - { - "embryo_id": embryo_id, - "detector_name": detector.name, - "strategy": "hardware_context", - "passed": not hardware_result.suspicious, - "summary": ( - "Hardware errors suspicious: " - + ( - "YES - " + ", ".join(hardware_result.concerns) - if hardware_result.suspicious - else "No" - ) - ), - "reasoning": hardware_result.reasoning, - }, - ) - self._emit_event( - EventType.VERIFICATION_PROGRESS, - { - "embryo_id": embryo_id, - "strategies_complete": strategies_complete, - "total_strategies": total_strategies, - }, - ) + self._emit_event(EventType.VERIFICATION_STRATEGY, { + 'embryo_id': embryo_id, + 'detector_name': detector.name, + 'strategy': 'hardware_context', + 'passed': not hardware_result.suspicious, + 'summary': f"Hardware errors suspicious: {'YES - ' + ', '.join(hardware_result.concerns) if hardware_result.suspicious else 'No'}", + 'reasoning': hardware_result.reasoning, + }) + self._emit_event(EventType.VERIFICATION_PROGRESS, { + 'embryo_id': embryo_id, + 'strategies_complete': strategies_complete, + 'total_strategies': total_strategies, + }) # Determine consensus (with hardware context) consensus, reasoning = self._evaluate_consensus_with_hardware( - original_result, - adversarial, - independent, - temporal, - ensemble_result, - hardware_result, + original_result, adversarial, independent, temporal, ensemble_result, hardware_result ) duration = (datetime.now() - start_time).total_seconds() @@ -519,37 +502,26 @@ async def verify_with_context( logger.info( f"Verification (with context) complete for {detector.name}: " f"consensus={consensus}, duration={duration:.2f}s" - + ( - f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" - if ensemble_result - else "" - ) + + (f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" if ensemble_result else "") + (f", hardware_suspicious={hardware_result.suspicious}" if hardware_result else "") ) # Emit VERIFICATION_COMPLETED event with full summary - self._emit_event( - EventType.VERIFICATION_COMPLETED, - { - "embryo_id": embryo_id, - "detector_name": detector.name, - "consensus": consensus, - "reasoning": reasoning, - "duration_seconds": duration, - "strategies": { - "adversarial": not adversarial.found_counter_evidence, - "independent": independent.detected, - "temporal": temporal.change_detected, - "ensemble": ensemble_result.consensus_detected if ensemble_result else None, - "hardware_context": (not hardware_result.suspicious) - if hardware_result - else None, - }, - "ensemble_votes": f"{ensemble_result.votes_yes}/{ensemble_result.total_votes}" - if ensemble_result - else None, + self._emit_event(EventType.VERIFICATION_COMPLETED, { + 'embryo_id': embryo_id, + 'detector_name': detector.name, + 'consensus': consensus, + 'reasoning': reasoning, + 'duration_seconds': duration, + 'strategies': { + 'adversarial': not adversarial.found_counter_evidence, + 'independent': independent.detected, + 'temporal': temporal.change_detected, + 'ensemble': ensemble_result.consensus_detected if ensemble_result else None, + 'hardware_context': (not hardware_result.suspicious) if hardware_result else None, }, - ) + 'ensemble_votes': f"{ensemble_result.votes_yes}/{ensemble_result.total_votes}" if ensemble_result else None, + }) return result @@ -577,8 +549,7 @@ async def _run_hardware_context_analysis( Analysis result """ try: - prompt = f"""You are analyzing hardware error context for a microscopy detection -verification. + prompt = f"""You are analyzing hardware error context for a microscopy detection verification. GLOBAL ERROR LOG: {global_error_context} @@ -591,28 +562,33 @@ async def _run_hardware_context_analysis( - Stage positioning errors could cause wrong embryo to be imaged - Acquisition timeouts could cause partial/blank images (blank images look like empty FOV = hatched) - Camera errors could produce corrupted data -- Errors on OTHER embryos in the same round could indicate systemic issues - (stage drift, hardware instability) +- Errors on OTHER embryos in the same round could indicate systemic issues (stage drift, hardware instability) - Multiple errors in quick succession suggests hardware problems -If ANY errors occurred that could have affected the image quality or positioning for -{embryo_id}, report as SUSPICIOUS. +If ANY errors occurred that could have affected the image quality or positioning for {embryo_id}, mark it suspicious. -Respond in EXACTLY this format: -SUSPICIOUS: [YES/NO] -CONCERNS: [list specific concerns, separated by semicolons] -REASONING: [brief explanation of your analysis] +Record your analysis with the record_hardware_context tool. """ response = await asyncio.to_thread( self.claude.messages.create, model=self.ensemble_model, # Use Haiku for speed max_tokens=300, - messages=[{"role": "user", "content": prompt}], + tools=[_HARDWARE_CONTEXT_TOOL], + tool_choice={"type": "tool", "name": _HARDWARE_CONTEXT_TOOL["name"]}, + messages=[{"role": "user", "content": prompt}] ) - response_text = response.content[0].text - return self._parse_hardware_context_response(response_text) + data = _tool_input(response) + if not isinstance(data, dict): + raise ValueError("no tool_use block in response") + concerns = data.get("concerns") or [] + return HardwareContextResult( + suspicious=bool(data.get("suspicious", True)), + concerns=[str(c) for c in concerns], + reasoning=str(data.get("reasoning", "")), + raw_response=str(data), + ) except Exception as e: logger.error(f"Hardware context analysis failed: {e}") @@ -623,38 +599,14 @@ async def _run_hardware_context_analysis( raw_response="", ) - def _parse_hardware_context_response(self, response: str) -> HardwareContextResult: - """Parse hardware context analysis response""" - suspicious = False - concerns = [] - reasoning = "" - - for line in response.split("\n"): - line = line.strip() - if line.startswith("SUSPICIOUS:"): - value = line.split(":", 1)[1].strip().upper() - suspicious = value == "YES" - elif line.startswith("CONCERNS:"): - concerns_str = line.split(":", 1)[1].strip() - concerns = [c.strip() for c in concerns_str.split(";") if c.strip()] - elif line.startswith("REASONING:"): - reasoning = line.split(":", 1)[1].strip() - - return HardwareContextResult( - suspicious=suspicious, - concerns=concerns, - reasoning=reasoning, - raw_response=response, - ) - def _evaluate_consensus_with_hardware( self, original: DetectionResult, adversarial: AdversarialResult, independent: IndependentResult, temporal: TemporalResult, - ensemble: EnsembleResult | None = None, - hardware_context: HardwareContextResult | None = None, + ensemble: Optional[EnsembleResult] = None, + hardware_context: Optional[HardwareContextResult] = None, ) -> tuple[bool, str]: """ Evaluate consensus across all verification strategies including hardware context. @@ -670,9 +622,7 @@ def _evaluate_consensus_with_hardware( if not adversarial.found_counter_evidence: agreements += 1 else: - disagreements.append( - f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}" - ) + disagreements.append(f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}") # Check independent: should also detect if independent.detected: @@ -712,19 +662,12 @@ def _evaluate_consensus_with_hardware( consensus = agreements == total_strategies if consensus: - parts = [ - "no counter-evidence found", - "independent analysis confirms", - "temporal change observed", - ] + parts = ["no counter-evidence found", "independent analysis confirms", "temporal change observed"] if ensemble: parts.append(f"ensemble confirms ({ensemble.votes_yes}/{ensemble.total_votes} YES)") if hardware_context: parts.append("no hardware error concerns") - reasoning = ( - f"All verification strategies agree ({total_strategies}/{total_strategies}): " - + ", ".join(parts) - ) + reasoning = f"All verification strategies agree ({total_strategies}/{total_strategies}): " + ", ".join(parts) else: reasoning = ( f"Verification disagreement ({agreements}/{total_strategies} agree): " @@ -753,12 +696,11 @@ async def _run_adversarial( return AdversarialResult( found_counter_evidence=False, concerns=["No images available for verification"], - confidence_in_original=None, raw_response="", ) # Build detector-specific critical review guidance - if detector.name == "hatching": + if detector.name == 'hatching': specific_guidance = """ For HATCHING specifically, look for these common FALSE POSITIVE patterns: - Is the worm still COILED/PRETZEL-SHAPED inside the eggshell? @@ -771,12 +713,10 @@ async def _run_adversarial( else: specific_guidance = "" - prompt = f"""You are reviewing a detection result for a C. elegans embryo -(diSPIM max projection). + prompt = f"""You are reviewing a detection result for a C. elegans embryo (diSPIM max projection). The system detected: {detector.name} -Original confidence: {original_result.confidence.value if original_result.confidence else "unknown"} -Original reasoning: {original_result.reasoning or "not provided"} +Original reasoning: {original_result.reasoning or 'not provided'} NOW ACT AS A CRITICAL REVIEWER. Your job is to find reasons why this detection might be INCORRECT: - Could this be a false positive? @@ -784,10 +724,7 @@ async def _run_adversarial( - Is the evidence actually conclusive, or could it be interpreted differently? - Are there alternative explanations for what is observed? {specific_guidance} -Analyze the image(s) carefully and respond in EXACTLY this format: -COUNTER_EVIDENCE_FOUND: [YES/NO] -CONCERNS: [list specific doubts or alternative explanations, separated by semicolons] -CONFIDENCE_IN_ORIGINAL: [HIGH/MEDIUM/LOW] +Analyze the image(s) carefully and record your review with the record_adversarial_review tool. """ content = [{"type": "text", "text": prompt}] + images @@ -796,18 +733,26 @@ async def _run_adversarial( self.claude.messages.create, model=self.model, max_tokens=500, - messages=[{"role": "user", "content": content}], + tools=[_ADVERSARIAL_TOOL], + tool_choice={"type": "tool", "name": _ADVERSARIAL_TOOL["name"]}, + messages=[{"role": "user", "content": content}] ) - response_text = response.content[0].text - return self._parse_adversarial_response(response_text) + data = _tool_input(response) + if not isinstance(data, dict): + raise ValueError("no tool_use block in response") + concerns = data.get("concerns") or [] + return AdversarialResult( + found_counter_evidence=bool(data.get("found_counter_evidence", False)), + concerns=[str(c) for c in concerns], + raw_response=str(data), + ) except Exception as e: logger.error(f"Adversarial verification failed: {e}") return AdversarialResult( found_counter_evidence=False, concerns=[f"Verification error: {str(e)}"], - confidence_in_original=None, raw_response="", ) @@ -829,13 +774,12 @@ async def _run_independent( if not images: return IndependentResult( detected=False, - confidence=None, key_evidence="No images available", raw_response="", ) # Build detector-specific criteria - if detector.name == "hatching": + if detector.name == 'hatching': criteria = """ TRUE HATCHING criteria (must meet at least one): - Worm body is OUTSIDE the eggshell boundary (free-floating, elongated) @@ -849,8 +793,7 @@ async def _run_independent( criteria = detector.description # Use a neutral prompt that doesn't reveal the previous detection - prompt = f"""Analyze this C. elegans embryo image (diSPIM max projection) at -timepoint {timepoint}. + prompt = f"""Analyze this C. elegans embryo image (diSPIM max projection) at timepoint {timepoint}. Question: Has '{detector.name}' occurred in this embryo? @@ -859,10 +802,7 @@ async def _run_independent( Provide an independent assessment based SOLELY on what you observe in this image. Do not assume any prior state - analyze only what is visible now. -Respond in EXACTLY this format: -DETECTED: [YES/NO] -CONFIDENCE: [HIGH/MEDIUM/LOW] -KEY_EVIDENCE: [what specifically do you observe that supports your conclusion?] +Record your assessment with the record_independent_assessment tool. """ content = [{"type": "text", "text": prompt}] + images @@ -871,17 +811,24 @@ async def _run_independent( self.claude.messages.create, model=self.model, max_tokens=400, - messages=[{"role": "user", "content": content}], + tools=[_INDEPENDENT_TOOL], + tool_choice={"type": "tool", "name": _INDEPENDENT_TOOL["name"]}, + messages=[{"role": "user", "content": content}] ) - response_text = response.content[0].text - return self._parse_independent_response(response_text) + data = _tool_input(response) + if not isinstance(data, dict): + raise ValueError("no tool_use block in response") + return IndependentResult( + detected=bool(data.get("detected", False)), + key_evidence=str(data.get("key_evidence", "")), + raw_response=str(data), + ) except Exception as e: logger.error(f"Independent verification failed: {e}") return IndependentResult( detected=False, - confidence=None, key_evidence=f"Verification error: {str(e)}", raw_response="", ) @@ -904,7 +851,6 @@ async def _run_temporal_check( return TemporalResult( change_detected=True, # Can't disprove without history description="Insufficient temporal history for comparison", - confidence=ConfidenceLevel.LOW, raw_response="", ) @@ -915,27 +861,24 @@ async def _run_temporal_check( prev_images = [] for img in embryo_state.recent_images[-3:-1]: if img.max_projection_b64: - prev_images.append( - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": img.max_projection_b64, - }, + prev_images.append({ + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": img.max_projection_b64, } - ) + }) if not prev_images: return TemporalResult( change_detected=True, description="No previous images available", - confidence=ConfidenceLevel.LOW, raw_response="", ) # Build detector-specific temporal criteria - if detector.name == "hatching": + if detector.name == 'hatching': temporal_criteria = """For HATCHING, look for: - A visible BREACH appearing in the eggshell boundary (not just expansion) - The worm physically EXITING the shell (part of body moves outside) @@ -947,11 +890,10 @@ async def _run_temporal_check( - Not just a static state that could have existed before - Clear evidence of progression or event occurrence""" - prompt = f"""Compare these sequential timepoints of a C. elegans embryo -(diSPIM max projection). + prompt = f"""Compare these sequential timepoints of a C. elegans embryo (diSPIM max projection). PREVIOUS TIMEPOINTS (shown first): -These are from t={timepoint - 2} to t={timepoint - 1} +These are from t={timepoint-2} to t={timepoint-1} CURRENT TIMEPOINT (shown last): This is t={timepoint} @@ -960,10 +902,7 @@ async def _run_temporal_check( {temporal_criteria} -Respond in EXACTLY this format: -CHANGE_DETECTED: [YES/NO] -DESCRIPTION: [what specific change do you see between the previous and current frames?] -CONFIDENCE: [HIGH/MEDIUM/LOW] +Record your comparison with the record_temporal_comparison tool. """ # Combine: previous images first, then prompt, then current @@ -973,38 +912,47 @@ async def _run_temporal_check( self.claude.messages.create, model=self.model, max_tokens=400, - messages=[{"role": "user", "content": content}], + tools=[_TEMPORAL_TOOL], + tool_choice={"type": "tool", "name": _TEMPORAL_TOOL["name"]}, + messages=[{"role": "user", "content": content}] ) - response_text = response.content[0].text - return self._parse_temporal_response(response_text) + data = _tool_input(response) + if not isinstance(data, dict): + raise ValueError("no tool_use block in response") + return TemporalResult( + change_detected=bool(data.get("change_detected", True)), + description=str(data.get("description", "")), + raw_response=str(data), + ) except Exception as e: logger.error(f"Temporal verification failed: {e}") return TemporalResult( change_detected=True, # Don't block on error description=f"Verification error: {str(e)}", - confidence=None, raw_response="", ) - def _get_image_content(self, embryo_state: EmbryoState, num_images: int = 1) -> list[dict]: + def _get_image_content( + self, + embryo_state: EmbryoState, + num_images: int = 1 + ) -> List[Dict]: """Get image content blocks for Claude API""" images = [] recent = embryo_state.recent_images[-num_images:] if embryo_state.recent_images else [] for img in recent: if img.max_projection_b64: - images.append( - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": img.max_projection_b64, - }, + images.append({ + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": img.max_projection_b64, } - ) + }) return images @@ -1038,10 +986,8 @@ async def _run_ensemble_hatching(self, embryo_state: EmbryoState) -> EnsembleRes Answer ONE question: Has the embryo HATCHED? -HATCHED means: The worm body is OUTSIDE the eggshell (free-floating, elongated, or field is -empty because worm left). -NOT HATCHED means: The worm is still INSIDE the eggshell (coiled/pretzel-shaped, even if -shell looks expanded). +HATCHED means: The worm body is OUTSIDE the eggshell (free-floating, elongated, or field is empty because worm left). +NOT HATCHED means: The worm is still INSIDE the eggshell (coiled/pretzel-shaped, even if shell looks expanded). Respond with ONLY: YES or NO""" @@ -1053,7 +999,7 @@ async def single_vote() -> str: self.claude.messages.create, model=self.ensemble_model, max_tokens=10, # Very short response expected - messages=[{"role": "user", "content": content}], + messages=[{"role": "user", "content": content}] ) return response.content[0].text.strip().upper() except Exception as e: @@ -1061,10 +1007,7 @@ async def single_vote() -> str: return "ERROR" # Run all votes in parallel - logger.info( - f"[ENSEMBLE] Running {self.ensemble_size} parallel Haiku calls" - " for hatching verification" - ) + logger.info(f"[ENSEMBLE] Running {self.ensemble_size} parallel Haiku calls for hatching verification") tasks = [single_vote() for _ in range(self.ensemble_size)] responses = await asyncio.gather(*tasks) @@ -1113,95 +1056,13 @@ async def single_vote() -> str: raw_responses=[f"Error: {str(e)}"], ) - def _parse_adversarial_response(self, response: str) -> AdversarialResult: - """Parse adversarial strategy response""" - found_counter = False - concerns = [] - confidence = None - - for line in response.split("\n"): - line = line.strip() - if line.startswith("COUNTER_EVIDENCE_FOUND:"): - value = line.split(":", 1)[1].strip().upper() - found_counter = value == "YES" - elif line.startswith("CONCERNS:"): - concerns_str = line.split(":", 1)[1].strip() - concerns = [c.strip() for c in concerns_str.split(";") if c.strip()] - elif line.startswith("CONFIDENCE_IN_ORIGINAL:"): - value = line.split(":", 1)[1].strip().upper() - try: - confidence = ConfidenceLevel(value) - except ValueError: - pass - - return AdversarialResult( - found_counter_evidence=found_counter, - concerns=concerns, - confidence_in_original=confidence, - raw_response=response, - ) - - def _parse_independent_response(self, response: str) -> IndependentResult: - """Parse independent strategy response""" - detected = False - confidence = None - evidence = "" - - for line in response.split("\n"): - line = line.strip() - if line.startswith("DETECTED:"): - value = line.split(":", 1)[1].strip().upper() - detected = value == "YES" - elif line.startswith("CONFIDENCE:"): - value = line.split(":", 1)[1].strip().upper() - try: - confidence = ConfidenceLevel(value) - except ValueError: - pass - elif line.startswith("KEY_EVIDENCE:"): - evidence = line.split(":", 1)[1].strip() - - return IndependentResult( - detected=detected, - confidence=confidence, - key_evidence=evidence, - raw_response=response, - ) - - def _parse_temporal_response(self, response: str) -> TemporalResult: - """Parse temporal strategy response""" - change_detected = False - description = "" - confidence = None - - for line in response.split("\n"): - line = line.strip() - if line.startswith("CHANGE_DETECTED:"): - value = line.split(":", 1)[1].strip().upper() - change_detected = value == "YES" - elif line.startswith("DESCRIPTION:"): - description = line.split(":", 1)[1].strip() - elif line.startswith("CONFIDENCE:"): - value = line.split(":", 1)[1].strip().upper() - try: - confidence = ConfidenceLevel(value) - except ValueError: - pass - - return TemporalResult( - change_detected=change_detected, - description=description, - confidence=confidence, - raw_response=response, - ) - def _evaluate_consensus( self, original: DetectionResult, adversarial: AdversarialResult, independent: IndependentResult, temporal: TemporalResult, - ensemble: EnsembleResult | None = None, + ensemble: Optional[EnsembleResult] = None, ) -> tuple[bool, str]: """ Evaluate consensus across all verification strategies. @@ -1217,9 +1078,7 @@ def _evaluate_consensus( if not adversarial.found_counter_evidence: agreements += 1 else: - disagreements.append( - f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}" - ) + disagreements.append(f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}") # Check independent: should also detect if independent.detected: @@ -1253,14 +1112,13 @@ def _evaluate_consensus( f"All verification strategies agree ({total_strategies}/{total_strategies}): " f"no counter-evidence found, independent analysis confirms detection, " f"temporal change observed, ensemble voting confirms " - f"({ensemble.votes_yes}/{ensemble.total_votes}" - f" = {ensemble.agreement_ratio:.0%} YES)." + f"({ensemble.votes_yes}/{ensemble.total_votes} = {ensemble.agreement_ratio:.0%} YES)." ) else: reasoning = ( - "All verification strategies agree: " - "no counter-evidence found, independent analysis confirms detection, " - "temporal change observed." + f"All verification strategies agree: " + f"no counter-evidence found, independent analysis confirms detection, " + f"temporal change observed." ) else: reasoning = ( From 4910d061e43a1fc1058742477429ee1203d7f498 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 07:39:28 +0530 Subject: [PATCH 15/34] Fix recurring port-8080 false positive: SO_REUSEADDR on the viz preflight bind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uvicorn binds with SO_REUSEADDR; the preflight check did not, making it stricter than the server it guards — a just-exited instance leaves client sockets in TIME_WAIT that fail a bare bind() even though uvicorn would bind fine, so quick restarts hit 'port in use' repeatedly. Set SO_REUSEADDR on the preflight so it fails only on a genuine live listener. Co-Authored-By: Claude Fable 5 --- gently/ui/web/server.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/gently/ui/web/server.py b/gently/ui/web/server.py index faf2a2d8..daf91dba 100644 --- a/gently/ui/web/server.py +++ b/gently/ui/web/server.py @@ -784,6 +784,15 @@ async def on_start(self): import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + # Match uvicorn's own bind semantics. uvicorn sets SO_REUSEADDR before it + # binds, so a bare preflight bind WITHOUT it is *stricter* than the real + # server: when a previous instance has just exited, its browser/websocket + # connections linger in TIME_WAIT holding this local port, and a plain + # bind() fails with EADDRINUSE even though uvicorn would bind fine. That + # false positive was the recurring "port in use" on quick restarts. With + # SO_REUSEADDR the preflight now fails only on a genuine live listener + # (a real second instance) — exactly when uvicorn would also fail. + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: sock.bind((self.host, self.port)) except OSError: From 5ee9e326c31ca24267269ff3c449e42033f4a541 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 07:40:14 +0530 Subject: [PATCH 16/34] chore: gitignore the stray D:/ storage dir On Linux the Windows default GENTLY_STORAGE_PATH (D:\Gently3) gets created literally as ./D:/ under the repo, full of logs/sessions. Ignore it so it stops cluttering status and can't be committed by accident. Co-Authored-By: Claude Fable 5 --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index cce868ec..8bdd129d 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,7 @@ electron/ /stage_definitions_for_review.txt gently/ui/tui/node_modules/ gently/ui/tui/dist/ + +# Stray local storage: on Linux the Windows default GENTLY_STORAGE_PATH +# (D:\Gently3) is created literally as ./D:/ under the repo. Not data we track. +/D:/ From a8fd3e53ef38eab241337ef0177d47c7930f6734 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 13:17:07 +0530 Subject: [PATCH 17/34] =?UTF-8?q?UX=20v2=20landing:=20fix=20welcome?= =?UTF-8?q?=E2=86=92plan=20jump,=20dark=20mode,=20and=20the=20entry=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polish + flow fixes for the agent-first landing and in-page plan wizard. - Kill the welcome→plan "lift-up" lurch: top-anchor both screens to one shared offset so the orb no longer teleports ~140px on swap (now ~1px), with a single coordinated cross-fade keyframe. - Fix broken dark mode: define the tokens landing.css relied on but the theme never set (--bg, --text-secondary, --accent-soft, --accent-green-soft), scoped to body.ux-v2 with a light override; route the page background, drift glow, and accent-keyed shadows through real per-theme tokens. - A11y + polish: visible :focus-visible rings, animated tool-card reveal (grid-rows), feed scrolls internally with anchored header/footer, single- column mobile without a nested-scroll trap, consistent type scale + 4px spacing, ~40px touch targets, aria-expanded on disclosures, expanded prefers-reduced-motion coverage. - Entry flow: under ux_v2 the landing owns session entry, so suppress the legacy connect-time resolution picker server-side (it duplicated and contradicted the landing's Plan/Standalone choice). Guard the design kickoff so it fires once per session (no Back/forward pile-up). - "Plan an experiment" now offers continue-vs-fresh when an active campaign exists: continue it (default) or start a brand-new campaign from scratch. Co-Authored-By: Claude Fable 5 --- gently/ui/web/routes/agent_ws.py | 15 +- gently/ui/web/static/css/landing.css | 239 +++++++++++++---- gently/ui/web/static/js/landing.js | 386 +++++++++++++++++++++++---- gently/ui/web/templates/index.html | 9 +- 4 files changed, 548 insertions(+), 101 deletions(-) diff --git a/gently/ui/web/routes/agent_ws.py b/gently/ui/web/routes/agent_ws.py index fdf2fc5f..887fdd25 100644 --- a/gently/ui/web/routes/agent_ws.py +++ b/gently/ui/web/routes/agent_ws.py @@ -14,6 +14,8 @@ from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from gently.settings import settings + logger = logging.getLogger(__name__) @@ -744,9 +746,18 @@ async def _run_resolution_bootstrap(): pass if not wizard_ran: - if bridge.should_enter_resolution(): + enter_resolution = bridge.should_enter_resolution() + # Under ux_v2 the agent-first landing owns the session-entry + # decision ("Plan an experiment" / "Take a quick look"), so the + # legacy connect-time resolution picker would just duplicate it — + # and contradict it, by offering "Standalone" after the user has + # already chosen to plan. Stay quiet on connect for new sessions; + # the landing drives plan-mode (/plan) or standalone instead. + if enter_resolution and not settings.ui.ux_v2: bootstrap_task = asyncio.create_task(_run_resolution_bootstrap()) - else: + elif not enter_resolution: + # Resume / already-resolved sessions still get their briefing + # (it sits behind the landing overlay until dismissed). briefing = bridge.get_session_briefing() if briefing: await send_fn({"type": "stream_start"}) diff --git a/gently/ui/web/static/css/landing.css b/gently/ui/web/static/css/landing.css index c5152cf5..fff753e8 100644 --- a/gently/ui/web/static/css/landing.css +++ b/gently/ui/web/static/css/landing.css @@ -6,19 +6,46 @@ but reuses production's CSS variables (with the prototype hexes as fallback) so it tracks the app theme. */ +/* ux_v2 landing fills the gaps in the production token set (main.css defines + --bg-dark/-card/-hover, --border, --text, --text-muted, --accent, --accent-green + but NOT a page-bg alias, a secondary-text, or accent tints). Scope to + body.ux-v2 so v1 is untouched; both themes resolved here so landing.css can + reference these like any real token. dark is the default theme (main.css :root). */ +body.ux-v2 { + --bg: var(--bg-dark); /* page background, theme-aware */ + --text-secondary: var(--text-muted); + --accent-soft: rgba(96,165,250,.15); /* tint of dark --accent #60a5fa */ + --accent-green-soft: rgba(74,222,128,.15); /* tint of dark --accent-green */ + /* one disciplined type scale for the landing/plan surface */ + --v2-fs-body: 14px; + --v2-fs-sm: 13px; + --v2-fs-cap: 12px; + --v2-fs-eyebrow: 11px; +} +body.ux-v2[data-theme="light"] { + --accent-soft: rgba(59,130,246,.10); /* tint of light --accent #3b82f6 */ + --accent-green-soft: rgba(34,197,94,.12); /* tint of light --accent-green */ +} +/* accent-keyed glows can't put var() inside rgba channels, so the dark defaults + live on the elements (re-keyed off the dead #2f6df6 onto the real #60a5fa) and + light overrides ride here next to the tokens. */ +body.ux-v2[data-theme="light"] .v2-landing-orb { box-shadow: 0 6px 22px rgba(59,130,246,.35), inset 0 0 12px rgba(255,255,255,.6); } +body.ux-v2[data-theme="light"] .v2-escape-field input:focus { box-shadow: 0 0 0 4px rgba(59,130,246,.12); } +body.ux-v2[data-theme="light"] .v2-escape-send { box-shadow: 0 6px 16px rgba(59,130,246,.35); } + .v2-landing { position: fixed; inset: 0; z-index: 200; display: flex; - align-items: center; + align-items: flex-start; /* BOTH screens top-anchored — no discrete switch on swap */ justify-content: center; padding: 24px; overflow: hidden; background: - radial-gradient(1100px 700px at 78% -8%, #eaf1ff 0%, transparent 55%), - radial-gradient(900px 600px at 8% 108%, #eafaf0 0%, transparent 55%), - var(--bg, #f6f8fb); + radial-gradient(1100px 700px at 78% -8%, var(--accent-soft) 0%, transparent 55%), + radial-gradient(900px 600px at 8% 108%, var(--accent-green-soft) 0%, transparent 55%), + var(--bg); transition: opacity .5s cubic-bezier(.22,1,.36,1), transform .5s cubic-bezier(.22,1,.36,1), visibility .5s; } /* The calm screen "unfolds" into the workspace: fade + slight scale-up, then @@ -34,9 +61,10 @@ content: ""; position: absolute; inset: -20vmax; - background: radial-gradient(closest-side, rgba(47,109,246,.10), transparent 70%); + background: radial-gradient(closest-side, var(--accent-soft), transparent 70%); filter: blur(30px); animation: v2land-drift 26s cubic-bezier(.22,1,.36,1) infinite alternate; + will-change: transform; pointer-events: none; } @keyframes v2land-drift { @@ -52,6 +80,8 @@ align-items: center; max-width: 760px; width: 100%; + margin-top: 7vh; /* shared anchor for welcome AND plan — orb stays put on swap */ + margin-bottom: 5vh; } .v2-landing-rise { animation: v2land-rise .6s cubic-bezier(.22,1,.36,1) backwards; } .v2-landing-rise[data-i="1"] { animation-delay: .07s; } @@ -64,7 +94,7 @@ .v2-landing-orb { width: 52px; height: 52px; border-radius: 50%; background: radial-gradient(closest-side at 38% 34%, #ffffff, #bcd3ff 40%, var(--accent, #2f6df6) 100%); - box-shadow: 0 6px 22px rgba(47,109,246,.45), inset 0 0 12px rgba(255,255,255,.6); + box-shadow: 0 6px 22px rgba(96,165,250,.45), inset 0 0 12px rgba(255,255,255,.6); animation: v2land-breathe 4s ease-in-out infinite; } @keyframes v2land-breathe { 0%,100% { transform: scale(1); } 50% { transform: scale(1.06); } } @@ -76,33 +106,44 @@ /* choice cards */ .v2-landing-choices { - display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 18px; - margin-top: 34px; width: min(720px, 92vw); + display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 16px; + margin-top: 32px; width: min(720px, 92vw); } @media (max-width: 620px) { .v2-landing-choices { grid-template-columns: 1fr; } } .v2-choice { text-align: left; cursor: pointer; position: relative; overflow: hidden; border: 1px solid var(--border, #e4e9f0); background: var(--bg-card, #fff); - border-radius: 18px; padding: 22px 22px 20px; + border-radius: 18px; padding: 20px; box-shadow: 0 1px 2px rgba(15,23,42,.04), 0 8px 28px rgba(15,23,42,.06); font: inherit; color: var(--text, #0f172a); transition: transform .26s cubic-bezier(.22,1,.36,1), box-shadow .26s cubic-bezier(.22,1,.36,1), border-color .26s; } .v2-choice:hover { - transform: translateY(-4px); border-color: #cfdcf5; - box-shadow: 0 2px 6px rgba(15,23,42,.06), 0 18px 50px rgba(47,109,246,.14); + transform: translateY(-4px); + border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); + box-shadow: 0 2px 6px rgba(15,23,42,.06), + 0 18px 50px color-mix(in srgb, var(--accent) 16%, transparent); } .v2-choice:active { transform: translateY(-1px) scale(.995); } + +/* Visible keyboard focus for every landing/plan control (mouse clicks get no ring) */ +#v2-landing :focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 12px; /* hug the pill/card corners */ +} +#v2-landing .v2-choice:focus-visible { outline-offset: -2px; } /* inset: the card clips outside outlines */ +#v2-landing .v2-escape-field input:focus-visible { outline-offset: 0; } .v2-choice-ic { width: 40px; height: 40px; border-radius: 11px; display: grid; place-items: center; background: var(--accent-soft, #eaf1ff); color: var(--accent, #2f6df6); margin-bottom: 14px; } .v2-choice.alt .v2-choice-ic { background: var(--accent-green-soft, #e7f6ec); color: var(--accent-green, #16a34a); } .v2-choice h3 { margin: 0 0 6px; font-size: 17px; letter-spacing: -.01em; } -.v2-choice p { margin: 0; color: var(--text-secondary, #475569); font-size: 13.5px; line-height: 1.5; } +.v2-choice p { margin: 0; color: var(--text-secondary, #475569); font-size: var(--v2-fs-sm); line-height: 1.5; } .v2-choice-tag { position: absolute; top: 16px; right: 16px; - font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-muted, #94a3b8); + font-size: var(--v2-fs-eyebrow); letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #94a3b8); border: 1px solid var(--border, #e4e9f0); border-radius: 999px; padding: 3px 9px; } .v2-choice-go { @@ -114,15 +155,15 @@ .v2-choice:hover .v2-choice-go { opacity: 1; transform: none; } /* escape hatch — chat is the last resort, an obvious pill */ -.v2-escape { margin-top: 26px; display: flex; flex-direction: column; align-items: center; } +.v2-escape { margin-top: 24px; display: flex; flex-direction: column; align-items: center; } .v2-escape-toggle { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font: inherit; font-size: 13px; background: var(--bg-card, #fff); border: 1px solid var(--border, #e4e9f0); color: var(--text-secondary, #475569); - padding: 8px 15px; border-radius: 999px; + padding: 11px 16px; border-radius: 999px; box-shadow: 0 1px 2px rgba(15,23,42,.04), 0 8px 28px rgba(15,23,42,.06); transition: color .2s, border-color .2s, transform .2s cubic-bezier(.22,1,.36,1); } -.v2-escape-toggle:hover { color: var(--text, #0f172a); border-color: #cfd9e6; transform: translateY(-1px); } +.v2-escape-toggle:hover { color: var(--text, #0f172a); border-color: var(--border-strong); transform: translateY(-1px); } .v2-escape-toggle .v2-escape-caret { display: inline-block; transition: transform .3s cubic-bezier(.22,1,.36,1); opacity: .55; } .v2-escape.open .v2-escape-toggle .v2-escape-caret { transform: rotate(180deg); } .v2-escape-field { @@ -133,22 +174,22 @@ .v2-escape.open .v2-escape-field { max-height: 64px; opacity: 1; margin-top: 12px; } .v2-escape-field input { flex: 1; min-width: 0; border: 1px solid var(--border, #e4e9f0); background: var(--bg-card, #fff); - border-radius: 12px; padding: 12px 14px; font: inherit; font-size: 14px; color: var(--text, #0f172a); + border-radius: 12px; padding: 12px 14px; font: inherit; font-size: var(--v2-fs-body); color: var(--text, #0f172a); outline: none; box-shadow: 0 1px 2px rgba(15,23,42,.04); transition: border-color .2s, box-shadow .2s; } -.v2-escape-field input:focus { border-color: var(--accent, #2f6df6); box-shadow: 0 0 0 4px rgba(47,109,246,.12); } +.v2-escape-field input:focus { border-color: var(--accent, #2f6df6); box-shadow: 0 0 0 4px rgba(96,165,250,.18); } .v2-escape-send { appearance: none; border: 0; cursor: pointer; flex: none; width: 42px; height: 42px; border-radius: 12px; background: var(--accent, #2f6df6); color: #fff; display: grid; place-items: center; - box-shadow: 0 6px 16px rgba(47,109,246,.35); transition: transform .2s cubic-bezier(.22,1,.36,1), filter .2s; + box-shadow: 0 6px 16px rgba(96,165,250,.40); transition: transform .2s cubic-bezier(.22,1,.36,1), filter .2s; } .v2-escape-send:hover { transform: translateY(-1px); filter: brightness(1.05); } /* one-way skip into the workspace (e.g. a reload mid-session) */ .v2-landing-skip { - margin-top: 28px; background: none; border: 0; cursor: pointer; font: inherit; font-size: 12.5px; - color: var(--text-muted, #94a3b8); padding: 6px 10px; border-radius: 8px; + margin-top: 24px; background: none; border: 0; cursor: pointer; font: inherit; font-size: var(--v2-fs-cap); + color: var(--text-muted, #94a3b8); padding: 10px 12px; border-radius: 8px; transition: color .2s; } .v2-landing-skip:hover { color: var(--text-secondary, #475569); } @@ -164,29 +205,46 @@ body.ux-v2 .home-hero { display: none; } .v2-landing[data-screen="welcome"] .v2-screen-welcome { display: flex; flex-direction: column; align-items: center; max-width: 760px; margin: 0 auto; + animation: v2land-plan-in .42s cubic-bezier(.22,1,.36,1) backwards; } .v2-landing[data-screen="plan"] .v2-screen-plan { display: flex; flex-direction: column; - animation: v2land-rise .45s cubic-bezier(.22,1,.36,1) backwards; + animation: v2land-plan-in .42s cubic-bezier(.22,1,.36,1) backwards; +} +/* One swap motion shared by both screens: a soft opacity + rise + settle. The + scale .992→1 echoes the dismissed-state scale(1.015) so the surface feels like + one continuous fabric folding, not two slides swapping. */ +@keyframes v2land-plan-in { + from { opacity: 0; transform: translateY(10px) scale(.992); } + to { opacity: 1; transform: none; } } /* plan header */ -.v2-plan-head { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; } -.v2-plan-orb { width: 34px; height: 34px; } -.v2-plan-who { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #94a3b8); } +.v2-plan-head { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; } +.v2-plan-orb { width: 40px; height: 40px; transition: width .42s cubic-bezier(.22,1,.36,1), height .42s cubic-bezier(.22,1,.36,1); } +.v2-plan-who { font-size: var(--v2-fs-eyebrow); letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #94a3b8); } .v2-plan-title { font-size: 18px; font-weight: 600; letter-spacing: -.01em; color: var(--text, #0f172a); } .v2-plan-back { margin-left: auto; background: none; border: 0; cursor: pointer; font: inherit; font-size: 13px; - color: var(--text-muted, #94a3b8); padding: 6px 10px; border-radius: 8px; transition: color .2s, background .2s; + color: var(--text-muted, #94a3b8); padding: 9px 12px; border-radius: 8px; transition: color .2s, background .2s; } .v2-plan-back:hover { color: var(--text, #0f172a); background: rgba(15,23,42,.05); } /* plan body: ask stage + assembling plan */ .v2-plan-wrap { display: grid; grid-template-columns: 1.35fr .9fr; gap: 20px; align-items: start; } -@media (max-width: 720px) { .v2-plan-wrap { grid-template-columns: 1fr; } } +@media (max-width: 720px) { + .v2-plan-wrap { grid-template-columns: 1fr; } + /* single column: THE PLAN sits BELOW the feed — drop the internal scroll + and let the whole plan screen scroll as one document instead. The + descendant selector outranks the plain `.v2-plan-main { max-height }` + rule that appears later in the file (equal specificity → source order), + so the cap is genuinely lifted here, not silently re-applied. */ + .v2-landing[data-screen="plan"] .v2-plan-main { height: auto; max-height: none; overflow-y: visible; padding-right: 0; } + .v2-landing[data-screen="plan"] { overflow-y: auto; } +} .v2-plan-main { min-height: 220px; } .v2-plan-ask:empty { display: none; } -.v2-plan-thinking { display: flex; align-items: center; gap: 9px; color: var(--text-muted, #94a3b8); font-size: 13.5px; padding: 22px 4px; } +.v2-plan-thinking { display: flex; align-items: center; gap: 9px; color: var(--text-muted, #94a3b8); font-size: var(--v2-fs-sm); padding: 20px 4px; } .v2-plan-thinking.hidden { display: none; } .v2-typing { display: inline-flex; gap: 5px; align-items: center; } .v2-typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--accent, #2f6df6); opacity: .4; animation: v2-blink 1.1s infinite; } @@ -195,33 +253,124 @@ body.ux-v2 .home-hero { display: none; } @keyframes v2-blink { 0%,100% { opacity: .25; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-3px); } } .v2-plan-side { - background: var(--bg-card, #fff); border: 1px solid var(--border, #e4e9f0); border-radius: 14px; padding: 16px 18px; - box-shadow: 0 1px 2px rgba(15,23,42,.04), 0 8px 28px rgba(15,23,42,.06); + background: var(--bg-card, #fff); border: 1px solid var(--border, #e4e9f0); border-radius: 14px; padding: 14px 16px; + box-shadow: 0 1px 2px rgba(15,23,42,.04); } -.v2-plan-side-h { font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-muted, #94a3b8); margin-bottom: 12px; } -.v2-plan-side-empty { color: var(--text-muted, #94a3b8); font-size: 13px; font-style: italic; } +.v2-plan-side-h { font-size: var(--v2-fs-eyebrow); letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #94a3b8); margin-bottom: 10px; } +.v2-plan-side-empty { color: var(--text-muted, #94a3b8); font-size: var(--v2-fs-sm); font-style: italic; } .v2-plan-row { display: flex; flex-direction: column; gap: 2px; padding: 9px 0; border-top: 1px dashed var(--border, #e4e9f0); } .v2-plan-row:first-child { border-top: 0; } -.v2-plan-row .k { font-size: 10.5px; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted, #94a3b8); } -.v2-plan-row .v { font-size: 14px; color: var(--text, #0f172a); font-weight: 550; } +.v2-plan-row .k { font-size: var(--v2-fs-eyebrow); letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #94a3b8); } +.v2-plan-row .v { font-size: var(--v2-fs-body); color: var(--text, #0f172a); font-weight: 600; } -/* plan footer */ -.v2-plan-foot { display: flex; align-items: center; gap: 10px; margin-top: 22px; } +/* plan footer: "Open conversation" (quiet, left), a spacer, then "Continue in + workspace" demoted to a text link (right). The agent's recommended option in + the ask card is the real primary action now — the footer no longer competes. */ +.v2-plan-foot { display: flex; align-items: center; gap: 10px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--border, #e4e9f0); } +.v2-plan-foot-spacer { flex: 1; } .v2-plan-chat { - margin-right: auto; background: none; border: 1px solid var(--border, #e4e9f0); color: var(--text-secondary, #475569); - border-radius: 999px; padding: 8px 14px; font: inherit; font-size: 13px; cursor: pointer; transition: border-color .2s, color .2s; + background: none; border: 1px solid var(--border, #e4e9f0); color: var(--text-secondary, #475569); + border-radius: 999px; padding: 11px 16px; font: inherit; font-size: var(--v2-fs-sm); cursor: pointer; transition: border-color .2s, color .2s; } -.v2-plan-chat:hover { border-color: #cfd9e6; color: var(--text, #0f172a); } -.v2-plan-continue { - background: var(--accent, #2f6df6); color: #fff; border: 0; border-radius: 10px; padding: 10px 16px; - font: inherit; font-size: 13.5px; font-weight: 600; cursor: pointer; transition: transform .2s cubic-bezier(.22,1,.36,1), filter .2s; +.v2-plan-chat:hover { border-color: var(--border-strong); color: var(--text, #0f172a); } +.v2-plan-skip { + background: none; border: 0; cursor: pointer; font: inherit; font-size: var(--v2-fs-sm); + color: var(--text-muted, #94a3b8); padding: 11px 10px; border-radius: 8px; transition: color .2s; } -.v2-plan-continue:hover { transform: translateY(-1px); filter: brightness(1.05); } +.v2-plan-skip:hover { color: var(--text-secondary, #475569); } + +/* ── Agent-activity feed: claude.ai-style collapsible tool cards ──────────── */ +/* Both screens share the .v2-landing-inner top anchor (no per-screen align flip — + that was the welcome→plan lurch). The feed is a fixed-height viewport (height + set above) so the streaming activity column scrolls on its own without ever + reflowing the anchored header/footer around it. Short feeds stay compact + (min-height above); long feeds cap at 66vh and scroll internally. The header + never moves because the inner is top-anchored — only the footer rides down as + the feed grows, up to the cap. */ +.v2-plan-main { max-height: 66vh; overflow-y: auto; padding-right: 4px; } + +.v2-plan-activity { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; } +.v2-plan-activity:empty { display: none; margin: 0; } + +/* agent prose between tool calls */ +.v2-act-text { font-size: var(--v2-fs-sm); line-height: 1.55; color: var(--text-secondary, #475569); white-space: pre-wrap; } + +/* collapsed-by-default tool card; the header toggles .open to reveal the body */ +.v2-act-tool { border: 1px solid var(--border, #e4e9f0); border-radius: 11px; background: var(--bg-card, #fff); overflow: hidden; } +.v2-act-tool-head { + display: flex; align-items: center; gap: 8px; width: 100%; + background: none; border: 0; cursor: pointer; text-align: left; font: inherit; + padding: 11px 12px; color: var(--text, #0f172a); +} +.v2-act-tool-head:hover { background: var(--bg-hover, #f1f5f9); } +.v2-act-ic { width: 16px; flex: none; text-align: center; font-size: 12px; } +.v2-act-tool.done .v2-act-ic { color: var(--accent-green, #16a34a); } +.v2-act-tool.err .v2-act-ic { color: #ea580c; } +body.ux-v2[data-theme="dark"] .v2-act-tool.err .v2-act-ic { color: #fb923c; } +.v2-act-spin { + display: inline-block; width: 11px; height: 11px; border-radius: 50%; + border: 2px solid var(--border, #e4e9f0); border-top-color: var(--accent, #2f6df6); + animation: v2-act-spin .7s linear infinite; +} +@keyframes v2-act-spin { to { transform: rotate(360deg); } } +.v2-act-label { font-size: var(--v2-fs-sm); font-weight: 600; flex: none; } +.v2-act-summary { font-size: var(--v2-fs-cap); color: var(--text-muted, #94a3b8); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.v2-act-chev { flex: none; color: var(--text-muted, #94a3b8); transition: transform .2s; font-size: 13px; } +.v2-act-tool.open .v2-act-chev { transform: rotate(90deg); } +/* animatable collapse: grid-template-rows 0fr→1fr eases in step with the chevron + (display:none isn't animatable). Needs exactly ONE min-height:0 child — landing.js + wraps the blocks in a single inner div for this. */ +.v2-act-tool-body { + display: grid; grid-template-rows: 0fr; opacity: 0; + padding: 0 12px 0 37px; + transition: grid-template-rows .26s cubic-bezier(.22,1,.36,1), + opacity .26s cubic-bezier(.22,1,.36,1), + padding-bottom .26s cubic-bezier(.22,1,.36,1); +} +.v2-act-tool-body > * { min-height: 0; overflow: hidden; } +.v2-act-tool.open .v2-act-tool-body { grid-template-rows: 1fr; opacity: 1; padding-bottom: 11px; } +.v2-act-tool.open .v2-act-summary { white-space: normal; } +.v2-act-block-label { font-size: var(--v2-fs-eyebrow); letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted, #94a3b8); margin-top: 8px; } +.v2-act-block { + font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; + background: var(--bg-hover); border: 1px solid var(--border, #e4e9f0); border-radius: 8px; + padding: 8px 10px; margin-top: 4px; white-space: pre-wrap; word-break: break-word; + color: var(--text-secondary, #475569); max-height: 220px; overflow: auto; +} + +/* error + fallback states */ +.v2-plan-error { + font-size: var(--v2-fs-sm); color: #b91c1c; + background: rgba(239,68,68,.10); border: 1px solid rgba(239,68,68,.35); + border-radius: 11px; padding: 11px 13px; +} +body.ux-v2[data-theme="dark"] .v2-plan-error { + color: #fca5a5; background: rgba(239,68,68,.14); border-color: rgba(239,68,68,.40); +} +.v2-plan-error.hidden { display: none; } +.v2-plan-fallback { font-size: 13px; color: var(--text-muted, #94a3b8); padding: 8px 2px; } +.v2-plan-fallback a { color: var(--accent, #2f6df6); cursor: pointer; } + +/* plan-panel: phases + tasks (real plan), and a free-text-answer row variant */ +.v2-plan-phase { margin-top: 12px; } +.v2-plan-phase:first-child { margin-top: 0; } +.v2-plan-phase-h { font-size: 13px; font-weight: 600; letter-spacing: -.01em; color: var(--text, #0f172a); margin-bottom: 5px; } +.v2-plan-task { display: flex; gap: 8px; font-size: var(--v2-fs-cap); color: var(--text-secondary, #475569); padding: 2px 0; } +.v2-plan-task::before { content: "·"; color: var(--text-muted, #94a3b8); } +.v2-plan-title-row { font-size: var(--v2-fs-sm); font-weight: 600; letter-spacing: -.01em; color: var(--text, #0f172a); margin-bottom: 8px; } +.v2-plan-row.v2-plan-row-freetext .v { font-style: italic; } @media (prefers-reduced-motion: reduce) { - .v2-landing, .v2-landing::before, .v2-landing-rise, .v2-landing-orb, - .v2-landing[data-screen="plan"] .v2-screen-plan, .v2-typing i { + .v2-landing, .v2-landing::before, .v2-landing-rise, .v2-landing-orb, .v2-plan-orb, + .v2-landing[data-screen="plan"] .v2-screen-plan, + .v2-landing[data-screen="welcome"] .v2-screen-welcome, + .v2-typing i, .v2-act-spin, .v2-act-chev, + .v2-act-tool-body, .v2-act-tool-head, + .v2-choice, .v2-escape-field, .v2-escape-toggle { animation: none !important; transition-duration: .12s !important; } + /* keep the collapsible usable without the height tween */ + .v2-act-tool-body { transition: none !important; } + .v2-act-tool.open .v2-act-tool-body { grid-template-rows: 1fr; opacity: 1; } } diff --git a/gently/ui/web/static/js/landing.js b/gently/ui/web/static/js/landing.js index 35eaf1fa..2f524c1e 100644 --- a/gently/ui/web/static/js/landing.js +++ b/gently/ui/web/static/js/landing.js @@ -1,23 +1,26 @@ /** * V2Landing (ux_v2): the agent-first welcome AND the in-place plan wizard. * - * The key paradigm fix: the plan dialogue renders IN THE LANDING, not the chat - * REPL. Clicking "Plan an experiment" doesn't recede to the dashboard — it - * switches the landing to a plan screen, enters plan mode, and renders the - * agent's ask_user_choice questions as button cards right there (#v2-plan-ask), - * reusing AgentChat.buildAskCard so it's the same card/answer path as elsewhere. - * The plan assembles on the right from each pick. Chat is never the surface - * (it's one click away via "Open conversation" as the last resort). - * - * Plan an experiment → switch to plan screen; /plan + kickoff; asks render here - * Take a quick look → scope (Devices view) - * "just tell me…" → free-text into the agent loop (then chat as transcript) + * Clicking "Plan an experiment" switches the landing to a plan screen, enters + * plan mode, and renders the agent's work IN THE WIZARD — not the chat REPL: + * - the agent's reasoning + tool calls render as a tidy, claude.ai-style + * collapsible activity feed (#v2-plan-activity), fed by the AGENT_ACTIVITY + * event that agent-chat.js mirrors off the /ws/agent stream; + * - the agent's ask_user_choice questions render as button cards + * (#v2-plan-ask) via AgentChat.buildAskCard; + * - "THE PLAN" panel (#v2-plan-summary) mirrors the REAL plan (phases→tasks) + * fetched from /api/campaigns once a turn settles. + * Chat is the last resort (the escape pill / "Open conversation"). * * No-ops unless #v2-landing is present (flag off → v1 untouched, overlay absent). */ const V2Landing = (() => { let el = null; - let current = null; // { reqId, data, isWake } — the ask showing in the plan stage + let current = null; // the ask currently in #v2-plan-ask + let feedTextEl = null; // current accumulating prose paragraph in the feed + let runningTools = {}; // tool name -> stack of running card elements + let feedHadContent = false; // did this turn surface anything in the feed? + let capturedCampaignId = null; // best-effort id scraped from tool results const $ = (id) => document.getElementById(id); @@ -50,32 +53,255 @@ const V2Landing = (() => { setTimeout(finish, 650); } - // ── plan stage helpers ──────────────────────────────────────────── - function showThinking(on) { - const t = $('v2-plan-thinking'); - if (t) t.classList.toggle('hidden', !on); + // ── status / error helpers ──────────────────────────────────────── + function showThinking(on) { const t = $('v2-plan-thinking'); if (t) t.classList.toggle('hidden', !on); } + function errorVisible() { const e = $('v2-plan-error'); return !!e && !e.classList.contains('hidden'); } + function showPlanError(msg) { + const e = $('v2-plan-error'); if (!e) return; + e.textContent = msg; e.classList.remove('hidden'); + showThinking(false); } + function hidePlanError() { const e = $('v2-plan-error'); if (e) e.classList.add('hidden'); } + function clearAsk() { const m = $('v2-plan-ask'); if (m) m.innerHTML = ''; } function resetSummary() { const list = $('v2-plan-summary'); - if (list) list.innerHTML = '
Assembling from your choices…
'; + if (list) list.innerHTML = '
The plan will take shape here as Gently designs it.
'; + } + + // ── activity feed (claude.ai-style collapsible tool cards) ───────── + function feedEl() { return $('v2-plan-activity'); } + function clearActivity() { + const f = feedEl(); if (f) f.innerHTML = ''; + feedTextEl = null; runningTools = {}; feedHadContent = false; + capturedCampaignId = null; + hidePlanError(); + } + function scrollFeedIfNearBottom() { + const m = document.querySelector('.v2-screen-plan .v2-plan-main'); + if (m && (m.scrollHeight - m.scrollTop - m.clientHeight) < 140) m.scrollTop = m.scrollHeight; + } + function clearFallback() { feedEl()?.querySelectorAll('.v2-plan-fallback').forEach(n => n.remove()); } + + // Render the agent's prose like the chat does (reuses AgentChat.mdToHtml — + // escapes then renders bold/italic/code/line-breaks), so the feed isn't raw + // markdown. Falls back to escaped text if the helper isn't available. + function renderMd(s) { + if (typeof AgentChat !== 'undefined' && AgentChat.mdToHtml) return AgentChat.mdToHtml(s); + const esc = (typeof escapeHtml === 'function') ? escapeHtml(String(s)) : String(s); + return esc.replace(/\n/g, '
'); + } + + // Plan-writing tools → refresh THE PLAN panel during the turn (debounced), + // not only at turn_end (ask_user_choice pauses the turn before it ends). + const PLAN_TOOLS = new Set([ + 'create_campaign', 'create_plan_item', 'link_plan_items', 'update_plan_item', + 'delete_plan_item', 'propose_plan', 'get_plan_status', 'validate_plan', + ]); + let planRefreshTimer = null; + function schedulePlanRefresh() { + if (planRefreshTimer) clearTimeout(planRefreshTimer); + planRefreshTimer = setTimeout(() => { planRefreshTimer = null; refreshPlanPanel(); }, 600); + } + + function safeStringify(v) { + try { + const s = (typeof v === 'string') ? v : JSON.stringify(v, null, 2); + return s.length > 4000 ? s.slice(0, 4000) + '\n…' : s; + } catch (e) { return String(v); } + } + function fillToolBody(body, act) { + body.innerHTML = ''; + // grid-template-rows reveal (landing.css) needs ONE collapsible child — + // append blocks into a single inner wrapper, not directly onto body. + const inner = document.createElement('div'); + body.appendChild(inner); + const inputStr = (act.input != null) ? safeStringify(act.input) : ''; + const full = act.full || act.summary || ''; + const block = (label, text) => { + const l = document.createElement('div'); l.className = 'v2-act-block-label'; l.textContent = label; + const b = document.createElement('pre'); b.className = 'v2-act-block'; b.textContent = text; + inner.append(l, b); + }; + if (inputStr) block('input', inputStr); + if (full) block('result', full); + if (!inputStr && !full) { + const e = document.createElement('div'); e.className = 'v2-act-block-label'; e.textContent = 'no details'; + inner.append(e); + } } + function buildToolCard(act, done) { + const card = document.createElement('div'); + card.className = 'v2-act-tool' + (done ? (act.is_error ? ' done err' : ' done') : ''); + const head = document.createElement('button'); + head.className = 'v2-act-tool-head'; head.type = 'button'; + head.setAttribute('aria-expanded', 'false'); + const ic = document.createElement('span'); ic.className = 'v2-act-ic'; + ic.innerHTML = done ? (act.is_error ? '⚠' : '✓') : ''; + const label = document.createElement('span'); label.className = 'v2-act-label'; + label.textContent = act.label || act.name || 'tool'; + const sum = document.createElement('span'); sum.className = 'v2-act-summary'; + sum.textContent = done ? (act.summary || '') : ''; + const chev = document.createElement('span'); chev.className = 'v2-act-chev'; chev.textContent = '›'; + head.append(ic, label, sum, chev); + const body = document.createElement('div'); body.className = 'v2-act-tool-body'; + fillToolBody(body, act); + head.addEventListener('click', () => { + const open = card.classList.toggle('open'); + head.setAttribute('aria-expanded', open ? 'true' : 'false'); + }); + card.append(head, body); + return card; + } + function updateToolCard(card, act) { + card.classList.add('done'); + if (act.is_error) card.classList.add('err'); + const ic = card.querySelector('.v2-act-ic'); if (ic) ic.textContent = act.is_error ? '⚠' : '✓'; + const sum = card.querySelector('.v2-act-summary'); if (sum) sum.textContent = act.summary || ''; + const body = card.querySelector('.v2-act-tool-body'); if (body) fillToolBody(body, act); + } + function captureCampaignId(text) { + if (!text) return; + const s = String(text); + const m = s.match(/campaign_id[=:\s]+([0-9a-f]{6,})/i) || s.match(/\(id:\s*([0-9a-f]{6,})/i); + if (m) capturedCampaignId = m[1]; + } + + function applyActivity(act) { + if (!planActive() || !act) return; + const f = feedEl(); if (!f) return; + switch (act.kind) { + case 'turn_start': + feedTextEl = null; hidePlanError(); clearFallback(); showThinking(true); + break; + case 'thinking': + showThinking(true); + break; + case 'text': { + const chunk = act.text || ''; + if (!chunk) break; + if (!feedTextEl) { + feedTextEl = document.createElement('div'); + feedTextEl.className = 'v2-act-text'; + feedTextEl._raw = ''; + f.appendChild(feedTextEl); + } + feedTextEl._raw += chunk; + feedTextEl.innerHTML = renderMd(feedTextEl._raw); + feedHadContent = true; showThinking(true); scrollFeedIfNearBottom(); + break; + } + case 'tool_start': { + // ask_user_choice IS the active question (rendered separately in + // #v2-plan-ask) — don't also show it as a feed card. + if (act.name === 'ask_user_choice') break; + feedTextEl = null; + const card = buildToolCard(act, false); + f.appendChild(card); + (runningTools[act.name] = runningTools[act.name] || []).push(card); + feedHadContent = true; showThinking(true); scrollFeedIfNearBottom(); + break; + } + case 'tool_result': { + captureCampaignId(act.summary); + captureCampaignId(act.full); + if (PLAN_TOOLS.has(act.name)) schedulePlanRefresh(); + if (act.name === 'ask_user_choice') break; + feedTextEl = null; + const arr = runningTools[act.name] || []; + const card = arr.pop(); + if (card) updateToolCard(card, act); + else f.appendChild(buildToolCard(act, true)); + feedHadContent = true; scrollFeedIfNearBottom(); + break; + } + case 'turn_end': + showThinking(false); feedTextEl = null; + refreshPlanPanel(); + if (!current && !feedHadContent) showFallback(); + break; + case 'turn_error': + showPlanError(act.error || 'Something went wrong — open the conversation for detail.'); + break; + } + } + + function showFallback() { + const f = feedEl(); if (!f || f.querySelector('.v2-plan-fallback')) return; + const d = document.createElement('div'); + d.className = 'v2-plan-fallback'; + d.innerHTML = 'Gently replied in prose — open the conversation to read it.'; + d.querySelector('a').addEventListener('click', openChat); + f.appendChild(d); + } + + // ── THE PLAN panel: mirror the real campaign tree ────────────────── + async function refreshPlanPanel() { + try { + let tree = null; + if (capturedCampaignId) { + const r = await fetch(`/api/campaigns/${encodeURIComponent(capturedCampaignId)}/tree`); + if (r.ok) tree = await r.json(); + } + if (!tree) { + const r = await fetch('/api/campaigns'); + if (r.ok) { const d = await r.json(); tree = (d.campaigns || [])[0] || null; } + } + if (tree) renderPlanTree(tree); + } catch (e) { /* keep whatever is shown */ } + } + function planName(c) { + c = c || {}; + return c.shorthand || c.display_name || c.description || 'Plan'; + } + function renderPlanTree(tree) { + const list = $('v2-plan-summary'); + if (!list || !tree) return; + const phases = tree.children || []; + const rootItems = tree.items || []; + if (!phases.length && !rootItems.length) return; // nothing to show yet — keep placeholder + list.innerHTML = ''; + const title = document.createElement('div'); + title.className = 'v2-plan-title-row'; + title.textContent = planName(tree.campaign); + list.appendChild(title); + const addTask = (parent, it) => { + const t = document.createElement('div'); + t.className = 'v2-plan-task'; + t.textContent = it.title || it.shorthand || '(task)'; + parent.appendChild(t); + }; + rootItems.forEach(it => addTask(list, it)); + phases.forEach(phase => { + if (!phase) return; + const wrap = document.createElement('div'); + wrap.className = 'v2-plan-phase'; + const h = document.createElement('div'); + h.className = 'v2-plan-phase-h'; + h.textContent = planName(phase.campaign); + wrap.appendChild(h); + (phase.items || []).forEach(it => addTask(wrap, it)); + list.appendChild(wrap); + }); + } + + // ── ask rendering (the active question) ──────────────────────────── function labelFor(data, sel) { const opts = (data && data.options) || []; const one = (s) => { const o = opts.find(o => o && (o.id === s || o.value === s || o.label === s)); return o ? o.label : String(s); }; - if (Array.isArray(sel)) return sel.map(one).join(', '); - return one(sel); + return Array.isArray(sel) ? sel.map(one).join(', ') : one(sel); } function recordPick(data, sel) { const list = $('v2-plan-summary'); if (!list) return; const empty = list.querySelector('.v2-plan-side-empty'); if (empty) empty.remove(); + const matched = (data && data.options || []).some(o => o && (o.id === sel || o.value === sel || o.label === sel)); const row = document.createElement('div'); - row.className = 'v2-plan-row'; + row.className = 'v2-plan-row' + (matched ? '' : ' v2-plan-row-freetext'); row.innerHTML = ''; row.querySelector('.k').textContent = (data && data.question) || 'Choice'; row.querySelector('.v').textContent = labelFor(data, sel); @@ -84,7 +310,7 @@ const V2Landing = (() => { function renderAsk() { const mount = $('v2-plan-ask'); if (!mount || !current || typeof AgentChat === 'undefined' || !AgentChat.buildAskCard) return; - showThinking(false); + showThinking(false); hidePlanError(); clearFallback(); const data = current.data, reqId = current.reqId; const hasControl = AgentChat.hasControl ? AgentChat.hasControl() : true; const card = AgentChat.buildAskCard(data, { @@ -92,39 +318,104 @@ const V2Landing = (() => { onPick: (sel) => { recordPick(data, sel); AgentChat.answerChoice(reqId, sel); - current = null; - clearAsk(); - showThinking(true); // agent computing the next step + current = null; clearAsk(); showThinking(true); }, }); mount.innerHTML = ''; mount.appendChild(card); + const first = mount.querySelector('button:not([disabled])'); + if (first) setTimeout(() => first.focus(), 30); } - function startPlan() { - setScreen('plan'); // stay on the overlay; swap welcome → plan wizard - resetSummary(); - clearAsk(); + let planKickedOff = false; // guard: design-kickoff fires once per session + async function startPlan() { + setScreen('plan'); + // Re-entering the wizard (Back → Plan again) must NOT re-fire the + // kickoff — that stacked duplicate "/plan" + design turns. Just show + // the wizard with its existing state. + if (planKickedOff) return; + planKickedOff = true; + resetSummary(); clearAsk(); clearActivity(); current = null; showThinking(true); - // /plan deterministically enters plan mode; the kickoff draws out the - // first question, which the agent asks via ask_user_choice (its prompt - // mandates it) → renders here, not in chat. runCommand connects on its - // own and flushes both in order without opening the chat panel. - if (typeof AgentChat !== 'undefined' && AgentChat.runCommand) { - AgentChat.runCommand('/plan'); + // Campaigns are persistent agent memory (not session state), so the + // agent always builds on an existing one — which leaves a user wanting a + // fresh plan stuck. So if an active campaign exists, ask up front: + // continue it (the default) or start a brand-new one. With NO campaign + // there's nothing to continue, so skip the gate and design straight away + // (that path is fresh anyway). + let campaign = null; + try { + const r = await fetch('/api/campaigns'); + if (r.ok) { const d = await r.json(); campaign = (d.campaigns || [])[0] || null; } + } catch (e) { /* offline / no API — just design */ } + if (campaign) renderCampaignChoice(campaign); + else kickoffDesign('continue'); + } + + // Enter plan mode, then prompt design. The prompt differs by intent: build + // on the active campaign, or set it aside and create a new one. A free-typed + // answer from the choice card becomes the design brief directly. + function kickoffDesign(mode) { + showThinking(true); + if (typeof AgentChat === 'undefined' || !AgentChat.runCommand) return; + AgentChat.runCommand('/plan'); + if (mode === 'fresh') { + AgentChat.runCommand( + "I want to start a brand-new experiment, not continue any existing " + + "campaign. Create a new campaign and let's design it from scratch — " + + "what should we capture?" + ); + } else if (mode === 'continue') { AgentChat.runCommand("Let's design this run — what should it capture?"); + } else { + // free text from the choice card's "Something else…" escape + AgentChat.runCommand(String(mode)); } } + // Continue-vs-fresh gate, shown only when an active campaign exists. Reuses + // the agent ask-card styling so it's visually identical to the agent's own + // questions; picking routes into kickoffDesign rather than the agent bridge. + function renderCampaignChoice(tree) { + const mount = $('v2-plan-ask'); + if (!mount || typeof AgentChat === 'undefined' || !AgentChat.buildAskCard) { + kickoffDesign('continue'); + return; + } + showThinking(false); hidePlanError(); clearFallback(); + const name = planName((tree && tree.campaign) || {}); + const data = { + question: `You have an active campaign — **${name}**. Design the next run inside it, or start something new?`, + options: [ + { id: 'continue', label: `Continue ${name}`, description: 'Design the next run inside your existing campaign' }, + { id: 'fresh', label: 'Start a brand-new campaign', description: 'Set the existing plan aside and plan from scratch' }, + ], + }; + const hasControl = AgentChat.hasControl ? AgentChat.hasControl() : true; + const card = AgentChat.buildAskCard(data, { + reqId: 'landing-campaign-choice', isWake: false, hasControl, + onPick: (sel) => { clearAsk(); kickoffDesign(sel); }, + }); + mount.innerHTML = ''; + mount.appendChild(card); + const first = mount.querySelector('button:not([disabled])'); + if (first) setTimeout(() => first.focus(), 30); + } + function openScope() { dismiss(); if (typeof switchTab === 'function') switchTab('devices'); } - + function openChat() { + dismiss(); + if (typeof AgentChat !== 'undefined' && AgentChat.togglePanel) { + setTimeout(() => AgentChat.togglePanel(true), 300); + } + } function sendFreeform(text) { const v = (text || '').trim(); - dismiss(); // free-text is the chat last-resort → recede, then open chat + dismiss(); if (typeof AgentChat !== 'undefined' && AgentChat.togglePanel) { AgentChat.togglePanel(true); if (v && AgentChat.runCommand) setTimeout(() => AgentChat.runCommand(v), 300); @@ -136,47 +427,40 @@ const V2Landing = (() => { if (!el || typeof ClientEventBus === 'undefined') return; // flag off → no-op greet(); - // welcome choices el.querySelectorAll('[data-landing]').forEach(btn => btn.addEventListener('click', () => { const kind = btn.dataset.landing; if (kind === 'plan') startPlan(); else if (kind === 'standalone') openScope(); })); - // welcome escape field const esc = $('v2-escape'), escToggle = $('v2-escape-toggle'), escInput = $('v2-escape-input'), escSend = $('v2-escape-send'); if (escToggle && esc && escInput) { escToggle.addEventListener('click', () => { const open = esc.classList.toggle('open'); + escToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); if (open) setTimeout(() => escInput.focus(), 120); }); const submit = () => sendFreeform(escInput.value); if (escSend) escSend.addEventListener('click', submit); escInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); submit(); } - else if (e.key === 'Escape') { e.stopPropagation(); esc.classList.remove('open'); } + else if (e.key === 'Escape') { e.stopPropagation(); esc.classList.remove('open'); escToggle.setAttribute('aria-expanded', 'false'); } }); } const skip = $('v2-landing-skip'); if (skip) skip.addEventListener('click', dismiss); - // plan-screen controls const back = $('v2-plan-back'); if (back) back.addEventListener('click', () => setScreen('welcome')); const planChat = $('v2-plan-chat'); - if (planChat) planChat.addEventListener('click', () => { - dismiss(); // chat panel lives under the overlay → recede first - if (typeof AgentChat !== 'undefined' && AgentChat.togglePanel) { - setTimeout(() => AgentChat.togglePanel(true), 300); - } - }); + if (planChat) planChat.addEventListener('click', openChat); const cont = $('v2-plan-continue'); if (cont) cont.addEventListener('click', dismiss); - // The agent's questions render in the plan stage while it's active; once - // we've receded into the workspace, AskStage (#ask-stage) takes over. + // The agent's questions + work render in the plan stage while it's active; + // once we've receded into the workspace, AskStage (#ask-stage) takes over. ClientEventBus.on('AGENT_ASK', ({ request_id, choice_data, origin }) => { if (!planActive()) return; current = { reqId: request_id, data: choice_data || {}, isWake: origin === 'wake' }; @@ -184,12 +468,12 @@ const V2Landing = (() => { }); ClientEventBus.on('ASK_CLEARED', ({ request_id }) => { if (request_id === '*' || (current && request_id === current.reqId)) { - current = null; - clearAsk(); - if (planActive()) showThinking(true); + current = null; clearAsk(); + if (planActive() && !errorVisible()) showThinking(true); } }); ClientEventBus.on('AGENT_CONTROL', () => { if (current && planActive()) renderAsk(); }); + ClientEventBus.on('AGENT_ACTIVITY', (act) => applyActivity(act)); document.addEventListener('keydown', e => { if (e.key !== 'Escape' || !el || el.classList.contains('dismissed')) return; diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index c972dc3d..36688f8a 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -53,7 +53,7 @@

Take a quick look

-
+
thinking through the next step…
+
- + +
From 1cefe2ffc51cfa44b954c00f5da0c8fb56dc89ed Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 13:18:53 +0530 Subject: [PATCH 18/34] UX v2: agent-activity events + GFM markdown rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agent-chat.js mirrors the agent stream onto a new AGENT_ACTIVITY event (turn/thinking/text/tool_start/tool_result/turn_end/error) so the plan wizard can render the agent's work as collapsible tool cards instead of leaving it in the chat. Replaces the inline-only mdToHtml with a block-aware, escape-first GFM renderer (headings, pipe tables, lists, fenced code, links — XSS-safe and streaming-safe) and exports it; agent-chat.css styles the ac-md-* output for both the chat and the wizard feed. Co-Authored-By: Claude Fable 5 --- gently/ui/web/static/css/agent-chat.css | 39 ++++++++++++++++++++++++ gently/ui/web/static/js/agent-chat.js | Bin 51841 -> 63820 bytes 2 files changed, 39 insertions(+) diff --git a/gently/ui/web/static/css/agent-chat.css b/gently/ui/web/static/css/agent-chat.css index fdaaa8e3..29d64646 100644 --- a/gently/ui/web/static/css/agent-chat.css +++ b/gently/ui/web/static/css/agent-chat.css @@ -441,3 +441,42 @@ body.chat-docked .agent-chat:not(.open) { color: var(--text-muted); cursor: pointer; font-size: 12px; line-height: 1; } .ac-queue-remove:hover { color: var(--color-danger, #f87171); } + +/* ── Rendered markdown (mdToHtml output, ac-md-* classes) ────────────────── + Shared by the chat transcript and the ux_v2 plan-wizard activity feed — the + same renderer feeds both, so these styles cover headings, lists, tables, + code blocks, quotes and links the agent emits. */ +.ac-md { line-height: 1.55; } +.ac-md > :first-child { margin-top: 0; } +.ac-md > :last-child { margin-bottom: 0; } +.ac-md-h1, .ac-md-h2, .ac-md-h3, .ac-md-h4, .ac-md-h5, .ac-md-h6 { + margin: 14px 0 6px; font-weight: 650; line-height: 1.3; letter-spacing: -.01em; color: var(--text); +} +.ac-md-h1 { font-size: 1.25em; } +.ac-md-h2 { font-size: 1.15em; } +.ac-md-h3 { font-size: 1.05em; } +.ac-md-h4, .ac-md-h5, .ac-md-h6 { font-size: 1em; } +.ac-md-p { margin: 7px 0; } +.ac-md-ul, .ac-md-ol { margin: 7px 0; padding-left: 22px; } +.ac-md-li { margin: 3px 0; } +.ac-md-quote { + margin: 8px 0; padding: 4px 12px; border-left: 3px solid var(--border, #e4e9f0); + color: var(--text-muted); font-style: italic; +} +.ac-md-hr { border: 0; border-top: 1px solid var(--border, #e4e9f0); margin: 12px 0; } +.ac-md-link { color: var(--accent, #2f6df6); text-decoration: underline; text-underline-offset: 2px; } +.ac-md-pre { + margin: 8px 0; padding: 10px 12px; border-radius: 8px; overflow-x: auto; + background: var(--bg, #f6f8fb); border: 1px solid var(--border, #e4e9f0); +} +.ac-md-pre .ac-md-code-block, .ac-md-pre code { + font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 12px; + color: var(--text); background: none; padding: 0; white-space: pre; +} +/* GFM tables — wrapped so a wide table scrolls instead of blowing out the column */ +.ac-md-table-wrap { margin: 9px 0; overflow-x: auto; border: 1px solid var(--border, #e4e9f0); border-radius: 8px; } +.ac-md-table { border-collapse: collapse; width: 100%; font-size: 12.5px; } +.ac-md-table th, .ac-md-table td { padding: 6px 10px; text-align: left; border-bottom: 1px solid var(--border, #e4e9f0); border-right: 1px solid var(--border, #e4e9f0); } +.ac-md-table th:last-child, .ac-md-table td:last-child { border-right: 0; } +.ac-md-table tr:last-child td { border-bottom: 0; } +.ac-md-table thead th { background: var(--bg, #f6f8fb); font-weight: 650; color: var(--text); } diff --git a/gently/ui/web/static/js/agent-chat.js b/gently/ui/web/static/js/agent-chat.js index 33dcf2b37908d8f5cf612d0a59c73cde286e66cd..0e8514df3e603df1220675ba2cff1c2ce566e7d4 100644 GIT binary patch delta 10374 zcmbVS+ix3JdXJkp7srj0wd~m2q&|`rb4KKllAJm)bunbwv5oo`+X~H@2*)MLroP1e->G%yZShxL6Pki;~KM|qXl;Y~W8`qN` z|FD((Zt`HSQV|zj-&!}!O*4?<%DXp27&k&0RYhBxw&S(JlIS{JDI&AsO8jU_&yse< z@oecu(iWC)%hDl%Kfv+>TLyR_-Hm9RQ{on602^cH7~ca6H%X zq-gjq07s_lSd|Q77zNI{tPqw8Hm!$caY=?w%LD4BABem6?p$8FaqZ@ns<2Gg#cCkC z(gbDhw(vb+idNunf{zSfH;_$dt0Y2SI8je@;xH1P+>n8Y+SmyeMBDUQbQUy5j_HED zZX5}%ZMsdsV?mUJ>17aO&vN~c=poVIiLe0x#J0rL6v125%VNoOMI^T(;e>waoPx|C6CHtU$t|gNyH3xOW*{_R@ni_ORZRPVX+gOn_8NW+J=@}= z*z_W&>Bt}yzLJ&9Ackf~3d;pyx`=#XZukzY!i-F)&hNGzOEgStJqqxTL_#{T0U|U5 zl5OPod{_8!1QCzY%9g1>3lCUAp^?hNXq$wqSA}UA9b0Idf!XcKK!@mQ!D!Ev%jvgs zebZD6-E2?nX5WB6`|3K`H#XJm2kbRtHiZ(9ni&@vroq;ot+0I z!eb3edC17fLjxJb0YA%dhI+XNhjF2Btuj$*l|t)D)79?NqKZMV+> z42rzQgS&BfVQW-22w%M;A4n?_56lfSv;wCaRVy}3qgvT$q`yUvY#1((iNx|f7>+Bf zwh3!9VJ=Yw+ku02oK_oF86p_@BJ9MimMh_H3K_XZ1RsVilI3T%1PWX;ayF!R|IT$7 z5X@Zd*8$oVr%kVCwS7Va*FgaBH#48^0jLSgn7D) zVzcc)ZCyxKXbwU|nIcEUlbeu`B<$OpZ3IivbG*KS0FpQWsT7~^bsL`K6(WQR;@c+1 z8-X;}d0Waj^NSszU2#pS1~m{z9JpeEsq|VzzS!9jg}iE4YFoYOX`{9^J6+p)r@6|S z&Xi1WK+#erVMHZYMiG3sVmmD-3M<8mQ7OX~mE1&QTNJ7VkODV_G!fP+;(|CmN>v9m zfwFSPjI6d+uH4sFOylDv;|Hth=d^LYSgskXQ+ln*F!o(@Npfbo2Bc>MwdP`F5g{Vx_$W)NBpw5MOhIMo<)Z|g^(fmY-aLgdtIOf z5JW=JbOPwEr?FeMkrXMX>ZwII0b3g4Gutc~M<9)E;9D|;h4dp8jK|~D01ElL>~z~^ z=)fs2?i1D5rR-uKsTk&I21e9#I-^0QV=K(b?d zC<Tw;zNqq1qmHQ{or8YNzxnc)Q#49&f?_kd<7r*IK3aBdTn!6--}thFup=h0I(+t-xlJdM_}Pmo`pw`6w|hJ z7XhV0pLvqtBzI6zQYs(^Qz1{lJ&?d$F;8o~MIl{Os|aRKl{mYkRH!=Nc{U9%z{b(ufZK8) z7M65fMA56LAJ7qH`?6S0%@XR2+KwtP(5XNG@D`+XS9Mes)WQyYq$L_XZj-pdN(Iob zI+&VeLZwke-Y3-3vyo6sp|KG+v05k$Je`VWjUG98FpYK# zqoJkRWv_nrsHZOqMwDg;loFr#=8rb~>nD1M-Hd*f9)ar)n5ge5vjT zO;P|QOsecR1UfO)(nxT?kBs-#4?mv~hORuiZ_rM7k46ZWoDBzl^c16xQu|A39Hhhh zwVj>X&Ll^ie2^hQjz?MgNC(tPFe2%ag0N zQIR)XBMrQ>r#dS#G#Kq|9+O~7LY{yWNYx|iM6^@{Nm?G{ZQh<)gWuZv=~j@JyuAmJtpVXN;VP z77NMDKO9Rwxqa|C+MJAkb~5>o+M!;XZ7Yl14hJ!K{t2!Z8|IVqkng9Ab7NHCx2|arBNN9eSdUYg26g{ojY)K4? z-p5oe=r|sYu==V?!SR3z$))4i21*TSc65ymA%>PHWqG#YHw`tf;?WTgwrHMJPG0LA zU1k8_f`U{Bjjrmz(15KyJX_k`c|`wptvsbq=KC(fPJYIx6Z7%dp}v~6WajOYV>G-% zuvkzNF!W?A)2qp^PMtUbKCYt;89Yb}YI>*>(|6F5W>06cP?x$+r4|+|I)VEA8z+-b z&K{hn6WE>}90Z4z`>KCL-&il!jB?h-mP6EkQkyP`v$I6#1y;nc=Mi)ChZH!-m}*H- z4?koGyZ`t`@-ySPK3uVOZn5(Sb}j!K46?e@0Z84{RNs z`e3Uw5EtY+S;3?SaWTcZh*p|aMcM_22VY1f!fX-8)3ZmD^0^n2FOBb!1P+rV)8@$~ zE}x|m;@8Goq^C2apkV1iH_sF0#rX=5EGGZ8c&wKdDMLP&j=3^(fJa{h zZ|kh~YCcd>M@?~c=rk!S^|XFH@KD8P3X#a)Y<1+p(c4*tGz0+%m+t*pHj1xelu|y; ze`>NoK7D~owmtrQ8qLW$w2Wa9-i!`7oH~tEm&;eg64BS*$Divi!UkD)8?Kz1$X?&b zsjkh6;6qTtu(pPu^%YdvuQX||%&uno89uj|95^+`HukGi$F8Vya!68pHFebwoR1+{ zgx4T;WmTYxDPbZ%g)uS=q0}| zjGfA}kC9B?aNEJe5WT%`MQ^_aK{dr1SLN5xbPZPv`O=K$s~^P<90%>ikT_sCgPlF3 z2G|(JN*(mX1?qTJucKkeXbRUyX5%{^&T(O6Zij+P3SCW;a^{bKh}zF|agYW91d6Uj z(rkn>S^-9rhvAjuv6+fqj0S+N9ZdV_9#Wx>x+Tn;rDX~WW=$I$xT zaxz@u&16Ck^U_GVj}MNdtP^y*#VKo~&!RMO8#UH4JerC$weP7;0E7!-R?SPO0-{8m zf-Vj;kbM2->t!lP?)aOdEn6nsZ%Q>h#T`mD=g4<4Lgg%zbsIF@#oCY{sShzQyy7a4 zv(y_IAu4?5_zzq?7s@AlROv+x3Iv6D+u0a4az4k`90<}%>qAb!OZ14ufe(~$dt*qy zGo@l1ov8&yXTqQB39DGdof0UZeiD$l*VmMMSFBay7fDR@{Pc7$g8pGN-H7%XF(iWh zw)aH?!Y;pnHb+APl>GMgamsnG^no!mlxD7fBLk!K+lqdG+(SGbv2w=xIEdhsq2!nE zok)JU_2NSAA#(tRsHFm9P*1iyGbsA(2PvF{Wgl;Rt4PH6ZhY%hMIRxlTlC+&|# z0erAE(v+0D5psMV13X-4P*8oXa(}TFUPxOj+DZX*#09T22IxIME6#`?@rJzP?|}`s zUnTgYBaMM7%g!8TQ1~dSxbn#vI@c`cNuVG`o&PSK31ucki+(%8b-vJud{IdK7;UE^z%I!mI62IVH&WhzQ>q(2gC;*M<;q-M)*)D|0#NjXN1zR;S`YXd5h z00P}!co_eKMDnEQD2PNGNO32ubK_Es-5YdS1O<^+2Ym|)8-2x?Dz2(Zhx!)k z%-;9EVSmj=addunrL5;p4Tlbjt{F-MnH1VSrP9$%Knr4gJllrm=KGOJol5o;^{?;J z)SwW%$rqU>6jG8&D{<;^em~O z@5y`8oi+|OQ4K)$$ohjMM@L6{mbX}vDmLs_eRjC|%ms$Ya?Y)(#3iNX3S3$uMdlli ziwX$^5Db=iO6@wsnbUPt6u@(woX2Y|7*M1Q3S~1&EJv`O!}y(o!zI}?V;67m48}t0 zT@lNQJ25;}ssT4e1e(L7vmTx=4L2}6rQvBRY2bUK?E^YK+>`!kzb@WqAi1}2P`s01 zRZEuTZ_Z#({`2s^y;+u+Jucxjh7Bj$)(Uh9TSrkEM1>N@%Vr$;p5O6f#J?sAQJMT~ z>v(ee!pSGUnfl^@@vYBBLEJx@{IvAJ@sX2gyy<-vitaF^7m~GwqfeeUesQ2UvJYcG zrijA1+N&(w5L@65jY;vek;8TSsbgF-2J{Kipoh=Y9uM@A5En}1~IAl|8A4*EDs z!j1#HKjC}oN%pHmCl*oeVg#{`afbSW;UgxbYNtY8rv=a-)^UC3V|?j^mxJ_mnzXEW z3Cbf0jX(Lk`sIO9HIVWV8HUvG7xS$@m>l7k_Yn$KMv5%?^&;jqTgRUK&*G1+)1~bV zhqiDo4T*)q|PZEQUNtXj~ha-OxdWhU&=g;guE~#|t@|PSl12;20*jzNf}~fEMB%IT_(y zG23={u1h04@={EQBE$7tDL&4=9=)c93n<(*AQR)YADZ&*d3PlzrPsDK7&NX$VV;3W zEmHEHb~gFDzkPFoc4>vBE0=HHt1m6zyY|7gdmloMw;J^FP8lN^++t}D1sxZ=DFCwY fhr75#NLF`FJ^Adje_edBW8d>>;*xy$&)WY1&ggNl delta 434 zcmX@}iMeqq^9FxzPc1D4-^{$s+{7Fmh2q4tRE6BcqU@CX@;obr)Z*mCf>edH%%b8F z9fgvN)I5bmg|yUih0MI1%)C^ElA^@C;KYc2&SfGPk{dPS)PIf==s8u|$u(Qyf}+L{Ua={gGPHp%%Zsdg%cHu_+O zIw9RLS}|Hc1DLg9wI?qV*Nnkx1y~7Cg=UNv#FErpuoXaxumyP#ElEXolNGCkSwI$S z{%3YocJhW;_R0HlcqbPGvTZg9-M|PJxDd`h*)}{B#@~D)d@V>)ck`>rFy_r0VtqM) oJdMq!>E{u83IaJcdu66FZDz^o^_e_>F6U;ST^H;otNm010GiyCBLDyZ From 64f9338398444cf9a00164bcad5f875daf46e0c3 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 13:19:13 +0530 Subject: [PATCH 19/34] Models: migrate to Fable 5 / Opus 4.8 / Sonnet 4.6 with refusal+400 fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit settings.py tiers: main→claude-fable-5, perception+medium→claude-opus-4-8, fast→claude-sonnet-4-6, plus a refusal_fallback (claude-opus-4-8). Strip the params the new models reject: thinking budget_tokens → output_config.effort (conversation.py, sam_detection.py); drop the obsolete interleaved-thinking beta header (agent.py). conversation.py: a main-tier 400 (e.g. Fable 5 under <30-day org data retention) OR a stop_reason='refusal' transparently retries the turn on Opus 4.8 in both the streaming and non-streaming paths; get_tool_call guards empty refusal content; dopaminergic detector guarded. chat.py model centralized to settings. Quiet log noise: the per-response diagnostic WARNING→DEBUG (it fired on every tool-use turn), and the benign send-after-close websocket WARNING→DEBUG. Co-Authored-By: Claude Fable 5 --- gently/app/agent.py | 505 ++++++++------------ gently/app/detectors/dopaminergic_signal.py | 8 +- gently/hardware/dispim/sam_detection.py | 471 ++++++++---------- gently/harness/conversation.py | 458 +++++++++--------- gently/settings.py | 54 ++- gently/ui/web/connection_manager.py | 5 +- gently/ui/web/routes/chat.py | 4 +- 7 files changed, 701 insertions(+), 804 deletions(-) diff --git a/gently/app/agent.py b/gently/app/agent.py index 4a5f6602..40618718 100644 --- a/gently/app/agent.py +++ b/gently/app/agent.py @@ -14,41 +14,38 @@ import asyncio import logging import os -from collections.abc import Callable +from typing import Dict, List, Optional, Callable, Any, TYPE_CHECKING from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Any import anthropic import numpy as np -from ..exceptions import StorageError +from ..exceptions import StorageError, AgentError from ..settings import settings if TYPE_CHECKING: from ..ui.web.server import VisualizationServer -from gently_perception import Perceiver -from ..core import EventType, emit, get_event_bus -from ..core.file_store import FileStore -from ..harness.conversation import ConversationManager -from ..harness.orchestration.plan_synthesis import ( - PlanLibrary, - PlanSynthesizer, - PlanValidator, -) -from ..harness.prompts.manager import PromptManager -from ..harness.session.interaction_logger import InteractionLogger -from ..harness.session.manager import SessionManager -from ..harness.session.timeline import TimelineManager -from ..harness.state import ExperimentState +logger = logging.getLogger(__name__) + +from ..harness.state import ExperimentState, EmbryoState, ImageRecord +from ..harness.orchestration.plan_synthesis import PlanSynthesizer, PlanLibrary, PlanValidator from ..harness.tools.registry import get_tool_registry +from gently_perception import Perceiver # Import tools package to trigger @tool decorator registration from . import tools as _tools # noqa: F401 +from ..harness.session.interaction_logger import InteractionLogger from .orchestration.timelapse import TimelapseOrchestrator +from ..harness.session.timeline import TimelineManager +from ..core import EventType, get_event_bus, emit +from ..core.file_store import FileStore + +from ..harness.conversation import ConversationManager +from ..harness.session.manager import SessionManager +from ..harness.prompts.manager import PromptManager -logger = logging.getLogger(__name__) # Shown when the agent is launched in UI-only mode (--no-api). The web UI is # fully browsable, but anything that would call Claude is disabled. @@ -74,7 +71,7 @@ class MicroscopyAgent: def __init__( self, - api_key: str | None = None, + api_key: Optional[str] = None, storage_path: Path = Path("./experiment_data"), model: str = settings.models.main, microscope_client=None, @@ -112,12 +109,11 @@ def __init__( # the message entry points refuse to call Claude. self.api_enabled = not no_api - # API client with interleaved thinking support + # Shared API client. No interleaved-thinking beta header: it's GA on the + # 4.6+ models and obsolete on Fable 5 (always-on thinking); the header is + # dropped so it can't conflict with the new model family. self.claude = anthropic.Anthropic( - api_key=api_key - or os.getenv("ANTHROPIC_API_KEY") - or ("no-api-mode" if no_api else None), - default_headers={"anthropic-beta": "interleaved-thinking-2025-05-14"}, + api_key=api_key or os.getenv("ANTHROPIC_API_KEY") or ("no-api-mode" if no_api else None), ) self.model = model @@ -129,7 +125,7 @@ def __init__( self.mode: str = "run" # Context store (agent's mind — set via set_context_store) - self.context_store: Any | None = None + self.context_store: Optional[Any] = None # Experiment state self.experiment = ExperimentState() @@ -145,7 +141,8 @@ def __init__( # Plan synthesis self.plan_synthesizer = PlanSynthesizer( - plan_library=PlanLibrary(), validator=PlanValidator() + plan_library=PlanLibrary(), + validator=PlanValidator() ) # Event bus for async messaging (must be before perception manager) @@ -166,8 +163,8 @@ def __init__( self.client = self.microscope # Callbacks - self.on_message_callback: Callable | None = None - self.choice_handler: Callable | None = None + self.on_message_callback: Optional[Callable] = None + self.choice_handler: Optional[Callable] = None # Serializes conversation turns: user turns and autonomous wake turns # must not interleave on the shared conversation_history. @@ -178,18 +175,14 @@ def __init__( # human). User turns are unaffected. _wake_choice_factory is set by the # web bridge so ASK-mode wake turns can round-trip an approval picker. self._autonomous_active = False - self._autonomous_blocked_tools = frozenset( - { - "set_laser_power", - "remove_embryo", - "stop_timelapse", - } - ) + self._autonomous_blocked_tools = frozenset({ + "set_laser_power", "remove_embryo", "stop_timelapse", + }) self._wake_choice_factory = None self._wake_choice_discard = None # Interaction logger for structured logging (research data collection) - self.interaction_logger: InteractionLogger | None = None + self.interaction_logger: Optional[InteractionLogger] = None # Event capture — durable log of every EventBus event during this # session. Substrate for offline replay / shadow-mode A/B of @@ -202,13 +195,13 @@ def __init__( self.decision_log = None # Timelapse orchestrator (initialized when microscope connected) - self.timelapse_orchestrator: TimelapseOrchestrator | None = None + self.timelapse_orchestrator: Optional[TimelapseOrchestrator] = None # Timeline manager for tracking events - self.timeline_manager: TimelineManager | None = None + self.timeline_manager: Optional[TimelineManager] = None # Visualization server for real-time feedback - self.viz_server: VisualizationServer | None = None + self.viz_server: Optional["VisualizationServer"] = None # Device-state monitor (bridges device-layer SSE → EventBus) self.device_state_monitor = None @@ -226,10 +219,10 @@ def __init__( ) # Wire tool execution context self.conversation._tool_context = { - "agent": self, - "client": getattr(self, "microscope", None), - "microscope": getattr(self, "microscope", None), - "databroker": getattr(self, "databroker", None), + 'agent': self, + 'client': getattr(self, 'microscope', None), + 'microscope': getattr(self, 'microscope', None), + 'databroker': getattr(self, 'databroker', None), } # Session manager (persistence) @@ -249,22 +242,16 @@ def __init__( success, history = self.sessions._resume_session(session_id, self.experiment) if success: self.conversation.conversation_history = history - self._emit_event( - EventType.SESSION_RESTORED, - { - "session_id": session_id, - "embryo_count": len(self.experiment.embryos), - "message_count": len(self.conversation.conversation_history), - }, - ) + self._emit_event(EventType.SESSION_RESTORED, { + 'session_id': session_id, + 'embryo_count': len(self.experiment.embryos), + 'message_count': len(self.conversation.conversation_history), + }) else: self.sessions.create_session() - self._emit_event( - EventType.SESSION_STARTED, - { - "session_id": self.sessions.session_id, - }, - ) + self._emit_event(EventType.SESSION_STARTED, { + 'session_id': self.sessions.session_id, + }) # Initialize interaction logger (for research data collection) self._init_interaction_logger() @@ -297,7 +284,6 @@ def __init__( # autonomously; enabled via the set_autonomy tool. try: from gently.app.wake_router import WakeRouter - self.wake_router = WakeRouter(self, self._event_bus) except Exception: logger.exception("Failed to init wake-router") @@ -314,7 +300,7 @@ def session_id(self) -> str: return self.sessions.session_id @property - def _session_id(self) -> str | None: + def _session_id(self) -> Optional[str]: """Internal session ID (backward compat).""" return self.sessions._session_id @@ -323,7 +309,7 @@ def _session_id(self, value): self.sessions._session_id = value @property - def conversation_history(self) -> list[dict]: + def conversation_history(self) -> List[Dict]: """Get conversation history.""" return self.conversation.conversation_history @@ -368,7 +354,6 @@ def set_context_store(self, context_store) -> None: self.prompts.context_store = context_store # Create agent memory harness from ..harness.memory.interface import AgentMemory - self.memory = AgentMemory(context_store, session_id=self.session_id) self.prompts.memory = self.memory @@ -378,13 +363,8 @@ def enter_plan_mode(self) -> str: return "Already in plan mode." self.mode = "plan" import gently.harness.plan_mode.tools # noqa: F401 - self._update_system_prompt() - emit( - EventType.STATUS_CHANGED, - {"field": "agent_mode", "value": "plan"}, - source="agent", - ) + emit(EventType.STATUS_CHANGED, {"field": "agent_mode", "value": "plan"}, source="agent") logger.info("Entered plan mode") return "Switched to plan mode. I'm now your experimental design collaborator." @@ -462,8 +442,7 @@ def exit_plan_mode(self) -> str: if item and self.session_id and self.context_store: try: self.context_store.link_session_campaign( - self.session_id, - item.campaign_id, + self.session_id, item.campaign_id, ) except Exception: pass @@ -480,11 +459,7 @@ def exit_plan_mode(self) -> str: self.prompts.invalidate_context_cache() self._update_system_prompt() - emit( - EventType.STATUS_CHANGED, - {"field": "agent_mode", "value": "run"}, - source="agent", - ) + emit(EventType.STATUS_CHANGED, {"field": "agent_mode", "value": "run"}, source="agent") logger.info("Exited plan mode") return result @@ -493,14 +468,11 @@ def exit_plan_mode(self) -> str: def _update_system_prompt(self, context_summary: str | None = None): """Rebuild system prompt via PromptManager.""" self.system_prompt = self.prompts.update_system_prompt( - self.experiment, - self.client, - self.mode, - context_summary, + self.experiment, self.client, self.mode, context_summary, perceiver=getattr(self, "perceiver", None), ) - def _get_active_plan_summary(self) -> str | None: + def _get_active_plan_summary(self) -> Optional[str]: """Delegation shim for agent bridge access.""" return self.prompts.get_active_plan_summary() @@ -530,7 +502,7 @@ def _auto_save(self): self.experiment, self.conversation.conversation_history, self.system_prompt ) - def list_sessions(self) -> list[dict]: + def list_sessions(self) -> List[Dict]: """List available sessions.""" return self.sessions.list_sessions() @@ -563,7 +535,6 @@ def _init_event_capture(self): a stripped-down agent) — replay just won't have a log to read. """ from gently.eval import EventCapture - try: session_dir = None sid = self.session_id @@ -571,8 +542,7 @@ def _init_event_capture(self): session_dir = self.store._session_dir(sid) if session_dir is None: logging.getLogger(__name__).debug( - "EventCapture: no session dir for %s — skipping", sid - ) + "EventCapture: no session dir for %s — skipping", sid) return path = session_dir / "events.jsonl" self.event_capture = EventCapture(path) @@ -599,7 +569,6 @@ def _init_decision_log(self): logs and the two are diffed offline. """ from gently.eval import DecisionLog - try: session_dir = None sid = self.session_id @@ -607,8 +576,7 @@ def _init_decision_log(self): session_dir = self.store._session_dir(sid) if session_dir is None: logging.getLogger(__name__).debug( - "DecisionLog: no session dir for %s — skipping", sid - ) + "DecisionLog: no session dir for %s — skipping", sid) return path = session_dir / "decisions.jsonl" self.decision_log = DecisionLog(path) @@ -706,15 +674,12 @@ def on_stage_detected(event): embryo_id = data.get("embryo_id") if embryo_id and embryo_id in self.experiment.embryos: embryo = self.experiment.embryos[embryo_id] - embryo.add_cv_result( - "stage_classification", - { - "stage": data.get("stage"), - "confidence": data.get("confidence"), - "nuclei_count": data.get("nuclei_count"), - "timepoint": data.get("timepoint"), - }, - ) + embryo.add_cv_result("stage_classification", { + "stage": data.get("stage"), + "confidence": data.get("confidence"), + "nuclei_count": data.get("nuclei_count"), + "timepoint": data.get("timepoint"), + }) except Exception as e: logger.warning(f"Error handling stage detected event: {e}") @@ -737,34 +702,23 @@ def on_perception(event): stage = data.get("stage") # 'no_object' is an empty-field sentinel, not a developmental # stage — don't mirror it into latest_developmental_stage. - if ( - not stage - or stage == "no_object" - or not embryo_id - or embryo_id not in self.experiment.embryos - ): + if (not stage or stage == "no_object" or not embryo_id + or embryo_id not in self.experiment.embryos): return embryo = self.experiment.embryos[embryo_id] if stage == getattr(embryo, "latest_developmental_stage", None): return # steady state — nothing new to mirror - embryo.add_cv_result( - "stage_classification", - { - "stage": stage, - "timepoint": data.get("timepoint"), - "stability": data.get("stability"), - "temporal_analysis": data.get("temporal_analysis"), - "detector_name": "perception", - }, - ) + embryo.add_cv_result("stage_classification", { + "stage": stage, + "timepoint": data.get("timepoint"), + "stability": data.get("stability"), + "temporal_analysis": data.get("temporal_analysis"), + "detector_name": "perception", + }) self.invalidate_context_cache() self._auto_save() - logger.info( - "Perception: %s -> stage %s (t%s)", - embryo_id, - stage, - data.get("timepoint"), - ) + logger.info("Perception: %s -> stage %s (t%s)", + embryo_id, stage, data.get("timepoint")) except Exception as e: logger.warning(f"Error handling perception event: {e}") @@ -779,9 +733,7 @@ def on_perception(event): # ===== Visualization Server Methods ===== - async def start_viz_server( - self, port: int = settings.network.viz_port, ssl_certfile=None, ssl_keyfile=None - ): + async def start_viz_server(self, port: int = settings.network.viz_port, ssl_certfile=None, ssl_keyfile=None): """Start the visualization server for real-time feedback.""" if self.viz_server is not None: logger.info("Visualization server already running") @@ -831,7 +783,6 @@ async def start_viz_server( if self.microscope is not None and self.device_state_monitor is None: try: from .device_state_monitor import DeviceStateMonitor - self.device_state_monitor = DeviceStateMonitor(self.microscope) await self.device_state_monitor.start() logger.info("Device-state monitor started") @@ -845,7 +796,6 @@ async def start_viz_server( if self.microscope is not None and self.bottom_camera_monitor is None: try: from .bottom_camera_monitor import BottomCameraStreamMonitor - self.bottom_camera_monitor = BottomCameraStreamMonitor(self.microscope) logger.info("Bottom-camera monitor ready (not started)") except Exception as e: @@ -876,14 +826,16 @@ def push_viz( array: np.ndarray, uid: str, data_type: str = "image", - metadata: dict[str, Any] | None = None, + metadata: Optional[Dict[str, Any]] = None, ): """Non-blocking push of image to visualization server.""" if self.viz_server is None: return try: - asyncio.create_task(self.viz_server.push_image(array, uid, data_type, metadata or {})) + asyncio.create_task( + self.viz_server.push_image(array, uid, data_type, metadata or {}) + ) except RuntimeError: pass except Exception as e: @@ -902,7 +854,7 @@ def _has_microscope(self) -> bool: """ return self.client is not None - def _emit_event(self, event_type: EventType, data: dict | None = None): + def _emit_event(self, event_type: EventType, data: Optional[Dict] = None): """Emit an event to the event bus.""" self._event_bus.publish( event_type=event_type, @@ -943,13 +895,10 @@ def _publish_embryos_update(self) -> None: def _mark_significant_action(self, action_type: str): """Mark that a significant action occurred (triggers auto-save).""" self._auto_save() - self._emit_event( - EventType.SESSION_SAVED, - { - "session_id": self.sessions._session_id, - "action_type": action_type, - }, - ) + self._emit_event(EventType.SESSION_SAVED, { + 'session_id': self.sessions._session_id, + 'action_type': action_type, + }) # ===== Public Message API ===== @@ -968,11 +917,8 @@ async def handle_message(self, user_message: str) -> str: Response from agent """ if quick_response := self.conversation.try_quick_response( - user_message, - self.experiment, - self.mode, - self.enter_plan_mode, - self.exit_plan_mode, + user_message, self.experiment, self.mode, + self.enter_plan_mode, self.exit_plan_mode, ): return quick_response @@ -986,7 +932,10 @@ async def handle_message(self, user_message: str) -> str: self._update_system_prompt(context_summary) # Add user message to history - self.conversation.conversation_history.append({"role": "user", "content": user_message}) + self.conversation.conversation_history.append({ + "role": "user", + "content": user_message + }) tools = self._get_tools_for_mode() cached_prompt = self._get_cached_system_prompt() @@ -1013,17 +962,14 @@ async def handle_message_stream(self, user_message: str): Chunks with 'type' and data """ if quick_response := self.conversation.try_quick_response( - user_message, - self.experiment, - self.mode, - self.enter_plan_mode, - self.exit_plan_mode, + user_message, self.experiment, self.mode, + self.enter_plan_mode, self.exit_plan_mode, ): - yield {"type": "text", "text": quick_response} + yield {'type': 'text', 'text': quick_response} return if not self.api_enabled: - yield {"type": "text", "text": _NO_API_NOTICE} + yield {'type': 'text', 'text': _NO_API_NOTICE} return # Hold the turn-lock for the whole streamed turn so an autonomous wake @@ -1039,14 +985,16 @@ async def handle_message_stream(self, user_message: str): ) self._update_system_prompt(context_summary) - self.conversation.conversation_history.append({"role": "user", "content": user_message}) + self.conversation.conversation_history.append({ + "role": "user", + "content": user_message + }) tools = self._get_tools_for_mode() cached_prompt = self._get_cached_system_prompt() inner_gen = self.conversation.call_claude_stream( - cached_prompt, - tools, + cached_prompt, tools, tool_label_fn=self.conversation.tool_label, auto_save_fn=self._auto_save, ) @@ -1141,11 +1089,8 @@ async def _resolve_wake_choice(self, chunk, emit, interactive): choice_data = chunk.get("choice_data", {}) if isinstance(chunk, dict) else {} factory = getattr(self, "_wake_choice_factory", None) if not interactive or factory is None: - logger.info( - "Wake picker auto-cancelled (interactive=%s, channel=%s)", - interactive, - factory is not None, - ) + logger.info("Wake picker auto-cancelled (interactive=%s, channel=%s)", + interactive, factory is not None) return "cancelled" try: future = factory(choice_data) # registers future + sets request_id @@ -1155,7 +1100,6 @@ async def _resolve_wake_choice(self, chunk, emit, interactive): request_id = choice_data.get("request_id", "") await emit({**chunk, "origin": "wake", "request_id": request_id}) from gently.app.wake_router import ASK_TIMEOUT_SEC - try: selected = await asyncio.wait_for(future, timeout=ASK_TIMEOUT_SEC) except asyncio.TimeoutError: @@ -1178,7 +1122,7 @@ async def _resolve_wake_choice(self, chunk, emit, interactive): pass return selected or "skip" - async def get_tool_call(self, user_message: str) -> dict | None: + async def get_tool_call(self, user_message: str) -> Optional[Dict]: """Dry-run tool call (for benchmarking).""" context_summary = await self.prompts.get_cached_context_summary( self.experiment, self.timelapse_orchestrator, self.timeline_manager @@ -1190,25 +1134,25 @@ async def get_tool_call(self, user_message: str) -> dict | None: # === Experiment Management Methods === - def load_embryos_from_database(self, database: dict): + def load_embryos_from_database(self, database: Dict): """Load embryos from calibration database.""" - if "embryos" not in database: + if 'embryos' not in database: return - for embryo_id, embryo_data in database["embryos"].items(): - position = embryo_data.get("stage_position_after_centering_um", {}) - calibration = embryo_data.get("calibration", {}) + for embryo_id, embryo_data in database['embryos'].items(): + position = embryo_data.get('stage_position_after_centering_um', {}) + calibration = embryo_data.get('calibration', {}) self.experiment.add_embryo( embryo_id=embryo_id, position=position, calibration=calibration, - uid=embryo_data.get("uid"), + uid=embryo_data.get('uid'), ) self._update_system_prompt() - def import_embryos_from_session(self, session_id: str, clear_existing: bool = False) -> dict: + def import_embryos_from_session(self, session_id: str, clear_existing: bool = False) -> Dict: """ Import embryos from another session into the current experiment. @@ -1264,14 +1208,14 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa if not embryo_states: session_data = self.store.load_session_snapshot(session_id) if session_data: - embryo_states = session_data.get("embryo_states", {}) + embryo_states = session_data.get('embryo_states', {}) if not embryo_states: return { - "success": False, - "error": "No embryos found in session", - "imported": [], - "skipped": [], + 'success': False, + 'error': "No embryos found in session", + 'imported': [], + 'skipped': [], } if clear_existing: @@ -1291,30 +1235,30 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa # Prefer explicit coarse/fine when the snapshot has them # (FileStore path); fall back to flat stage_position for the # legacy JSON-snapshot path which only carries the resolved view. - position_coarse = embryo_data.get("position_coarse") - position_fine = embryo_data.get("position_fine") + position_coarse = embryo_data.get('position_coarse') + position_fine = embryo_data.get('position_fine') if position_coarse is None and position_fine is None: - position_coarse = embryo_data.get("stage_position", {}) - calibration = embryo_data.get("calibration", {}) - source_uid = embryo_data.get("uid") or f"{session_id}_{embryo_id}" + position_coarse = embryo_data.get('stage_position', {}) + calibration = embryo_data.get('calibration', {}) + source_uid = embryo_data.get('uid') or f"{session_id}_{embryo_id}" self.experiment.add_embryo( embryo_id=embryo_id, position=position_coarse or {}, position_fine=position_fine or {}, calibration=calibration, - user_label=embryo_data.get("user_label"), + user_label=embryo_data.get('user_label'), uid=source_uid, - role=embryo_data.get("role") or "unassigned", + role=embryo_data.get('role') or 'unassigned', ) embryo = self.experiment.embryos[embryo_id] - embryo.nickname = embryo_data.get("nickname") - embryo.interval_seconds = embryo_data.get("interval_seconds") - embryo.num_slices = embryo_data.get("num_slices", 50) - embryo.exposure_ms = embryo_data.get("exposure_ms", 10.0) - embryo.priority = embryo_data.get("priority", "normal") - embryo.acquisition_mode = embryo_data.get("acquisition_mode", "volume") + embryo.nickname = embryo_data.get('nickname') + embryo.interval_seconds = embryo_data.get('interval_seconds') + embryo.num_slices = embryo_data.get('num_slices', 50) + embryo.exposure_ms = embryo_data.get('exposure_ms', 10.0) + embryo.priority = embryo_data.get('priority', 'normal') + embryo.acquisition_mode = embryo_data.get('acquisition_mode', 'volume') # Light budget import. Prefer fields already on embryo_data # (future schema may persist these directly on embryo.yaml); @@ -1325,22 +1269,27 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa # removed and the import should fail loudly if dose is # missing. dose = self._compute_imported_dose(session_id, embryo_id) - embryo.exposure_count = embryo_data.get("exposure_count") or dose["exposure_count"] + embryo.exposure_count = ( + embryo_data.get('exposure_count') + or dose['exposure_count'] + ) embryo.total_exposure_ms = ( - embryo_data.get("total_exposure_ms") or dose["total_exposure_ms"] + embryo_data.get('total_exposure_ms') + or dose['total_exposure_ms'] ) embryo.timepoints_acquired = ( - embryo_data.get("timepoints_acquired") or dose["exposure_count"] + embryo_data.get('timepoints_acquired') + or dose['exposure_count'] ) - last_imaged_str = embryo_data.get("last_imaged") + last_imaged_str = embryo_data.get('last_imaged') if last_imaged_str: try: embryo.last_imaged = datetime.fromisoformat(last_imaged_str) except (ValueError, TypeError): - embryo.last_imaged = dose["last_imaged"] + embryo.last_imaged = dose['last_imaged'] else: - embryo.last_imaged = dose["last_imaged"] + embryo.last_imaged = dose['last_imaged'] imported.append(embryo_id) @@ -1351,14 +1300,14 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa self._mark_significant_action("embryo_import") return { - "success": len(imported) > 0, - "imported": imported, - "skipped": skipped, - "errors": errors, - "source_session": session_id, + 'success': len(imported) > 0, + 'imported': imported, + 'skipped': skipped, + 'errors': errors, + 'source_session': session_id, } - def _compute_imported_dose(self, source_session_id: str, embryo_id: str) -> dict: + def _compute_imported_dose(self, source_session_id: str, embryo_id: str) -> Dict: """Reconstruct an embryo's realized 488 nm photodose from the source session's per-volume meta files. @@ -1373,68 +1322,66 @@ def _compute_imported_dose(self, source_session_id: str, embryo_id: str) -> dict TODO: replace with reading a persisted ``dose:`` block from embryo.yaml once dose-tracking is first-class. """ + import yaml from datetime import datetime from pathlib import Path - import yaml - result = { - "exposure_count": 0, - "total_exposure_ms": 0.0, - "last_imaged": None, + 'exposure_count': 0, + 'total_exposure_ms': 0.0, + 'last_imaged': None, } if not self.store: return result # FileStore exposes _session_dir(session_id) → resolved Path. - session_dir_fn = getattr(self.store, "_session_dir", None) + session_dir_fn = getattr(self.store, '_session_dir', None) sd = session_dir_fn(source_session_id) if callable(session_dir_fn) else None if sd is None: return result - vols_dir = Path(sd) / "embryos" / embryo_id / "volumes" + vols_dir = Path(sd) / 'embryos' / embryo_id / 'volumes' if not vols_dir.is_dir(): return result latest = None - for meta_path in sorted(vols_dir.glob("*.meta.yaml")): + for meta_path in sorted(vols_dir.glob('*.meta.yaml')): try: doc = yaml.safe_load(meta_path.read_text()) or {} except Exception: continue - md = doc.get("metadata") or {} - num_slices = md.get("num_slices") + md = doc.get('metadata') or {} + num_slices = md.get('num_slices') if num_slices is None: - shape = doc.get("shape") or [] + shape = doc.get('shape') or [] num_slices = shape[0] if shape else 0 - exposure_ms = md.get("exposure_ms") or 0.0 + exposure_ms = md.get('exposure_ms') or 0.0 try: - result["total_exposure_ms"] += float(num_slices) * float(exposure_ms) + result['total_exposure_ms'] += float(num_slices) * float(exposure_ms) except (TypeError, ValueError): pass - result["exposure_count"] += 1 - acq = doc.get("acquired_at") + result['exposure_count'] += 1 + acq = doc.get('acquired_at') if acq and (latest is None or acq > latest): latest = acq if latest: try: - result["last_imaged"] = datetime.fromisoformat(latest) + result['last_imaged'] = datetime.fromisoformat(latest) except (ValueError, TypeError): pass return result - async def on_volume_acquired( - self, embryo_id: str, timepoint: int, volume_data, volume_path=None - ): + async def on_volume_acquired(self, embryo_id: str, timepoint: int, + volume_data, volume_path=None): """Callback when a volume is acquired.""" embryo = self.experiment.embryos.get(embryo_id) if not embryo: return - if hasattr(volume_data, "read_volume"): + if hasattr(volume_data, 'read_volume'): volume = volume_data.read_volume() else: volume = volume_data @@ -1443,8 +1390,7 @@ async def on_volume_acquired( if self.store and self.session_id: try: self.store.register_embryo( - self.session_id, - embryo_id, + self.session_id, embryo_id, position_coarse=embryo.position_coarse or None, position_fine=embryo.position_fine or None, calibration=embryo.calibration, @@ -1461,19 +1407,14 @@ async def on_volume_acquired( } if volume_path is not None: stored_path = self.store.register_volume( - self.session_id, - embryo_id, - timepoint, + self.session_id, embryo_id, timepoint, incoming_path=Path(volume_path), metadata=acq_metadata, volume_data=volume, ) else: stored_path = self.store.put_volume( - self.session_id, - embryo_id, - timepoint, - volume, + self.session_id, embryo_id, timepoint, volume, metadata=acq_metadata, ) except StorageError: @@ -1488,9 +1429,9 @@ async def on_volume_acquired( if self.viz_server and volume is not None: try: from gently.core.imaging import ( - apply_crop_bounds, - compute_crop_bounds, projection_three_view, + compute_crop_bounds, + apply_crop_bounds, ) view_a = volume[0] if volume.ndim == 4 else volume @@ -1498,18 +1439,14 @@ async def on_volume_acquired( if view_a.ndim == 3: z_depth, height, width = view_a.shape if width > height * 2: - view_a = view_a[:, :, : width // 2] + view_a = view_a[:, :, :width // 2] bounds = compute_crop_bounds(view_a) cropped = apply_crop_bounds(view_a, bounds) three_view_img, _ = projection_three_view(cropped) else: three_view_img = view_a.astype(np.float32) if three_view_img.max() > three_view_img.min(): - three_view_img = ( - (three_view_img - three_view_img.min()) - / (three_view_img.max() - three_view_img.min()) - * 255 - ) + three_view_img = (three_view_img - three_view_img.min()) / (three_view_img.max() - three_view_img.min()) * 255 three_view_img = three_view_img.astype(np.uint8) self.push_viz( @@ -1517,32 +1454,29 @@ async def on_volume_acquired( uid=projection_uid, data_type="volume_projection", metadata={ - "embryo_id": embryo_id, - "timepoint": timepoint, - "shape": list(volume.shape), - "projection_uid": projection_uid, - "volume_uid": volume_uid, - "projection_type": "three_view", - }, + 'embryo_id': embryo_id, + 'timepoint': timepoint, + 'shape': list(volume.shape), + 'projection_uid': projection_uid, + 'volume_uid': volume_uid, + 'projection_type': 'three_view', + } ) except Exception as e: logger.warning(f"Failed to push to viz: {e}") - self._emit_event( - EventType.VOLUME_ACQUIRED, - { - "embryo_id": embryo_id, - "timepoint": timepoint, - "volume_uid": volume_uid, - "projection_uid": projection_uid, - "volume_path": str(stored_path) if stored_path else None, - "shape": list(volume.shape), - }, - ) + self._emit_event(EventType.VOLUME_ACQUIRED, { + 'embryo_id': embryo_id, + 'timepoint': timepoint, + 'volume_uid': volume_uid, + 'projection_uid': projection_uid, + 'volume_path': str(stored_path) if stored_path else None, + 'shape': list(volume.shape), + }) return { - "volume_uid": volume_uid, - "projection_uid": projection_uid, + 'volume_uid': volume_uid, + 'projection_uid': projection_uid, } def should_stop_experiment(self) -> bool: @@ -1551,31 +1485,22 @@ def should_stop_experiment(self) -> bool: return False return all(e.should_skip for e in self.experiment.embryos.values()) - def get_embryo_acquisition_order(self) -> list[str]: + def get_embryo_acquisition_order(self) -> List[str]: """Get embryo acquisition order based on priority.""" - high = [ - e.id - for e in self.experiment.embryos.values() - if e.priority == "high" and not e.should_skip - ] - normal = [ - e.id - for e in self.experiment.embryos.values() - if e.priority == "normal" and not e.should_skip - ] - low = [ - e.id - for e in self.experiment.embryos.values() - if e.priority == "low" and not e.should_skip - ] + high = [e.id for e in self.experiment.embryos.values() if e.priority == "high" and not e.should_skip] + normal = [e.id for e in self.experiment.embryos.values() if e.priority == "normal" and not e.should_skip] + low = [e.id for e in self.experiment.embryos.values() if e.priority == "low" and not e.should_skip] return high + normal + low - def decide_parameters(self, embryo_id: str, timepoint: int) -> dict: + def decide_parameters(self, embryo_id: str, timepoint: int) -> Dict: """Get current acquisition parameters for embryo.""" embryo = self.experiment.embryos.get(embryo_id) if not embryo: - return {"num_slices": 50, "exposure_ms": 10.0} - return {"num_slices": embryo.num_slices, "exposure_ms": embryo.exposure_ms} + return {'num_slices': 50, 'exposure_ms': 10.0} + return { + 'num_slices': embryo.num_slices, + 'exposure_ms': embryo.exposure_ms + } def decide_next_interval(self, timepoint: int) -> float: """Decide interval until next timepoint.""" @@ -1600,9 +1525,8 @@ async def check_blank_image( logger.warning(f"[BLANK_CHECK] {embryo_id}: Numerical check indicates blank image") return True - import base64 import io - + import base64 from PIL import Image if max_proj.max() > 0: @@ -1612,11 +1536,10 @@ async def check_blank_image( img = Image.fromarray(normalized) buffer = io.BytesIO() - img.save(buffer, format="PNG") + img.save(buffer, format='PNG') b64_image = base64.b64encode(buffer.getvalue()).decode() - prompt = """Look at this microscopy image. Is this a VALID microscopy image or a -BLANK/CORRUPTED image? + prompt = """Look at this microscopy image. Is this a VALID microscopy image or a BLANK/CORRUPTED image? A BLANK or CORRUPTED image shows: - Mostly uniform gray/black with no structure @@ -1634,22 +1557,20 @@ async def check_blank_image( self.claude.messages.create, model=settings.models.fast, max_tokens=10, - messages=[ - { - "role": "user", - "content": [ - {"type": "text", "text": prompt}, - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": b64_image, - }, - }, - ], - } - ], + messages=[{ + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": b64_image + } + } + ] + }] ) result = response.content[0].text.strip().upper() @@ -1660,11 +1581,7 @@ async def check_blank_image( return is_blank - except ( - anthropic.APIConnectionError, - anthropic.RateLimitError, - anthropic.APIStatusError, - ) as e: + except (anthropic.APIConnectionError, anthropic.RateLimitError, anthropic.APIStatusError) as e: logger.error(f"[BLANK_CHECK] Claude API error for {embryo_id}: {e}") return False except Exception as e: diff --git a/gently/app/detectors/dopaminergic_signal.py b/gently/app/detectors/dopaminergic_signal.py index 7fb09408..9e9edbd9 100644 --- a/gently/app/detectors/dopaminergic_signal.py +++ b/gently/app/detectors/dopaminergic_signal.py @@ -273,7 +273,9 @@ async def _call_perceiver( } ], ) - raw = response.content[0].text if response.content else "" + if response.stop_reason == "refusal" or not response.content: + return "(perception model declined the request)", "" + raw = response.content[0].text return raw.strip(), raw async def _call_classifier( @@ -290,7 +292,9 @@ async def _call_classifier( max_tokens=300, messages=[{"role": "user", "content": prompt}], ) - raw = response.content[0].text if response.content else "" + if response.stop_reason == "refusal" or not response.content: + return dict(_DEFAULT_FINDINGS), "", "Safety refusal" + raw = response.content[0].text findings, parse_err = _parse_response(raw) return findings, raw, parse_err diff --git a/gently/hardware/dispim/sam_detection.py b/gently/hardware/dispim/sam_detection.py index cea9990a..7014338a 100644 --- a/gently/hardware/dispim/sam_detection.py +++ b/gently/hardware/dispim/sam_detection.py @@ -5,28 +5,29 @@ Returns embryo positions (pixel + stage coordinates) for calibration workflow. """ -import base64 -import json import logging -import os +import time +import json import uuid -from io import BytesIO +import numpy as np from pathlib import Path - -import anthropic import cv2 -import numpy as np +import base64 +from io import BytesIO from PIL import Image +import anthropic +from typing import Dict, List, Tuple, Optional +import os from gently.settings import settings logger = logging.getLogger(__name__) -from gently.core.coordinates import ( # noqa: E402 - DEFAULT_OBJECTIVE_MAG, - DEFAULT_PIXEL_SIZE_UM, - get_um_per_pixel, +from gently.core.coordinates import ( pixel_to_stage_position, + get_um_per_pixel, + DEFAULT_PIXEL_SIZE_UM, + DEFAULT_OBJECTIVE_MAG, ) @@ -41,13 +42,11 @@ class SAMEmbryoDetector: - Returns embryo positions as simple list of coordinates """ - def __init__( - self, - sam_checkpoint: str = "sam_vit_b_01ec64.pth", - sam_model_type: str = "vit_b", - device: str = "cpu", - anthropic_api_key: str | None = None, - ): + def __init__(self, + sam_checkpoint: str = "sam_vit_b_01ec64.pth", + sam_model_type: str = "vit_b", + device: str = "cpu", + anthropic_api_key: Optional[str] = None): """ Initialize SAM detector @@ -86,7 +85,7 @@ def _load_sam(self): if self._mask_generator is not None: return - from segment_anything import SamAutomaticMaskGenerator, SamPredictor, sam_model_registry + from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor if not Path(self.sam_checkpoint).exists(): raise FileNotFoundError(f"SAM checkpoint not found: {self.sam_checkpoint}") @@ -109,15 +108,13 @@ def _load_sam(self): self._predictor = SamPredictor(sam) logger.info("SAM model loaded") - def preprocess_image( - self, - image: np.ndarray, - bg_kernel_size: int = 150, - use_clahe: bool = True, - clahe_clip_limit: float = 3.0, - clahe_tile_size: int = 16, - gaussian_sigma: float = 2.0, - ) -> np.ndarray: + def preprocess_image(self, + image: np.ndarray, + bg_kernel_size: int = 150, + use_clahe: bool = True, + clahe_clip_limit: float = 3.0, + clahe_tile_size: int = 16, + gaussian_sigma: float = 2.0) -> np.ndarray: """ Preprocess image for better SAM detection. @@ -154,18 +151,14 @@ def preprocess_image( # This stretches the narrow range (e.g., 84-354) to full 0-255 logger.debug("Percentile normalization (2-98%%)...") p2, p98 = np.percentile(image, (2, 98)) - img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype( - np.uint8 - ) + img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype(np.uint8) logger.debug("Normalized to 0-255") # Step 2: Background subtraction with large morphological opening # Removes large-scale illumination variations if bg_kernel_size > 0: logger.debug("Background subtraction (kernel=%d)...", bg_kernel_size) - kernel_bg = cv2.getStructuringElement( - cv2.MORPH_ELLIPSE, (bg_kernel_size, bg_kernel_size) - ) + kernel_bg = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (bg_kernel_size, bg_kernel_size)) background = cv2.morphologyEx(img_norm, cv2.MORPH_OPEN, kernel_bg) img_no_bg = cv2.subtract(img_norm, background) img_no_bg = cv2.normalize(img_no_bg, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) @@ -178,7 +171,8 @@ def preprocess_image( if use_clahe: logger.debug("CLAHE (clip=%.1f, tile=%d)...", clahe_clip_limit, clahe_tile_size) clahe = cv2.createCLAHE( - clipLimit=clahe_clip_limit, tileGridSize=(clahe_tile_size, clahe_tile_size) + clipLimit=clahe_clip_limit, + tileGridSize=(clahe_tile_size, clahe_tile_size) ) img_enhanced = clahe.apply(img_no_bg) logger.debug("CLAHE applied") @@ -193,20 +187,16 @@ def preprocess_image( else: img_smooth = img_enhanced - logger.debug( - "Preprocessing complete (output range: %s - %s)", img_smooth.min(), img_smooth.max() - ) + logger.debug("Preprocessing complete (output range: %s - %s)", img_smooth.min(), img_smooth.max()) return img_smooth - def find_embryo_candidates( - self, - image: np.ndarray, - brightness_percentile: float = 99.0, - min_area: int = 5000, - max_area: int = 150000, - clahe_clip: float = 3.0, - clahe_tile: int = 16, - ) -> tuple[list[dict], np.ndarray]: + def find_embryo_candidates(self, + image: np.ndarray, + brightness_percentile: float = 99.0, + min_area: int = 5000, + max_area: int = 150000, + clahe_clip: float = 3.0, + clahe_tile: int = 16) -> Tuple[List[Dict], np.ndarray]: """ Find embryo candidates using brightness-based detection. @@ -239,16 +229,12 @@ def find_embryo_candidates( enhanced_image : np.ndarray Contrast-enhanced 8-bit image for SAM """ - logger.info( - "Finding embryo candidates (brightness percentile=%.1f)...", brightness_percentile - ) + logger.info("Finding embryo candidates (brightness percentile=%.1f)...", brightness_percentile) logger.debug("Input range: %s - %s", image.min(), image.max()) # Step 1: Percentile normalization (handles low dynamic range) p2, p98 = np.percentile(image, (2, 98)) - img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype( - np.uint8 - ) + img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype(np.uint8) logger.debug("Normalized to 0-255") # Step 2: CLAHE for local contrast enhancement @@ -275,9 +261,7 @@ def find_embryo_candidates( logger.debug("Morphological cleanup complete") # Step 7: Find connected components and filter by area - num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats( - mask, connectivity=8 - ) + num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8) candidates = [] for i in range(1, num_labels): # Skip background (label 0) @@ -289,14 +273,19 @@ def find_embryo_candidates( h = stats[i, cv2.CC_STAT_HEIGHT] cx, cy = centroids[i] - candidates.append({"bbox": (x, y, w, h), "centroid": (cx, cy), "area": area}) + candidates.append({ + 'bbox': (x, y, w, h), + 'centroid': (cx, cy), + 'area': area + }) logger.info("Found %d embryo candidates", len(candidates)) return candidates, img_smooth - def refine_with_sam( - self, image: np.ndarray, candidates: list[dict], padding: int = 20 - ) -> list[dict]: + def refine_with_sam(self, + image: np.ndarray, + candidates: List[Dict], + padding: int = 20) -> List[Dict]: """ Refine embryo candidates using SAM with bounding box prompts. @@ -333,7 +322,7 @@ def refine_with_sam( h, w = image.shape[:2] for i, candidate in enumerate(candidates): - x, y, bw, bh = candidate["bbox"] + x, y, bw, bh = candidate['bbox'] # Add padding and clip to image bounds x1 = max(0, x - padding) @@ -346,7 +335,10 @@ def refine_with_sam( # Get SAM prediction with box prompt masks, scores, _ = self._predictor.predict( - point_coords=None, point_labels=None, box=input_box, multimask_output=True + point_coords=None, + point_labels=None, + box=input_box, + multimask_output=True ) # Take best mask (highest score) @@ -356,7 +348,9 @@ def refine_with_sam( # Calculate properties from SAM mask contours, _ = cv2.findContours( - mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + mask.astype(np.uint8), + cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_SIMPLE ) if contours: @@ -370,47 +364,41 @@ def refine_with_sam( cx = M["m10"] / M["m00"] cy = M["m01"] / M["m00"] else: - cx, cy = candidate["centroid"] + cx, cy = candidate['centroid'] # Calculate circularity perimeter = cv2.arcLength(contour, True) - circularity = 4 * np.pi * area / (perimeter**2) if perimeter > 0 else 0 + circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0 # Get bounding box from contour bx, by, bw, bh = cv2.boundingRect(contour) - embryos.append( - { - "embryo_id": f"embryo_{i + 1}", - "uid": str( - uuid.uuid4() - ), # Global unique identifier for cross-session tracking - "pixel_x": float(cx), - "pixel_y": float(cy), - "bbox": (bx, by, bw, bh), # Used by visualization functions - "area_pixels": int(area), - "circularity": float(circularity), - "confidence": float(score), - "mask": mask, - } - ) + embryos.append({ + 'embryo_id': f'embryo_{i + 1}', + 'uid': str(uuid.uuid4()), # Global unique identifier for cross-session tracking + 'pixel_x': float(cx), + 'pixel_y': float(cy), + 'bbox': (bx, by, bw, bh), # Used by visualization functions + 'area_pixels': int(area), + 'circularity': float(circularity), + 'confidence': float(score), + 'mask': mask + }) logger.info("SAM refined %d embryos", len(embryos)) return embryos - async def detect_embryos( - self, - image: np.ndarray, - stage_position: tuple[float, float], - pixel_size_um: float = DEFAULT_PIXEL_SIZE_UM, - objective_mag: float = DEFAULT_OBJECTIVE_MAG, - use_claude_review: bool = True, - save_visualizations: bool = True, - output_dir: Path | None = None, - brightness_percentile: float = 99.0, - min_area: int = 5000, - max_area: int = 150000, - ) -> dict: + async def detect_embryos(self, + image: np.ndarray, + stage_position: Tuple[float, float], + pixel_size_um: float = DEFAULT_PIXEL_SIZE_UM, + objective_mag: float = DEFAULT_OBJECTIVE_MAG, + use_claude_review: bool = True, + save_visualizations: bool = True, + output_dir: Optional[Path] = None, + brightness_percentile: float = 99.0, + min_area: int = 5000, + max_area: int = 150000) -> Dict: """ Detect embryos using brightness-based detection + SAM refinement. @@ -474,17 +462,20 @@ async def detect_embryos( # Step 1: Find candidates using brightness detection logger.info("[1/4] Finding embryo candidates (brightness-based)...") candidates, image_enhanced = self.find_embryo_candidates( - image, brightness_percentile=brightness_percentile, min_area=min_area, max_area=max_area + image, + brightness_percentile=brightness_percentile, + min_area=min_area, + max_area=max_area ) if len(candidates) == 0: logger.warning("No embryo candidates found!") return { - "embryos": [], - "initial_detections": 0, - "final_detections": 0, - "verification": {"verified": False}, - "images": {}, + 'embryos': [], + 'initial_detections': 0, + 'final_detections': 0, + 'verification': {'verified': False}, + 'images': {} } # Step 2: Refine with SAM @@ -498,11 +489,11 @@ async def detect_embryos( if len(embryos_sam) == 0: logger.warning("No embryos detected by SAM!") return { - "embryos": [], - "initial_detections": 0, - "final_detections": 0, - "verification": {"verified": False}, - "images": {}, + 'embryos': [], + 'initial_detections': 0, + 'final_detections': 0, + 'verification': {'verified': False}, + 'images': {} } # Save initial detection @@ -512,8 +503,8 @@ async def detect_embryos( # Claude review (if enabled) embryos_final = embryos_sam - verification = {"verified": True, "skipped": not use_claude_review} - changes = {"round1": {"removed": [], "added": []}} + verification = {'verified': True, 'skipped': not use_claude_review} + changes = {'round1': {'removed': [], 'added': []}} if use_claude_review and self.claude_client: logger.info("[2/4] Claude Vision review (Round 1)...") @@ -521,7 +512,7 @@ async def detect_embryos( review_r1 = await self._review_with_claude(image_8bit, annotated, embryos_sam) logger.info("[3/4] Applying corrections...") - embryos_r1, changes["round1"] = self._apply_corrections( + embryos_r1, changes['round1'] = self._apply_corrections( embryos_sam, review_r1, image, self._predictor ) @@ -533,22 +524,22 @@ async def detect_embryos( logger.info("[4/4] Claude verification (Round 2)...") r1_viz = self._create_annotated_image(image_8bit, embryos_r1) verification = await self._verify_with_claude( - image_8bit, r1_viz, embryos_r1, changes["round1"] + image_8bit, r1_viz, embryos_r1, changes['round1'] ) # Apply round 2 corrections if needed has_r2_changes = ( - len(verification.get("additional_false_positives", [])) > 0 - or len(verification.get("additional_false_negatives", [])) > 0 + len(verification.get('additional_false_positives', [])) > 0 or + len(verification.get('additional_false_negatives', [])) > 0 ) if has_r2_changes: logger.info("Applying Round 2 corrections...") review_r2 = { - "false_positives": verification.get("additional_false_positives", []), - "false_negatives": verification.get("additional_false_negatives", []), + 'false_positives': verification.get('additional_false_positives', []), + 'false_negatives': verification.get('additional_false_negatives', []) } - embryos_final, changes["round2"] = self._apply_corrections( + embryos_final, changes['round2'] = self._apply_corrections( embryos_r1, review_r2, image, self._predictor ) else: @@ -562,7 +553,7 @@ async def detect_embryos( stage_position, pixel_size_um, objective_mag, - image_shape=image.shape[:2], # (height, width) + image_shape=image.shape[:2] # (height, width) ) # Save final visualization @@ -572,19 +563,19 @@ async def detect_embryos( # Package results results = { - "embryos": embryo_positions, - "initial_detections": len(embryos_sam), - "final_detections": len(embryos_final), - "verification": verification, - "changes": changes, - "images": { - "initial": str(output_dir / "detection_initial.png"), - "final": str(output_dir / "detection_final.png"), - }, + 'embryos': embryo_positions, + 'initial_detections': len(embryos_sam), + 'final_detections': len(embryos_final), + 'verification': verification, + 'changes': changes, + 'images': { + 'initial': str(output_dir / "detection_initial.png"), + 'final': str(output_dir / "detection_final.png") + } } if use_claude_review and save_visualizations: - results["images"]["round1"] = str(output_dir / "detection_round1.png") + results['images']['round1'] = str(output_dir / "detection_round1.png") logger.info("=" * 70) logger.info("DETECTION COMPLETE: %d embryos", len(embryo_positions)) @@ -603,7 +594,7 @@ def _to_rgb8(image: np.ndarray) -> np.ndarray: return cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) return image - def _detect_with_sam(self, image: np.ndarray) -> tuple[list[dict], np.ndarray]: + def _detect_with_sam(self, image: np.ndarray) -> Tuple[List[Dict], np.ndarray]: """Run SAM automatic segmentation (extracted from test script)""" image_rgb = self._to_rgb8(image) @@ -613,16 +604,14 @@ def _detect_with_sam(self, image: np.ndarray) -> tuple[list[dict], np.ndarray]: # Filter candidates embryo_candidates = [] for mask_data in masks: - area = mask_data["area"] + area = mask_data['area'] if not (self.min_area <= area <= self.max_area): continue - bbox = mask_data["bbox"] - mask = mask_data["segmentation"] - contours, _ = cv2.findContours( - mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE - ) + bbox = mask_data['bbox'] + mask = mask_data['segmentation'] + contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) == 0: continue @@ -631,44 +620,40 @@ def _detect_with_sam(self, image: np.ndarray) -> tuple[list[dict], np.ndarray]: if perimeter == 0: continue - circularity = 4 * np.pi * area / (perimeter**2) + circularity = 4 * np.pi * area / (perimeter ** 2) if circularity < self.min_circularity: continue - embryo_candidates.append( - { - "mask": mask, - "bbox": bbox, - "area": area, - "circularity": circularity, - "stability_score": mask_data["stability_score"], - "predicted_iou": mask_data["predicted_iou"], - } - ) + embryo_candidates.append({ + 'mask': mask, + 'bbox': bbox, + 'area': area, + 'circularity': circularity, + 'stability_score': mask_data['stability_score'], + 'predicted_iou': mask_data['predicted_iou'] + }) # Sort by quality and apply spatial separation - embryo_candidates.sort(key=lambda x: x["area"] * x["stability_score"], reverse=True) + embryo_candidates.sort(key=lambda x: (x['area'] * x['stability_score']), reverse=True) selected_embryos = [] for candidate in embryo_candidates: if len(selected_embryos) >= self.max_embryos: break - bbox = candidate["bbox"] + bbox = candidate['bbox'] candidate_center_x = bbox[0] + bbox[2] / 2 candidate_center_y = bbox[1] + bbox[3] / 2 too_close = False for selected in selected_embryos: - sel_bbox = selected["bbox"] + sel_bbox = selected['bbox'] sel_center_x = sel_bbox[0] + sel_bbox[2] / 2 sel_center_y = sel_bbox[1] + sel_bbox[3] / 2 - distance = np.sqrt( - (candidate_center_x - sel_center_x) ** 2 - + (candidate_center_y - sel_center_y) ** 2 - ) + distance = np.sqrt((candidate_center_x - sel_center_x)**2 + + (candidate_center_y - sel_center_y)**2) if distance < self.min_separation_pixels: too_close = True @@ -677,27 +662,19 @@ def _detect_with_sam(self, image: np.ndarray) -> tuple[list[dict], np.ndarray]: if not too_close: selected_embryos.append(candidate) - return selected_embryos, image_rgb + return selected_embryos, image_8bit - def _create_annotated_image(self, image: np.ndarray, embryos: list[dict]) -> np.ndarray: + def _create_annotated_image(self, image: np.ndarray, embryos: List[Dict]) -> np.ndarray: """Create annotated image with numbered boxes""" viz = image.copy() if len(viz.shape) == 2: viz = cv2.cvtColor(viz, cv2.COLOR_GRAY2RGB) - colors = [ - (255, 0, 0), - (0, 255, 0), - (0, 0, 255), - (255, 255, 0), - (255, 0, 255), - (0, 255, 255), - (128, 128, 0), - (128, 0, 128), - ] + colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), + (255, 0, 255), (0, 255, 255), (128, 128, 0), (128, 0, 128)] for i, embryo in enumerate(embryos): - bbox = embryo["bbox"] + bbox = embryo['bbox'] x, y, w, h = bbox color = colors[i % len(colors)] @@ -736,7 +713,7 @@ def _encode_image_base64(self, image: np.ndarray) -> str: buffered = BytesIO() pil_image.save(buffered, format="JPEG", quality=quality, optimize=True) if buffered.tell() <= max_bytes: - return base64.b64encode(buffered.getvalue()).decode("utf-8") + return base64.b64encode(buffered.getvalue()).decode('utf-8') quality -= 5 # Last resort @@ -745,21 +722,18 @@ def _encode_image_base64(self, image: np.ndarray) -> str: pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS) buffered = BytesIO() pil_image.save(buffered, format="JPEG", quality=85, optimize=True) - return base64.b64encode(buffered.getvalue()).decode("utf-8") + return base64.b64encode(buffered.getvalue()).decode('utf-8') - async def _review_with_claude( - self, image: np.ndarray, annotated: np.ndarray, embryos: list[dict] - ) -> dict: + async def _review_with_claude(self, image: np.ndarray, annotated: np.ndarray, embryos: List[Dict]) -> Dict: """Round 1: Claude reviews detections (from test script)""" if not self.claude_client: - return {"false_positives": [], "false_negatives": []} + return {'false_positives': [], 'false_negatives': []} image_base64 = self._encode_image_base64(annotated) - prompt = f"""You are a microscopy expert analyzing embryo detections from a bottom -camera view. + prompt = f"""You are a microscopy expert analyzing embryo detections from a bottom camera view. -CURRENT DETECTIONS: {len(embryos)} embryos labeled 0-{len(embryos) - 1} with colored bounding boxes. +CURRENT DETECTIONS: {len(embryos)} embryos labeled 0-{len(embryos)-1} with colored bounding boxes. EMBRYO CHARACTERISTICS: - Small, BRIGHT white/light gray oval or rice grain shapes @@ -789,23 +763,14 @@ async def _review_with_claude( message = self.claude_client.messages.create( model=settings.models.perception, max_tokens=8000, - thinking={"type": "enabled", "budget_tokens": 5000}, - messages=[ - { - "role": "user", - "content": [ - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/jpeg", - "data": image_base64, - }, - }, - {"type": "text", "text": prompt}, - ], - } - ], + output_config={"effort": "high"}, # was thinking budget_tokens (Opus 4.8 rejects it) + messages=[{ + "role": "user", + "content": [ + {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_base64}}, + {"type": "text", "text": prompt} + ] + }] ) response_text = next((b.text for b in message.content if b.type == "text"), "") @@ -822,19 +787,18 @@ async def _review_with_claude( except Exception as e: logger.warning("Claude review failed: %s", e) - return {"false_positives": [], "false_negatives": []} + return {'false_positives': [], 'false_negatives': []} - async def _verify_with_claude( - self, image: np.ndarray, annotated: np.ndarray, embryos: list[dict], previous_changes: dict - ) -> dict: + async def _verify_with_claude(self, image: np.ndarray, annotated: np.ndarray, + embryos: List[Dict], previous_changes: Dict) -> Dict: """Round 2: Claude verifies corrections (from test script)""" if not self.claude_client: - return {"verified": True, "skipped": True} + return {'verified': True, 'skipped': True} image_base64 = self._encode_image_base64(annotated) - removed = previous_changes.get("removed", []) - added = previous_changes.get("added", []) + removed = previous_changes.get('removed', []) + added = previous_changes.get('added', []) prompt = f"""VERIFICATION ROUND - You previously reviewed this image. @@ -842,7 +806,7 @@ async def _verify_with_claude( - Removed: {removed if removed else "none"} - Added: {added if added else "none"} -CURRENT: {len(embryos)} detections (numbered 0-{len(embryos) - 1}) +CURRENT: {len(embryos)} detections (numbered 0-{len(embryos)-1}) TASK: Verify corrections and catch any remaining issues. Only report CLEAR remaining problems. @@ -859,23 +823,14 @@ async def _verify_with_claude( message = self.claude_client.messages.create( model=settings.models.perception, max_tokens=6000, - thinking={"type": "enabled", "budget_tokens": 4000}, - messages=[ - { - "role": "user", - "content": [ - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/jpeg", - "data": image_base64, - }, - }, - {"type": "text", "text": prompt}, - ], - } - ], + output_config={"effort": "high"}, # was thinking budget_tokens (Opus 4.8 rejects it) + messages=[{ + "role": "user", + "content": [ + {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_base64}}, + {"type": "text", "text": prompt} + ] + }] ) response_text = next((b.text for b in message.content if b.type == "text"), "") @@ -891,41 +846,38 @@ async def _verify_with_claude( except Exception as e: logger.warning("Verification failed: %s", e) - return {"verified": False} + return {'verified': False} - def _apply_corrections( - self, embryos: list[dict], review: dict, image: np.ndarray, predictor - ) -> tuple[list[dict], dict]: + def _apply_corrections(self, embryos: List[Dict], review: Dict, + image: np.ndarray, predictor) -> Tuple[List[Dict], Dict]: """Apply Claude's corrections (from test script)""" corrected = [] - changes = {"removed": [], "added": []} + changes = {'removed': [], 'added': []} # Remove false positives - false_positives = set(review.get("false_positives", [])) + false_positives = set(review.get('false_positives', [])) if false_positives: - changes["removed"] = list(false_positives) + changes['removed'] = list(false_positives) for i, embryo in enumerate(embryos): if i not in false_positives: corrected.append(embryo) # Add false negatives - false_negatives = review.get("false_negatives", []) + false_negatives = review.get('false_negatives', []) if false_negatives: for fn in false_negatives: - point = (fn["x"], fn["y"]) + point = (fn['x'], fn['y']) new_embryo = self._segment_with_sam(image, predictor, point) - if new_embryo and ( - self.min_area <= new_embryo["area"] <= self.max_area - and new_embryo["circularity"] >= self.min_circularity - ): + if new_embryo and (self.min_area <= new_embryo['area'] <= self.max_area and + new_embryo['circularity'] >= self.min_circularity): corrected.append(new_embryo) - changes["added"].append(point) + changes['added'].append(point) return corrected, changes - def _segment_with_sam(self, image: np.ndarray, predictor, point: tuple) -> dict | None: + def _segment_with_sam(self, image: np.ndarray, predictor, point: Tuple) -> Optional[Dict]: """Use SAM predictor to segment region (from test script)""" image_rgb = self._to_rgb8(image) predictor.set_image(image_rgb) @@ -934,7 +886,9 @@ def _segment_with_sam(self, image: np.ndarray, predictor, point: tuple) -> dict point_labels = np.array([1]) masks, scores, _ = predictor.predict( - point_coords=point_coords, point_labels=point_labels, multimask_output=True + point_coords=point_coords, + point_labels=point_labels, + multimask_output=True ) best_idx = np.argmax(scores) @@ -949,33 +903,26 @@ def _segment_with_sam(self, image: np.ndarray, predictor, point: tuple) -> dict bbox = [x_min, y_min, x_max - x_min, y_max - y_min] area = mask.sum() - contours, _ = cv2.findContours( - mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE - ) + contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) > 0: perimeter = cv2.arcLength(contours[0], True) - circularity = 4 * np.pi * area / (perimeter**2) if perimeter > 0 else 0 + circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0 else: circularity = 0 return { - "mask": mask, - "bbox": bbox, - "area": int(area), - "circularity": float(circularity), - "stability_score": float(scores[best_idx]), - "predicted_iou": float(scores[best_idx]), + 'mask': mask, + 'bbox': bbox, + 'area': int(area), + 'circularity': float(circularity), + 'stability_score': float(scores[best_idx]), + 'predicted_iou': float(scores[best_idx]) } - def _pixel_to_stage_coordinates( - self, - embryos: list[dict], - stage_pos: tuple[float, float], - pixel_size_um: float, - objective_mag: float, - image_shape: tuple[int, int] = (2048, 2048), - ) -> list[dict]: + def _pixel_to_stage_coordinates(self, embryos: List[Dict], stage_pos: Tuple[float, float], + pixel_size_um: float, objective_mag: float, + image_shape: Tuple[int, int] = (2048, 2048)) -> List[Dict]: """ Convert pixel coordinates to stage coordinates. @@ -991,7 +938,7 @@ def _pixel_to_stage_coordinates( embryo_positions = [] for i, embryo in enumerate(embryos): - bbox = embryo["bbox"] + bbox = embryo['bbox'] x, y, w, h = bbox center_x_px = x + w / 2 @@ -1006,26 +953,24 @@ def _pixel_to_stage_coordinates( image_center_y=image_center_y, stage_x=stage_x, stage_y=stage_y, - um_per_pixel=effective_pixel_um, + um_per_pixel=effective_pixel_um ) - embryo_positions.append( - { - "embryo_id": f"embryo_{i + 1}", - "pixel_x": float(center_x_px), - "pixel_y": float(center_y_px), - "stage_x_um": float(embryo_stage_x), - "stage_y_um": float(embryo_stage_y), - "bbox_pixel": tuple(bbox), - "area_pixels": embryo.get("area_pixels", embryo.get("area", 0)), - "circularity": embryo.get("circularity", 0), - "confidence": embryo.get("confidence", embryo.get("stability_score", 0)), - } - ) + embryo_positions.append({ + 'embryo_id': f'embryo_{i + 1}', + 'pixel_x': float(center_x_px), + 'pixel_y': float(center_y_px), + 'stage_x_um': float(embryo_stage_x), + 'stage_y_um': float(embryo_stage_y), + 'bbox_pixel': tuple(bbox), + 'area_pixels': embryo.get('area_pixels', embryo.get('area', 0)), + 'circularity': embryo.get('circularity', 0), + 'confidence': embryo.get('confidence', embryo.get('stability_score', 0)) + }) return embryo_positions - def show_in_napari(self, image: np.ndarray, embryos: list[dict], block: bool = False): + def show_in_napari(self, image: np.ndarray, embryos: List[Dict], block: bool = False): """Deprecated: napari display was retired in Phase 1. SAM detection results are now reviewed via the web map view — diff --git a/gently/harness/conversation.py b/gently/harness/conversation.py index e154c724..29573e6a 100644 --- a/gently/harness/conversation.py +++ b/gently/harness/conversation.py @@ -10,12 +10,14 @@ import logging import re import time -from typing import Any +from typing import Dict, List, Optional, Any + +from ..settings import settings logger = logging.getLogger(__name__) -def _extend_tool_calls(out: list[dict[str, Any]], content_blocks) -> None: +def _extend_tool_calls(out: List[Dict[str, Any]], content_blocks) -> None: """Append every tool_use block in content_blocks to out. Tolerates absent attributes (some SDK versions / mock objects) so it @@ -27,13 +29,11 @@ def _extend_tool_calls(out: list[dict[str, Any]], content_blocks) -> None: try: if getattr(block, "type", None) != "tool_use": continue - out.append( - { - "name": getattr(block, "name", None), - "input": getattr(block, "input", None), - "id": getattr(block, "id", None), - } - ) + out.append({ + "name": getattr(block, "name", None), + "input": getattr(block, "input", None), + "id": getattr(block, "id", None), + }) except Exception: continue @@ -55,7 +55,7 @@ def __init__(self, client, model, tool_registry): self._tool_registry = tool_registry # Conversation state - self.conversation_history: list[dict] = [] + self.conversation_history: List[Dict] = [] # Token counters self.total_input_tokens: int = 0 @@ -76,9 +76,8 @@ def __init__(self, client, model, tool_registry): # ===== Quick Response ===== - def try_quick_response( - self, message: str, experiment, mode: str, enter_plan_fn, exit_plan_fn - ) -> str | None: + def try_quick_response(self, message: str, experiment, mode: str, + enter_plan_fn, exit_plan_fn) -> Optional[str]: """ Answer simple queries from state without LLM call. @@ -107,13 +106,7 @@ def try_quick_response( return experiment.get_summary() # Plan mode switching via natural language - plan_enter_phrases = ( - "plan mode", - "enter plan", - "switch to plan", - "let's plan", - "design an experiment", - ) + plan_enter_phrases = ("plan mode", "enter plan", "switch to plan", "let's plan", "design an experiment") plan_exit_phrases = ("exit plan", "leave plan", "back to run", "run mode") if mode != "plan" and any(p in message_lower for p in plan_enter_phrases): @@ -149,36 +142,54 @@ def should_use_thinking(self, message: str, mode: str) -> bool: if mode == "plan": return True + import re msg_lower = message.lower() - if re.search(r"\bthink(ing)?\b", message, re.IGNORECASE): + if re.search(r'\bthink(ing)?\b', message, re.IGNORECASE): return True - if re.search(r"\bcalibrat", msg_lower): + if re.search(r'\bcalibrat', msg_lower): return True - if re.search(r"\b(plan|timelapse|time-lapse|acquisition)\b", msg_lower): + if re.search(r'\b(plan|timelapse|time-lapse|acquisition)\b', msg_lower): return True - if re.search( - r"\b(analy[sz]e|look at|check|inspect|review).*(image|volume|embryo)", - msg_lower, - ): + if re.search(r'\b(analy[sz]e|look at|check|inspect|review).*(image|volume|embryo)', msg_lower): return True - if re.search(r"\b(all|every|each)\s+(embryo|sample)", msg_lower): + if re.search(r'\b(all|every|each)\s+(embryo|sample)', msg_lower): return True - if re.search( - r"\b(first|then|after|next|finally)\b.*\b(first|then|after|next|finally)\b", - msg_lower, - ): + if re.search(r'\b(first|then|after|next|finally)\b.*\b(first|then|after|next|finally)\b', msg_lower): return True - if re.search(r"\b(why|problem|issue|error|wrong|fail|debug|troubleshoot)", msg_lower): + if re.search(r'\b(why|problem|issue|error|wrong|fail|debug|troubleshoot)', msg_lower): return True return False # ===== Non-Streaming API Call ===== - async def call_claude( - self, user_message: str, system_prompt, tools, mode: str, auto_save_fn - ) -> str: + async def _create_with_refusal_fallback(self, api_kwargs): + """messages.create with main-tier resilience: if the model rejects the + request with a 400 (e.g. Fable 5 under <30-day org data retention, or + unavailable) OR declines it (stop_reason="refusal", empty content), retry + the SAME request once on the fallback model (Opus 4.8) — so gently keeps + working whether or not Fable 5 is currently serviceable. The moment the + org retention is fixed, Fable 5 serves with no code change.""" + from anthropic import BadRequestError + fb = settings.models.refusal_fallback + model = api_kwargs.get("model") + try: + response = await self._call_api_with_retry(self.claude.messages.create, **api_kwargs) + except BadRequestError: + if not fb or fb == model: + raise + logger.warning("Model %s rejected the request (400); falling back to %s", model, fb) + return await self._call_api_with_retry(self.claude.messages.create, **{**api_kwargs, "model": fb}) + if response.stop_reason == "refusal" and fb and fb != model: + logger.warning("Model %s declined the turn; retrying on %s", model, fb) + response = await self._call_api_with_retry( + self.claude.messages.create, **{**api_kwargs, "model": fb} + ) + return response + + async def call_claude(self, user_message: str, system_prompt, tools, + mode: str, auto_save_fn) -> str: """ Call Claude API with full context and tool access (non-streaming). @@ -210,8 +221,8 @@ async def call_claude( interaction = self.interaction_logger.start_interaction( user_prompt=user_message, system_state={ - "acquisition_status": "unknown", - }, + 'acquisition_status': 'unknown', + } ) # Snapshot inputs for decision capture BEFORE the tool loop starts @@ -222,15 +233,13 @@ async def call_claude( if self.decision_log is not None: try: from gently.eval import prompt_hash as _prompt_hash - decision_prompt_hash = _prompt_hash( - system_prompt, - list(self.conversation_history), + system_prompt, list(self.conversation_history), ) except Exception: logger.exception("Failed to compute decision prompt_hash") - tool_calls_collected: list[dict[str, Any]] = [] + tool_calls_collected: List[Dict[str, Any]] = [] assistant_message = "" error_occurred = None @@ -243,38 +252,51 @@ async def call_claude( "max_tokens": 16000 if use_thinking else 4096, } if use_thinking: - budget = 30000 if mode == "plan" else 10000 - api_kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget} + # Fable 5 / Opus 4.8 reject thinking budget_tokens (400) — thinking + # is adaptive; control depth via effort instead of a token budget. + api_kwargs["output_config"] = {"effort": "high" if mode == "plan" else "medium"} - response = await self._call_api_with_retry(self.claude.messages.create, **api_kwargs) + response = await self._create_with_refusal_fallback(api_kwargs) self._track_token_usage(response) _extend_tool_calls(tool_calls_collected, response.content) # Process tool calls while response.stop_reason == "tool_use": - tool_results = await self._execute_tools_with_logging(response.content, interaction) + tool_results = await self._execute_tools_with_logging( + response.content, interaction + ) - self.conversation_history.append({"role": "assistant", "content": response.content}) - self.conversation_history.append({"role": "user", "content": tool_results}) + self.conversation_history.append({ + "role": "assistant", + "content": response.content + }) + self.conversation_history.append({ + "role": "user", + "content": tool_results + }) api_kwargs["messages"] = self.conversation_history - response = await self._call_api_with_retry( - self.claude.messages.create, **api_kwargs - ) + response = await self._create_with_refusal_fallback(api_kwargs) self._track_token_usage(response) _extend_tool_calls(tool_calls_collected, response.content) - # Extract text response - assistant_message = "" - for block in response.content: - if hasattr(block, "text"): - assistant_message += block.text + # Extract text response. Fable 5 may refuse (stop_reason="refusal") + # with empty content — surface it instead of returning blank. + if response.stop_reason == "refusal": + assistant_message = "(The request was declined by the model's safety system. Try rephrasing.)" + else: + assistant_message = "" + for block in response.content: + if hasattr(block, 'text'): + assistant_message += block.text - self.conversation_history.append({"role": "assistant", "content": response.content}) + self.conversation_history.append({ + "role": "assistant", + "content": response.content + }) except Exception as e: import traceback - error_occurred = str(e) error_tb = traceback.format_exc() assistant_message = f"Error: {error_occurred}" @@ -321,11 +343,11 @@ def _write_production_decision( self, *, user_message: str, - tool_calls: list[dict[str, Any]], + tool_calls: List[Dict[str, Any]], response_text: str, duration_ms: float, - prompt_hash_value: str | None, - error: str | None, + prompt_hash_value: Optional[str], + error: Optional[str], ) -> None: """Persist one production Decision row (best-effort). @@ -337,28 +359,24 @@ def _write_production_decision( return try: from datetime import datetime - from gently.eval import Decision, DecisionTrigger - - self.decision_log.append( - Decision( - timestamp=datetime.now(), - agent="production", - trigger=DecisionTrigger.USER_MESSAGE, - trigger_detail=(user_message or "")[:200], - tool_calls=tool_calls, - response_text=response_text, - prompt_hash=prompt_hash_value, - duration_ms=duration_ms, - error=error, - ) - ) + self.decision_log.append(Decision( + timestamp=datetime.now(), + agent="production", + trigger=DecisionTrigger.USER_MESSAGE, + trigger_detail=(user_message or "")[:200], + tool_calls=tool_calls, + response_text=response_text, + prompt_hash=prompt_hash_value, + duration_ms=duration_ms, + error=error, + )) except Exception: logger.exception("Failed to write production Decision") # ===== Dry-Run Tool Call (Benchmarking) ===== - async def get_tool_call(self, user_message: str, system_prompt, tools) -> dict | None: + async def get_tool_call(self, user_message: str, system_prompt, tools) -> Optional[Dict]: """ Get what tool Claude would call without executing it (dry-run mode). @@ -381,7 +399,10 @@ async def get_tool_call(self, user_message: str, system_prompt, tools) -> dict | start_time = time.time() messages = self.conversation_history.copy() - messages.append({"role": "user", "content": user_message}) + messages.append({ + "role": "user", + "content": user_message + }) try: api_kwargs = { @@ -392,13 +413,19 @@ async def get_tool_call(self, user_message: str, system_prompt, tools) -> dict | "max_tokens": 4096, } - response = await self._call_api_with_retry(self.claude.messages.create, **api_kwargs) + response = await self._call_api_with_retry( + self.claude.messages.create, + **api_kwargs + ) latency_ms = (time.time() - start_time) * 1000 - input_tokens = getattr(response.usage, "input_tokens", 0) - output_tokens = getattr(response.usage, "output_tokens", 0) + input_tokens = getattr(response.usage, 'input_tokens', 0) + output_tokens = getattr(response.usage, 'output_tokens', 0) + # A refusal returns empty content — treat as "no tool call". + if response.stop_reason == "refusal" or not response.content: + return None for block in response.content: if block.type == "tool_use": return { @@ -417,7 +444,7 @@ async def get_tool_call(self, user_message: str, system_prompt, tools) -> dict | # ===== Tool Execution ===== - async def _execute_tools_with_logging(self, content_blocks, interaction) -> list[dict]: + async def _execute_tools_with_logging(self, content_blocks, interaction) -> List[Dict]: """ Execute Claude's tool calls with interaction logging. @@ -449,10 +476,7 @@ async def _execute_tools_with_logging(self, content_blocks, interaction) -> list if self.choice_handler and isinstance(result, str): try: choice_data = json.loads(result) - if ( - isinstance(choice_data, dict) - and choice_data.get("_type") == CHOICE_RESPONSE_TYPE - ): + if isinstance(choice_data, dict) and choice_data.get("_type") == CHOICE_RESPONSE_TYPE: user_selection = await self.choice_handler(choice_data) result = user_selection except (json.JSONDecodeError, TypeError): @@ -476,20 +500,19 @@ async def _execute_tools_with_logging(self, content_blocks, interaction) -> list error_message=error_message, ) - results.append( - { - "type": "tool_result", - "tool_use_id": block.id, - "content": result, - "is_error": is_error, - } - ) + results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": result, + "is_error": is_error, + }) return results # ===== Streaming API Call ===== - async def call_claude_stream(self, system_prompt, tools, tool_label_fn, auto_save_fn): + async def call_claude_stream(self, system_prompt, tools, + tool_label_fn, auto_save_fn): """ Call Claude API with streaming enabled. @@ -509,24 +532,31 @@ async def call_claude_stream(self, system_prompt, tools, tool_label_fn, auto_sav dict Chunks as they arrive from Claude """ - from anthropic import APIStatusError - - def stream_and_collect(): - events = [] - final_message = None - - with self.claude.messages.stream( - model=self.model, - system=system_prompt, - messages=self.conversation_history, - tools=tools, - max_tokens=4096, - ) as stream: - for event in stream: - events.append(event) - final_message = stream.get_final_message() - - return events, final_message + from anthropic import APIStatusError, BadRequestError + + def stream_and_collect(model): + def _run(m): + events = [] + with self.claude.messages.stream( + model=m, + system=system_prompt, + messages=self.conversation_history, + tools=tools, + max_tokens=4096 + ) as stream: + for event in stream: + events.append(event) + return events, stream.get_final_message() + try: + return _run(model) + except BadRequestError: + # Fable 5 under <30-day org data retention (or unavailable) rejects + # with a 400 — fall back to Opus 4.8 so the turn still streams. + fb = settings.models.refusal_fallback + if not fb or fb == model: + raise + logger.warning("Stream model %s rejected the request (400); falling back to %s", model, fb) + return _run(fb) # Run streaming in thread with retry logic max_retries = 3 @@ -534,61 +564,66 @@ def stream_and_collect(): for attempt in range(max_retries): try: - events, final_message = await asyncio.to_thread(stream_and_collect) + events, final_message = await asyncio.to_thread(stream_and_collect, self.model) self._track_token_usage(final_message) break except APIStatusError as e: - error_type = getattr(e, "body", {}) + error_type = getattr(e, 'body', {}) if isinstance(error_type, dict): - error_type = error_type.get("error", {}).get("type", "") + error_type = error_type.get('error', {}).get('type', '') - if ( - error_type in ("overloaded_error", "rate_limit_error") - or "overloaded" in str(e).lower() - ): + if error_type in ('overloaded_error', 'rate_limit_error') or 'overloaded' in str(e).lower(): if attempt < max_retries - 1: - wait_time = retry_delay * (2**attempt) - logger.warning( - f"API overloaded, retrying in {wait_time:.1f}s" - f" (attempt {attempt + 1}/{max_retries})" - ) - yield { - "type": "text", - "text": f"\n*[API busy, retrying in {wait_time:.0f}s...]*\n", - } + wait_time = retry_delay * (2 ** attempt) + logger.warning(f"API overloaded, retrying in {wait_time:.1f}s (attempt {attempt + 1}/{max_retries})") + yield {'type': 'text', 'text': f"\n*[API busy, retrying in {wait_time:.0f}s...]*\n"} await asyncio.sleep(wait_time) continue raise else: raise RuntimeError("API overloaded after multiple retries") - # Diagnostic: log stop_reason and tool block counts + # Refusal → retry the whole streamed turn on the fallback model (Opus 4.8) + # before giving up. The original partial output is discarded (we re-collect + # and only yield the fallback's events below). + fb = settings.models.refusal_fallback + if final_message.stop_reason == "refusal" and fb and fb != self.model: + logger.warning("Model %s declined the streamed turn; retrying on %s", self.model, fb) + events, final_message = await asyncio.to_thread(stream_and_collect, fb) + self._track_token_usage(final_message) + + # Last resort: if even the fallback declined, surface it and stop — + # discard any partial, don't iterate empty content or process tools. + if final_message.stop_reason == "refusal": + logger.warning("Claude declined the request (model=%s)", self.model) + yield {'type': 'text', 'text': "(The request was declined by the model's safety system. Try rephrasing.)"} + return + + # Diagnostic: per-response counts. DEBUG, not WARNING — stop_reason=tool_use + # with matching tool blocks is normal; the genuine anomaly is the + # logger.error below (tool blocks present but stop_reason != tool_use). tool_block_count = sum( - 1 for b in final_message.content if hasattr(b, "type") and b.type == "tool_use" + 1 for b in final_message.content + if hasattr(b, 'type') and b.type == 'tool_use' ) - logger.warning( - "Claude response: stop_reason=%s, content_blocks=%d, tool_use_blocks=%d," - " tools_passed=%d, model=%s", - final_message.stop_reason, - len(final_message.content), - tool_block_count, - len(tools), - self.model, + logger.debug( + "Claude response: stop_reason=%s, content_blocks=%d, tool_use_blocks=%d, tools_passed=%d, model=%s", + final_message.stop_reason, len(final_message.content), + tool_block_count, len(tools), self.model, ) if tool_block_count > 0 and final_message.stop_reason != "tool_use": logger.error( "BUG: Claude returned %d tool_use blocks but stop_reason=%s (expected 'tool_use')", - tool_block_count, - final_message.stop_reason, + tool_block_count, final_message.stop_reason, ) # Process events and yield text full_text = [] for event in events: if event.type == "content_block_delta": - if hasattr(event.delta, "text"): + if hasattr(event.delta, 'text'): full_text.append(event.delta.text) - yield {"type": "text", "text": event.delta.text} + yield {'type': 'text', 'text': event.delta.text} # Detect fake XML tool calls in text (Claude writing tool_use as text) joined_text = "".join(full_text) @@ -596,8 +631,7 @@ def stream_and_collect(): logger.error( "DETECTED: Claude wrote XML tool tags as plain text instead of " "using API tool_use mechanism. stop_reason=%s, text_preview=%.200s", - final_message.stop_reason, - joined_text[:200], + final_message.stop_reason, joined_text[:200], ) response_content = final_message.content @@ -611,14 +645,14 @@ def stream_and_collect(): tool_results = [] for block in response_content: - if hasattr(block, "type") and block.type == "tool_use": + if hasattr(block, 'type') and block.type == "tool_use": start_time = time.time() yield { - "type": "tool_start", - "tool_name": block.name, - "tool_input": block.input, - "tool_label": tool_label_fn(block.name, block.input), + 'type': 'tool_start', + 'tool_name': block.name, + 'tool_input': block.input, + 'tool_label': tool_label_fn(block.name, block.input), } is_error_flag = False @@ -628,44 +662,32 @@ def stream_and_collect(): if isinstance(tool_result, str): try: - from gently.app.tools.interaction_tools import ( - CHOICE_RESPONSE_TYPE, - ) - + from gently.app.tools.interaction_tools import CHOICE_RESPONSE_TYPE choice_data = json.loads(tool_result) - if ( - isinstance(choice_data, dict) - and choice_data.get("_type") == CHOICE_RESPONSE_TYPE - ): + if isinstance(choice_data, dict) and choice_data.get("_type") == CHOICE_RESPONSE_TYPE: user_selection = yield { - "type": "choice_request", - "choice_data": choice_data, + 'type': 'choice_request', + 'choice_data': choice_data } tool_result = user_selection or "cancelled" except (json.JSONDecodeError, TypeError): pass - result_text = ( - tool_result if isinstance(tool_result, str) else str(tool_result) - ) - tool_results.append( - { - "type": "tool_result", - "tool_use_id": block.id, - "content": tool_result, - } - ) + result_text = tool_result if isinstance(tool_result, str) else str(tool_result) + tool_results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": tool_result + }) except Exception as e: is_error_flag = True result_text = f"Error: {str(e)}" - tool_results.append( - { - "type": "tool_result", - "tool_use_id": block.id, - "content": result_text, - "is_error": True, - } - ) + tool_results.append({ + "type": "tool_result", + "tool_use_id": block.id, + "content": result_text, + "is_error": True + }) # First non-empty line of the result, trimmed — gives the chat # UI a one-line summary so the operator can see what a tool did @@ -678,16 +700,22 @@ def stream_and_collect(): result_summary = result_summary[:139] + "…" yield { - "type": "tool_call", - "tool_name": block.name, - "tool_input": block.input, - "duration": time.time() - start_time, - "result_summary": result_summary, - "is_error": is_error_flag, + 'type': 'tool_call', + 'tool_name': block.name, + 'tool_input': block.input, + 'duration': time.time() - start_time, + 'result_summary': result_summary, + 'is_error': is_error_flag, } - self.conversation_history.append({"role": "assistant", "content": response_content}) - self.conversation_history.append({"role": "user", "content": tool_results}) + self.conversation_history.append({ + "role": "assistant", + "content": response_content + }) + self.conversation_history.append({ + "role": "user", + "content": tool_results + }) auto_save_fn() @@ -709,12 +737,15 @@ def stream_and_collect(): else: # No tool calls - add final message to history - self.conversation_history.append({"role": "assistant", "content": response_content}) + self.conversation_history.append({ + "role": "assistant", + "content": response_content + }) auto_save_fn() # ===== Tool Label ===== - def tool_label(self, tool_name: str, tool_input: dict) -> str: + def tool_label(self, tool_name: str, tool_input: Dict) -> str: """Build a human-readable label for a tool call. Used in tool_start chunks so the TUI shows biologist-friendly @@ -728,13 +759,8 @@ def tool_label(self, tool_name: str, tool_input: dict) -> str: campaign = self.context_store.get_campaign(campaign_id) if campaign: campaign_label = campaign.shorthand or campaign.description - if tool_name in ( - "propose_plan", - "get_plan_status", - "export_plan", - "snapshot_plan", - "list_plan_versions", - ): + if tool_name in ("propose_plan", "get_plan_status", "export_plan", + "snapshot_plan", "list_plan_versions"): return campaign_label if tool_name == "create_campaign" and inp.get("parent_id"): return f"phase under {campaign_label}" @@ -752,12 +778,8 @@ def tool_label(self, tool_name: str, tool_input: dict) -> str: # Item reference tools item_ref = inp.get("item_ref") or inp.get("ref") or inp.get("item_id") - if item_ref and tool_name in ( - "get_plan_item", - "update_plan_item", - "delete_plan_item", - "move_plan_item", - ): + if item_ref and tool_name in ("get_plan_item", "update_plan_item", + "delete_plan_item", "move_plan_item"): if self.context_store: item = self.context_store.resolve_plan_item(str(item_ref), campaign_id=campaign_id) if item: @@ -784,9 +806,8 @@ def tool_label(self, tool_name: str, tool_input: dict) -> str: return "" - async def _execute_single_tool( - self, tool_name: str, tool_input: dict, context: dict | None = None - ) -> str: + async def _execute_single_tool(self, tool_name: str, tool_input: Dict, + context: Optional[Dict] = None) -> str: """Execute a single tool call using the tool registry. Parameters @@ -806,13 +827,13 @@ async def _execute_single_tool( def _track_token_usage(self, response): """Track token usage from API response, including cache metrics.""" - if hasattr(response, "usage"): + if hasattr(response, 'usage'): usage = response.usage self.total_input_tokens += usage.input_tokens self.total_output_tokens += usage.output_tokens self.api_call_count += 1 - self.cache_creation_tokens += getattr(usage, "cache_creation_input_tokens", 0) - self.cache_read_tokens += getattr(usage, "cache_read_input_tokens", 0) + self.cache_creation_tokens += getattr(usage, 'cache_creation_input_tokens', 0) + self.cache_read_tokens += getattr(usage, 'cache_read_input_tokens', 0) @property def current_context_tokens(self) -> int: @@ -823,14 +844,14 @@ def current_context_tokens(self) -> int: conv_chars = 0 for msg in self.conversation_history: - content = msg.get("content", "") + content = msg.get('content', '') if isinstance(content, str): conv_chars += len(content) elif isinstance(content, list): for block in content: if isinstance(block, dict): - conv_chars += len(str(block.get("text", ""))) - elif hasattr(block, "text"): + conv_chars += len(str(block.get('text', ''))) + elif hasattr(block, 'text'): conv_chars += len(str(block.text)) else: conv_chars += len(str(block)) @@ -896,22 +917,19 @@ async def _call_api_with_retry(self, api_func, *args, max_retries=3, **kwargs): try: return await asyncio.to_thread(api_func, *args, **kwargs) except APIStatusError as e: - error_type = getattr(e, "body", {}) + error_type = getattr(e, 'body', {}) if isinstance(error_type, dict): - error_type = error_type.get("error", {}).get("type", "") + error_type = error_type.get('error', {}).get('type', '') is_retryable = ( - error_type in ("overloaded_error", "rate_limit_error") - or "overloaded" in str(e).lower() - or "rate_limit" in str(e).lower() + error_type in ('overloaded_error', 'rate_limit_error') or + 'overloaded' in str(e).lower() or + 'rate_limit' in str(e).lower() ) if is_retryable and attempt < max_retries - 1: - wait_time = retry_delay * (2**attempt) - logger.warning( - f"API error ({error_type}), retrying in {wait_time:.1f}s" - f" (attempt {attempt + 1}/{max_retries})" - ) + wait_time = retry_delay * (2 ** attempt) + logger.warning(f"API error ({error_type}), retrying in {wait_time:.1f}s (attempt {attempt + 1}/{max_retries})") await asyncio.sleep(wait_time) continue diff --git a/gently/settings.py b/gently/settings.py index d197bc0d..33254621 100644 --- a/gently/settings.py +++ b/gently/settings.py @@ -4,7 +4,6 @@ All configurable values live here. Override via environment variables prefixed with GENTLY_ (e.g., GENTLY_VIZ_PORT=9090). """ - import os from dataclasses import dataclass, field from pathlib import Path @@ -30,7 +29,6 @@ def _env(key: str, default): @dataclass(frozen=True) class NetworkSettings: """Ports, hosts, and bind addresses.""" - viz_port: int = field(default_factory=lambda: _env("VIZ_PORT", 8080)) viz_host: str = field(default_factory=lambda: _env("VIZ_HOST", "0.0.0.0")) device_port: int = field(default_factory=lambda: _env("DEVICE_PORT", 60610)) @@ -42,10 +40,7 @@ class NetworkSettings: @dataclass(frozen=True) class MeshSettings: """Mesh networking parameters.""" - - broadcast_interval_s: float = field( - default_factory=lambda: _env("MESH_BROADCAST_INTERVAL", 5.0) - ) + broadcast_interval_s: float = field(default_factory=lambda: _env("MESH_BROADCAST_INTERVAL", 5.0)) replay_window_s: float = field(default_factory=lambda: _env("MESH_REPLAY_WINDOW", 30.0)) reaper_interval_s: float = field(default_factory=lambda: _env("MESH_REAPER_INTERVAL", 10.0)) status_refresh_s: float = field(default_factory=lambda: _env("MESH_STATUS_REFRESH", 30.0)) @@ -56,20 +51,40 @@ class MeshSettings: @dataclass(frozen=True) class ModelSettings: - """Claude model identifiers.""" - - main: str = field(default_factory=lambda: _env("MODEL_MAIN", "claude-opus-4-6")) - perception: str = field( - default_factory=lambda: _env("MODEL_PERCEPTION", "claude-opus-4-5-20251101") - ) - fast: str = field(default_factory=lambda: _env("MODEL_FAST", "claude-haiku-4-5-20251001")) - medium: str = field(default_factory=lambda: _env("MODEL_MEDIUM", "claude-sonnet-4-5-20250929")) + """Claude model identifiers — the single source of truth for every tier. + + Tiers are split by role; capability-first per the latest models: + - main: Claude Fable 5 ($10/$50 per MTok). Per-user-turn reasoning + + tool orchestration (plan mode) and the dopaminergic classifier + stage. Always-on thinking (no thinking budget — control depth + via output_config.effort), ~30%-heavier tokenizer, may refuse + (stop_reason="refusal", empty content); needs ≥30-day org + data retention. + - perception: Opus 4.8 (high-res vision, $5/$25). Highest-frequency tier + (per timepoint); Opus-tier vision for perception accuracy. + - medium: Opus 4.8. Onboarding / wizard summaries. + - fast: Sonnet 4.6 ($3/$15). The cheaper/faster tier — drives the + verifier's parallel ensemble (ensemble_size calls per + verification) and blank-image / summary checks. + + API note: Fable 5 and Opus 4.8 reject thinking budget_tokens and sampling + params (temperature/top_p/top_k) — adaptive thinking only, depth via effort. + Sonnet 4.6 supports adaptive thinking. No assistant prefills anywhere (4.6+ + family rejects them). + """ + main: str = field(default_factory=lambda: _env("MODEL_MAIN", "claude-fable-5")) + perception: str = field(default_factory=lambda: _env("MODEL_PERCEPTION", "claude-opus-4-8")) + fast: str = field(default_factory=lambda: _env("MODEL_FAST", "claude-sonnet-4-6")) + medium: str = field(default_factory=lambda: _env("MODEL_MEDIUM", "claude-opus-4-8")) + # When the main tier (Fable 5) declines a turn (stop_reason="refusal"), the + # main-tier calls transparently retry it on this model instead of surfacing + # the refusal. Empty disables the fallback. + refusal_fallback: str = field(default_factory=lambda: _env("MODEL_REFUSAL_FALLBACK", "claude-opus-4-8")) @dataclass(frozen=True) class StorageSettings: """File paths for data storage.""" - base_path: Path = field(default_factory=lambda: _env("STORAGE_PATH", Path("D:/Gently3"))) @property @@ -84,7 +99,6 @@ def traces_dir(self) -> Path: @dataclass(frozen=True) class TimeoutSettings: """Timeout values in seconds.""" - plan_execution: int = field(default_factory=lambda: _env("TIMEOUT_PLAN", 300)) rpc_call: int = field(default_factory=lambda: _env("TIMEOUT_RPC", 60)) volume_acquisition: int = field(default_factory=lambda: _env("TIMEOUT_VOLUME", 15)) @@ -94,7 +108,6 @@ class TimeoutSettings: @dataclass(frozen=True) class ApiSettings: """External API configuration.""" - ncbi_tool: str = field(default_factory=lambda: _env("NCBI_TOOL", "gently")) ncbi_email: str = field(default_factory=lambda: _env("NCBI_EMAIL", "pskeshu@gmail.com")) @@ -102,7 +115,6 @@ class ApiSettings: @dataclass(frozen=True) class MlSettings: """Machine learning training parameters.""" - model_cache_dir: Path = field(default_factory=lambda: _env("ML_MODEL_CACHE", Path("models"))) default_batch_size: int = field(default_factory=lambda: _env("ML_BATCH_SIZE", 32)) default_epochs: int = field(default_factory=lambda: _env("ML_EPOCHS", 50)) @@ -112,12 +124,9 @@ class MlSettings: @dataclass(frozen=True) class TransferSettings: """Bulk transfer protocol parameters.""" - transfer_port: int = field(default_factory=lambda: _env("TRANSFER_PORT", 19548)) chunk_size: int = field(default_factory=lambda: _env("TRANSFER_CHUNK_SIZE", 1048576)) # 1MB - max_concurrent_transfers: int = field( - default_factory=lambda: _env("TRANSFER_MAX_CONCURRENT", 4) - ) + max_concurrent_transfers: int = field(default_factory=lambda: _env("TRANSFER_MAX_CONCURRENT", 4)) @dataclass(frozen=True) @@ -133,7 +142,6 @@ class UISettings: @dataclass(frozen=True) class Settings: """Top-level settings container.""" - network: NetworkSettings = field(default_factory=NetworkSettings) mesh: MeshSettings = field(default_factory=MeshSettings) models: ModelSettings = field(default_factory=ModelSettings) diff --git a/gently/ui/web/connection_manager.py b/gently/ui/web/connection_manager.py index 11cc3fe4..00aeaef2 100644 --- a/gently/ui/web/connection_manager.py +++ b/gently/ui/web/connection_manager.py @@ -158,7 +158,10 @@ async def broadcast(self, message: dict): try: await connection.send_text(message_json) except Exception as e: - logger.warning(f"Failed to send to websocket: {e}") + # Expected when a client disconnects/reloads mid-broadcast + # (send after websocket.close). The connection is dropped + # below, so this is debug-level, not a warning. + logger.debug("Dropping a websocket that errored on send (client likely gone): %s", e) disconnected.append(connection) # Remove disconnected clients diff --git a/gently/ui/web/routes/chat.py b/gently/ui/web/routes/chat.py index 8c66dd45..1fffcb7f 100644 --- a/gently/ui/web/routes/chat.py +++ b/gently/ui/web/routes/chat.py @@ -20,10 +20,12 @@ from pydantic import BaseModel from gently.ui.web.auth import require_control +from gently.settings import settings logger = logging.getLogger(__name__) -CHAT_MODEL = "claude-opus-4-7" +# Per-timepoint VLM chat → perception tier (Opus 4.8); centralized, not hardcoded. +CHAT_MODEL = settings.models.perception SYSTEM_PROMPT = ( "You are helping a biologist interpret a microscopy perception " "assessment of a C. elegans embryo at a specific timepoint. You can " From 0457975b0fa9434cb3a9f63ecc5663083b648c4d Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Fri, 12 Jun 2026 19:52:35 +0530 Subject: [PATCH 20/34] Add UX v2 landing screenshot for the PR Co-Authored-By: Claude Fable 5 --- screenshots/ux-v2-landing.png | Bin 0 -> 299550 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshots/ux-v2-landing.png diff --git a/screenshots/ux-v2-landing.png b/screenshots/ux-v2-landing.png new file mode 100644 index 0000000000000000000000000000000000000000..62502ce3dfe59b147cb61f8aa119e4e87b907228 GIT binary patch literal 299550 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

E#T1)1H znJ(RChX+^Z<>364B|G4xGa6I(&5sU@r4LiGBYv4ayW86rtWxgK4e|+zxZCp(Z`Lhu zm1@$`OBr?{BUin(T$=xvh}xL}U_x$z&c_J6^c%HA@zf{%PYYlPtL1V$|5L-8)SB_p z#e8cYzN(bAkE($HPb)ZA&IpCR;u{Jv znHuMqM}p9ua^yrCG1_AbjkF7-U@5gj%7z1GVafU)7f8j1C&V+TX5d0b-1PRK*ox4} z0_w3d6K>ldw6J90A$RJ=0Ds`uHG#?UR9IIU$zrP!QoF~hZm$Qc<>2(;Z~Ri1CaWe4 z-NF7VIumwfO)hzl2C^1d!)V6;;xpn3uYOdPUA>lOPc4XEbpqZ9bS+6OUEIB4mbFNx zd9AlBG3ueo2off_ZoS3Fk1$vMhPb`_))1om_3-4gflWT(8Qw6C>-w}mnikq_t4@bfy#&@*|9H<4PSSyvS?$~l)FCj8Pik#qaNV{k90 z<~+Tba3~X}xaG8=m;3sx~csJczXQLnKG-t0W8qraG!+}sAXrwSeP z--{2p?Sag>V5^3&w;CwfbGlz2 z&r3?fpk<_O!B^-xwEG{FfUkC3CN7^lAuP2=;}N3H2P49eBYJYqpDP7YOd8A^-Nq}> z3U9~zU1BQ!mOuKm_URQ^0~UTuujR}yZgU23=BJm;$xv)`pYB!jY`f^-47`l3bT;OD zkYq5ODrf?rM+e%t>eSHl@)@bI*MAN&+uQ-6i4=#&u=wr{;z+g@h#2Qca^WiQxHr(3x|bWsE~0tbJy1)iZnI4|M8QC0CzLP*u2YxCvmZ6KC^ zO_zemi8QFXmD}O==;A^s4+oj`Ft1Rt~{nq39uf6u~i?J z1*9{y+FcxRZnm1tXQB*lE{4ly*C0GcC=69L;$1P{Ir~f%RoHRueMgXtw=4S@=K2^I z$%yH2>}sYhoQK7O+p-+A3J^l%CpgX=-UPSvdazSRv{TS!YJD*gPg1i6!Q$HAGMZ@N2ky4Fn_`)r)AP z@M5Medk<6L4)dYdnD4jQ&vxU$VNuNWmHsVES*8!}dS7y7&Qy-u_^Y^eq}+WqPOXy4 zb=xt|mu7|p;!>kD5Vjc=N7cBD^Wt?M+ap&b+zcWVbOl*mR?Hy>&InxOZ zMEyZn_ChWvvO$87P7M5wYsl`hP)DxEz3a?TesjylSy%1<3MB9c=f3M6tashCrSiN(r<7hY zvpb%$+^<4&p-=olwp9lBq*S^tR8>(Qys*-_U)01PPQnbHW$1BP5rUeuJx-TqH@p#j z^N!--7vh2_?R36vHZ1GCMBf8;4W}}evSxOCZjuzR_o^OC-!C!^S}f*1>zs`y6e=j6 zcJRENP>8&RBr~PnKQrlBz=>9BRIfr)q+x|jOPDCYR+ih@Iz9PwuLShR^S-cVPAUe% z;-Q%B5t~1Z2EbB#9$607S|YMwPU0**?ft8)+g;l<5#htfapI`akDS=}_#@sS*=u z;94^Sa=_#kZ>_KMXqmCPjv$Uk@;O#$Ub;Z2A5_I}6mlkzItJd79+>zvNAz{aVQ#|{ zf9)8aCH=+|loFh>yBmyAX(?L+6qxa%)Y$7N}O7e4%BQi4!U!rk)g4cL$ z)%A!`tG|hu??H`@bVXw0>9R3D6y>*PyQ2FPjmFvrd{tEa^*%oH{tKsTA3R-Rh2!cwcc1g2kaHkLr&(~= z{ZPR@55u4vG9|*EMnmocz5Ed9Hxpx$x#}#Jg61;!EBzsZ1%5tQsV`codXol;kT-=E zSY-gZzt0LpRke`usY;&B%6-5~dSPq|?|sdU7oSDfVQyh2smqKKZ0FZ9$NN24(kRWZ za_I9=+?|{V&0bYp`bVG9P}EXDetg3(7X(xkGL)hIWHUs{LnH@9xKFTz=T!w?r^CN& z{O099&i(GMIX~C011cU47ct8T;P`r?eQ)Lq^GDtmres;Tw*q2>8I96Ex-z}ADoAw)jVr*YSyCzcmD`uhdZ(Nx70*VFL z?VEC_HFFx7HChhkF`py)0P$=F&skcbVYkR=C(RJ(Q0qhP3cI{=%JF`zsnd}cb4moA z6R8iCr}?}QS_U$nhGykm0(0N>gX4Op;N2VngV@N zp98x%8ZQT9UBV$)=4^o(+~e8H(oej^>f?DCoPPmUpI$S5(IGnCdz$<;OcCdpj03}K z9z2*k!JwTpd|)+PZMoKrz&gAYDT zJNz)$nGV+N;S0gP09)9`?A{akztMe@0DYs9k4MWjcsNun`C_Pzyg9|>9C*K&C3e`S z@h1O7w)gjnd;qlUP-NwhZ%f2l_Eg5GWw)LMn_i0j64ZCj^;`xEUBYUX=G3c`sTV_4 z>}kQv#Uu+bG^0JRRIt^xkxUn{-@b-SxW6sezFk=ByFvE~tdR_&*~JMr4={Wie(!py zV0xdjhurl5k6LQ9x-#Z7370KPYeG~!?81tpOTZdJ*ILeq&bYU;`GxLVU{U8&sKrU@ zJg5gCWd1j*(Q=~DJ9m}h=S`cfi`o6MjOQ1OFffG&p)`f9pf{+#IL>yb51y6O!y>NK zn#;J(e&=)a-FRwO@4PJzPum4ipzRQ|8S-4nWYmd~+9qo;^tpK4Y(>iNwKo>dRZ_4u5VqMyhdpB6cO32 zxgQK-Pf&;g2<(Yc#Y%AGfz1@f8p8tRHz|*zXZq&|r&K2M67YZv5?u2TO?&Q-k*;{6 zJs*Zg&mDKO5Ehpu;~jri2*uE-6fY9WhKcxmi9sjli-ua*mL5N*@^nG36l3<7pIY&QbyknnYaA+!@WkCH<4M(jm_M4}9DRAZ6 zf_jYMbol4ke11ayB|B`v9n}6-_aQ_6?gIl)!OA48xGIlj)Uh(5e}v?9utKpWwn+01 zhPtC1aK7ejf5hShtN#w;>RF=0D@mvt@2=2#Qmc~2n8!zO#01r2`#NKbm>E!^3&0b0 zVSUvaFjH6VQm3>j0g9?zpZ%*iyG;qsvZ|{G{Wh>$GL?XdcD_7RWH{vXI9BBNp?Hv7*Z;Htb%jP#Rg&X?C+w^0OLN%FjV(kau0ldmAlOdnVu31i zD2gD*X`c9YnJ~~)KGo@Pc5gr#^U_moVkO%x(%bTMpKg|X=}@2F8^ZqrDBKv7!J4`0 zqV*`@UF?j|y+9DzaxUrCbB$;V8FcCZ1SFpsKjT}cR6lX4ID&>Q&ix} zXT^T8p>{8Hu1{gntDi){>_5R6RB>=vxe}@#ISgf#7&R%~5DwYRx z=&GNuWV3o)t>f`bC#}pIw@EfcZ7huc(q7o+Zm=J^E52&YbzV*v)SKAP_h&5bn89CE zbg}ZXdd~R>fWDN|TUzZrdTqbgB9A7B`>&)~ct3^qR5LyVES?^cYX52$NaN6S?Y~no z1w3z<$~Q_mjyL4;MG|t~j+lvmPjNVzO#rXEJ3+<258uePLD|85Z}3Pv_uW`Kz=4en zcQ?AtJ9co;#(`EG10zavfK79DVEHWF!FYjwVO4`?k}o8E4iEs74^%K)p*J$u}-JzS-X5zl=-LheV;euYQK9m-e}2YG_heI z3~Zh<8H+&KFyFiNxDa-4Ik`A7Ih^+*?~Zr(-vXYsR!y}=3rss`W;_;(!xX1q_ycOi zH@pyOu{s!$ZY7GO?1`#D{R^T1Hrsc zy?_Q2_AurBW~HE=R%Ab^nQ;UU-l`&E3w3WhdP7H+P8ZGwTpqZV()t8bkB*HSh1srv zboPa9a<>0Gl^T7h+C4Xb?7FHl*G`;SyK#4tL(vy1y$f7#14CEzF>JpUgK(tBn9n`4 z>7(nP2A81OBh#lc}?0NJVDc3D!!g;Npj{J6TdpQk=F;nY%A1Xt>3VLHU$8yzLF0y>&kR&|F_=eWgz8lC4fl59^5+{g z_Y_;gUha1hzF_}%5yV~2CP_g@ z4H7D9X=|Npr=3e$dM|pd$Sf>@Ht6>(kh|n6ea*(7J zLqh)+pe^H;(y8t+<#};%@>xwS+m06&j?87Qs}}l!TVWxM#zMpEFt>v6z-Y#T|0>^Lp{nn^|*3t4r zBcP%djq_%Y&v)wOCUg-AxeO7J4TY%|m`;9a)f-}&W%X6xTJ3y^g+-e#yr@sG3h2EU zjap75<%Vxj|AkaqEF4Jw$`seZuQfh!_l^C^3u>6 za!azyskDngVzNleL;Z}`g<@ZF^b?p^i2);ZYW)XH!edmZIR;j&)Lxq-!-z3tqo>0z;<_Y7UdfFx7&o@te3jbvidY?B8?Wm%>=0h&^=| z3urUVVA%YRLst9%VE3)A+nYTaXw7I%{jFu2-OK-`0Bu_Dt9{M7jx%8+Rs5xZo9_)x zuI2u31<`vJdyirdNQ@6nBKx}aIfQG=;7~DLe#IX5i(E98yCe{FM45)lKhv3KfNgw8 zXhl`Z3n%q1$L3hFoqjjJF^Wl3y2x>mEx8^3=*ur16#q>~!fRCYBpNvOD@oFcD&>32 z?MMFE?T%{l$9A8*X&9*re3HNp7Va>#xmZMfT5ly|Skv)+- zR0-|1@ft=gKM^JgAHOD&2_p9?`8x;U!oXN)(s+gSQ*fJ)&LvM^&@na!qZ)Y}4E_dh z-Pk5;V~H7*hkrC#I;T?-LsGpvtQ&wf7vqQIexwsuA&+pj=x!}1$21I1qt}IB9BF;E z9-eg{A{yySxe0T6Gd=(QEXHg<@OI6+E*Fj+%3AI235UDGq1f~*0BuGD9A!Y}dvjIe zO<_|Y3ngBZgK7=j*E`!@Jbky<&TBjNO2S%%=YfZ4_>+L^?(7wEnA%6NkE-9J5u$nD zc1x-SyG(Xq!_BS_ zU?Oy3fJ($SOCArN$YJm%DP-jHm`>K`E1SKWy~21nT0;9$##C0Vg7&;uq-^!~YFUc&B6ksNj8R_rEuV3z{WZ1!{$6+m@TD4TBK#{U zDXo3qt7h}LI)dULhvvCL=k-zPCg>*T+po99wZ5eh`qlhKM>;j0N20)xP~viFDLdpp zs$Tny!I&mIY;d&E9^Z(Ox_StlGF+4muxY#wvc;5x*0@J;;Z>3>Nz>wlXPr^!z|9&P z-p3#1D#w=pDNBNfEbD*%^;g)4APhRc+UJGHg*Wo@AU($f8vJsaP^oqCv{GVbsS#0M%Uo8y zby`&-C3a!%9EM~HrKPbw2?mxI>3AhJNvRv{KLtsoOmRoDYQt5W0VFAGl1?i%`O)8i zW+`dfq9@fhKA^_aZzonW7S12yT?%9i#Jl=h9o(0hYq7%r;D*XAOE*C7>T=>X(uODZN&ai2Sjm5kqlk%#kL0Qsl~BZQ=l=yS509j zqNjW9A1M+{CN#+J%sC@ROtgE%sFTnpab9uAp zzm{^c@SB$gtqy%NkcV^22h>5aYcivE*pJK}V`>_jbCrls!U+?ekK=gagq|-R!e#oo zZsXtO@mctt4o^y^;soD-H6;cNxs7Iha387o&nHzrEpI)G%t|^p+xS-Xv9<1!)?DH! z5;l=9ORRG)P6Q}C40g3El~6irHvJ@(;gVB!0$y#?CC;_};=?9#C&v(U8Rg%yS=@D4 zMORL5UJCVjtiWqWyGzwn?l?=qyfh~@a#?Ku*8%}!!EhAdDU{<(N%I&RpN$vfJWirX zMLEe-GRs|DZ&Wg=GXogeQoDe6qK+N zJ>Hci4dg9#BSS`^Yh>5XDWmtLBZdn+M}l*=NLvkep%l)%w0vH& zn;N@?tIbF+qs}pT=47{|*nqMS4I*!$DN>uGaY_C_bzO%&X@nuGSzc|i0Q=dly(1CL z?i|16i(LuYHKq64X`=$P(c=RgE!>dqerN+^eXpF!6nLKGkQw2y-E0JPFX!e8m`|^n zSD$Y#$h5>@rfYYYC1>6_kLW#1XzT_CASyaz8i;t@%t~O{hX`uA{s}+bbiI~7fdw=& zmrhv%_kE(1n-7CJuVO$z%VhJ~b-kuEou0?9YDQ+(7`# z9#|@aSLo#Nr?Lf*r1w^)CfDZdcIu+ShBJ(8X|>y&hEKs4pc>aElgZw>&~iL50U2?BKg_K{+cx zfmDh*(If2d>%W!yh^I0FStnx4_FM2WK?U(A?u_PC#W2*nzNkH`DIszC^Hf%;BN}p ziPe#cEdOmp!Y)78ldzW8*3-UvQ+Y*wZ)%Rc&A9|kh4}M?Fz%SFCkztBT#;IEI!xg~ zm~^?Qo8Rnoc87iEDR-=rf)W$I#`)snudK`8o&j?>W{E2B25&E}uN%$_!M4bX!j_li zrB$}^B;Ed%Bvw#HHs$?EeV}O0Yw$DWND~gr*kq6r+G%cUza^yB6}I{>JjP z1i~an0i)-hj41tX6#G1(B5sbU=UL%nDxMPrAAuZx%JVI#V4q98?N5!bl=K!`4Gukj zzT?gAItQ*t<#+DgoeFD5T9-hcZl3uClaum2luisf%)nV=UYajvlJ`F*CU2<#r^_ld z)H5GdX~Z2}9J;?4mQ4qoBW&^eNT#v>iB`6#irM`W%PkZTr?}1F5>n_%EsR_uO-7KK zIC2K_yFGTc<2yspms7&L^B3D>sDTklY!w;8{kG1~9s7C!BEcmT% zwYOV3VUPT6Rt3Ipx{T;>;K*Wu$b3IyExSJg6}}Qx%kz~<4HwDz5&ot+UU-wxUD^@G z`l7&EKo4|jju4+L#}X)QY44SOZE0$a7qD-VFw5)ga6I%K#f*_tI%mDa3}I-f>!^33 zOE{o}9$7L|^mDK_+)CmXIweP>0h@YF(-Vd`tizMG6xl4T3?0ac+7-|+(Wt4MwoQr< zv4}C@YvqCQ8J8CEfQ|PF2TMa3pLhhjXgx|9Oxb(X0d)Tw>mcz|&Wcp~rH8rStQCIX z8VBn;w9+?N&B^uMis2bJdAQ}+0#DmFG&T#@+b$fdz?`1~axjDfWUwyBv&2!mQSRK6<*%59p9vQi1Vybj0N zbwIb0dD1sJ^7~P`BV>&9lNdbLmu&jq?7qK5VQqInjhv2XWMciEhbbVCFyRe^QG8ZS z(KwWQI4nb3PZJV<_9U5d-Yh7VCl!S+*bH&Z8FJRI)W`gfH6D`B#VV}D5q|B)@_C&b zX}&kI@`ALn>49VH4c>vVdJHuwAmx*~HEER8Mv6{vi~D6P|Fft#JMY)gg!1DWd*A1< zT+lvGN@nSA3`7s;ax=Kg@|$Z&W}J=huQK2B!i^p9TilIqn?}#Y2WP-!UDGe&foYLv z<{X13XhscA7?E^ZfHHe;dxg0hE%xtUW^^KRu5K4vGdh}uy5SEyQfk{@KnnlL=jOe) zpA$N+y$x4ip1h$%-2*|7+W?jf)!YPI*U4d$;vsmQDc2XJu@FSy|6GSe;rE|xE8u?J zby?&6pFnAS)F`mdRT(1Xd#ST>y;>L6fgTn|4w)oCk6K^Q{JVY{`-{Kzfrtj;e+@3Y z+2JFZ*6em+tWcpff<#mFkxRYcXg;t;g3P;`Ho02#v%9By8MindZN%@;1ZDi7Sk2_**i3rMDT-@ zbaOEp3rSv{xYjFN0I}937RN}qg%G;!_0jq3RHLVf?Os| z91W}lf1Tz#pWL`^bq7ofQ4X-;XSKcrn@*$%-VVMMf~C{hl>FBbPhi?grQoG!~bp5cEcSrY=Gze zkSTL^%#|ma+Qt9&Esn*Y`;cJUOF^<J2mc}`4bv*0>H|1|xW7KY$; zqR&R7A#m0m zbOvCOZ}?H^-e**b0B`AaU3X`;?*o&eAw>6UPsILIItPHo-sjQ18$6J0O9A!Y@Zh;! zrvJ1;>Hm7wV?RFZ@wR0D`dT1`_P!?IcLsgS`=J95Z}$P^FVrO z*O}>z2K1ZgdTcASY&z}?D@@IH0(>ccVsOH{Ofea}Vo`u~H`Zk(FmiK!9+xOUi>VnN zC(!?by&}2RV&_Qn^)7zX8G@0@weS0gIE1IsjI(-^9kiU_*wTu?x>E! z8WAt~O~u*V*OM=NC7(%6-(TS2oG|4!Sy`lcjE3~wvMPukk`k{njTbk|fOXi;;OkuS z{|rp-ZhXZtg!!nCpEXse&?*KWGFv`IB2ijkKISmq~VNDrIh_#4b9hUDYnw z>w+os2*{Llh*(^$8w=&EqY`EssUOIjN^@KCQ4Ae*eKBYLKpC$+C?$zm$aAsG$&Psf zt1x=J;(uf-0-U>4D9q#*sW-pwR#no!pRcTGg=RCEm9@S{Q6-@ZSc0vfy{Wi6mw&09 zH{)T=g7v3+GQ>Vtn|#nR6wA?0oTW6^ZQi}6$MOI^)BR$#$!)3vpzC*=JkuSF_L`y5 zdGm6;GcWl)mR!OA<|?aU8S>Nv1VK4VNOhdu#uIQFDk$mJ`GBA;Zr`g0HcM*ZcqD)w zZxGVIXApjJG%TbE?T*z##%FvU77VtVZDnxWvNCe)3OG^?oc5S%{raX_Viip@_~i`x z2BLEOrin_kU#HIPe2OgXHV&Yz1N!zxpVR$RUDqKHAo~LyHkNU=_jLOEorZtS+aN+W)%-y@E<_3^{rVZZURU9mb8+m*Gfzlpe446mwb?^-mhDBld z@cvMm4arFDao}aqXA*ESiM)1X^9}u1MxO=*juxmJc!D^j@tb~Y6+ZvR+Uz9ZhrsMp z%@|%rR4!-x$r*2;5lr_0|Jve z&HHR|o{lVLRl!EiPxFa{EOmNRjhvv+r32S7kaUNcjT%3ClrRM*Bhv`$=NwB-&3NEn zS6yAx2N;C2vN4k-*!h~?p)y{VNVMfS{@=8l{AQ;~<9~IZS*u{*_KBZ~6SeQ6!--g| zdX7e3wBS6TUE2AyQ6>|=b5{pDeR@4@bZK)8lhiZs@+W5U_#>oCwG}-&>Z5-#0YP0b zqltVTHojZi(05oUbn6^^C-e#toVL%KG=uHvQs4|BP;9iv~eb1h|( z4$G8k%4!%1enJwL`b1Lqk5m}_&$n} zbv+lmU_Zn4$vpJDfa7@BYXL%l`IWA_A^o>@y*vCF(Vmf!3!HJaj!T2AX)s%kX}EL? zUj;QIOe5-bjsdxX`9}!hoxmIh0=i}NYlkOzfiUu%UB~JA7LE!OAh1Y$ZJU z1FhI2WOH0ogueOC5)K8xTf?cC7UZ{nyPAQe_g6xXQ(_8ChzrHGG^=KXu>1-vRi=)4 znyJc`iLqDb%Trz7f|s>S`bZG9Pd%qPB_6_g8P}H{cWeMWUkZHj9gw+Gk0b0YowYzQ z81pV%K~gV6uZc@K)c=$+Qp>QFPtSQ^yN>5eA{?P{e|9c0@&CCX{0%E=G$&Pc|h{g}qQHLks6s*u1L3Ianmg~`_bi_XLvjXXUXujvacSy>#+#`h@Cq4cJN=$YD-Uo(P`q) zUtZGsF7I{w4>iKD;gjXE_&wwMsmZE&x6pOjc5I(_q^0nIrR}pgs=LX*8pZ7gKr@gy zn+-pIFziTWO(!~?o()3UreRP~P|4`^71b_s>Id!iUw+{?eUjgO zT#0(}Tn0MwylC_hNZJCTkBwAf*yri~MA>C(<7s@*v;6VGQr13kTE-|YT4AL9?ZzU_ zdujMPkElbwaE5~Y{L-$2twWn6W;pJz*zjdO0_^y-@_GrW6`F$oEX#*8KY!xmK+`la znL0s7TM|3t$;crm^QCPQEW*TA`s9L-Ws)^(a`U{nr`jk&5sUT9gP#?sSzt-uyLq~%3%m!1${HlT+v6CO;(GWE` zRHp6f6^S|4^`(;Fuy#4B(~tNjlI^z$;jAlRlhga6b&;=yqEuY88x4@Ew4b%vfpoN+A{^^_jwB%Job|LO!z0gRJ<6>S_4jAyQt7{hHgc!bbeLYjzcE?m z={5@{L1H)Gi>f22Yyja7?%;ho+5f#pfNCmNIb zWB1=#D=!U5_>DivE&FVSfrLOVrYU(yfe*MvOGPvE`p_#&R2U~#8NQ2M9JRFG_~T~( zH%64U@JW+(<2bvyUtklt*Qr}MLC%Sls0KQfaDl$s=y)N1tn#TPOaaD(RuKNQ4L=R; zGib@wConp@zmE_t(%3H|hasc*JBc~BP(j@+HYy|l>De0nVhb)>AYjRpDNd~|nKSD2 zOIOWC{CJmmllb%g!rgcRD|o1#^DqW#C8;)|_xdxncu}(Wn>n6&dnw|8oL*?Gc!3}5 z3+sae*c&;kIH`g=o+gZDiLjqKnLh;Nrsk#-)~cpP$vg)FAoIN;Eg?jGh>Sdc{4ttcTE|x*1l#0&ELIxG%t$ua zO>{`!emgnT@`J=|~*17kw6^_pNMn&fbHyw=~%sxrT;E<^F z3&pSK5bO6aMhaft^^Zu1vG(ZIm}EO?R+B8n+sZD|?%z~mzcjxDHY!DlX29VS^RMw5 z*HyyF=zQjML?E9wwJcO2YLZL9iWOtuNh|p*z-Xh|c;*X{;(Po>xMs3@XVRD>c3RF7_Ju#{iS3oQ}2 zvsBjg5FfPabr;4m(jI&=<>B_5RWA*>JMWA>cG$#YgjJUKRr?5ax`~m$k_>C~x;!$@HWk!{eP<-calswejDdt(2A=Fb(&m%ig|*POKW#7tJ0e$uhX1+j;AJw{0RlhTXh z`(x!reDbqjz44~oR1>d%^-HdN_9ckg8?o?o3EB@KXJZ+3(-UCQTVQIp%9!3MGUd=H z6>wC)ONgeN-jhKXi=U-3bST8{b?HnF2BLDrDIF-C43CzOzoR#_PT0|JsW9>G52dGY zKtziVxHz-v#`#$iXF`7dP88Dysfj=aBwD83GmOLVBfWW;OFOtP$X^C_ek-)#3ee zMIO3lwg9bPzn#sIA+mR$+_8GPfZTLz#duonuZFe^eo(2 z6>VHLj%HX)kTdWxL11n4Tum5e=;kC-Ujy+cc!4~;K_`G!qSSERZlNM9;)d~?@|Za6 zk{3gK(5>28o|3L*I9F*tBI?|mPo6xGId{u~L>A^{mDOyz(y@zPL6V)#twxtSIqLSFvC}wO|f~}9x1cvTz=Q5rM->~wqtwV z4xr@b#%nT1$JS(EtcM-Ap_t++d&}aMVS!oJ((oB(tq-h*JY1`+Kc9Av1S53Xq99>2 ze&d!_3Sj4`g5TPu2tz_AC74lrZPcCSBcMtkGiQ39-iWUJbBeBh3O2u2~5E z6jKStq_mRXipLGdjaB%S<`=(Lr%G?`^J*b@H_4`}!Yd(aJjZ+S_ z$YPV#_s+3kPZbu-4AO65@UiyQv2c~`Xe*c2A2L7P@_XY^KXI(NR zH-E~V*35V~XFki~2M}vL=Wt5$$pVi=YI36!!x|=s&YX|3jS*h* ziO_azMGn4&A9HXQ27}$X>vW7clmpuTg6qT>60d^Y zDHwuW)zR7sH70G|{Z|g=-6@Vt0?cZtp%@cb4S?|gs{v34u$p0x8T=jhOWms@rMk!% zjufAJ*K>OzUS6BQxsLUlE?%jexeW^E*B{*1gPO(b;JG{z6k@%3D8qWB8WTqy?nKGiZt_qDI z(WIhNeNs(ITr0?-llnk35B%gNO*isH^oU@9t|`L0;lL^VJtKTaB`}zKo69bnJj*q7K&;rJF4#d!*aJ;R6S@$>5MxNIw%#`q13vxp!61clmn8gHZ2~*Oq zj2&SH6Jo19xn(&FSc#8h=fNyiS!&4BRgqvRmMrDTCkPR4HM-o%{X^@&*0oMN9p*mp zbFMV>oyEq$e{1oNwb`?w;-^6!b1HubMHxidRaN_#pqD<5hYlV~sF&n5d-YuMf+6(m zNaoF*PspPCS{GN|p#G)T$;mRZMU0zOS#UNM#VwWt<*!I`CTW+)pWbbrm?8m7>_i& zOp#^^F#0bGSO8!GQD9+!GUz*kh5q^t9m+rG1y!Q>!CPqwvvH}V zkS4`v2AMwpi1!Y{xas|u-vl*j8B;*>qF<04J3uiFD20p~V|r$R zX+>aJ7edq~qZUqa<0WibM`l6#ROp$B0eX0Ldjx`n(2-S0to)SjeO`p>8ZZNXCfa{PG@n!~L|m&^S( z!V`l`p)=1gctkB*%&>rSG2fU%dh++Q2E7}qq`IPZJ3V{C48rWHD!dAG>H6&J#Zw6S zWo*rsfy+Lc$NzUe5^P^W-xq}(}BDRE?0)@EM=yIpg90yNG^6bGjq3NDW8K#)mn=-vMUu<@m(`dZi zZl)gK5o6aQr=}uc;9nY8-^`#1)P#P+(ZvA90SWnS0lQ0^d1rOn<2sVGM$+5=oYjV1P!12-<8c9O0nZP~0w=>bnS{&{Qj- zQ>tFD^v0CRz{(n6U}}t?ip8wcUX19K{yH8hS>X41LFbwmi?<%0ks@Tr#7SYK+l*+~ zgENtu5~u@eL8h2|)yBL(&!I!O`ljQ3p)i8u0**={nq<=O;ad z>IlWzCM9gF3y#Wey>-Nb;G;YBF~SGv!jh26sbgt?v6Qy;e2mn|=EEY^qDJ3e$2p_Q zN4G6DWZ_9%s5mgoLbEk8i@md+tPJLgK{}bg@+dkBeuXxV7?E z?;uhJA51Vo>%V=3$?jUlra42N+}x`)x7?k{N+o53C8yb|_5I znnhz>a{fp!u~}y^@^~;Z->e<8&c-nD(bxApH-mXIX|@Ds#(o7Y_Oy<2Z9^=RP$3n| z@V;oq0ZIEHbg5P5*D&#Rn4wQux~I|@k}#W|!s}%7(bpW)XKZmb=xzGbzqe#l^WdstWv81=biIU(cQsFYFIBZb;4vrqX6;ox`o zEPN;vaH3s7FWoJf`^`jJk|Y)IB()G6{dDC1p$;|Z4x^$wxH?yh(jid41|1{y8~I1m ziLo5XNl(c-04eQR_(%m98Ii=O$#P&)SZ~~0@})Lm;c9lEZ%y;fBP5hSCsCN4BPdhZ zQe@VP>I3g8;jAiR#g47{IwvTw;u$#5mC&W)53e&Vs&$ zYel$8JuonJGK?As4b6Eb~+*06n9lY_kBQXMGAaB@tA{WcBjfQ zvUZy>@yMgntv)6w%)knW(`{dlK7PUU3Nhbvp;z=Oi%AtpIAqCFD|(g3C(WwlN9O)~ zpg^Q7b=H!q7j&53#t+oTtfz^uSB@k1YJAZvL#1VaNel16+-e9aw30Qmb32upb=3)^ z3<$=*av8<|2obRyD9p+k8(`|+lL=;PPfLv97|ZRK(<4rI*^+nW=_EwCkHnt@LKoT_vH{Vgky|m~+SVFI zqG3%3lC~s9c?hm*T1^U9!W`k4`kRtUdFJ_9Ba#{^O1DNZMyKV#LkG(ev~81f zk-+J64qd7Q+Fn|84Aqx(@X^h4wS4vYJnGL!&@sXXlz!5Zqglt+n3s=7!>tKMG|xQW z0rZ_+6|xX+X)1pTvn&k^ixqgY7J;^E39>#Yx)jKq9mbjl<{U&Y(*LX(YRp`7M)NN} zx@)g@O{Ob}CvN3kk-%BOK&*I-u6WXN2=1p7Rhf1LgboH>jV~Cd>G{Up&bKn?fMi87 zDYP+Wn3}Venolv0=7CUvXdBEDh*nJvj0p-eumTcwixy!%D}*9#bd$%u9! zm#wZwatylf=O0?1YVnUiZWCFfQgOECv5Vg{7L|5}S`VNx!wl>ip2TpB#_hPs%*xBj zDi$yQhCaDPbU!`9Wi9oaJQAE&qGw~FmB65HAPtt{v`3O|$VUFl%GuY=LZaYL)nR%o zB)m#O_iEVIDx#(<&q}+5PLwdE0t}t8W#G{-$FX$B9W`k7w$+(<;shcFk?b40OlS14 z0NupfJS>;a?E1m4XDYg-D}R02$G&{bK3epl#xcb~h!xS2vhuV6vIL)yAzO46+XBa0 zRQ?q8exg?AxAm;0l?qA+P0|tVuZyyTf*^E=7Y>YH2#f{5OE*^-C8L~S9(u_$cPtPY zcMdtYpOMS7QUZwa*HDD&ISrxs4e7RG?1|2P=uhnbn=AF>XfURgHn{KHa)31sx%1Y{#(sqJ7lkfF9=Kj$Y+9f%$7g zh32q}c{(>a_h;v=)P>yMFcVdOiv(;Gv!1gj|<3Cu_T@TuI}Q34F=d-bf!;Svh-qaVyRM@ zS)$gPDu+Wo6grCmDUKH1t6>W_w2j158t~pBxY+>^!$*m3gq|E7Pl@e0Ws;&48Y!KO z33D_dgRXHYI2z8Tr}Sj>SU6^?LFDzA^i~bDK8Dmr`9M<5sD`T_pD2AG=evw)yB0F!c|%2(<|~Jcd6g1WJ$mRobIP4@E>31EhGwo_0q80L`Xnso z%?85Iie%&N3Z_Z{%qhcwrKN8@kBnDr9GQ(9a%?C2iZtp3KgQ`F{3Fy%7OK* z0|7P>pNtiExi+Ni%^3H{SSOe!8^~#rWkk$7NIM3o_7y@0u=H9udWHG=8LLx))_+rw z7AAvJy&lbE^ePRta<3}FjP(vZ1nkT$tA)M{A<3|OXv&4eOe07rvq$&QBag`(`T|yv zKXswwfY{lC_g8C?jiopEAso!#$rT>Y?$MDQ#U_k{Vvkx`lp-59l%;%s z-Y=WZg=GFAu;^_$(!crpJOjUmt@E>(rklPim!E}CD6=WN{%~lI0*Lx8F5Ot0E)~J? zfyQNGo*cmxjxHU<$eJe*F&9S%#|(NH$DI1|+lw|G#DV*asFR-HL!)DIL)hj4x^#uN z=Qp8Chf+RKx}ty%kHjRGb1kSPZp~@UV~md){m{sQ);yxmLa0UY1+U05DbAJST0Wf- zWTs%731w$E%DE|mOoMi4dFh{_B3siw#|i=~fQ)fVy0emiT#L+-|3_1_O=%p!`NO{w zx+|x--gHV77~z)B zs`ogS``&2%*Sgl#nGebtT@AXkh3=x7D!`hYJ&gn)D{eJYZFPNUVVqDyjrmaP$ueV7 z%+dyqLm0oAjn7^%e?Vt)HaUXXjdu|y=bpORSal;al>h(`07*naRO$x0Qx_gZ``}kN z5IVUz+j}B3*d*Y9Y7g&GiZ}xdD0mENdlQ;|AD{vO$!wef^Nee{T!`QSSd05zok13T~ z4&@#W;~J1?yiVd2Gmf(apZxCtMow$XO~OKAOOEv#`BO%HFU~}Z0Y27D9C~M^n3N2h znQrwAz*!~0Ll1*rH5HSv;%dvYHv>;IO*R&8CboV$qY#tun8KJcu(0?-IV%@>MXypq z6x~0wZdd|zxIG3L3&639UWN3Z9lc7xxT9B5`@tesDX6G?lOfh(7-i>Wi1{q2XJfvGyaXUg&t6% zx*4?rTQIxvaoJ>sHZmGT^1HX^xBd2h?u$CVRDW@of25WmSf zqre!=;2`)={D`Z^#m3R1gM-t9py-J|zR{E$1|WPu*I@*wClIkyCR82Ndf+ICy*9twGjeOE*G~o3Zw`h^~LW~vWsw-m$lfE9!y#jzr z!sK--dxx6hLsq%Ye6l@_u^fFBG@C3%W~eAvB+d^@S%q)NW|eDt#j+*1VS-!Xa#n6; zHHNs4pDa{>^*QSn!Z53{uCll!t7Df&D?zQNNyVmS3-@CeOR5axIoP7Ma;oy1ojF;C zRQ_hmgvr_urB#vFC=&$+O82}WeS^SR@aDV{>Bf`Uf<<(iWUuxx=;xB5BCpq1OZlX> zod=|H1Q^Fx)XsvOh{;tLB96-e6Q205@N*+V`s(~ z`S6UqFi>%>zIuqfjsF3X;Z_)0dJ4eGd>IREv8x;@S4aaDeuprm)T)SN4o-7LDojIB{WxjAoU`0D*!yD z)Gea9GiID?dzBf(L2!r8{B%a>!y!K*OVD+W0C@5RNa>uP#l7g^Ff<~$bRZ+e`yib> zN=KqN6b2Ea+oKG2P8J<;hYse6NW^kXx>;Z6@sT<*PA2IxKggylO9N_GVY)h3D_S2b z@iFM$nnM`6J@WMw1N4B#0wWS1S?5$We|=uc$!XJ>PD*SEmZee4bS<{yj6nH?P}#aO ztF1IWot95{3 z`O;R>W$4>$=-6?^G&b)$bM~|Yh5XC(-d8`J9$*+DR8n2J3<0Pn&sZMyWlSsTcGhlO z#%euXv$tvsRfAu(ER0G}_k^sR+O-mbq^~b=FmMK--acdUf{8O0yP1?iZX5TXO*537 zl*2*lB%9Kb#~No|x}h2e#+8TV%gz8My=9Poud2g{Z|RJVsOOHFyn*0GiApJC)N;?m zBBK+kpFXS0-5h$=Ni+C@kTDRdgNZw{#x!CZWR@|T?&HI<9#Skq&GN*ee3USd!vI-m zr1Ttj=ssM*|=qwMajU?W&fs%%GtdLSn9ODwsdgyWnR(O|;k?vy|t19wDTF$**I-Ch% zaOR>{{r&%JVOl;omgtphQi2cW$eyTC7QO1kDj+6yU=;@J7z&9ogn0C-oMB8tPr$CX z_4Zp^Fe1iSqgNhn&^yZ3qw(n{)SM}zNr-iAd@=-}S3VB<{&fV5{)++gJ*P==KqGSmY5hJqsKhvg&Y(%$%}kC3t5m_-8RM)k1EpD@zz4TMGiK4!UB&}~L|Z;A=#3FRc9 zgj*IwN3$El7(Z50J*!@)UK^Y-z`i$U!QV@*mvxo6C)tZmkFnTdSxIjtG6bN4B-I}B z#f)6xkGl=W@dd7}^f+Lv-W2G{DO>ol&sL8lEb(^|SZRv|cgN~7!_f$yjfE(;WY*=9 zQsh{ebg?R6*3+4hMT4>d-}l`>FwW!aKuw)XhAhF!F(W6-gMs$3MXxP?4^`X;#e>IecrF+RdbYJ}nnV2ldij}g;k%t-o>CVSw7rKz0hsC-JL#)Ld zo4`o9RzY!TODtkJw8fxJ+TTm1vw)u#=gbK2OkoByGUFl#sD;6QaVC?n@)l!r{Vam? z_!c&7;)r9De3n6R0IOTH2#Z*S+{Yp&gje!nDtZN(@}ykQ-b@IuB%6#nX+By09#-Ph z^73F7pDZn6HF=Ux+qChd$X-^xyIDPY#Te_f-eH7ccHJI4%OIF}UnL3?;!!VpRly|6 zN7=8k8HrofT2%Y69}6Q(=Hq>Um=zsx9I zE0m**sqxZPM!8W8iR+BA$*^vj%BXVb+$h5JS%ME3bR2ymGE$$pbTUT5jOZv^bfLE) z$A>mOP#;Af-B5(W1E#F-_6TmiH!}4v`anF9; z&U%(LBET|7D$0kUJ{jGxD6;;H`W2lKEHYCJKA7S&^z$+27qu2L>5ZArymS$pGnWIs zk}zk5A*&Dr&ys1F2JbIoc=hw)d1Yu#w#0JAI*8XP#R7bM?<^|@uwrV!B320bO6?X< zv#zIN5H)-xtidEt7`g;>W>O$bido(!8xwykJ~Pxc$ql>kK`0@6x%KO2p<|OU_D-Rz zLJ4yls1TqD@C_PsH@4)i9x|8d9Xqy?ygykTyENAi>P~@t#^Yi)NwAtFkROv`J?@3V z08PxEd(5`ka8y>skAovl0}sWdt(o$1aymWDGTtBMcTV8z}BE z?;PJi7)_TNSPnnzqJqWbL|1QbRz~1r8SOEBz-W4LFWAoy1B]OO$H9RI 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 ?ee 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�VM#dZfYn4M;>BYkT1x6>Hj9h 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%0Dc#OOa6 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 o6qKhYB{Z`> Date: Mon, 15 Jun 2026 02:05:57 +0530 Subject: [PATCH 21/34] lint: conform UX v2 + model-migration code to #47 ruff tooling Rebasing feature/ux-v2 onto development (now carrying #47's ruff/format gate) left the model-migration and detector-rewrite files in their pre-#47 formatting. Run ruff --fix + ruff format and fix the violations that aren't auto-fixable: - model.py: provenance annotation used unimported `Dict` (F821, would NameError at import) -> `dict[str, dict[str, str]]` (PEP 585). - sam_detection._detect_with_sam: returned undefined `image_8bit` (F821) -> `image_rgb`, the 8-bit RGB image computed at the top. - agent.py / sam_detection.py: moved the `logger = ...` assignment below the import block to clear E402 (import execution order unchanged). - verifier.py / conversation.py: wrapped long log/summary f-strings and tool-schema descriptions; reflowed prompt prose (content preserved) to satisfy E501. ruff check . and ruff format --check . both pass (lint.yml CI gate). Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/app/agent.py | 480 +++++++++++++--------- gently/app/detectors/hatching.py | 50 ++- gently/hardware/dispim/sam_detection.py | 480 ++++++++++++---------- gently/harness/conversation.py | 350 +++++++++------- gently/harness/detection/verifier.py | 525 +++++++++++++++--------- gently/harness/memory/file_store.py | 3 +- gently/harness/memory/model.py | 2 +- gently/settings.py | 23 +- gently/ui/web/connection_manager.py | 4 +- gently/ui/web/routes/chat.py | 2 +- gently/ui/web/routes/context.py | 15 +- 11 files changed, 1134 insertions(+), 800 deletions(-) diff --git a/gently/app/agent.py b/gently/app/agent.py index 40618718..f68d67a7 100644 --- a/gently/app/agent.py +++ b/gently/app/agent.py @@ -14,38 +14,38 @@ import asyncio import logging import os -from typing import Dict, List, Optional, Callable, Any, TYPE_CHECKING +from collections.abc import Callable from datetime import datetime from pathlib import Path +from typing import TYPE_CHECKING, Any import anthropic import numpy as np -from ..exceptions import StorageError, AgentError +from ..exceptions import StorageError from ..settings import settings if TYPE_CHECKING: from ..ui.web.server import VisualizationServer -logger = logging.getLogger(__name__) +from gently_perception import Perceiver -from ..harness.state import ExperimentState, EmbryoState, ImageRecord -from ..harness.orchestration.plan_synthesis import PlanSynthesizer, PlanLibrary, PlanValidator +from ..core import EventType, emit, get_event_bus +from ..core.file_store import FileStore +from ..harness.conversation import ConversationManager +from ..harness.orchestration.plan_synthesis import PlanLibrary, PlanSynthesizer, PlanValidator +from ..harness.prompts.manager import PromptManager +from ..harness.session.interaction_logger import InteractionLogger +from ..harness.session.manager import SessionManager +from ..harness.session.timeline import TimelineManager +from ..harness.state import ExperimentState from ..harness.tools.registry import get_tool_registry -from gently_perception import Perceiver # Import tools package to trigger @tool decorator registration from . import tools as _tools # noqa: F401 -from ..harness.session.interaction_logger import InteractionLogger from .orchestration.timelapse import TimelapseOrchestrator -from ..harness.session.timeline import TimelineManager -from ..core import EventType, get_event_bus, emit -from ..core.file_store import FileStore - -from ..harness.conversation import ConversationManager -from ..harness.session.manager import SessionManager -from ..harness.prompts.manager import PromptManager +logger = logging.getLogger(__name__) # Shown when the agent is launched in UI-only mode (--no-api). The web UI is # fully browsable, but anything that would call Claude is disabled. @@ -71,7 +71,7 @@ class MicroscopyAgent: def __init__( self, - api_key: Optional[str] = None, + api_key: str | None = None, storage_path: Path = Path("./experiment_data"), model: str = settings.models.main, microscope_client=None, @@ -113,7 +113,9 @@ def __init__( # 4.6+ models and obsolete on Fable 5 (always-on thinking); the header is # dropped so it can't conflict with the new model family. self.claude = anthropic.Anthropic( - api_key=api_key or os.getenv("ANTHROPIC_API_KEY") or ("no-api-mode" if no_api else None), + api_key=api_key + or os.getenv("ANTHROPIC_API_KEY") + or ("no-api-mode" if no_api else None), ) self.model = model @@ -125,7 +127,7 @@ def __init__( self.mode: str = "run" # Context store (agent's mind — set via set_context_store) - self.context_store: Optional[Any] = None + self.context_store: Any | None = None # Experiment state self.experiment = ExperimentState() @@ -141,8 +143,7 @@ def __init__( # Plan synthesis self.plan_synthesizer = PlanSynthesizer( - plan_library=PlanLibrary(), - validator=PlanValidator() + plan_library=PlanLibrary(), validator=PlanValidator() ) # Event bus for async messaging (must be before perception manager) @@ -163,8 +164,8 @@ def __init__( self.client = self.microscope # Callbacks - self.on_message_callback: Optional[Callable] = None - self.choice_handler: Optional[Callable] = None + self.on_message_callback: Callable | None = None + self.choice_handler: Callable | None = None # Serializes conversation turns: user turns and autonomous wake turns # must not interleave on the shared conversation_history. @@ -175,14 +176,18 @@ def __init__( # human). User turns are unaffected. _wake_choice_factory is set by the # web bridge so ASK-mode wake turns can round-trip an approval picker. self._autonomous_active = False - self._autonomous_blocked_tools = frozenset({ - "set_laser_power", "remove_embryo", "stop_timelapse", - }) + self._autonomous_blocked_tools = frozenset( + { + "set_laser_power", + "remove_embryo", + "stop_timelapse", + } + ) self._wake_choice_factory = None self._wake_choice_discard = None # Interaction logger for structured logging (research data collection) - self.interaction_logger: Optional[InteractionLogger] = None + self.interaction_logger: InteractionLogger | None = None # Event capture — durable log of every EventBus event during this # session. Substrate for offline replay / shadow-mode A/B of @@ -195,13 +200,13 @@ def __init__( self.decision_log = None # Timelapse orchestrator (initialized when microscope connected) - self.timelapse_orchestrator: Optional[TimelapseOrchestrator] = None + self.timelapse_orchestrator: TimelapseOrchestrator | None = None # Timeline manager for tracking events - self.timeline_manager: Optional[TimelineManager] = None + self.timeline_manager: TimelineManager | None = None # Visualization server for real-time feedback - self.viz_server: Optional["VisualizationServer"] = None + self.viz_server: VisualizationServer | None = None # Device-state monitor (bridges device-layer SSE → EventBus) self.device_state_monitor = None @@ -219,10 +224,10 @@ def __init__( ) # Wire tool execution context self.conversation._tool_context = { - 'agent': self, - 'client': getattr(self, 'microscope', None), - 'microscope': getattr(self, 'microscope', None), - 'databroker': getattr(self, 'databroker', None), + "agent": self, + "client": getattr(self, "microscope", None), + "microscope": getattr(self, "microscope", None), + "databroker": getattr(self, "databroker", None), } # Session manager (persistence) @@ -242,16 +247,22 @@ def __init__( success, history = self.sessions._resume_session(session_id, self.experiment) if success: self.conversation.conversation_history = history - self._emit_event(EventType.SESSION_RESTORED, { - 'session_id': session_id, - 'embryo_count': len(self.experiment.embryos), - 'message_count': len(self.conversation.conversation_history), - }) + self._emit_event( + EventType.SESSION_RESTORED, + { + "session_id": session_id, + "embryo_count": len(self.experiment.embryos), + "message_count": len(self.conversation.conversation_history), + }, + ) else: self.sessions.create_session() - self._emit_event(EventType.SESSION_STARTED, { - 'session_id': self.sessions.session_id, - }) + self._emit_event( + EventType.SESSION_STARTED, + { + "session_id": self.sessions.session_id, + }, + ) # Initialize interaction logger (for research data collection) self._init_interaction_logger() @@ -284,6 +295,7 @@ def __init__( # autonomously; enabled via the set_autonomy tool. try: from gently.app.wake_router import WakeRouter + self.wake_router = WakeRouter(self, self._event_bus) except Exception: logger.exception("Failed to init wake-router") @@ -300,7 +312,7 @@ def session_id(self) -> str: return self.sessions.session_id @property - def _session_id(self) -> Optional[str]: + def _session_id(self) -> str | None: """Internal session ID (backward compat).""" return self.sessions._session_id @@ -309,7 +321,7 @@ def _session_id(self, value): self.sessions._session_id = value @property - def conversation_history(self) -> List[Dict]: + def conversation_history(self) -> list[dict]: """Get conversation history.""" return self.conversation.conversation_history @@ -354,6 +366,7 @@ def set_context_store(self, context_store) -> None: self.prompts.context_store = context_store # Create agent memory harness from ..harness.memory.interface import AgentMemory + self.memory = AgentMemory(context_store, session_id=self.session_id) self.prompts.memory = self.memory @@ -363,6 +376,7 @@ def enter_plan_mode(self) -> str: return "Already in plan mode." self.mode = "plan" import gently.harness.plan_mode.tools # noqa: F401 + self._update_system_prompt() emit(EventType.STATUS_CHANGED, {"field": "agent_mode", "value": "plan"}, source="agent") logger.info("Entered plan mode") @@ -442,7 +456,8 @@ def exit_plan_mode(self) -> str: if item and self.session_id and self.context_store: try: self.context_store.link_session_campaign( - self.session_id, item.campaign_id, + self.session_id, + item.campaign_id, ) except Exception: pass @@ -468,11 +483,14 @@ def exit_plan_mode(self) -> str: def _update_system_prompt(self, context_summary: str | None = None): """Rebuild system prompt via PromptManager.""" self.system_prompt = self.prompts.update_system_prompt( - self.experiment, self.client, self.mode, context_summary, + self.experiment, + self.client, + self.mode, + context_summary, perceiver=getattr(self, "perceiver", None), ) - def _get_active_plan_summary(self) -> Optional[str]: + def _get_active_plan_summary(self) -> str | None: """Delegation shim for agent bridge access.""" return self.prompts.get_active_plan_summary() @@ -502,7 +520,7 @@ def _auto_save(self): self.experiment, self.conversation.conversation_history, self.system_prompt ) - def list_sessions(self) -> List[Dict]: + def list_sessions(self) -> list[dict]: """List available sessions.""" return self.sessions.list_sessions() @@ -535,6 +553,7 @@ def _init_event_capture(self): a stripped-down agent) — replay just won't have a log to read. """ from gently.eval import EventCapture + try: session_dir = None sid = self.session_id @@ -542,7 +561,8 @@ def _init_event_capture(self): session_dir = self.store._session_dir(sid) if session_dir is None: logging.getLogger(__name__).debug( - "EventCapture: no session dir for %s — skipping", sid) + "EventCapture: no session dir for %s — skipping", sid + ) return path = session_dir / "events.jsonl" self.event_capture = EventCapture(path) @@ -569,6 +589,7 @@ def _init_decision_log(self): logs and the two are diffed offline. """ from gently.eval import DecisionLog + try: session_dir = None sid = self.session_id @@ -576,7 +597,8 @@ def _init_decision_log(self): session_dir = self.store._session_dir(sid) if session_dir is None: logging.getLogger(__name__).debug( - "DecisionLog: no session dir for %s — skipping", sid) + "DecisionLog: no session dir for %s — skipping", sid + ) return path = session_dir / "decisions.jsonl" self.decision_log = DecisionLog(path) @@ -674,12 +696,15 @@ def on_stage_detected(event): embryo_id = data.get("embryo_id") if embryo_id and embryo_id in self.experiment.embryos: embryo = self.experiment.embryos[embryo_id] - embryo.add_cv_result("stage_classification", { - "stage": data.get("stage"), - "confidence": data.get("confidence"), - "nuclei_count": data.get("nuclei_count"), - "timepoint": data.get("timepoint"), - }) + embryo.add_cv_result( + "stage_classification", + { + "stage": data.get("stage"), + "confidence": data.get("confidence"), + "nuclei_count": data.get("nuclei_count"), + "timepoint": data.get("timepoint"), + }, + ) except Exception as e: logger.warning(f"Error handling stage detected event: {e}") @@ -702,23 +727,31 @@ def on_perception(event): stage = data.get("stage") # 'no_object' is an empty-field sentinel, not a developmental # stage — don't mirror it into latest_developmental_stage. - if (not stage or stage == "no_object" or not embryo_id - or embryo_id not in self.experiment.embryos): + if ( + not stage + or stage == "no_object" + or not embryo_id + or embryo_id not in self.experiment.embryos + ): return embryo = self.experiment.embryos[embryo_id] if stage == getattr(embryo, "latest_developmental_stage", None): return # steady state — nothing new to mirror - embryo.add_cv_result("stage_classification", { - "stage": stage, - "timepoint": data.get("timepoint"), - "stability": data.get("stability"), - "temporal_analysis": data.get("temporal_analysis"), - "detector_name": "perception", - }) + embryo.add_cv_result( + "stage_classification", + { + "stage": stage, + "timepoint": data.get("timepoint"), + "stability": data.get("stability"), + "temporal_analysis": data.get("temporal_analysis"), + "detector_name": "perception", + }, + ) self.invalidate_context_cache() self._auto_save() - logger.info("Perception: %s -> stage %s (t%s)", - embryo_id, stage, data.get("timepoint")) + logger.info( + "Perception: %s -> stage %s (t%s)", embryo_id, stage, data.get("timepoint") + ) except Exception as e: logger.warning(f"Error handling perception event: {e}") @@ -733,7 +766,9 @@ def on_perception(event): # ===== Visualization Server Methods ===== - async def start_viz_server(self, port: int = settings.network.viz_port, ssl_certfile=None, ssl_keyfile=None): + async def start_viz_server( + self, port: int = settings.network.viz_port, ssl_certfile=None, ssl_keyfile=None + ): """Start the visualization server for real-time feedback.""" if self.viz_server is not None: logger.info("Visualization server already running") @@ -783,6 +818,7 @@ async def start_viz_server(self, port: int = settings.network.viz_port, ssl_cert if self.microscope is not None and self.device_state_monitor is None: try: from .device_state_monitor import DeviceStateMonitor + self.device_state_monitor = DeviceStateMonitor(self.microscope) await self.device_state_monitor.start() logger.info("Device-state monitor started") @@ -796,6 +832,7 @@ async def start_viz_server(self, port: int = settings.network.viz_port, ssl_cert if self.microscope is not None and self.bottom_camera_monitor is None: try: from .bottom_camera_monitor import BottomCameraStreamMonitor + self.bottom_camera_monitor = BottomCameraStreamMonitor(self.microscope) logger.info("Bottom-camera monitor ready (not started)") except Exception as e: @@ -826,16 +863,14 @@ def push_viz( array: np.ndarray, uid: str, data_type: str = "image", - metadata: Optional[Dict[str, Any]] = None, + metadata: dict[str, Any] | None = None, ): """Non-blocking push of image to visualization server.""" if self.viz_server is None: return try: - asyncio.create_task( - self.viz_server.push_image(array, uid, data_type, metadata or {}) - ) + asyncio.create_task(self.viz_server.push_image(array, uid, data_type, metadata or {})) except RuntimeError: pass except Exception as e: @@ -854,7 +889,7 @@ def _has_microscope(self) -> bool: """ return self.client is not None - def _emit_event(self, event_type: EventType, data: Optional[Dict] = None): + def _emit_event(self, event_type: EventType, data: dict | None = None): """Emit an event to the event bus.""" self._event_bus.publish( event_type=event_type, @@ -895,10 +930,13 @@ def _publish_embryos_update(self) -> None: def _mark_significant_action(self, action_type: str): """Mark that a significant action occurred (triggers auto-save).""" self._auto_save() - self._emit_event(EventType.SESSION_SAVED, { - 'session_id': self.sessions._session_id, - 'action_type': action_type, - }) + self._emit_event( + EventType.SESSION_SAVED, + { + "session_id": self.sessions._session_id, + "action_type": action_type, + }, + ) # ===== Public Message API ===== @@ -917,8 +955,11 @@ async def handle_message(self, user_message: str) -> str: Response from agent """ if quick_response := self.conversation.try_quick_response( - user_message, self.experiment, self.mode, - self.enter_plan_mode, self.exit_plan_mode, + user_message, + self.experiment, + self.mode, + self.enter_plan_mode, + self.exit_plan_mode, ): return quick_response @@ -932,10 +973,7 @@ async def handle_message(self, user_message: str) -> str: self._update_system_prompt(context_summary) # Add user message to history - self.conversation.conversation_history.append({ - "role": "user", - "content": user_message - }) + self.conversation.conversation_history.append({"role": "user", "content": user_message}) tools = self._get_tools_for_mode() cached_prompt = self._get_cached_system_prompt() @@ -962,14 +1000,17 @@ async def handle_message_stream(self, user_message: str): Chunks with 'type' and data """ if quick_response := self.conversation.try_quick_response( - user_message, self.experiment, self.mode, - self.enter_plan_mode, self.exit_plan_mode, + user_message, + self.experiment, + self.mode, + self.enter_plan_mode, + self.exit_plan_mode, ): - yield {'type': 'text', 'text': quick_response} + yield {"type": "text", "text": quick_response} return if not self.api_enabled: - yield {'type': 'text', 'text': _NO_API_NOTICE} + yield {"type": "text", "text": _NO_API_NOTICE} return # Hold the turn-lock for the whole streamed turn so an autonomous wake @@ -985,16 +1026,14 @@ async def handle_message_stream(self, user_message: str): ) self._update_system_prompt(context_summary) - self.conversation.conversation_history.append({ - "role": "user", - "content": user_message - }) + self.conversation.conversation_history.append({"role": "user", "content": user_message}) tools = self._get_tools_for_mode() cached_prompt = self._get_cached_system_prompt() inner_gen = self.conversation.call_claude_stream( - cached_prompt, tools, + cached_prompt, + tools, tool_label_fn=self.conversation.tool_label, auto_save_fn=self._auto_save, ) @@ -1089,8 +1128,11 @@ async def _resolve_wake_choice(self, chunk, emit, interactive): choice_data = chunk.get("choice_data", {}) if isinstance(chunk, dict) else {} factory = getattr(self, "_wake_choice_factory", None) if not interactive or factory is None: - logger.info("Wake picker auto-cancelled (interactive=%s, channel=%s)", - interactive, factory is not None) + logger.info( + "Wake picker auto-cancelled (interactive=%s, channel=%s)", + interactive, + factory is not None, + ) return "cancelled" try: future = factory(choice_data) # registers future + sets request_id @@ -1100,6 +1142,7 @@ async def _resolve_wake_choice(self, chunk, emit, interactive): request_id = choice_data.get("request_id", "") await emit({**chunk, "origin": "wake", "request_id": request_id}) from gently.app.wake_router import ASK_TIMEOUT_SEC + try: selected = await asyncio.wait_for(future, timeout=ASK_TIMEOUT_SEC) except asyncio.TimeoutError: @@ -1122,7 +1165,7 @@ async def _resolve_wake_choice(self, chunk, emit, interactive): pass return selected or "skip" - async def get_tool_call(self, user_message: str) -> Optional[Dict]: + async def get_tool_call(self, user_message: str) -> dict | None: """Dry-run tool call (for benchmarking).""" context_summary = await self.prompts.get_cached_context_summary( self.experiment, self.timelapse_orchestrator, self.timeline_manager @@ -1134,25 +1177,25 @@ async def get_tool_call(self, user_message: str) -> Optional[Dict]: # === Experiment Management Methods === - def load_embryos_from_database(self, database: Dict): + def load_embryos_from_database(self, database: dict): """Load embryos from calibration database.""" - if 'embryos' not in database: + if "embryos" not in database: return - for embryo_id, embryo_data in database['embryos'].items(): - position = embryo_data.get('stage_position_after_centering_um', {}) - calibration = embryo_data.get('calibration', {}) + for embryo_id, embryo_data in database["embryos"].items(): + position = embryo_data.get("stage_position_after_centering_um", {}) + calibration = embryo_data.get("calibration", {}) self.experiment.add_embryo( embryo_id=embryo_id, position=position, calibration=calibration, - uid=embryo_data.get('uid'), + uid=embryo_data.get("uid"), ) self._update_system_prompt() - def import_embryos_from_session(self, session_id: str, clear_existing: bool = False) -> Dict: + def import_embryos_from_session(self, session_id: str, clear_existing: bool = False) -> dict: """ Import embryos from another session into the current experiment. @@ -1208,14 +1251,14 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa if not embryo_states: session_data = self.store.load_session_snapshot(session_id) if session_data: - embryo_states = session_data.get('embryo_states', {}) + embryo_states = session_data.get("embryo_states", {}) if not embryo_states: return { - 'success': False, - 'error': "No embryos found in session", - 'imported': [], - 'skipped': [], + "success": False, + "error": "No embryos found in session", + "imported": [], + "skipped": [], } if clear_existing: @@ -1235,30 +1278,30 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa # Prefer explicit coarse/fine when the snapshot has them # (FileStore path); fall back to flat stage_position for the # legacy JSON-snapshot path which only carries the resolved view. - position_coarse = embryo_data.get('position_coarse') - position_fine = embryo_data.get('position_fine') + position_coarse = embryo_data.get("position_coarse") + position_fine = embryo_data.get("position_fine") if position_coarse is None and position_fine is None: - position_coarse = embryo_data.get('stage_position', {}) - calibration = embryo_data.get('calibration', {}) - source_uid = embryo_data.get('uid') or f"{session_id}_{embryo_id}" + position_coarse = embryo_data.get("stage_position", {}) + calibration = embryo_data.get("calibration", {}) + source_uid = embryo_data.get("uid") or f"{session_id}_{embryo_id}" self.experiment.add_embryo( embryo_id=embryo_id, position=position_coarse or {}, position_fine=position_fine or {}, calibration=calibration, - user_label=embryo_data.get('user_label'), + user_label=embryo_data.get("user_label"), uid=source_uid, - role=embryo_data.get('role') or 'unassigned', + role=embryo_data.get("role") or "unassigned", ) embryo = self.experiment.embryos[embryo_id] - embryo.nickname = embryo_data.get('nickname') - embryo.interval_seconds = embryo_data.get('interval_seconds') - embryo.num_slices = embryo_data.get('num_slices', 50) - embryo.exposure_ms = embryo_data.get('exposure_ms', 10.0) - embryo.priority = embryo_data.get('priority', 'normal') - embryo.acquisition_mode = embryo_data.get('acquisition_mode', 'volume') + embryo.nickname = embryo_data.get("nickname") + embryo.interval_seconds = embryo_data.get("interval_seconds") + embryo.num_slices = embryo_data.get("num_slices", 50) + embryo.exposure_ms = embryo_data.get("exposure_ms", 10.0) + embryo.priority = embryo_data.get("priority", "normal") + embryo.acquisition_mode = embryo_data.get("acquisition_mode", "volume") # Light budget import. Prefer fields already on embryo_data # (future schema may persist these directly on embryo.yaml); @@ -1269,27 +1312,22 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa # removed and the import should fail loudly if dose is # missing. dose = self._compute_imported_dose(session_id, embryo_id) - embryo.exposure_count = ( - embryo_data.get('exposure_count') - or dose['exposure_count'] - ) + embryo.exposure_count = embryo_data.get("exposure_count") or dose["exposure_count"] embryo.total_exposure_ms = ( - embryo_data.get('total_exposure_ms') - or dose['total_exposure_ms'] + embryo_data.get("total_exposure_ms") or dose["total_exposure_ms"] ) embryo.timepoints_acquired = ( - embryo_data.get('timepoints_acquired') - or dose['exposure_count'] + embryo_data.get("timepoints_acquired") or dose["exposure_count"] ) - last_imaged_str = embryo_data.get('last_imaged') + last_imaged_str = embryo_data.get("last_imaged") if last_imaged_str: try: embryo.last_imaged = datetime.fromisoformat(last_imaged_str) except (ValueError, TypeError): - embryo.last_imaged = dose['last_imaged'] + embryo.last_imaged = dose["last_imaged"] else: - embryo.last_imaged = dose['last_imaged'] + embryo.last_imaged = dose["last_imaged"] imported.append(embryo_id) @@ -1300,14 +1338,14 @@ def import_embryos_from_session(self, session_id: str, clear_existing: bool = Fa self._mark_significant_action("embryo_import") return { - 'success': len(imported) > 0, - 'imported': imported, - 'skipped': skipped, - 'errors': errors, - 'source_session': session_id, + "success": len(imported) > 0, + "imported": imported, + "skipped": skipped, + "errors": errors, + "source_session": session_id, } - def _compute_imported_dose(self, source_session_id: str, embryo_id: str) -> Dict: + def _compute_imported_dose(self, source_session_id: str, embryo_id: str) -> dict: """Reconstruct an embryo's realized 488 nm photodose from the source session's per-volume meta files. @@ -1322,66 +1360,68 @@ def _compute_imported_dose(self, source_session_id: str, embryo_id: str) -> Dict TODO: replace with reading a persisted ``dose:`` block from embryo.yaml once dose-tracking is first-class. """ - import yaml from datetime import datetime from pathlib import Path + import yaml + result = { - 'exposure_count': 0, - 'total_exposure_ms': 0.0, - 'last_imaged': None, + "exposure_count": 0, + "total_exposure_ms": 0.0, + "last_imaged": None, } if not self.store: return result # FileStore exposes _session_dir(session_id) → resolved Path. - session_dir_fn = getattr(self.store, '_session_dir', None) + session_dir_fn = getattr(self.store, "_session_dir", None) sd = session_dir_fn(source_session_id) if callable(session_dir_fn) else None if sd is None: return result - vols_dir = Path(sd) / 'embryos' / embryo_id / 'volumes' + vols_dir = Path(sd) / "embryos" / embryo_id / "volumes" if not vols_dir.is_dir(): return result latest = None - for meta_path in sorted(vols_dir.glob('*.meta.yaml')): + for meta_path in sorted(vols_dir.glob("*.meta.yaml")): try: doc = yaml.safe_load(meta_path.read_text()) or {} except Exception: continue - md = doc.get('metadata') or {} - num_slices = md.get('num_slices') + md = doc.get("metadata") or {} + num_slices = md.get("num_slices") if num_slices is None: - shape = doc.get('shape') or [] + shape = doc.get("shape") or [] num_slices = shape[0] if shape else 0 - exposure_ms = md.get('exposure_ms') or 0.0 + exposure_ms = md.get("exposure_ms") or 0.0 try: - result['total_exposure_ms'] += float(num_slices) * float(exposure_ms) + result["total_exposure_ms"] += float(num_slices) * float(exposure_ms) except (TypeError, ValueError): pass - result['exposure_count'] += 1 - acq = doc.get('acquired_at') + result["exposure_count"] += 1 + acq = doc.get("acquired_at") if acq and (latest is None or acq > latest): latest = acq if latest: try: - result['last_imaged'] = datetime.fromisoformat(latest) + result["last_imaged"] = datetime.fromisoformat(latest) except (ValueError, TypeError): pass return result - async def on_volume_acquired(self, embryo_id: str, timepoint: int, - volume_data, volume_path=None): + async def on_volume_acquired( + self, embryo_id: str, timepoint: int, volume_data, volume_path=None + ): """Callback when a volume is acquired.""" embryo = self.experiment.embryos.get(embryo_id) if not embryo: return - if hasattr(volume_data, 'read_volume'): + if hasattr(volume_data, "read_volume"): volume = volume_data.read_volume() else: volume = volume_data @@ -1390,7 +1430,8 @@ async def on_volume_acquired(self, embryo_id: str, timepoint: int, if self.store and self.session_id: try: self.store.register_embryo( - self.session_id, embryo_id, + self.session_id, + embryo_id, position_coarse=embryo.position_coarse or None, position_fine=embryo.position_fine or None, calibration=embryo.calibration, @@ -1407,14 +1448,19 @@ async def on_volume_acquired(self, embryo_id: str, timepoint: int, } if volume_path is not None: stored_path = self.store.register_volume( - self.session_id, embryo_id, timepoint, + self.session_id, + embryo_id, + timepoint, incoming_path=Path(volume_path), metadata=acq_metadata, volume_data=volume, ) else: stored_path = self.store.put_volume( - self.session_id, embryo_id, timepoint, volume, + self.session_id, + embryo_id, + timepoint, + volume, metadata=acq_metadata, ) except StorageError: @@ -1429,9 +1475,9 @@ async def on_volume_acquired(self, embryo_id: str, timepoint: int, if self.viz_server and volume is not None: try: from gently.core.imaging import ( - projection_three_view, - compute_crop_bounds, apply_crop_bounds, + compute_crop_bounds, + projection_three_view, ) view_a = volume[0] if volume.ndim == 4 else volume @@ -1439,14 +1485,18 @@ async def on_volume_acquired(self, embryo_id: str, timepoint: int, if view_a.ndim == 3: z_depth, height, width = view_a.shape if width > height * 2: - view_a = view_a[:, :, :width // 2] + view_a = view_a[:, :, : width // 2] bounds = compute_crop_bounds(view_a) cropped = apply_crop_bounds(view_a, bounds) three_view_img, _ = projection_three_view(cropped) else: three_view_img = view_a.astype(np.float32) if three_view_img.max() > three_view_img.min(): - three_view_img = (three_view_img - three_view_img.min()) / (three_view_img.max() - three_view_img.min()) * 255 + three_view_img = ( + (three_view_img - three_view_img.min()) + / (three_view_img.max() - three_view_img.min()) + * 255 + ) three_view_img = three_view_img.astype(np.uint8) self.push_viz( @@ -1454,29 +1504,32 @@ async def on_volume_acquired(self, embryo_id: str, timepoint: int, uid=projection_uid, data_type="volume_projection", metadata={ - 'embryo_id': embryo_id, - 'timepoint': timepoint, - 'shape': list(volume.shape), - 'projection_uid': projection_uid, - 'volume_uid': volume_uid, - 'projection_type': 'three_view', - } + "embryo_id": embryo_id, + "timepoint": timepoint, + "shape": list(volume.shape), + "projection_uid": projection_uid, + "volume_uid": volume_uid, + "projection_type": "three_view", + }, ) except Exception as e: logger.warning(f"Failed to push to viz: {e}") - self._emit_event(EventType.VOLUME_ACQUIRED, { - 'embryo_id': embryo_id, - 'timepoint': timepoint, - 'volume_uid': volume_uid, - 'projection_uid': projection_uid, - 'volume_path': str(stored_path) if stored_path else None, - 'shape': list(volume.shape), - }) + self._emit_event( + EventType.VOLUME_ACQUIRED, + { + "embryo_id": embryo_id, + "timepoint": timepoint, + "volume_uid": volume_uid, + "projection_uid": projection_uid, + "volume_path": str(stored_path) if stored_path else None, + "shape": list(volume.shape), + }, + ) return { - 'volume_uid': volume_uid, - 'projection_uid': projection_uid, + "volume_uid": volume_uid, + "projection_uid": projection_uid, } def should_stop_experiment(self) -> bool: @@ -1485,22 +1538,31 @@ def should_stop_experiment(self) -> bool: return False return all(e.should_skip for e in self.experiment.embryos.values()) - def get_embryo_acquisition_order(self) -> List[str]: + def get_embryo_acquisition_order(self) -> list[str]: """Get embryo acquisition order based on priority.""" - high = [e.id for e in self.experiment.embryos.values() if e.priority == "high" and not e.should_skip] - normal = [e.id for e in self.experiment.embryos.values() if e.priority == "normal" and not e.should_skip] - low = [e.id for e in self.experiment.embryos.values() if e.priority == "low" and not e.should_skip] + high = [ + e.id + for e in self.experiment.embryos.values() + if e.priority == "high" and not e.should_skip + ] + normal = [ + e.id + for e in self.experiment.embryos.values() + if e.priority == "normal" and not e.should_skip + ] + low = [ + e.id + for e in self.experiment.embryos.values() + if e.priority == "low" and not e.should_skip + ] return high + normal + low - def decide_parameters(self, embryo_id: str, timepoint: int) -> Dict: + def decide_parameters(self, embryo_id: str, timepoint: int) -> dict: """Get current acquisition parameters for embryo.""" embryo = self.experiment.embryos.get(embryo_id) if not embryo: - return {'num_slices': 50, 'exposure_ms': 10.0} - return { - 'num_slices': embryo.num_slices, - 'exposure_ms': embryo.exposure_ms - } + return {"num_slices": 50, "exposure_ms": 10.0} + return {"num_slices": embryo.num_slices, "exposure_ms": embryo.exposure_ms} def decide_next_interval(self, timepoint: int) -> float: """Decide interval until next timepoint.""" @@ -1525,8 +1587,9 @@ async def check_blank_image( logger.warning(f"[BLANK_CHECK] {embryo_id}: Numerical check indicates blank image") return True - import io import base64 + import io + from PIL import Image if max_proj.max() > 0: @@ -1536,10 +1599,11 @@ async def check_blank_image( img = Image.fromarray(normalized) buffer = io.BytesIO() - img.save(buffer, format='PNG') + img.save(buffer, format="PNG") b64_image = base64.b64encode(buffer.getvalue()).decode() - prompt = """Look at this microscopy image. Is this a VALID microscopy image or a BLANK/CORRUPTED image? + prompt = """\ +Look at this microscopy image. Is this a VALID microscopy image or a BLANK/CORRUPTED image? A BLANK or CORRUPTED image shows: - Mostly uniform gray/black with no structure @@ -1557,20 +1621,22 @@ async def check_blank_image( self.claude.messages.create, model=settings.models.fast, max_tokens=10, - messages=[{ - "role": "user", - "content": [ - {"type": "text", "text": prompt}, - { - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": b64_image - } - } - ] - }] + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": prompt}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": b64_image, + }, + }, + ], + } + ], ) result = response.content[0].text.strip().upper() @@ -1581,7 +1647,11 @@ async def check_blank_image( return is_blank - except (anthropic.APIConnectionError, anthropic.RateLimitError, anthropic.APIStatusError) as e: + except ( + anthropic.APIConnectionError, + anthropic.RateLimitError, + anthropic.APIStatusError, + ) as e: logger.error(f"[BLANK_CHECK] Claude API error for {embryo_id}: {e}") return False except Exception as e: diff --git a/gently/app/detectors/hatching.py b/gently/app/detectors/hatching.py index ca63acd3..6dc0705d 100644 --- a/gently/app/detectors/hatching.py +++ b/gently/app/detectors/hatching.py @@ -14,7 +14,7 @@ import asyncio import logging import time -from typing import Any, Dict, Optional +from typing import Any import numpy as np @@ -24,7 +24,9 @@ logger = logging.getLogger(__name__) -_HATCHING_PROMPT = """You are observing a C. elegans embryo on a microscope. Decide whether the embryo has HATCHED, then record your decision with the record_hatching tool. +_HATCHING_PROMPT = """\ +You are observing a C. elegans embryo on a microscope. Decide whether the embryo has HATCHED, +then record your decision with the record_hatching tool. A HATCHED embryo: - Has visibly broken out of the eggshell @@ -69,19 +71,21 @@ class HatchingDetector(Detector): name = "hatching" - def __init__(self, claude_client=None, model: Optional[str] = None): + def __init__(self, claude_client=None, model: str | None = None): self._claude = claude_client self._model = model async def run( self, volume: np.ndarray, - context: Dict[str, Any], + context: dict[str, Any], ) -> DetectorResult: - from gently.settings import settings import json + import anthropic + from gently.settings import settings + embryo_id = context.get("embryo_id", "?") timepoint = int(context.get("timepoint", 0)) start = time.time() @@ -113,24 +117,28 @@ async def run( max_tokens=256, tools=[_HATCHING_TOOL], tool_choice={"type": "tool", "name": _HATCHING_TOOL["name"]}, - messages=[{ - "role": "user", - "content": [ - {"type": "text", "text": _HATCHING_PROMPT}, - {"type": "image", "source": { - "type": "base64", - "media_type": "image/png", - "data": b64_image, - }}, - ], - }], + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": _HATCHING_PROMPT}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": b64_image, + }, + }, + ], + } + ], ) # Forced tool_choice guarantees a tool_use block; read its parsed # input directly. No regex, no JSON-from-prose fallback. tool_input = next( - (b.input for b in response.content - if getattr(b, "type", None) == "tool_use"), + (b.input for b in response.content if getattr(b, "type", None) == "tool_use"), None, ) @@ -156,7 +164,11 @@ async def run( error=err, ) - except (anthropic.APIConnectionError, anthropic.RateLimitError, anthropic.APIStatusError) as e: + except ( + anthropic.APIConnectionError, + anthropic.RateLimitError, + anthropic.APIStatusError, + ) as e: logger.error("[%s] Claude API error for %s: %s", self.name, embryo_id, e) return DetectorResult( detector_name=self.name, diff --git a/gently/hardware/dispim/sam_detection.py b/gently/hardware/dispim/sam_detection.py index 7014338a..45355438 100644 --- a/gently/hardware/dispim/sam_detection.py +++ b/gently/hardware/dispim/sam_detection.py @@ -5,30 +5,28 @@ Returns embryo positions (pixel + stage coordinates) for calibration workflow. """ -import logging -import time +import base64 import json +import logging +import os import uuid -import numpy as np +from io import BytesIO from pathlib import Path + +import anthropic import cv2 -import base64 -from io import BytesIO +import numpy as np from PIL import Image -import anthropic -from typing import Dict, List, Tuple, Optional -import os - -from gently.settings import settings - -logger = logging.getLogger(__name__) from gently.core.coordinates import ( - pixel_to_stage_position, - get_um_per_pixel, - DEFAULT_PIXEL_SIZE_UM, DEFAULT_OBJECTIVE_MAG, + DEFAULT_PIXEL_SIZE_UM, + get_um_per_pixel, + pixel_to_stage_position, ) +from gently.settings import settings + +logger = logging.getLogger(__name__) class SAMEmbryoDetector: @@ -42,11 +40,13 @@ class SAMEmbryoDetector: - Returns embryo positions as simple list of coordinates """ - def __init__(self, - sam_checkpoint: str = "sam_vit_b_01ec64.pth", - sam_model_type: str = "vit_b", - device: str = "cpu", - anthropic_api_key: Optional[str] = None): + def __init__( + self, + sam_checkpoint: str = "sam_vit_b_01ec64.pth", + sam_model_type: str = "vit_b", + device: str = "cpu", + anthropic_api_key: str | None = None, + ): """ Initialize SAM detector @@ -85,7 +85,7 @@ def _load_sam(self): if self._mask_generator is not None: return - from segment_anything import sam_model_registry, SamAutomaticMaskGenerator, SamPredictor + from segment_anything import SamAutomaticMaskGenerator, SamPredictor, sam_model_registry if not Path(self.sam_checkpoint).exists(): raise FileNotFoundError(f"SAM checkpoint not found: {self.sam_checkpoint}") @@ -108,13 +108,15 @@ def _load_sam(self): self._predictor = SamPredictor(sam) logger.info("SAM model loaded") - def preprocess_image(self, - image: np.ndarray, - bg_kernel_size: int = 150, - use_clahe: bool = True, - clahe_clip_limit: float = 3.0, - clahe_tile_size: int = 16, - gaussian_sigma: float = 2.0) -> np.ndarray: + def preprocess_image( + self, + image: np.ndarray, + bg_kernel_size: int = 150, + use_clahe: bool = True, + clahe_clip_limit: float = 3.0, + clahe_tile_size: int = 16, + gaussian_sigma: float = 2.0, + ) -> np.ndarray: """ Preprocess image for better SAM detection. @@ -151,14 +153,18 @@ def preprocess_image(self, # This stretches the narrow range (e.g., 84-354) to full 0-255 logger.debug("Percentile normalization (2-98%%)...") p2, p98 = np.percentile(image, (2, 98)) - img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype(np.uint8) + img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype( + np.uint8 + ) logger.debug("Normalized to 0-255") # Step 2: Background subtraction with large morphological opening # Removes large-scale illumination variations if bg_kernel_size > 0: logger.debug("Background subtraction (kernel=%d)...", bg_kernel_size) - kernel_bg = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (bg_kernel_size, bg_kernel_size)) + kernel_bg = cv2.getStructuringElement( + cv2.MORPH_ELLIPSE, (bg_kernel_size, bg_kernel_size) + ) background = cv2.morphologyEx(img_norm, cv2.MORPH_OPEN, kernel_bg) img_no_bg = cv2.subtract(img_norm, background) img_no_bg = cv2.normalize(img_no_bg, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) @@ -171,8 +177,7 @@ def preprocess_image(self, if use_clahe: logger.debug("CLAHE (clip=%.1f, tile=%d)...", clahe_clip_limit, clahe_tile_size) clahe = cv2.createCLAHE( - clipLimit=clahe_clip_limit, - tileGridSize=(clahe_tile_size, clahe_tile_size) + clipLimit=clahe_clip_limit, tileGridSize=(clahe_tile_size, clahe_tile_size) ) img_enhanced = clahe.apply(img_no_bg) logger.debug("CLAHE applied") @@ -187,16 +192,20 @@ def preprocess_image(self, else: img_smooth = img_enhanced - logger.debug("Preprocessing complete (output range: %s - %s)", img_smooth.min(), img_smooth.max()) + logger.debug( + "Preprocessing complete (output range: %s - %s)", img_smooth.min(), img_smooth.max() + ) return img_smooth - def find_embryo_candidates(self, - image: np.ndarray, - brightness_percentile: float = 99.0, - min_area: int = 5000, - max_area: int = 150000, - clahe_clip: float = 3.0, - clahe_tile: int = 16) -> Tuple[List[Dict], np.ndarray]: + def find_embryo_candidates( + self, + image: np.ndarray, + brightness_percentile: float = 99.0, + min_area: int = 5000, + max_area: int = 150000, + clahe_clip: float = 3.0, + clahe_tile: int = 16, + ) -> tuple[list[dict], np.ndarray]: """ Find embryo candidates using brightness-based detection. @@ -229,12 +238,16 @@ def find_embryo_candidates(self, enhanced_image : np.ndarray Contrast-enhanced 8-bit image for SAM """ - logger.info("Finding embryo candidates (brightness percentile=%.1f)...", brightness_percentile) + logger.info( + "Finding embryo candidates (brightness percentile=%.1f)...", brightness_percentile + ) logger.debug("Input range: %s - %s", image.min(), image.max()) # Step 1: Percentile normalization (handles low dynamic range) p2, p98 = np.percentile(image, (2, 98)) - img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype(np.uint8) + img_norm = np.clip((image.astype(np.float32) - p2) / (p98 - p2) * 255, 0, 255).astype( + np.uint8 + ) logger.debug("Normalized to 0-255") # Step 2: CLAHE for local contrast enhancement @@ -261,7 +274,9 @@ def find_embryo_candidates(self, logger.debug("Morphological cleanup complete") # Step 7: Find connected components and filter by area - num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, connectivity=8) + num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats( + mask, connectivity=8 + ) candidates = [] for i in range(1, num_labels): # Skip background (label 0) @@ -273,19 +288,14 @@ def find_embryo_candidates(self, h = stats[i, cv2.CC_STAT_HEIGHT] cx, cy = centroids[i] - candidates.append({ - 'bbox': (x, y, w, h), - 'centroid': (cx, cy), - 'area': area - }) + candidates.append({"bbox": (x, y, w, h), "centroid": (cx, cy), "area": area}) logger.info("Found %d embryo candidates", len(candidates)) return candidates, img_smooth - def refine_with_sam(self, - image: np.ndarray, - candidates: List[Dict], - padding: int = 20) -> List[Dict]: + def refine_with_sam( + self, image: np.ndarray, candidates: list[dict], padding: int = 20 + ) -> list[dict]: """ Refine embryo candidates using SAM with bounding box prompts. @@ -322,7 +332,7 @@ def refine_with_sam(self, h, w = image.shape[:2] for i, candidate in enumerate(candidates): - x, y, bw, bh = candidate['bbox'] + x, y, bw, bh = candidate["bbox"] # Add padding and clip to image bounds x1 = max(0, x - padding) @@ -335,10 +345,7 @@ def refine_with_sam(self, # Get SAM prediction with box prompt masks, scores, _ = self._predictor.predict( - point_coords=None, - point_labels=None, - box=input_box, - multimask_output=True + point_coords=None, point_labels=None, box=input_box, multimask_output=True ) # Take best mask (highest score) @@ -348,9 +355,7 @@ def refine_with_sam(self, # Calculate properties from SAM mask contours, _ = cv2.findContours( - mask.astype(np.uint8), - cv2.RETR_EXTERNAL, - cv2.CHAIN_APPROX_SIMPLE + mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) if contours: @@ -364,41 +369,47 @@ def refine_with_sam(self, cx = M["m10"] / M["m00"] cy = M["m01"] / M["m00"] else: - cx, cy = candidate['centroid'] + cx, cy = candidate["centroid"] # Calculate circularity perimeter = cv2.arcLength(contour, True) - circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0 + circularity = 4 * np.pi * area / (perimeter**2) if perimeter > 0 else 0 # Get bounding box from contour bx, by, bw, bh = cv2.boundingRect(contour) - embryos.append({ - 'embryo_id': f'embryo_{i + 1}', - 'uid': str(uuid.uuid4()), # Global unique identifier for cross-session tracking - 'pixel_x': float(cx), - 'pixel_y': float(cy), - 'bbox': (bx, by, bw, bh), # Used by visualization functions - 'area_pixels': int(area), - 'circularity': float(circularity), - 'confidence': float(score), - 'mask': mask - }) + embryos.append( + { + "embryo_id": f"embryo_{i + 1}", + "uid": str( + uuid.uuid4() + ), # Global unique identifier for cross-session tracking + "pixel_x": float(cx), + "pixel_y": float(cy), + "bbox": (bx, by, bw, bh), # Used by visualization functions + "area_pixels": int(area), + "circularity": float(circularity), + "confidence": float(score), + "mask": mask, + } + ) logger.info("SAM refined %d embryos", len(embryos)) return embryos - async def detect_embryos(self, - image: np.ndarray, - stage_position: Tuple[float, float], - pixel_size_um: float = DEFAULT_PIXEL_SIZE_UM, - objective_mag: float = DEFAULT_OBJECTIVE_MAG, - use_claude_review: bool = True, - save_visualizations: bool = True, - output_dir: Optional[Path] = None, - brightness_percentile: float = 99.0, - min_area: int = 5000, - max_area: int = 150000) -> Dict: + async def detect_embryos( + self, + image: np.ndarray, + stage_position: tuple[float, float], + pixel_size_um: float = DEFAULT_PIXEL_SIZE_UM, + objective_mag: float = DEFAULT_OBJECTIVE_MAG, + use_claude_review: bool = True, + save_visualizations: bool = True, + output_dir: Path | None = None, + brightness_percentile: float = 99.0, + min_area: int = 5000, + max_area: int = 150000, + ) -> dict: """ Detect embryos using brightness-based detection + SAM refinement. @@ -462,20 +473,17 @@ async def detect_embryos(self, # Step 1: Find candidates using brightness detection logger.info("[1/4] Finding embryo candidates (brightness-based)...") candidates, image_enhanced = self.find_embryo_candidates( - image, - brightness_percentile=brightness_percentile, - min_area=min_area, - max_area=max_area + image, brightness_percentile=brightness_percentile, min_area=min_area, max_area=max_area ) if len(candidates) == 0: logger.warning("No embryo candidates found!") return { - 'embryos': [], - 'initial_detections': 0, - 'final_detections': 0, - 'verification': {'verified': False}, - 'images': {} + "embryos": [], + "initial_detections": 0, + "final_detections": 0, + "verification": {"verified": False}, + "images": {}, } # Step 2: Refine with SAM @@ -489,11 +497,11 @@ async def detect_embryos(self, if len(embryos_sam) == 0: logger.warning("No embryos detected by SAM!") return { - 'embryos': [], - 'initial_detections': 0, - 'final_detections': 0, - 'verification': {'verified': False}, - 'images': {} + "embryos": [], + "initial_detections": 0, + "final_detections": 0, + "verification": {"verified": False}, + "images": {}, } # Save initial detection @@ -503,8 +511,8 @@ async def detect_embryos(self, # Claude review (if enabled) embryos_final = embryos_sam - verification = {'verified': True, 'skipped': not use_claude_review} - changes = {'round1': {'removed': [], 'added': []}} + verification = {"verified": True, "skipped": not use_claude_review} + changes = {"round1": {"removed": [], "added": []}} if use_claude_review and self.claude_client: logger.info("[2/4] Claude Vision review (Round 1)...") @@ -512,7 +520,7 @@ async def detect_embryos(self, review_r1 = await self._review_with_claude(image_8bit, annotated, embryos_sam) logger.info("[3/4] Applying corrections...") - embryos_r1, changes['round1'] = self._apply_corrections( + embryos_r1, changes["round1"] = self._apply_corrections( embryos_sam, review_r1, image, self._predictor ) @@ -524,22 +532,22 @@ async def detect_embryos(self, logger.info("[4/4] Claude verification (Round 2)...") r1_viz = self._create_annotated_image(image_8bit, embryos_r1) verification = await self._verify_with_claude( - image_8bit, r1_viz, embryos_r1, changes['round1'] + image_8bit, r1_viz, embryos_r1, changes["round1"] ) # Apply round 2 corrections if needed has_r2_changes = ( - len(verification.get('additional_false_positives', [])) > 0 or - len(verification.get('additional_false_negatives', [])) > 0 + len(verification.get("additional_false_positives", [])) > 0 + or len(verification.get("additional_false_negatives", [])) > 0 ) if has_r2_changes: logger.info("Applying Round 2 corrections...") review_r2 = { - 'false_positives': verification.get('additional_false_positives', []), - 'false_negatives': verification.get('additional_false_negatives', []) + "false_positives": verification.get("additional_false_positives", []), + "false_negatives": verification.get("additional_false_negatives", []), } - embryos_final, changes['round2'] = self._apply_corrections( + embryos_final, changes["round2"] = self._apply_corrections( embryos_r1, review_r2, image, self._predictor ) else: @@ -553,7 +561,7 @@ async def detect_embryos(self, stage_position, pixel_size_um, objective_mag, - image_shape=image.shape[:2] # (height, width) + image_shape=image.shape[:2], # (height, width) ) # Save final visualization @@ -563,19 +571,19 @@ async def detect_embryos(self, # Package results results = { - 'embryos': embryo_positions, - 'initial_detections': len(embryos_sam), - 'final_detections': len(embryos_final), - 'verification': verification, - 'changes': changes, - 'images': { - 'initial': str(output_dir / "detection_initial.png"), - 'final': str(output_dir / "detection_final.png") - } + "embryos": embryo_positions, + "initial_detections": len(embryos_sam), + "final_detections": len(embryos_final), + "verification": verification, + "changes": changes, + "images": { + "initial": str(output_dir / "detection_initial.png"), + "final": str(output_dir / "detection_final.png"), + }, } if use_claude_review and save_visualizations: - results['images']['round1'] = str(output_dir / "detection_round1.png") + results["images"]["round1"] = str(output_dir / "detection_round1.png") logger.info("=" * 70) logger.info("DETECTION COMPLETE: %d embryos", len(embryo_positions)) @@ -594,7 +602,7 @@ def _to_rgb8(image: np.ndarray) -> np.ndarray: return cv2.cvtColor(image, cv2.COLOR_GRAY2RGB) return image - def _detect_with_sam(self, image: np.ndarray) -> Tuple[List[Dict], np.ndarray]: + def _detect_with_sam(self, image: np.ndarray) -> tuple[list[dict], np.ndarray]: """Run SAM automatic segmentation (extracted from test script)""" image_rgb = self._to_rgb8(image) @@ -604,14 +612,16 @@ def _detect_with_sam(self, image: np.ndarray) -> Tuple[List[Dict], np.ndarray]: # Filter candidates embryo_candidates = [] for mask_data in masks: - area = mask_data['area'] + area = mask_data["area"] if not (self.min_area <= area <= self.max_area): continue - bbox = mask_data['bbox'] - mask = mask_data['segmentation'] - contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + bbox = mask_data["bbox"] + mask = mask_data["segmentation"] + contours, _ = cv2.findContours( + mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) if len(contours) == 0: continue @@ -620,40 +630,44 @@ def _detect_with_sam(self, image: np.ndarray) -> Tuple[List[Dict], np.ndarray]: if perimeter == 0: continue - circularity = 4 * np.pi * area / (perimeter ** 2) + circularity = 4 * np.pi * area / (perimeter**2) if circularity < self.min_circularity: continue - embryo_candidates.append({ - 'mask': mask, - 'bbox': bbox, - 'area': area, - 'circularity': circularity, - 'stability_score': mask_data['stability_score'], - 'predicted_iou': mask_data['predicted_iou'] - }) + embryo_candidates.append( + { + "mask": mask, + "bbox": bbox, + "area": area, + "circularity": circularity, + "stability_score": mask_data["stability_score"], + "predicted_iou": mask_data["predicted_iou"], + } + ) # Sort by quality and apply spatial separation - embryo_candidates.sort(key=lambda x: (x['area'] * x['stability_score']), reverse=True) + embryo_candidates.sort(key=lambda x: x["area"] * x["stability_score"], reverse=True) selected_embryos = [] for candidate in embryo_candidates: if len(selected_embryos) >= self.max_embryos: break - bbox = candidate['bbox'] + bbox = candidate["bbox"] candidate_center_x = bbox[0] + bbox[2] / 2 candidate_center_y = bbox[1] + bbox[3] / 2 too_close = False for selected in selected_embryos: - sel_bbox = selected['bbox'] + sel_bbox = selected["bbox"] sel_center_x = sel_bbox[0] + sel_bbox[2] / 2 sel_center_y = sel_bbox[1] + sel_bbox[3] / 2 - distance = np.sqrt((candidate_center_x - sel_center_x)**2 + - (candidate_center_y - sel_center_y)**2) + distance = np.sqrt( + (candidate_center_x - sel_center_x) ** 2 + + (candidate_center_y - sel_center_y) ** 2 + ) if distance < self.min_separation_pixels: too_close = True @@ -662,19 +676,27 @@ def _detect_with_sam(self, image: np.ndarray) -> Tuple[List[Dict], np.ndarray]: if not too_close: selected_embryos.append(candidate) - return selected_embryos, image_8bit + return selected_embryos, image_rgb - def _create_annotated_image(self, image: np.ndarray, embryos: List[Dict]) -> np.ndarray: + def _create_annotated_image(self, image: np.ndarray, embryos: list[dict]) -> np.ndarray: """Create annotated image with numbered boxes""" viz = image.copy() if len(viz.shape) == 2: viz = cv2.cvtColor(viz, cv2.COLOR_GRAY2RGB) - colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0), - (255, 0, 255), (0, 255, 255), (128, 128, 0), (128, 0, 128)] + colors = [ + (255, 0, 0), + (0, 255, 0), + (0, 0, 255), + (255, 255, 0), + (255, 0, 255), + (0, 255, 255), + (128, 128, 0), + (128, 0, 128), + ] for i, embryo in enumerate(embryos): - bbox = embryo['bbox'] + bbox = embryo["bbox"] x, y, w, h = bbox color = colors[i % len(colors)] @@ -713,7 +735,7 @@ def _encode_image_base64(self, image: np.ndarray) -> str: buffered = BytesIO() pil_image.save(buffered, format="JPEG", quality=quality, optimize=True) if buffered.tell() <= max_bytes: - return base64.b64encode(buffered.getvalue()).decode('utf-8') + return base64.b64encode(buffered.getvalue()).decode("utf-8") quality -= 5 # Last resort @@ -722,18 +744,21 @@ def _encode_image_base64(self, image: np.ndarray) -> str: pil_image = pil_image.resize(new_size, Image.Resampling.LANCZOS) buffered = BytesIO() pil_image.save(buffered, format="JPEG", quality=85, optimize=True) - return base64.b64encode(buffered.getvalue()).decode('utf-8') + return base64.b64encode(buffered.getvalue()).decode("utf-8") - async def _review_with_claude(self, image: np.ndarray, annotated: np.ndarray, embryos: List[Dict]) -> Dict: + async def _review_with_claude( + self, image: np.ndarray, annotated: np.ndarray, embryos: list[dict] + ) -> dict: """Round 1: Claude reviews detections (from test script)""" if not self.claude_client: - return {'false_positives': [], 'false_negatives': []} + return {"false_positives": [], "false_negatives": []} image_base64 = self._encode_image_base64(annotated) - prompt = f"""You are a microscopy expert analyzing embryo detections from a bottom camera view. + prompt = f"""\ +You are a microscopy expert analyzing embryo detections from a bottom camera view. -CURRENT DETECTIONS: {len(embryos)} embryos labeled 0-{len(embryos)-1} with colored bounding boxes. +CURRENT DETECTIONS: {len(embryos)} embryos labeled 0-{len(embryos) - 1} with colored bounding boxes. EMBRYO CHARACTERISTICS: - Small, BRIGHT white/light gray oval or rice grain shapes @@ -763,14 +788,25 @@ async def _review_with_claude(self, image: np.ndarray, annotated: np.ndarray, em message = self.claude_client.messages.create( model=settings.models.perception, max_tokens=8000, - output_config={"effort": "high"}, # was thinking budget_tokens (Opus 4.8 rejects it) - messages=[{ - "role": "user", - "content": [ - {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_base64}}, - {"type": "text", "text": prompt} - ] - }] + output_config={ + "effort": "high" + }, # was thinking budget_tokens (Opus 4.8 rejects it) + messages=[ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": image_base64, + }, + }, + {"type": "text", "text": prompt}, + ], + } + ], ) response_text = next((b.text for b in message.content if b.type == "text"), "") @@ -787,18 +823,19 @@ async def _review_with_claude(self, image: np.ndarray, annotated: np.ndarray, em except Exception as e: logger.warning("Claude review failed: %s", e) - return {'false_positives': [], 'false_negatives': []} + return {"false_positives": [], "false_negatives": []} - async def _verify_with_claude(self, image: np.ndarray, annotated: np.ndarray, - embryos: List[Dict], previous_changes: Dict) -> Dict: + async def _verify_with_claude( + self, image: np.ndarray, annotated: np.ndarray, embryos: list[dict], previous_changes: dict + ) -> dict: """Round 2: Claude verifies corrections (from test script)""" if not self.claude_client: - return {'verified': True, 'skipped': True} + return {"verified": True, "skipped": True} image_base64 = self._encode_image_base64(annotated) - removed = previous_changes.get('removed', []) - added = previous_changes.get('added', []) + removed = previous_changes.get("removed", []) + added = previous_changes.get("added", []) prompt = f"""VERIFICATION ROUND - You previously reviewed this image. @@ -806,7 +843,7 @@ async def _verify_with_claude(self, image: np.ndarray, annotated: np.ndarray, - Removed: {removed if removed else "none"} - Added: {added if added else "none"} -CURRENT: {len(embryos)} detections (numbered 0-{len(embryos)-1}) +CURRENT: {len(embryos)} detections (numbered 0-{len(embryos) - 1}) TASK: Verify corrections and catch any remaining issues. Only report CLEAR remaining problems. @@ -823,14 +860,25 @@ async def _verify_with_claude(self, image: np.ndarray, annotated: np.ndarray, message = self.claude_client.messages.create( model=settings.models.perception, max_tokens=6000, - output_config={"effort": "high"}, # was thinking budget_tokens (Opus 4.8 rejects it) - messages=[{ - "role": "user", - "content": [ - {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_base64}}, - {"type": "text", "text": prompt} - ] - }] + output_config={ + "effort": "high" + }, # was thinking budget_tokens (Opus 4.8 rejects it) + messages=[ + { + "role": "user", + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": image_base64, + }, + }, + {"type": "text", "text": prompt}, + ], + } + ], ) response_text = next((b.text for b in message.content if b.type == "text"), "") @@ -846,38 +894,41 @@ async def _verify_with_claude(self, image: np.ndarray, annotated: np.ndarray, except Exception as e: logger.warning("Verification failed: %s", e) - return {'verified': False} + return {"verified": False} - def _apply_corrections(self, embryos: List[Dict], review: Dict, - image: np.ndarray, predictor) -> Tuple[List[Dict], Dict]: + def _apply_corrections( + self, embryos: list[dict], review: dict, image: np.ndarray, predictor + ) -> tuple[list[dict], dict]: """Apply Claude's corrections (from test script)""" corrected = [] - changes = {'removed': [], 'added': []} + changes = {"removed": [], "added": []} # Remove false positives - false_positives = set(review.get('false_positives', [])) + false_positives = set(review.get("false_positives", [])) if false_positives: - changes['removed'] = list(false_positives) + changes["removed"] = list(false_positives) for i, embryo in enumerate(embryos): if i not in false_positives: corrected.append(embryo) # Add false negatives - false_negatives = review.get('false_negatives', []) + false_negatives = review.get("false_negatives", []) if false_negatives: for fn in false_negatives: - point = (fn['x'], fn['y']) + point = (fn["x"], fn["y"]) new_embryo = self._segment_with_sam(image, predictor, point) - if new_embryo and (self.min_area <= new_embryo['area'] <= self.max_area and - new_embryo['circularity'] >= self.min_circularity): + if new_embryo and ( + self.min_area <= new_embryo["area"] <= self.max_area + and new_embryo["circularity"] >= self.min_circularity + ): corrected.append(new_embryo) - changes['added'].append(point) + changes["added"].append(point) return corrected, changes - def _segment_with_sam(self, image: np.ndarray, predictor, point: Tuple) -> Optional[Dict]: + def _segment_with_sam(self, image: np.ndarray, predictor, point: tuple) -> dict | None: """Use SAM predictor to segment region (from test script)""" image_rgb = self._to_rgb8(image) predictor.set_image(image_rgb) @@ -886,9 +937,7 @@ def _segment_with_sam(self, image: np.ndarray, predictor, point: Tuple) -> Optio point_labels = np.array([1]) masks, scores, _ = predictor.predict( - point_coords=point_coords, - point_labels=point_labels, - multimask_output=True + point_coords=point_coords, point_labels=point_labels, multimask_output=True ) best_idx = np.argmax(scores) @@ -903,26 +952,33 @@ def _segment_with_sam(self, image: np.ndarray, predictor, point: Tuple) -> Optio bbox = [x_min, y_min, x_max - x_min, y_max - y_min] area = mask.sum() - contours, _ = cv2.findContours(mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + contours, _ = cv2.findContours( + mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE + ) if len(contours) > 0: perimeter = cv2.arcLength(contours[0], True) - circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0 + circularity = 4 * np.pi * area / (perimeter**2) if perimeter > 0 else 0 else: circularity = 0 return { - 'mask': mask, - 'bbox': bbox, - 'area': int(area), - 'circularity': float(circularity), - 'stability_score': float(scores[best_idx]), - 'predicted_iou': float(scores[best_idx]) + "mask": mask, + "bbox": bbox, + "area": int(area), + "circularity": float(circularity), + "stability_score": float(scores[best_idx]), + "predicted_iou": float(scores[best_idx]), } - def _pixel_to_stage_coordinates(self, embryos: List[Dict], stage_pos: Tuple[float, float], - pixel_size_um: float, objective_mag: float, - image_shape: Tuple[int, int] = (2048, 2048)) -> List[Dict]: + def _pixel_to_stage_coordinates( + self, + embryos: list[dict], + stage_pos: tuple[float, float], + pixel_size_um: float, + objective_mag: float, + image_shape: tuple[int, int] = (2048, 2048), + ) -> list[dict]: """ Convert pixel coordinates to stage coordinates. @@ -938,7 +994,7 @@ def _pixel_to_stage_coordinates(self, embryos: List[Dict], stage_pos: Tuple[floa embryo_positions = [] for i, embryo in enumerate(embryos): - bbox = embryo['bbox'] + bbox = embryo["bbox"] x, y, w, h = bbox center_x_px = x + w / 2 @@ -953,24 +1009,26 @@ def _pixel_to_stage_coordinates(self, embryos: List[Dict], stage_pos: Tuple[floa image_center_y=image_center_y, stage_x=stage_x, stage_y=stage_y, - um_per_pixel=effective_pixel_um + um_per_pixel=effective_pixel_um, ) - embryo_positions.append({ - 'embryo_id': f'embryo_{i + 1}', - 'pixel_x': float(center_x_px), - 'pixel_y': float(center_y_px), - 'stage_x_um': float(embryo_stage_x), - 'stage_y_um': float(embryo_stage_y), - 'bbox_pixel': tuple(bbox), - 'area_pixels': embryo.get('area_pixels', embryo.get('area', 0)), - 'circularity': embryo.get('circularity', 0), - 'confidence': embryo.get('confidence', embryo.get('stability_score', 0)) - }) + embryo_positions.append( + { + "embryo_id": f"embryo_{i + 1}", + "pixel_x": float(center_x_px), + "pixel_y": float(center_y_px), + "stage_x_um": float(embryo_stage_x), + "stage_y_um": float(embryo_stage_y), + "bbox_pixel": tuple(bbox), + "area_pixels": embryo.get("area_pixels", embryo.get("area", 0)), + "circularity": embryo.get("circularity", 0), + "confidence": embryo.get("confidence", embryo.get("stability_score", 0)), + } + ) return embryo_positions - def show_in_napari(self, image: np.ndarray, embryos: List[Dict], block: bool = False): + def show_in_napari(self, image: np.ndarray, embryos: list[dict], block: bool = False): """Deprecated: napari display was retired in Phase 1. SAM detection results are now reviewed via the web map view — diff --git a/gently/harness/conversation.py b/gently/harness/conversation.py index 29573e6a..9085e96b 100644 --- a/gently/harness/conversation.py +++ b/gently/harness/conversation.py @@ -10,14 +10,14 @@ import logging import re import time -from typing import Dict, List, Optional, Any +from typing import Any from ..settings import settings logger = logging.getLogger(__name__) -def _extend_tool_calls(out: List[Dict[str, Any]], content_blocks) -> None: +def _extend_tool_calls(out: list[dict[str, Any]], content_blocks) -> None: """Append every tool_use block in content_blocks to out. Tolerates absent attributes (some SDK versions / mock objects) so it @@ -29,11 +29,13 @@ def _extend_tool_calls(out: List[Dict[str, Any]], content_blocks) -> None: try: if getattr(block, "type", None) != "tool_use": continue - out.append({ - "name": getattr(block, "name", None), - "input": getattr(block, "input", None), - "id": getattr(block, "id", None), - }) + out.append( + { + "name": getattr(block, "name", None), + "input": getattr(block, "input", None), + "id": getattr(block, "id", None), + } + ) except Exception: continue @@ -55,7 +57,7 @@ def __init__(self, client, model, tool_registry): self._tool_registry = tool_registry # Conversation state - self.conversation_history: List[Dict] = [] + self.conversation_history: list[dict] = [] # Token counters self.total_input_tokens: int = 0 @@ -76,8 +78,9 @@ def __init__(self, client, model, tool_registry): # ===== Quick Response ===== - def try_quick_response(self, message: str, experiment, mode: str, - enter_plan_fn, exit_plan_fn) -> Optional[str]: + def try_quick_response( + self, message: str, experiment, mode: str, enter_plan_fn, exit_plan_fn + ) -> str | None: """ Answer simple queries from state without LLM call. @@ -106,7 +109,13 @@ def try_quick_response(self, message: str, experiment, mode: str, return experiment.get_summary() # Plan mode switching via natural language - plan_enter_phrases = ("plan mode", "enter plan", "switch to plan", "let's plan", "design an experiment") + plan_enter_phrases = ( + "plan mode", + "enter plan", + "switch to plan", + "let's plan", + "design an experiment", + ) plan_exit_phrases = ("exit plan", "leave plan", "back to run", "run mode") if mode != "plan" and any(p in message_lower for p in plan_enter_phrases): @@ -142,22 +151,25 @@ def should_use_thinking(self, message: str, mode: str) -> bool: if mode == "plan": return True - import re msg_lower = message.lower() - if re.search(r'\bthink(ing)?\b', message, re.IGNORECASE): + if re.search(r"\bthink(ing)?\b", message, re.IGNORECASE): return True - if re.search(r'\bcalibrat', msg_lower): + if re.search(r"\bcalibrat", msg_lower): return True - if re.search(r'\b(plan|timelapse|time-lapse|acquisition)\b', msg_lower): + if re.search(r"\b(plan|timelapse|time-lapse|acquisition)\b", msg_lower): return True - if re.search(r'\b(analy[sz]e|look at|check|inspect|review).*(image|volume|embryo)', msg_lower): + if re.search( + r"\b(analy[sz]e|look at|check|inspect|review).*(image|volume|embryo)", msg_lower + ): return True - if re.search(r'\b(all|every|each)\s+(embryo|sample)', msg_lower): + if re.search(r"\b(all|every|each)\s+(embryo|sample)", msg_lower): return True - if re.search(r'\b(first|then|after|next|finally)\b.*\b(first|then|after|next|finally)\b', msg_lower): + if re.search( + r"\b(first|then|after|next|finally)\b.*\b(first|then|after|next|finally)\b", msg_lower + ): return True - if re.search(r'\b(why|problem|issue|error|wrong|fail|debug|troubleshoot)', msg_lower): + if re.search(r"\b(why|problem|issue|error|wrong|fail|debug|troubleshoot)", msg_lower): return True return False @@ -172,6 +184,7 @@ async def _create_with_refusal_fallback(self, api_kwargs): working whether or not Fable 5 is currently serviceable. The moment the org retention is fixed, Fable 5 serves with no code change.""" from anthropic import BadRequestError + fb = settings.models.refusal_fallback model = api_kwargs.get("model") try: @@ -180,7 +193,9 @@ async def _create_with_refusal_fallback(self, api_kwargs): if not fb or fb == model: raise logger.warning("Model %s rejected the request (400); falling back to %s", model, fb) - return await self._call_api_with_retry(self.claude.messages.create, **{**api_kwargs, "model": fb}) + return await self._call_api_with_retry( + self.claude.messages.create, **{**api_kwargs, "model": fb} + ) if response.stop_reason == "refusal" and fb and fb != model: logger.warning("Model %s declined the turn; retrying on %s", model, fb) response = await self._call_api_with_retry( @@ -188,8 +203,9 @@ async def _create_with_refusal_fallback(self, api_kwargs): ) return response - async def call_claude(self, user_message: str, system_prompt, tools, - mode: str, auto_save_fn) -> str: + async def call_claude( + self, user_message: str, system_prompt, tools, mode: str, auto_save_fn + ) -> str: """ Call Claude API with full context and tool access (non-streaming). @@ -221,8 +237,8 @@ async def call_claude(self, user_message: str, system_prompt, tools, interaction = self.interaction_logger.start_interaction( user_prompt=user_message, system_state={ - 'acquisition_status': 'unknown', - } + "acquisition_status": "unknown", + }, ) # Snapshot inputs for decision capture BEFORE the tool loop starts @@ -233,13 +249,15 @@ async def call_claude(self, user_message: str, system_prompt, tools, if self.decision_log is not None: try: from gently.eval import prompt_hash as _prompt_hash + decision_prompt_hash = _prompt_hash( - system_prompt, list(self.conversation_history), + system_prompt, + list(self.conversation_history), ) except Exception: logger.exception("Failed to compute decision prompt_hash") - tool_calls_collected: List[Dict[str, Any]] = [] + tool_calls_collected: list[dict[str, Any]] = [] assistant_message = "" error_occurred = None @@ -262,18 +280,10 @@ async def call_claude(self, user_message: str, system_prompt, tools, # Process tool calls while response.stop_reason == "tool_use": - tool_results = await self._execute_tools_with_logging( - response.content, interaction - ) + tool_results = await self._execute_tools_with_logging(response.content, interaction) - self.conversation_history.append({ - "role": "assistant", - "content": response.content - }) - self.conversation_history.append({ - "role": "user", - "content": tool_results - }) + self.conversation_history.append({"role": "assistant", "content": response.content}) + self.conversation_history.append({"role": "user", "content": tool_results}) api_kwargs["messages"] = self.conversation_history response = await self._create_with_refusal_fallback(api_kwargs) @@ -283,20 +293,20 @@ async def call_claude(self, user_message: str, system_prompt, tools, # Extract text response. Fable 5 may refuse (stop_reason="refusal") # with empty content — surface it instead of returning blank. if response.stop_reason == "refusal": - assistant_message = "(The request was declined by the model's safety system. Try rephrasing.)" + assistant_message = ( + "(The request was declined by the model's safety system. Try rephrasing.)" + ) else: assistant_message = "" for block in response.content: - if hasattr(block, 'text'): + if hasattr(block, "text"): assistant_message += block.text - self.conversation_history.append({ - "role": "assistant", - "content": response.content - }) + self.conversation_history.append({"role": "assistant", "content": response.content}) except Exception as e: import traceback + error_occurred = str(e) error_tb = traceback.format_exc() assistant_message = f"Error: {error_occurred}" @@ -343,11 +353,11 @@ def _write_production_decision( self, *, user_message: str, - tool_calls: List[Dict[str, Any]], + tool_calls: list[dict[str, Any]], response_text: str, duration_ms: float, - prompt_hash_value: Optional[str], - error: Optional[str], + prompt_hash_value: str | None, + error: str | None, ) -> None: """Persist one production Decision row (best-effort). @@ -359,24 +369,28 @@ def _write_production_decision( return try: from datetime import datetime + from gently.eval import Decision, DecisionTrigger - self.decision_log.append(Decision( - timestamp=datetime.now(), - agent="production", - trigger=DecisionTrigger.USER_MESSAGE, - trigger_detail=(user_message or "")[:200], - tool_calls=tool_calls, - response_text=response_text, - prompt_hash=prompt_hash_value, - duration_ms=duration_ms, - error=error, - )) + + self.decision_log.append( + Decision( + timestamp=datetime.now(), + agent="production", + trigger=DecisionTrigger.USER_MESSAGE, + trigger_detail=(user_message or "")[:200], + tool_calls=tool_calls, + response_text=response_text, + prompt_hash=prompt_hash_value, + duration_ms=duration_ms, + error=error, + ) + ) except Exception: logger.exception("Failed to write production Decision") # ===== Dry-Run Tool Call (Benchmarking) ===== - async def get_tool_call(self, user_message: str, system_prompt, tools) -> Optional[Dict]: + async def get_tool_call(self, user_message: str, system_prompt, tools) -> dict | None: """ Get what tool Claude would call without executing it (dry-run mode). @@ -399,10 +413,7 @@ async def get_tool_call(self, user_message: str, system_prompt, tools) -> Option start_time = time.time() messages = self.conversation_history.copy() - messages.append({ - "role": "user", - "content": user_message - }) + messages.append({"role": "user", "content": user_message}) try: api_kwargs = { @@ -413,15 +424,12 @@ async def get_tool_call(self, user_message: str, system_prompt, tools) -> Option "max_tokens": 4096, } - response = await self._call_api_with_retry( - self.claude.messages.create, - **api_kwargs - ) + response = await self._call_api_with_retry(self.claude.messages.create, **api_kwargs) latency_ms = (time.time() - start_time) * 1000 - input_tokens = getattr(response.usage, 'input_tokens', 0) - output_tokens = getattr(response.usage, 'output_tokens', 0) + input_tokens = getattr(response.usage, "input_tokens", 0) + output_tokens = getattr(response.usage, "output_tokens", 0) # A refusal returns empty content — treat as "no tool call". if response.stop_reason == "refusal" or not response.content: @@ -444,7 +452,7 @@ async def get_tool_call(self, user_message: str, system_prompt, tools) -> Option # ===== Tool Execution ===== - async def _execute_tools_with_logging(self, content_blocks, interaction) -> List[Dict]: + async def _execute_tools_with_logging(self, content_blocks, interaction) -> list[dict]: """ Execute Claude's tool calls with interaction logging. @@ -476,7 +484,10 @@ async def _execute_tools_with_logging(self, content_blocks, interaction) -> List if self.choice_handler and isinstance(result, str): try: choice_data = json.loads(result) - if isinstance(choice_data, dict) and choice_data.get("_type") == CHOICE_RESPONSE_TYPE: + if ( + isinstance(choice_data, dict) + and choice_data.get("_type") == CHOICE_RESPONSE_TYPE + ): user_selection = await self.choice_handler(choice_data) result = user_selection except (json.JSONDecodeError, TypeError): @@ -500,19 +511,20 @@ async def _execute_tools_with_logging(self, content_blocks, interaction) -> List error_message=error_message, ) - results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": result, - "is_error": is_error, - }) + results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": result, + "is_error": is_error, + } + ) return results # ===== Streaming API Call ===== - async def call_claude_stream(self, system_prompt, tools, - tool_label_fn, auto_save_fn): + async def call_claude_stream(self, system_prompt, tools, tool_label_fn, auto_save_fn): """ Call Claude API with streaming enabled. @@ -542,11 +554,12 @@ def _run(m): system=system_prompt, messages=self.conversation_history, tools=tools, - max_tokens=4096 + max_tokens=4096, ) as stream: for event in stream: events.append(event) return events, stream.get_final_message() + try: return _run(model) except BadRequestError: @@ -555,7 +568,9 @@ def _run(m): fb = settings.models.refusal_fallback if not fb or fb == model: raise - logger.warning("Stream model %s rejected the request (400); falling back to %s", model, fb) + logger.warning( + "Stream model %s rejected the request (400); falling back to %s", model, fb + ) return _run(fb) # Run streaming in thread with retry logic @@ -568,15 +583,24 @@ def _run(m): self._track_token_usage(final_message) break except APIStatusError as e: - error_type = getattr(e, 'body', {}) + error_type = getattr(e, "body", {}) if isinstance(error_type, dict): - error_type = error_type.get('error', {}).get('type', '') + error_type = error_type.get("error", {}).get("type", "") - if error_type in ('overloaded_error', 'rate_limit_error') or 'overloaded' in str(e).lower(): + if ( + error_type in ("overloaded_error", "rate_limit_error") + or "overloaded" in str(e).lower() + ): if attempt < max_retries - 1: - wait_time = retry_delay * (2 ** attempt) - logger.warning(f"API overloaded, retrying in {wait_time:.1f}s (attempt {attempt + 1}/{max_retries})") - yield {'type': 'text', 'text': f"\n*[API busy, retrying in {wait_time:.0f}s...]*\n"} + wait_time = retry_delay * (2**attempt) + logger.warning( + f"API overloaded, retrying in {wait_time:.1f}s " + f"(attempt {attempt + 1}/{max_retries})" + ) + yield { + "type": "text", + "text": f"\n*[API busy, retrying in {wait_time:.0f}s...]*\n", + } await asyncio.sleep(wait_time) continue raise @@ -596,34 +620,41 @@ def _run(m): # discard any partial, don't iterate empty content or process tools. if final_message.stop_reason == "refusal": logger.warning("Claude declined the request (model=%s)", self.model) - yield {'type': 'text', 'text': "(The request was declined by the model's safety system. Try rephrasing.)"} + yield { + "type": "text", + "text": "(The request was declined by the model's safety system. Try rephrasing.)", + } return # Diagnostic: per-response counts. DEBUG, not WARNING — stop_reason=tool_use # with matching tool blocks is normal; the genuine anomaly is the # logger.error below (tool blocks present but stop_reason != tool_use). tool_block_count = sum( - 1 for b in final_message.content - if hasattr(b, 'type') and b.type == 'tool_use' + 1 for b in final_message.content if hasattr(b, "type") and b.type == "tool_use" ) logger.debug( - "Claude response: stop_reason=%s, content_blocks=%d, tool_use_blocks=%d, tools_passed=%d, model=%s", - final_message.stop_reason, len(final_message.content), - tool_block_count, len(tools), self.model, + "Claude response: stop_reason=%s, content_blocks=%d, " + "tool_use_blocks=%d, tools_passed=%d, model=%s", + final_message.stop_reason, + len(final_message.content), + tool_block_count, + len(tools), + self.model, ) if tool_block_count > 0 and final_message.stop_reason != "tool_use": logger.error( "BUG: Claude returned %d tool_use blocks but stop_reason=%s (expected 'tool_use')", - tool_block_count, final_message.stop_reason, + tool_block_count, + final_message.stop_reason, ) # Process events and yield text full_text = [] for event in events: if event.type == "content_block_delta": - if hasattr(event.delta, 'text'): + if hasattr(event.delta, "text"): full_text.append(event.delta.text) - yield {'type': 'text', 'text': event.delta.text} + yield {"type": "text", "text": event.delta.text} # Detect fake XML tool calls in text (Claude writing tool_use as text) joined_text = "".join(full_text) @@ -631,7 +662,8 @@ def _run(m): logger.error( "DETECTED: Claude wrote XML tool tags as plain text instead of " "using API tool_use mechanism. stop_reason=%s, text_preview=%.200s", - final_message.stop_reason, joined_text[:200], + final_message.stop_reason, + joined_text[:200], ) response_content = final_message.content @@ -645,14 +677,14 @@ def _run(m): tool_results = [] for block in response_content: - if hasattr(block, 'type') and block.type == "tool_use": + if hasattr(block, "type") and block.type == "tool_use": start_time = time.time() yield { - 'type': 'tool_start', - 'tool_name': block.name, - 'tool_input': block.input, - 'tool_label': tool_label_fn(block.name, block.input), + "type": "tool_start", + "tool_name": block.name, + "tool_input": block.input, + "tool_label": tool_label_fn(block.name, block.input), } is_error_flag = False @@ -663,31 +695,37 @@ def _run(m): if isinstance(tool_result, str): try: from gently.app.tools.interaction_tools import CHOICE_RESPONSE_TYPE + choice_data = json.loads(tool_result) - if isinstance(choice_data, dict) and choice_data.get("_type") == CHOICE_RESPONSE_TYPE: + if ( + isinstance(choice_data, dict) + and choice_data.get("_type") == CHOICE_RESPONSE_TYPE + ): user_selection = yield { - 'type': 'choice_request', - 'choice_data': choice_data + "type": "choice_request", + "choice_data": choice_data, } tool_result = user_selection or "cancelled" except (json.JSONDecodeError, TypeError): pass - result_text = tool_result if isinstance(tool_result, str) else str(tool_result) - tool_results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": tool_result - }) + result_text = ( + tool_result if isinstance(tool_result, str) else str(tool_result) + ) + tool_results.append( + {"type": "tool_result", "tool_use_id": block.id, "content": tool_result} + ) except Exception as e: is_error_flag = True result_text = f"Error: {str(e)}" - tool_results.append({ - "type": "tool_result", - "tool_use_id": block.id, - "content": result_text, - "is_error": True - }) + tool_results.append( + { + "type": "tool_result", + "tool_use_id": block.id, + "content": result_text, + "is_error": True, + } + ) # First non-empty line of the result, trimmed — gives the chat # UI a one-line summary so the operator can see what a tool did @@ -700,22 +738,16 @@ def _run(m): result_summary = result_summary[:139] + "…" yield { - 'type': 'tool_call', - 'tool_name': block.name, - 'tool_input': block.input, - 'duration': time.time() - start_time, - 'result_summary': result_summary, - 'is_error': is_error_flag, + "type": "tool_call", + "tool_name": block.name, + "tool_input": block.input, + "duration": time.time() - start_time, + "result_summary": result_summary, + "is_error": is_error_flag, } - self.conversation_history.append({ - "role": "assistant", - "content": response_content - }) - self.conversation_history.append({ - "role": "user", - "content": tool_results - }) + self.conversation_history.append({"role": "assistant", "content": response_content}) + self.conversation_history.append({"role": "user", "content": tool_results}) auto_save_fn() @@ -737,15 +769,12 @@ def _run(m): else: # No tool calls - add final message to history - self.conversation_history.append({ - "role": "assistant", - "content": response_content - }) + self.conversation_history.append({"role": "assistant", "content": response_content}) auto_save_fn() # ===== Tool Label ===== - def tool_label(self, tool_name: str, tool_input: Dict) -> str: + def tool_label(self, tool_name: str, tool_input: dict) -> str: """Build a human-readable label for a tool call. Used in tool_start chunks so the TUI shows biologist-friendly @@ -759,8 +788,13 @@ def tool_label(self, tool_name: str, tool_input: Dict) -> str: campaign = self.context_store.get_campaign(campaign_id) if campaign: campaign_label = campaign.shorthand or campaign.description - if tool_name in ("propose_plan", "get_plan_status", "export_plan", - "snapshot_plan", "list_plan_versions"): + if tool_name in ( + "propose_plan", + "get_plan_status", + "export_plan", + "snapshot_plan", + "list_plan_versions", + ): return campaign_label if tool_name == "create_campaign" and inp.get("parent_id"): return f"phase under {campaign_label}" @@ -778,8 +812,12 @@ def tool_label(self, tool_name: str, tool_input: Dict) -> str: # Item reference tools item_ref = inp.get("item_ref") or inp.get("ref") or inp.get("item_id") - if item_ref and tool_name in ("get_plan_item", "update_plan_item", - "delete_plan_item", "move_plan_item"): + if item_ref and tool_name in ( + "get_plan_item", + "update_plan_item", + "delete_plan_item", + "move_plan_item", + ): if self.context_store: item = self.context_store.resolve_plan_item(str(item_ref), campaign_id=campaign_id) if item: @@ -806,8 +844,9 @@ def tool_label(self, tool_name: str, tool_input: Dict) -> str: return "" - async def _execute_single_tool(self, tool_name: str, tool_input: Dict, - context: Optional[Dict] = None) -> str: + async def _execute_single_tool( + self, tool_name: str, tool_input: dict, context: dict | None = None + ) -> str: """Execute a single tool call using the tool registry. Parameters @@ -827,13 +866,13 @@ async def _execute_single_tool(self, tool_name: str, tool_input: Dict, def _track_token_usage(self, response): """Track token usage from API response, including cache metrics.""" - if hasattr(response, 'usage'): + if hasattr(response, "usage"): usage = response.usage self.total_input_tokens += usage.input_tokens self.total_output_tokens += usage.output_tokens self.api_call_count += 1 - self.cache_creation_tokens += getattr(usage, 'cache_creation_input_tokens', 0) - self.cache_read_tokens += getattr(usage, 'cache_read_input_tokens', 0) + self.cache_creation_tokens += getattr(usage, "cache_creation_input_tokens", 0) + self.cache_read_tokens += getattr(usage, "cache_read_input_tokens", 0) @property def current_context_tokens(self) -> int: @@ -844,14 +883,14 @@ def current_context_tokens(self) -> int: conv_chars = 0 for msg in self.conversation_history: - content = msg.get('content', '') + content = msg.get("content", "") if isinstance(content, str): conv_chars += len(content) elif isinstance(content, list): for block in content: if isinstance(block, dict): - conv_chars += len(str(block.get('text', ''))) - elif hasattr(block, 'text'): + conv_chars += len(str(block.get("text", ""))) + elif hasattr(block, "text"): conv_chars += len(str(block.text)) else: conv_chars += len(str(block)) @@ -917,19 +956,22 @@ async def _call_api_with_retry(self, api_func, *args, max_retries=3, **kwargs): try: return await asyncio.to_thread(api_func, *args, **kwargs) except APIStatusError as e: - error_type = getattr(e, 'body', {}) + error_type = getattr(e, "body", {}) if isinstance(error_type, dict): - error_type = error_type.get('error', {}).get('type', '') + error_type = error_type.get("error", {}).get("type", "") is_retryable = ( - error_type in ('overloaded_error', 'rate_limit_error') or - 'overloaded' in str(e).lower() or - 'rate_limit' in str(e).lower() + error_type in ("overloaded_error", "rate_limit_error") + or "overloaded" in str(e).lower() + or "rate_limit" in str(e).lower() ) if is_retryable and attempt < max_retries - 1: - wait_time = retry_delay * (2 ** attempt) - logger.warning(f"API error ({error_type}), retrying in {wait_time:.1f}s (attempt {attempt + 1}/{max_retries})") + wait_time = retry_delay * (2**attempt) + logger.warning( + f"API error ({error_type}), retrying in {wait_time:.1f}s " + f"(attempt {attempt + 1}/{max_retries})" + ) await asyncio.sleep(wait_time) continue diff --git a/gently/harness/detection/verifier.py b/gently/harness/detection/verifier.py index 849fbdbf..1a714bcd 100644 --- a/gently/harness/detection/verifier.py +++ b/gently/harness/detection/verifier.py @@ -14,14 +14,15 @@ import logging from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Callable, Dict, List, Optional +from typing import Any import anthropic +from gently.core import EventType, get_event_bus from gently.settings import settings -from .detector import Detector, DetectionResult, ConfidenceLevel + from ..state import EmbryoState -from gently.core import EventType, get_event_bus +from .detector import ConfidenceLevel, DetectionResult, Detector logger = logging.getLogger(__name__) @@ -38,12 +39,22 @@ # many independent votes rather than introspected by one call. _ADVERSARIAL_TOOL = { "name": "record_adversarial_review", - "description": "Record the critical review verdict: whether counter-evidence against the detection was found.", + "description": ( + "Record the critical review verdict: whether counter-evidence " + "against the detection was found." + ), "input_schema": { "type": "object", "properties": { - "found_counter_evidence": {"type": "boolean", "description": "True only if there is real evidence the detection is wrong."}, - "concerns": {"type": "array", "items": {"type": "string"}, "description": "Specific doubts or alternative explanations; empty list if none."}, + "found_counter_evidence": { + "type": "boolean", + "description": "True only if there is real evidence the detection is wrong.", + }, + "concerns": { + "type": "array", + "items": {"type": "string"}, + "description": "Specific doubts or alternative explanations; empty list if none.", + }, }, "required": ["found_counter_evidence", "concerns"], }, @@ -51,12 +62,20 @@ _INDEPENDENT_TOOL = { "name": "record_independent_assessment", - "description": "Record an unbiased fresh assessment of whether the event occurred in this image.", + "description": ( + "Record an unbiased fresh assessment of whether the event occurred in this image." + ), "input_schema": { "type": "object", "properties": { - "detected": {"type": "boolean", "description": "True if the event is observed in this image."}, - "key_evidence": {"type": "string", "description": "What specifically supports the conclusion."}, + "detected": { + "type": "boolean", + "description": "True if the event is observed in this image.", + }, + "key_evidence": { + "type": "string", + "description": "What specifically supports the conclusion.", + }, }, "required": ["detected", "key_evidence"], }, @@ -64,12 +83,23 @@ _TEMPORAL_TOOL = { "name": "record_temporal_comparison", - "description": "Record whether a real change consistent with the event occurred between the previous and current frames.", + "description": ( + "Record whether a real change consistent with the event occurred " + "between the previous and current frames." + ), "input_schema": { "type": "object", "properties": { - "change_detected": {"type": "boolean", "description": "True if a clear change consistent with the event is visible across frames."}, - "description": {"type": "string", "description": "The specific change observed between previous and current frames."}, + "change_detected": { + "type": "boolean", + "description": ( + "True if a clear change consistent with the event is visible across frames." + ), + }, + "description": { + "type": "string", + "description": "The specific change observed between previous and current frames.", + }, }, "required": ["change_detected", "description"], }, @@ -81,8 +111,18 @@ "input_schema": { "type": "object", "properties": { - "suspicious": {"type": "boolean", "description": "True if hardware errors could have affected image quality or positioning for this embryo."}, - "concerns": {"type": "array", "items": {"type": "string"}, "description": "Specific hardware concerns; empty list if none."}, + "suspicious": { + "type": "boolean", + "description": ( + "True if hardware errors could have affected image quality " + "or positioning for this embryo." + ), + }, + "concerns": { + "type": "array", + "items": {"type": "string"}, + "description": "Specific hardware concerns; empty list if none.", + }, "reasoning": {"type": "string", "description": "Brief explanation of the analysis."}, }, "required": ["suspicious", "concerns", "reasoning"], @@ -90,7 +130,7 @@ } -def _tool_input(response) -> Optional[Dict[str, Any]]: +def _tool_input(response) -> dict[str, Any] | None: """Return the parsed input of the first tool_use block, or None.""" for block in getattr(response, "content", None) or []: if getattr(block, "type", None) == "tool_use": @@ -101,14 +141,16 @@ def _tool_input(response) -> Optional[Dict[str, Any]]: @dataclass class AdversarialResult: """Result of adversarial verification strategy""" + found_counter_evidence: bool - concerns: List[str] + concerns: list[str] raw_response: str @dataclass class IndependentResult: """Result of independent verification strategy""" + detected: bool key_evidence: str raw_response: str @@ -117,6 +159,7 @@ class IndependentResult: @dataclass class TemporalResult: """Result of temporal comparison strategy""" + change_detected: bool description: str raw_response: str @@ -125,19 +168,21 @@ class TemporalResult: @dataclass class EnsembleResult: """Result of ensemble voting strategy""" + votes_yes: int votes_no: int total_votes: int agreement_ratio: float # votes_yes / total_votes if detected, votes_no / total_votes if not consensus_detected: bool # True if >70% agree on YES - raw_responses: List[str] = field(default_factory=list) + raw_responses: list[str] = field(default_factory=list) @dataclass class HardwareContextResult: """Result of hardware context analysis strategy""" + suspicious: bool # True if hardware errors could have caused false positive - concerns: List[str] # Specific concerns identified + concerns: list[str] # Specific concerns identified reasoning: str raw_response: str @@ -145,15 +190,16 @@ class HardwareContextResult: @dataclass class VerificationResult: """Combined result of all verification strategies""" + original_detected: bool - original_confidence: Optional[ConfidenceLevel] + original_confidence: ConfidenceLevel | None # Strategy results adversarial: AdversarialResult independent: IndependentResult temporal: TemporalResult - ensemble: Optional[EnsembleResult] = None # Only for hatching detection - hardware_context: Optional[HardwareContextResult] = None # Only when errors present + ensemble: EnsembleResult | None = None # Only for hatching detection + hardware_context: HardwareContextResult | None = None # Only when errors present # Consensus consensus: bool = False @@ -163,41 +209,43 @@ class VerificationResult: timestamp: datetime = field(default_factory=datetime.now) verification_duration_seconds: float = 0.0 - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Serialize to dictionary""" result = { - 'original_detected': self.original_detected, - 'original_confidence': self.original_confidence.value if self.original_confidence else None, - 'adversarial': { - 'found_counter_evidence': self.adversarial.found_counter_evidence, - 'concerns': self.adversarial.concerns, + "original_detected": self.original_detected, + "original_confidence": self.original_confidence.value + if self.original_confidence + else None, + "adversarial": { + "found_counter_evidence": self.adversarial.found_counter_evidence, + "concerns": self.adversarial.concerns, }, - 'independent': { - 'detected': self.independent.detected, - 'key_evidence': self.independent.key_evidence, + "independent": { + "detected": self.independent.detected, + "key_evidence": self.independent.key_evidence, }, - 'temporal': { - 'change_detected': self.temporal.change_detected, - 'description': self.temporal.description, + "temporal": { + "change_detected": self.temporal.change_detected, + "description": self.temporal.description, }, - 'consensus': self.consensus, - 'consensus_reasoning': self.consensus_reasoning, - 'timestamp': self.timestamp.isoformat(), - 'verification_duration_seconds': self.verification_duration_seconds, + "consensus": self.consensus, + "consensus_reasoning": self.consensus_reasoning, + "timestamp": self.timestamp.isoformat(), + "verification_duration_seconds": self.verification_duration_seconds, } if self.ensemble: - result['ensemble'] = { - 'votes_yes': self.ensemble.votes_yes, - 'votes_no': self.ensemble.votes_no, - 'total_votes': self.ensemble.total_votes, - 'agreement_ratio': self.ensemble.agreement_ratio, - 'consensus_detected': self.ensemble.consensus_detected, + result["ensemble"] = { + "votes_yes": self.ensemble.votes_yes, + "votes_no": self.ensemble.votes_no, + "total_votes": self.ensemble.total_votes, + "agreement_ratio": self.ensemble.agreement_ratio, + "consensus_detected": self.ensemble.consensus_detected, } if self.hardware_context: - result['hardware_context'] = { - 'suspicious': self.hardware_context.suspicious, - 'concerns': self.hardware_context.concerns, - 'reasoning': self.hardware_context.reasoning, + result["hardware_context"] = { + "suspicious": self.hardware_context.suspicious, + "concerns": self.hardware_context.concerns, + "reasoning": self.hardware_context.reasoning, } return result @@ -242,7 +290,7 @@ def __init__( self.ensemble_threshold = ensemble_threshold self._event_bus = event_bus or get_event_bus() - def _emit_event(self, event_type: EventType, data: Dict): + def _emit_event(self, event_type: EventType, data: dict): """Emit event to viz server""" if self._event_bus: self._event_bus.publish(event_type, data) @@ -276,19 +324,13 @@ async def verify( start_time = datetime.now() # Run all strategies in parallel for speed - adversarial_task = self._run_adversarial( - detector, embryo_state, original_result, timepoint - ) - independent_task = self._run_independent( - detector, embryo_state, timepoint - ) - temporal_task = self._run_temporal_check( - detector, embryo_state, timepoint - ) + adversarial_task = self._run_adversarial(detector, embryo_state, original_result, timepoint) + independent_task = self._run_independent(detector, embryo_state, timepoint) + temporal_task = self._run_temporal_check(detector, embryo_state, timepoint) # For hatching detection, also run ensemble voting ensemble_result = None - if detector.name == 'hatching': + if detector.name == "hatching": ensemble_task = self._run_ensemble_hatching(embryo_state) adversarial, independent, temporal, ensemble_result = await asyncio.gather( adversarial_task, independent_task, temporal_task, ensemble_task @@ -320,7 +362,11 @@ async def verify( logger.info( f"Verification complete for {detector.name}: " f"consensus={consensus}, duration={duration:.2f}s" - + (f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" if ensemble_result else "") + + ( + f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" + if ensemble_result + else "" + ) ) return result @@ -359,21 +405,15 @@ async def verify_with_context( start_time = datetime.now() # Run all strategies in parallel for speed - adversarial_task = self._run_adversarial( - detector, embryo_state, original_result, timepoint - ) - independent_task = self._run_independent( - detector, embryo_state, timepoint - ) - temporal_task = self._run_temporal_check( - detector, embryo_state, timepoint - ) + adversarial_task = self._run_adversarial(detector, embryo_state, original_result, timepoint) + independent_task = self._run_independent(detector, embryo_state, timepoint) + temporal_task = self._run_temporal_check(detector, embryo_state, timepoint) # For hatching detection, also run ensemble voting ensemble_result = None hardware_result = None - if detector.name == 'hatching': + if detector.name == "hatching": ensemble_task = self._run_ensemble_hatching(embryo_state) # Run hardware context analysis if there are errors @@ -381,7 +421,13 @@ async def verify_with_context( hardware_task = self._run_hardware_context_analysis( global_error_context, embryo_state.id ) - adversarial, independent, temporal, ensemble_result, hardware_result = await asyncio.gather( + ( + adversarial, + independent, + temporal, + ensemble_result, + hardware_result, + ) = await asyncio.gather( adversarial_task, independent_task, temporal_task, ensemble_task, hardware_task ) else: @@ -400,84 +446,133 @@ async def verify_with_context( # Adversarial result strategies_complete += 1 - self._emit_event(EventType.VERIFICATION_STRATEGY, { - 'embryo_id': embryo_id, - 'detector_name': detector.name, - 'strategy': 'adversarial', - 'passed': not adversarial.found_counter_evidence, - 'summary': f"Counter-evidence: {'YES - ' + ', '.join(adversarial.concerns) if adversarial.found_counter_evidence else 'None found'}", - }) - self._emit_event(EventType.VERIFICATION_PROGRESS, { - 'embryo_id': embryo_id, - 'strategies_complete': strategies_complete, - 'total_strategies': total_strategies, - }) + adversarial_summary = ( + "YES - " + ", ".join(adversarial.concerns) + if adversarial.found_counter_evidence + else "None found" + ) + self._emit_event( + EventType.VERIFICATION_STRATEGY, + { + "embryo_id": embryo_id, + "detector_name": detector.name, + "strategy": "adversarial", + "passed": not adversarial.found_counter_evidence, + "summary": f"Counter-evidence: {adversarial_summary}", + }, + ) + self._emit_event( + EventType.VERIFICATION_PROGRESS, + { + "embryo_id": embryo_id, + "strategies_complete": strategies_complete, + "total_strategies": total_strategies, + }, + ) # Independent result strategies_complete += 1 - self._emit_event(EventType.VERIFICATION_STRATEGY, { - 'embryo_id': embryo_id, - 'detector_name': detector.name, - 'strategy': 'independent', - 'passed': independent.detected, - 'summary': f"Independent detection: {'YES' if independent.detected else 'NO'} - {independent.key_evidence}", - }) - self._emit_event(EventType.VERIFICATION_PROGRESS, { - 'embryo_id': embryo_id, - 'strategies_complete': strategies_complete, - 'total_strategies': total_strategies, - }) + self._emit_event( + EventType.VERIFICATION_STRATEGY, + { + "embryo_id": embryo_id, + "detector_name": detector.name, + "strategy": "independent", + "passed": independent.detected, + "summary": ( + f"Independent detection: {'YES' if independent.detected else 'NO'}" + f" - {independent.key_evidence}" + ), + }, + ) + self._emit_event( + EventType.VERIFICATION_PROGRESS, + { + "embryo_id": embryo_id, + "strategies_complete": strategies_complete, + "total_strategies": total_strategies, + }, + ) # Temporal result strategies_complete += 1 - self._emit_event(EventType.VERIFICATION_STRATEGY, { - 'embryo_id': embryo_id, - 'detector_name': detector.name, - 'strategy': 'temporal', - 'passed': temporal.change_detected, - 'summary': f"Change detected: {'YES' if temporal.change_detected else 'NO'} - {temporal.description}", - }) - self._emit_event(EventType.VERIFICATION_PROGRESS, { - 'embryo_id': embryo_id, - 'strategies_complete': strategies_complete, - 'total_strategies': total_strategies, - }) + self._emit_event( + EventType.VERIFICATION_STRATEGY, + { + "embryo_id": embryo_id, + "detector_name": detector.name, + "strategy": "temporal", + "passed": temporal.change_detected, + "summary": ( + f"Change detected: {'YES' if temporal.change_detected else 'NO'}" + f" - {temporal.description}" + ), + }, + ) + self._emit_event( + EventType.VERIFICATION_PROGRESS, + { + "embryo_id": embryo_id, + "strategies_complete": strategies_complete, + "total_strategies": total_strategies, + }, + ) # Ensemble result (if applicable) if ensemble_result: strategies_complete += 1 - self._emit_event(EventType.VERIFICATION_STRATEGY, { - 'embryo_id': embryo_id, - 'detector_name': detector.name, - 'strategy': 'ensemble', - 'passed': ensemble_result.consensus_detected, - 'summary': f"Ensemble vote: {ensemble_result.votes_yes}/{ensemble_result.total_votes} YES ({ensemble_result.agreement_ratio*100:.0f}%)", - 'votes_yes': ensemble_result.votes_yes, - 'votes_no': ensemble_result.votes_no, - 'total_votes': ensemble_result.total_votes, - }) - self._emit_event(EventType.VERIFICATION_PROGRESS, { - 'embryo_id': embryo_id, - 'strategies_complete': strategies_complete, - 'total_strategies': total_strategies, - }) + self._emit_event( + EventType.VERIFICATION_STRATEGY, + { + "embryo_id": embryo_id, + "detector_name": detector.name, + "strategy": "ensemble", + "passed": ensemble_result.consensus_detected, + "summary": ( + f"Ensemble vote: {ensemble_result.votes_yes}/{ensemble_result.total_votes}" + f" YES ({ensemble_result.agreement_ratio * 100:.0f}%)" + ), + "votes_yes": ensemble_result.votes_yes, + "votes_no": ensemble_result.votes_no, + "total_votes": ensemble_result.total_votes, + }, + ) + self._emit_event( + EventType.VERIFICATION_PROGRESS, + { + "embryo_id": embryo_id, + "strategies_complete": strategies_complete, + "total_strategies": total_strategies, + }, + ) # Hardware context result (if applicable) if hardware_result: strategies_complete += 1 - self._emit_event(EventType.VERIFICATION_STRATEGY, { - 'embryo_id': embryo_id, - 'detector_name': detector.name, - 'strategy': 'hardware_context', - 'passed': not hardware_result.suspicious, - 'summary': f"Hardware errors suspicious: {'YES - ' + ', '.join(hardware_result.concerns) if hardware_result.suspicious else 'No'}", - 'reasoning': hardware_result.reasoning, - }) - self._emit_event(EventType.VERIFICATION_PROGRESS, { - 'embryo_id': embryo_id, - 'strategies_complete': strategies_complete, - 'total_strategies': total_strategies, - }) + hardware_summary = ( + "YES - " + ", ".join(hardware_result.concerns) + if hardware_result.suspicious + else "No" + ) + self._emit_event( + EventType.VERIFICATION_STRATEGY, + { + "embryo_id": embryo_id, + "detector_name": detector.name, + "strategy": "hardware_context", + "passed": not hardware_result.suspicious, + "summary": f"Hardware errors suspicious: {hardware_summary}", + "reasoning": hardware_result.reasoning, + }, + ) + self._emit_event( + EventType.VERIFICATION_PROGRESS, + { + "embryo_id": embryo_id, + "strategies_complete": strategies_complete, + "total_strategies": total_strategies, + }, + ) # Determine consensus (with hardware context) consensus, reasoning = self._evaluate_consensus_with_hardware( @@ -502,26 +597,37 @@ async def verify_with_context( logger.info( f"Verification (with context) complete for {detector.name}: " f"consensus={consensus}, duration={duration:.2f}s" - + (f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" if ensemble_result else "") + + ( + f", ensemble={ensemble_result.votes_yes}/{ensemble_result.total_votes} YES" + if ensemble_result + else "" + ) + (f", hardware_suspicious={hardware_result.suspicious}" if hardware_result else "") ) # Emit VERIFICATION_COMPLETED event with full summary - self._emit_event(EventType.VERIFICATION_COMPLETED, { - 'embryo_id': embryo_id, - 'detector_name': detector.name, - 'consensus': consensus, - 'reasoning': reasoning, - 'duration_seconds': duration, - 'strategies': { - 'adversarial': not adversarial.found_counter_evidence, - 'independent': independent.detected, - 'temporal': temporal.change_detected, - 'ensemble': ensemble_result.consensus_detected if ensemble_result else None, - 'hardware_context': (not hardware_result.suspicious) if hardware_result else None, + self._emit_event( + EventType.VERIFICATION_COMPLETED, + { + "embryo_id": embryo_id, + "detector_name": detector.name, + "consensus": consensus, + "reasoning": reasoning, + "duration_seconds": duration, + "strategies": { + "adversarial": not adversarial.found_counter_evidence, + "independent": independent.detected, + "temporal": temporal.change_detected, + "ensemble": ensemble_result.consensus_detected if ensemble_result else None, + "hardware_context": (not hardware_result.suspicious) + if hardware_result + else None, + }, + "ensemble_votes": f"{ensemble_result.votes_yes}/{ensemble_result.total_votes}" + if ensemble_result + else None, }, - 'ensemble_votes': f"{ensemble_result.votes_yes}/{ensemble_result.total_votes}" if ensemble_result else None, - }) + ) return result @@ -549,7 +655,8 @@ async def _run_hardware_context_analysis( Analysis result """ try: - prompt = f"""You are analyzing hardware error context for a microscopy detection verification. + prompt = f"""\ +You are analyzing hardware error context for a microscopy detection verification. GLOBAL ERROR LOG: {global_error_context} @@ -562,10 +669,12 @@ async def _run_hardware_context_analysis( - Stage positioning errors could cause wrong embryo to be imaged - Acquisition timeouts could cause partial/blank images (blank images look like empty FOV = hatched) - Camera errors could produce corrupted data -- Errors on OTHER embryos in the same round could indicate systemic issues (stage drift, hardware instability) +- Errors on OTHER embryos in the same round could indicate systemic issues + (stage drift, hardware instability) - Multiple errors in quick succession suggests hardware problems -If ANY errors occurred that could have affected the image quality or positioning for {embryo_id}, mark it suspicious. +If ANY errors occurred that could have affected the image quality or positioning +for {embryo_id}, mark it suspicious. Record your analysis with the record_hardware_context tool. """ @@ -576,7 +685,7 @@ async def _run_hardware_context_analysis( max_tokens=300, tools=[_HARDWARE_CONTEXT_TOOL], tool_choice={"type": "tool", "name": _HARDWARE_CONTEXT_TOOL["name"]}, - messages=[{"role": "user", "content": prompt}] + messages=[{"role": "user", "content": prompt}], ) data = _tool_input(response) @@ -605,8 +714,8 @@ def _evaluate_consensus_with_hardware( adversarial: AdversarialResult, independent: IndependentResult, temporal: TemporalResult, - ensemble: Optional[EnsembleResult] = None, - hardware_context: Optional[HardwareContextResult] = None, + ensemble: EnsembleResult | None = None, + hardware_context: HardwareContextResult | None = None, ) -> tuple[bool, str]: """ Evaluate consensus across all verification strategies including hardware context. @@ -622,7 +731,9 @@ def _evaluate_consensus_with_hardware( if not adversarial.found_counter_evidence: agreements += 1 else: - disagreements.append(f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}") + disagreements.append( + f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}" + ) # Check independent: should also detect if independent.detected: @@ -662,12 +773,19 @@ def _evaluate_consensus_with_hardware( consensus = agreements == total_strategies if consensus: - parts = ["no counter-evidence found", "independent analysis confirms", "temporal change observed"] + parts = [ + "no counter-evidence found", + "independent analysis confirms", + "temporal change observed", + ] if ensemble: parts.append(f"ensemble confirms ({ensemble.votes_yes}/{ensemble.total_votes} YES)") if hardware_context: parts.append("no hardware error concerns") - reasoning = f"All verification strategies agree ({total_strategies}/{total_strategies}): " + ", ".join(parts) + reasoning = ( + f"All verification strategies agree ({total_strategies}/{total_strategies}): " + + ", ".join(parts) + ) else: reasoning = ( f"Verification disagreement ({agreements}/{total_strategies} agree): " @@ -700,7 +818,7 @@ async def _run_adversarial( ) # Build detector-specific critical review guidance - if detector.name == 'hatching': + if detector.name == "hatching": specific_guidance = """ For HATCHING specifically, look for these common FALSE POSITIVE patterns: - Is the worm still COILED/PRETZEL-SHAPED inside the eggshell? @@ -713,10 +831,11 @@ async def _run_adversarial( else: specific_guidance = "" - prompt = f"""You are reviewing a detection result for a C. elegans embryo (diSPIM max projection). + prompt = f"""\ +You are reviewing a detection result for a C. elegans embryo (diSPIM max projection). The system detected: {detector.name} -Original reasoning: {original_result.reasoning or 'not provided'} +Original reasoning: {original_result.reasoning or "not provided"} NOW ACT AS A CRITICAL REVIEWER. Your job is to find reasons why this detection might be INCORRECT: - Could this be a false positive? @@ -735,7 +854,7 @@ async def _run_adversarial( max_tokens=500, tools=[_ADVERSARIAL_TOOL], tool_choice={"type": "tool", "name": _ADVERSARIAL_TOOL["name"]}, - messages=[{"role": "user", "content": content}] + messages=[{"role": "user", "content": content}], ) data = _tool_input(response) @@ -779,7 +898,7 @@ async def _run_independent( ) # Build detector-specific criteria - if detector.name == 'hatching': + if detector.name == "hatching": criteria = """ TRUE HATCHING criteria (must meet at least one): - Worm body is OUTSIDE the eggshell boundary (free-floating, elongated) @@ -793,7 +912,8 @@ async def _run_independent( criteria = detector.description # Use a neutral prompt that doesn't reveal the previous detection - prompt = f"""Analyze this C. elegans embryo image (diSPIM max projection) at timepoint {timepoint}. + prompt = f"""\ +Analyze this C. elegans embryo image (diSPIM max projection) at timepoint {timepoint}. Question: Has '{detector.name}' occurred in this embryo? @@ -813,7 +933,7 @@ async def _run_independent( max_tokens=400, tools=[_INDEPENDENT_TOOL], tool_choice={"type": "tool", "name": _INDEPENDENT_TOOL["name"]}, - messages=[{"role": "user", "content": content}] + messages=[{"role": "user", "content": content}], ) data = _tool_input(response) @@ -861,14 +981,16 @@ async def _run_temporal_check( prev_images = [] for img in embryo_state.recent_images[-3:-1]: if img.max_projection_b64: - prev_images.append({ - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": img.max_projection_b64, + prev_images.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": img.max_projection_b64, + }, } - }) + ) if not prev_images: return TemporalResult( @@ -878,7 +1000,7 @@ async def _run_temporal_check( ) # Build detector-specific temporal criteria - if detector.name == 'hatching': + if detector.name == "hatching": temporal_criteria = """For HATCHING, look for: - A visible BREACH appearing in the eggshell boundary (not just expansion) - The worm physically EXITING the shell (part of body moves outside) @@ -890,10 +1012,11 @@ async def _run_temporal_check( - Not just a static state that could have existed before - Clear evidence of progression or event occurrence""" - prompt = f"""Compare these sequential timepoints of a C. elegans embryo (diSPIM max projection). + prompt = f"""\ +Compare these sequential timepoints of a C. elegans embryo (diSPIM max projection). PREVIOUS TIMEPOINTS (shown first): -These are from t={timepoint-2} to t={timepoint-1} +These are from t={timepoint - 2} to t={timepoint - 1} CURRENT TIMEPOINT (shown last): This is t={timepoint} @@ -914,7 +1037,7 @@ async def _run_temporal_check( max_tokens=400, tools=[_TEMPORAL_TOOL], tool_choice={"type": "tool", "name": _TEMPORAL_TOOL["name"]}, - messages=[{"role": "user", "content": content}] + messages=[{"role": "user", "content": content}], ) data = _tool_input(response) @@ -934,25 +1057,23 @@ async def _run_temporal_check( raw_response="", ) - def _get_image_content( - self, - embryo_state: EmbryoState, - num_images: int = 1 - ) -> List[Dict]: + def _get_image_content(self, embryo_state: EmbryoState, num_images: int = 1) -> list[dict]: """Get image content blocks for Claude API""" images = [] recent = embryo_state.recent_images[-num_images:] if embryo_state.recent_images else [] for img in recent: if img.max_projection_b64: - images.append({ - "type": "image", - "source": { - "type": "base64", - "media_type": "image/png", - "data": img.max_projection_b64, + images.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/png", + "data": img.max_projection_b64, + }, } - }) + ) return images @@ -986,8 +1107,10 @@ async def _run_ensemble_hatching(self, embryo_state: EmbryoState) -> EnsembleRes Answer ONE question: Has the embryo HATCHED? -HATCHED means: The worm body is OUTSIDE the eggshell (free-floating, elongated, or field is empty because worm left). -NOT HATCHED means: The worm is still INSIDE the eggshell (coiled/pretzel-shaped, even if shell looks expanded). +HATCHED means: The worm body is OUTSIDE the eggshell (free-floating, elongated, +or field is empty because worm left). +NOT HATCHED means: The worm is still INSIDE the eggshell (coiled/pretzel-shaped, +even if shell looks expanded). Respond with ONLY: YES or NO""" @@ -999,7 +1122,7 @@ async def single_vote() -> str: self.claude.messages.create, model=self.ensemble_model, max_tokens=10, # Very short response expected - messages=[{"role": "user", "content": content}] + messages=[{"role": "user", "content": content}], ) return response.content[0].text.strip().upper() except Exception as e: @@ -1007,7 +1130,10 @@ async def single_vote() -> str: return "ERROR" # Run all votes in parallel - logger.info(f"[ENSEMBLE] Running {self.ensemble_size} parallel Haiku calls for hatching verification") + logger.info( + f"[ENSEMBLE] Running {self.ensemble_size} parallel Haiku calls " + "for hatching verification" + ) tasks = [single_vote() for _ in range(self.ensemble_size)] responses = await asyncio.gather(*tasks) @@ -1062,7 +1188,7 @@ def _evaluate_consensus( adversarial: AdversarialResult, independent: IndependentResult, temporal: TemporalResult, - ensemble: Optional[EnsembleResult] = None, + ensemble: EnsembleResult | None = None, ) -> tuple[bool, str]: """ Evaluate consensus across all verification strategies. @@ -1078,7 +1204,9 @@ def _evaluate_consensus( if not adversarial.found_counter_evidence: agreements += 1 else: - disagreements.append(f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}") + disagreements.append( + f"Adversarial found counter-evidence: {', '.join(adversarial.concerns[:2])}" + ) # Check independent: should also detect if independent.detected: @@ -1112,13 +1240,14 @@ def _evaluate_consensus( f"All verification strategies agree ({total_strategies}/{total_strategies}): " f"no counter-evidence found, independent analysis confirms detection, " f"temporal change observed, ensemble voting confirms " - f"({ensemble.votes_yes}/{ensemble.total_votes} = {ensemble.agreement_ratio:.0%} YES)." + f"({ensemble.votes_yes}/{ensemble.total_votes} = " + f"{ensemble.agreement_ratio:.0%} YES)." ) else: reasoning = ( - f"All verification strategies agree: " - f"no counter-evidence found, independent analysis confirms detection, " - f"temporal change observed." + "All verification strategies agree: " + "no counter-evidence found, independent analysis confirms detection, " + "temporal change observed." ) else: reasoning = ( diff --git a/gently/harness/memory/file_store.py b/gently/harness/memory/file_store.py index 3672ba88..6a8c5eb3 100644 --- a/gently/harness/memory/file_store.py +++ b/gently/harness/memory/file_store.py @@ -1912,7 +1912,8 @@ def _notify_context_change(self, kind: str = "context") -> None: """Emit CONTEXT_UPDATED on the global bus so the shared-visibility surface refreshes live. Best-effort — a bus failure never breaks a write.""" try: - from gently.core.event_bus import emit, EventType + from gently.core.event_bus import EventType, emit + emit(EventType.CONTEXT_UPDATED, {"kind": kind}, source="context_store") except Exception: pass diff --git a/gently/harness/memory/model.py b/gently/harness/memory/model.py index 58c8ee84..3757a0fe 100644 --- a/gently/harness/memory/model.py +++ b/gently/harness/memory/model.py @@ -243,7 +243,7 @@ class ImagingSpec: # Per-field provenance for INFERRED values — field name -> {source, confidence}. # e.g. {"laser_wavelength_nm": {"source": "inferred:genotype", "confidence": "medium"}} # Lets the UI tag each value with where it came from and what to confirm. - provenance: Dict[str, Dict[str, str]] = field(default_factory=dict) + provenance: dict[str, dict[str, str]] = field(default_factory=dict) @dataclass diff --git a/gently/settings.py b/gently/settings.py index 33254621..37ea4336 100644 --- a/gently/settings.py +++ b/gently/settings.py @@ -4,6 +4,7 @@ All configurable values live here. Override via environment variables prefixed with GENTLY_ (e.g., GENTLY_VIZ_PORT=9090). """ + import os from dataclasses import dataclass, field from pathlib import Path @@ -29,6 +30,7 @@ def _env(key: str, default): @dataclass(frozen=True) class NetworkSettings: """Ports, hosts, and bind addresses.""" + viz_port: int = field(default_factory=lambda: _env("VIZ_PORT", 8080)) viz_host: str = field(default_factory=lambda: _env("VIZ_HOST", "0.0.0.0")) device_port: int = field(default_factory=lambda: _env("DEVICE_PORT", 60610)) @@ -40,7 +42,10 @@ class NetworkSettings: @dataclass(frozen=True) class MeshSettings: """Mesh networking parameters.""" - broadcast_interval_s: float = field(default_factory=lambda: _env("MESH_BROADCAST_INTERVAL", 5.0)) + + broadcast_interval_s: float = field( + default_factory=lambda: _env("MESH_BROADCAST_INTERVAL", 5.0) + ) replay_window_s: float = field(default_factory=lambda: _env("MESH_REPLAY_WINDOW", 30.0)) reaper_interval_s: float = field(default_factory=lambda: _env("MESH_REAPER_INTERVAL", 10.0)) status_refresh_s: float = field(default_factory=lambda: _env("MESH_STATUS_REFRESH", 30.0)) @@ -72,6 +77,7 @@ class ModelSettings: Sonnet 4.6 supports adaptive thinking. No assistant prefills anywhere (4.6+ family rejects them). """ + main: str = field(default_factory=lambda: _env("MODEL_MAIN", "claude-fable-5")) perception: str = field(default_factory=lambda: _env("MODEL_PERCEPTION", "claude-opus-4-8")) fast: str = field(default_factory=lambda: _env("MODEL_FAST", "claude-sonnet-4-6")) @@ -79,12 +85,15 @@ class ModelSettings: # When the main tier (Fable 5) declines a turn (stop_reason="refusal"), the # main-tier calls transparently retry it on this model instead of surfacing # the refusal. Empty disables the fallback. - refusal_fallback: str = field(default_factory=lambda: _env("MODEL_REFUSAL_FALLBACK", "claude-opus-4-8")) + refusal_fallback: str = field( + default_factory=lambda: _env("MODEL_REFUSAL_FALLBACK", "claude-opus-4-8") + ) @dataclass(frozen=True) class StorageSettings: """File paths for data storage.""" + base_path: Path = field(default_factory=lambda: _env("STORAGE_PATH", Path("D:/Gently3"))) @property @@ -99,6 +108,7 @@ def traces_dir(self) -> Path: @dataclass(frozen=True) class TimeoutSettings: """Timeout values in seconds.""" + plan_execution: int = field(default_factory=lambda: _env("TIMEOUT_PLAN", 300)) rpc_call: int = field(default_factory=lambda: _env("TIMEOUT_RPC", 60)) volume_acquisition: int = field(default_factory=lambda: _env("TIMEOUT_VOLUME", 15)) @@ -108,6 +118,7 @@ class TimeoutSettings: @dataclass(frozen=True) class ApiSettings: """External API configuration.""" + ncbi_tool: str = field(default_factory=lambda: _env("NCBI_TOOL", "gently")) ncbi_email: str = field(default_factory=lambda: _env("NCBI_EMAIL", "pskeshu@gmail.com")) @@ -115,6 +126,7 @@ class ApiSettings: @dataclass(frozen=True) class MlSettings: """Machine learning training parameters.""" + model_cache_dir: Path = field(default_factory=lambda: _env("ML_MODEL_CACHE", Path("models"))) default_batch_size: int = field(default_factory=lambda: _env("ML_BATCH_SIZE", 32)) default_epochs: int = field(default_factory=lambda: _env("ML_EPOCHS", 50)) @@ -124,14 +136,18 @@ class MlSettings: @dataclass(frozen=True) class TransferSettings: """Bulk transfer protocol parameters.""" + transfer_port: int = field(default_factory=lambda: _env("TRANSFER_PORT", 19548)) chunk_size: int = field(default_factory=lambda: _env("TRANSFER_CHUNK_SIZE", 1048576)) # 1MB - max_concurrent_transfers: int = field(default_factory=lambda: _env("TRANSFER_MAX_CONCURRENT", 4)) + max_concurrent_transfers: int = field( + default_factory=lambda: _env("TRANSFER_MAX_CONCURRENT", 4) + ) @dataclass(frozen=True) class UISettings: """Web UI feature flags.""" + # New agent-first UX paradigm (welcome→shell unfold, dual-rendered agent # asks, inference-first plan mode, shared-visibility surface). Now ON by # default; the v1 dashboard remains available as a fallback via @@ -142,6 +158,7 @@ class UISettings: @dataclass(frozen=True) class Settings: """Top-level settings container.""" + network: NetworkSettings = field(default_factory=NetworkSettings) mesh: MeshSettings = field(default_factory=MeshSettings) models: ModelSettings = field(default_factory=ModelSettings) diff --git a/gently/ui/web/connection_manager.py b/gently/ui/web/connection_manager.py index 00aeaef2..1d1f2a23 100644 --- a/gently/ui/web/connection_manager.py +++ b/gently/ui/web/connection_manager.py @@ -161,7 +161,9 @@ async def broadcast(self, message: dict): # Expected when a client disconnects/reloads mid-broadcast # (send after websocket.close). The connection is dropped # below, so this is debug-level, not a warning. - logger.debug("Dropping a websocket that errored on send (client likely gone): %s", e) + logger.debug( + "Dropping a websocket that errored on send (client likely gone): %s", e + ) disconnected.append(connection) # Remove disconnected clients diff --git a/gently/ui/web/routes/chat.py b/gently/ui/web/routes/chat.py index 1fffcb7f..e07d4501 100644 --- a/gently/ui/web/routes/chat.py +++ b/gently/ui/web/routes/chat.py @@ -19,8 +19,8 @@ from fastapi.responses import StreamingResponse from pydantic import BaseModel -from gently.ui.web.auth import require_control from gently.settings import settings +from gently.ui.web.auth import require_control logger = logging.getLogger(__name__) diff --git a/gently/ui/web/routes/context.py b/gently/ui/web/routes/context.py index 2d7c39df..66c7f25c 100644 --- a/gently/ui/web/routes/context.py +++ b/gently/ui/web/routes/context.py @@ -10,6 +10,7 @@ from fastapi import APIRouter, Body, Depends from gently.ui.web.auth import require_control + from .campaigns import _serialize @@ -36,8 +37,7 @@ async def get_context(): except Exception: return empty - @router.post("/api/context/questions/{q_id}/resolve", - dependencies=[Depends(require_control)]) + @router.post("/api/context/questions/{q_id}/resolve", dependencies=[Depends(require_control)]) async def resolve_question(q_id: str, resolution: str = Body("", embed=True)): cs = _store() if cs is None: @@ -45,8 +45,9 @@ async def resolve_question(q_id: str, resolution: str = Body("", embed=True)): cs.resolve_question(q_id, resolution or "") return {"ok": True} - @router.post("/api/context/watchpoints/{wp_id}/resolve", - dependencies=[Depends(require_control)]) + @router.post( + "/api/context/watchpoints/{wp_id}/resolve", dependencies=[Depends(require_control)] + ) async def resolve_watchpoint(wp_id: str): cs = _store() if cs is None: @@ -54,13 +55,15 @@ async def resolve_watchpoint(wp_id: str): cs.resolve_watchpoint(wp_id) return {"ok": True} - @router.post("/api/context/expectations/{exp_id}/resolve", - dependencies=[Depends(require_control)]) + @router.post( + "/api/context/expectations/{exp_id}/resolve", dependencies=[Depends(require_control)] + ) async def resolve_expectation(exp_id: str, status: str = Body("confirmed", embed=True)): cs = _store() if cs is None: return {"ok": False, "error": "context store unavailable"} from gently.harness.memory.model import ExpectationStatus + try: st = ExpectationStatus(status) except ValueError: From ec64daf6f8a2b46b36bd9d38ba31e38b62f7fc02 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Mon, 15 Jun 2026 02:15:48 +0530 Subject: [PATCH 22/34] Models: revert main tier from Fable 5 to Opus 4.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fable 5 was declining benign planning turns (stop_reason="refusal"), which tripped the refusal fallback on essentially every turn — adding a full extra round-trip of latency per message. Point MODEL_MAIN at Opus 4.8 so the common path is a single call; refusal_fallback is now inert (the guard skips it when fallback == main). Set MODEL_MAIN=claude-fable-5 to switch back once Fable 5 stops refusing. Prune the now-stale Fable-5 notes from the tier docstring. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/settings.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/gently/settings.py b/gently/settings.py index 37ea4336..9e7d2e68 100644 --- a/gently/settings.py +++ b/gently/settings.py @@ -59,12 +59,11 @@ class ModelSettings: """Claude model identifiers — the single source of truth for every tier. Tiers are split by role; capability-first per the latest models: - - main: Claude Fable 5 ($10/$50 per MTok). Per-user-turn reasoning + - tool orchestration (plan mode) and the dopaminergic classifier - stage. Always-on thinking (no thinking budget — control depth - via output_config.effort), ~30%-heavier tokenizer, may refuse - (stop_reason="refusal", empty content); needs ≥30-day org - data retention. + - main: Opus 4.8 ($5/$25). Per-user-turn reasoning + tool + orchestration (plan mode) and the dopaminergic classifier + stage. (Fable 5 was tried here but declined benign planning + turns — stop_reason="refusal" — forcing a fallback on every + turn; set MODEL_MAIN=claude-fable-5 to retry it.) - perception: Opus 4.8 (high-res vision, $5/$25). Highest-frequency tier (per timepoint); Opus-tier vision for perception accuracy. - medium: Opus 4.8. Onboarding / wizard summaries. @@ -72,19 +71,20 @@ class ModelSettings: verifier's parallel ensemble (ensemble_size calls per verification) and blank-image / summary checks. - API note: Fable 5 and Opus 4.8 reject thinking budget_tokens and sampling - params (temperature/top_p/top_k) — adaptive thinking only, depth via effort. - Sonnet 4.6 supports adaptive thinking. No assistant prefills anywhere (4.6+ - family rejects them). + API note: Opus 4.8 rejects thinking budget_tokens and sampling params + (temperature/top_p/top_k) — adaptive thinking only, depth via effort. + Sonnet 4.6 supports adaptive thinking. No assistant prefills anywhere + (4.6+ family rejects them). """ - main: str = field(default_factory=lambda: _env("MODEL_MAIN", "claude-fable-5")) + main: str = field(default_factory=lambda: _env("MODEL_MAIN", "claude-opus-4-8")) perception: str = field(default_factory=lambda: _env("MODEL_PERCEPTION", "claude-opus-4-8")) fast: str = field(default_factory=lambda: _env("MODEL_FAST", "claude-sonnet-4-6")) medium: str = field(default_factory=lambda: _env("MODEL_MEDIUM", "claude-opus-4-8")) - # When the main tier (Fable 5) declines a turn (stop_reason="refusal"), the - # main-tier calls transparently retry it on this model instead of surfacing - # the refusal. Empty disables the fallback. + # If the main tier declines a turn (stop_reason="refusal"), retry it on this + # model instead of surfacing the refusal. Inert while main is Opus 4.8 (the + # guard skips it when fallback == main); relevant if main is set to Fable 5. + # Empty disables the fallback. refusal_fallback: str = field( default_factory=lambda: _env("MODEL_REFUSAL_FALLBACK", "claude-opus-4-8") ) From d5dac56bde090c9a9ba9cd21d311b45afaffa31c Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 04:28:30 +0530 Subject: [PATCH 23/34] UX v2: expandable tool cards show full (bounded) tool results Stream a bounded result_full alongside the 140-char result_summary so the web UI's expandable tool card can show what a tool actually returned, not just the one-line preview. Frontend (landing.js/css) renders the expandable card; the thinking indicator's label is wrapped in a span so it can be updated live. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/conversation.py | 9 ++ gently/ui/web/static/css/landing.css | 41 ++++- gently/ui/web/static/js/landing.js | 222 +++++++++++++++++++++++---- gently/ui/web/templates/index.html | 2 +- 4 files changed, 241 insertions(+), 33 deletions(-) diff --git a/gently/harness/conversation.py b/gently/harness/conversation.py index 9085e96b..6ba26262 100644 --- a/gently/harness/conversation.py +++ b/gently/harness/conversation.py @@ -737,12 +737,21 @@ def _run(m): if len(result_summary) > 140: result_summary = result_summary[:139] + "…" + # Full result (bounded) so the UI's expandable tool card can + # show what the tool actually returned — not just the 140-char + # one-liner. The web client caps/scrolls this further; keep the + # streamed payload sane. + result_full = result_text or "" + if len(result_full) > 4000: + result_full = result_full[:4000] + "\n…(truncated)" + yield { "type": "tool_call", "tool_name": block.name, "tool_input": block.input, "duration": time.time() - start_time, "result_summary": result_summary, + "result_full": result_full, "is_error": is_error_flag, } diff --git a/gently/ui/web/static/css/landing.css b/gently/ui/web/static/css/landing.css index fff753e8..dad23d0b 100644 --- a/gently/ui/web/static/css/landing.css +++ b/gently/ui/web/static/css/landing.css @@ -292,6 +292,21 @@ body.ux-v2 .home-hero { display: none; } .v2-plan-activity { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; } .v2-plan-activity:empty { display: none; margin: 0; } +/* Paginated feed — one agent step (turn) per page, flipped with ‹ Prev / Next ›. + The pager bar / dots reuse .v2-plan-pager / .v2-plan-dots styling. */ +.v2-feed-pages { display: flex; flex-direction: column; } +.v2-act-page { display: none; flex-direction: column; gap: 8px; } +.v2-act-page.active { display: flex; } +/* beat .v2-plan-pager/.v2-plan-dots { display:flex } so [hidden] actually hides */ +.v2-plan-pager[hidden], .v2-plan-dots[hidden] { display: none; } +.v2-feed-pager-bar { margin: 0 0 4px; } +.v2-feed-dots { margin-top: 10px; } +/* the current question, pinned below the paged feed, set off by a divider — + only once there are steps above it (no stray line on the first choice card) */ +#v2-plan-activity:has(.v2-act-page) + .v2-plan-ask:not(:empty) { + margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border, #e4e9f0); +} + /* agent prose between tool calls */ .v2-act-text { font-size: var(--v2-fs-sm); line-height: 1.55; color: var(--text-secondary, #475569); white-space: pre-wrap; } @@ -359,6 +374,29 @@ body.ux-v2[data-theme="dark"] .v2-plan-error { .v2-plan-task::before { content: "·"; color: var(--text-muted, #94a3b8); } .v2-plan-title-row { font-size: var(--v2-fs-sm); font-weight: 600; letter-spacing: -.01em; color: var(--text, #0f172a); margin-bottom: 8px; } .v2-plan-row.v2-plan-row-freetext .v { font-style: italic; } +.v2-plan-task-empty { color: var(--text-muted, #94a3b8); font-style: italic; } +.v2-plan-task-empty::before { color: transparent; } + +/* THE PLAN pager: ‹ Prev · "Phase · i of N" · Next › + dots, one phase per page */ +.v2-plan-pager { display: flex; align-items: center; gap: 8px; margin: 2px 0 12px; } +.v2-plan-pager-btn { + flex: none; background: none; border: 0; cursor: pointer; font: inherit; + font-size: var(--v2-fs-cap); font-weight: 600; color: var(--accent, #2f6df6); + padding: 5px 7px; border-radius: 7px; transition: background .15s, color .15s, opacity .15s; +} +.v2-plan-pager-btn:hover:not(:disabled) { background: var(--accent-soft); } +.v2-plan-pager-btn:disabled { color: var(--text-muted, #94a3b8); opacity: .45; cursor: default; } +.v2-plan-pager-pos { + flex: 1; min-width: 0; text-align: center; font-size: var(--v2-fs-cap); font-weight: 600; + color: var(--text, #0f172a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.v2-plan-dots { display: flex; gap: 6px; justify-content: center; margin-top: 12px; } +.v2-plan-dot { + width: 7px; height: 7px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; + background: var(--border, #e4e9f0); transition: background .2s, transform .2s; +} +.v2-plan-dot:hover { transform: scale(1.25); } +.v2-plan-dot.active { background: var(--accent, #2f6df6); } @media (prefers-reduced-motion: reduce) { .v2-landing, .v2-landing::before, .v2-landing-rise, .v2-landing-orb, .v2-plan-orb, @@ -366,7 +404,8 @@ body.ux-v2[data-theme="dark"] .v2-plan-error { .v2-landing[data-screen="welcome"] .v2-screen-welcome, .v2-typing i, .v2-act-spin, .v2-act-chev, .v2-act-tool-body, .v2-act-tool-head, - .v2-choice, .v2-escape-field, .v2-escape-toggle { + .v2-choice, .v2-escape-field, .v2-escape-toggle, + .v2-plan-pager-btn, .v2-plan-dot { animation: none !important; transition-duration: .12s !important; } diff --git a/gently/ui/web/static/js/landing.js b/gently/ui/web/static/js/landing.js index 2f524c1e..bb4a0923 100644 --- a/gently/ui/web/static/js/landing.js +++ b/gently/ui/web/static/js/landing.js @@ -54,7 +54,22 @@ const V2Landing = (() => { } // ── status / error helpers ──────────────────────────────────────── - function showThinking(on) { const t = $('v2-plan-thinking'); if (t) t.classList.toggle('hidden', !on); } + function setThinkingLabel(text) { + const l = document.querySelector('#v2-plan-thinking .v2-plan-thinking-label'); + if (l && text) l.textContent = text; + } + function showThinking(on, label) { + const t = $('v2-plan-thinking'); + if (t) t.classList.toggle('hidden', !on); + if (on && label) setThinkingLabel(label); + } + // Human-readable "what's happening right now" from a tool activity event, + // so the status line names the live operation instead of a static string. + function prettyTool(act) { + const raw = (act && (act.label || act.name)) || 'the next step'; + const s = String(raw).replace(/_/g, ' ').trim(); + return s.charAt(0).toUpperCase() + s.slice(1) + '…'; + } function errorVisible() { const e = $('v2-plan-error'); return !!e && !e.classList.contains('hidden'); } function showPlanError(msg) { const e = $('v2-plan-error'); if (!e) return; @@ -67,17 +82,90 @@ const V2Landing = (() => { function resetSummary() { const list = $('v2-plan-summary'); if (list) list.innerHTML = '

The plan will take shape here as Gently designs it.
'; + planPage = 0; planPages = []; planTitleText = ''; } - // ── activity feed (claude.ai-style collapsible tool cards) ───────── + // ── activity feed: paginated, ONE agent step (turn) per page ─────── + // Instead of one ever-growing scroll, each agent turn — its reasoning + + // the tool calls it made — is a page you flip through with ‹ Prev / Next ›. + // The current question stays pinned below the feed (#v2-plan-ask). A new + // turn auto-advances to its page; you can flip back to review earlier steps. + let feedPages = []; // .v2-act-page elements, one per turn + let feedPage = 0; // index currently shown + let curPageEl = null; // page receiving this turn's content + let pendingNewPage = false; // a turn started; open a fresh page on first content + function feedEl() { return $('v2-plan-activity'); } + function feedPagesWrap() { return feedEl()?.querySelector('.v2-feed-pages'); } function clearActivity() { - const f = feedEl(); if (f) f.innerHTML = ''; + const f = feedEl(); + if (f) { + f.innerHTML = + '' + + '
' + + ''; + } + feedPages = []; feedPage = 0; curPageEl = null; pendingNewPage = false; feedTextEl = null; runningTools = {}; feedHadContent = false; capturedCampaignId = null; hidePlanError(); } + function newFeedPage() { + const wrap = feedPagesWrap(); if (!wrap) return null; + const page = document.createElement('div'); + page.className = 'v2-act-page'; + wrap.appendChild(page); + feedPages.push(page); + curPageEl = page; + feedPage = feedPages.length - 1; // auto-advance to the live step + feedTextEl = null; + drawFeedPager(); + return page; + } + // Where this turn's prose/tool cards land. Opens a fresh page the first time + // content arrives after a turn_start (so content-less command turns don't + // leave empty pages), and lazily on the very first content. + function feedTarget() { + if (pendingNewPage || !curPageEl) { newFeedPage(); pendingNewPage = false; } + return curPageEl; + } + function viewingLatest() { return feedPage >= feedPages.length - 1; } + function drawFeedPager() { + const f = feedEl(); if (!f) return; + const n = feedPages.length; + const i = Math.min(Math.max(feedPage, 0), Math.max(n - 1, 0)); + feedPages.forEach((p, idx) => p.classList.toggle('active', idx === i)); + const bar = f.querySelector('.v2-feed-pager-bar'); + const dots = f.querySelector('.v2-feed-dots'); + if (!bar || !dots) return; + if (n <= 1) { bar.hidden = true; dots.hidden = true; return; } + bar.hidden = false; dots.hidden = false; + bar.innerHTML = ''; + const mkBtn = (txt, disabled, fn) => { + const b = document.createElement('button'); + b.className = 'v2-plan-pager-btn'; b.type = 'button'; b.textContent = txt; + b.disabled = disabled; b.addEventListener('click', fn); + return b; + }; + const pos = document.createElement('span'); + pos.className = 'v2-plan-pager-pos'; pos.textContent = `Step ${i + 1} of ${n}`; + bar.append( + mkBtn('‹ Prev', i === 0, () => { if (feedPage > 0) { feedPage--; drawFeedPager(); } }), + pos, + mkBtn('Next ›', i === n - 1, () => { if (feedPage < n - 1) { feedPage++; drawFeedPager(); } }), + ); + dots.innerHTML = ''; + for (let d = 0; d < n; d++) { + const dot = document.createElement('button'); + dot.className = 'v2-plan-dot' + (d === i ? ' active' : ''); + dot.type = 'button'; + dot.setAttribute('aria-label', `Step ${d + 1} of ${n}`); + dot.addEventListener('click', () => { feedPage = d; drawFeedPager(); }); + dots.appendChild(dot); + } + } function scrollFeedIfNearBottom() { + if (!viewingLatest()) return; // don't yank the user off an earlier step const m = document.querySelector('.v2-screen-plan .v2-plan-main'); if (m && (m.scrollHeight - m.scrollTop - m.clientHeight) < 140) m.scrollTop = m.scrollHeight; } @@ -172,10 +260,11 @@ const V2Landing = (() => { const f = feedEl(); if (!f) return; switch (act.kind) { case 'turn_start': - feedTextEl = null; hidePlanError(); clearFallback(); showThinking(true); + feedTextEl = null; pendingNewPage = true; hidePlanError(); clearFallback(); + showThinking(true, 'reviewing your campaign and plan…'); break; case 'thinking': - showThinking(true); + showThinking(true, 'thinking through the next step…'); break; case 'text': { const chunk = act.text || ''; @@ -184,11 +273,11 @@ const V2Landing = (() => { feedTextEl = document.createElement('div'); feedTextEl.className = 'v2-act-text'; feedTextEl._raw = ''; - f.appendChild(feedTextEl); + feedTarget().appendChild(feedTextEl); } feedTextEl._raw += chunk; feedTextEl.innerHTML = renderMd(feedTextEl._raw); - feedHadContent = true; showThinking(true); scrollFeedIfNearBottom(); + feedHadContent = true; showThinking(true, 'composing the response…'); scrollFeedIfNearBottom(); break; } case 'tool_start': { @@ -197,9 +286,9 @@ const V2Landing = (() => { if (act.name === 'ask_user_choice') break; feedTextEl = null; const card = buildToolCard(act, false); - f.appendChild(card); + feedTarget().appendChild(card); (runningTools[act.name] = runningTools[act.name] || []).push(card); - feedHadContent = true; showThinking(true); scrollFeedIfNearBottom(); + feedHadContent = true; showThinking(true, prettyTool(act)); scrollFeedIfNearBottom(); break; } case 'tool_result': { @@ -211,8 +300,8 @@ const V2Landing = (() => { const arr = runningTools[act.name] || []; const card = arr.pop(); if (card) updateToolCard(card, act); - else f.appendChild(buildToolCard(act, true)); - feedHadContent = true; scrollFeedIfNearBottom(); + else feedTarget().appendChild(buildToolCard(act, true)); + feedHadContent = true; setThinkingLabel('working through the next step…'); scrollFeedIfNearBottom(); break; } case 'turn_end': @@ -232,7 +321,7 @@ const V2Landing = (() => { d.className = 'v2-plan-fallback'; d.innerHTML = 'Gently replied in prose — open the conversation to read it.'; d.querySelector('a').addEventListener('click', openChat); - f.appendChild(d); + feedTarget().appendChild(d); } // ── THE PLAN panel: mirror the real campaign tree ────────────────── @@ -254,35 +343,101 @@ const V2Landing = (() => { c = c || {}; return c.shorthand || c.display_name || c.description || 'Plan'; } + // THE PLAN renders as a pager — one phase per page with ‹ Prev / Next ›, + // a position label, and dots — instead of one long scroll. planPage is held + // across re-renders (the panel refetches on every plan-writing tool) so the + // page you're reading doesn't snap back to the start mid-design. + let planPage = 0; + let planPages = []; // [{ name, items }] + let planTitleText = ''; + function renderPlanTree(tree) { - const list = $('v2-plan-summary'); - if (!list || !tree) return; + if (!tree) return; const phases = tree.children || []; const rootItems = tree.items || []; if (!phases.length && !rootItems.length) return; // nothing to show yet — keep placeholder + const pages = []; + if (rootItems.length) pages.push({ name: 'Tasks', items: rootItems }); + phases.forEach(phase => { + if (!phase) return; + pages.push({ name: planName(phase.campaign), items: phase.items || [] }); + }); + planPages = pages; + planTitleText = planName(tree.campaign); + if (planPage >= pages.length) planPage = pages.length - 1; + if (planPage < 0) planPage = 0; + drawPlanPage(); + } + + function drawPlanPage() { + const list = $('v2-plan-summary'); + if (!list) return; + const pages = planPages; + const n = pages.length; + if (!n) return; + const i = Math.min(Math.max(planPage, 0), n - 1); + const page = pages[i]; + list.innerHTML = ''; const title = document.createElement('div'); title.className = 'v2-plan-title-row'; - title.textContent = planName(tree.campaign); + title.textContent = planTitleText; list.appendChild(title); - const addTask = (parent, it) => { - const t = document.createElement('div'); - t.className = 'v2-plan-task'; - t.textContent = it.title || it.shorthand || '(task)'; - parent.appendChild(t); - }; - rootItems.forEach(it => addTask(list, it)); - phases.forEach(phase => { - if (!phase) return; - const wrap = document.createElement('div'); - wrap.className = 'v2-plan-phase'; + + if (n > 1) { + const bar = document.createElement('div'); + bar.className = 'v2-plan-pager'; + const prev = document.createElement('button'); + prev.className = 'v2-plan-pager-btn'; prev.type = 'button'; prev.textContent = '‹ Prev'; + prev.disabled = i === 0; + prev.addEventListener('click', () => { if (planPage > 0) { planPage--; drawPlanPage(); } }); + const pos = document.createElement('span'); + pos.className = 'v2-plan-pager-pos'; + pos.textContent = `${page.name} · ${i + 1} of ${n}`; + const next = document.createElement('button'); + next.className = 'v2-plan-pager-btn'; next.type = 'button'; next.textContent = 'Next ›'; + next.disabled = i === n - 1; + next.addEventListener('click', () => { if (planPage < n - 1) { planPage++; drawPlanPage(); } }); + bar.append(prev, pos, next); + list.appendChild(bar); + } else { const h = document.createElement('div'); h.className = 'v2-plan-phase-h'; - h.textContent = planName(phase.campaign); - wrap.appendChild(h); - (phase.items || []).forEach(it => addTask(wrap, it)); - list.appendChild(wrap); - }); + h.textContent = page.name; + list.appendChild(h); + } + + const tasks = document.createElement('div'); + tasks.className = 'v2-plan-phase'; + const items = page.items || []; + if (items.length) { + items.forEach(it => { + const t = document.createElement('div'); + t.className = 'v2-plan-task'; + t.textContent = it.title || it.shorthand || '(task)'; + tasks.appendChild(t); + }); + } else { + const e = document.createElement('div'); + e.className = 'v2-plan-task v2-plan-task-empty'; + e.textContent = 'no items in this phase yet'; + tasks.appendChild(e); + } + list.appendChild(tasks); + + if (n > 1) { + const dots = document.createElement('div'); + dots.className = 'v2-plan-dots'; + for (let d = 0; d < n; d++) { + const dot = document.createElement('button'); + dot.className = 'v2-plan-dot' + (d === i ? ' active' : ''); + dot.type = 'button'; + dot.setAttribute('aria-label', `Go to ${pages[d].name} (${d + 1} of ${n})`); + dot.addEventListener('click', () => { planPage = d; drawPlanPage(); }); + dots.appendChild(dot); + } + list.appendChild(dots); + } } // ── ask rendering (the active question) ──────────────────────────── @@ -470,6 +625,11 @@ const V2Landing = (() => { if (request_id === '*' || (current && request_id === current.reqId)) { current = null; clearAsk(); if (planActive() && !errorVisible()) showThinking(true); + // A question was just answered — the agent's continuation is the + // next step, so open a fresh feed page for it. (A turn stays one + // stream across an ask_user_choice pause, so turn_start alone + // would lump every step of the design into a single page.) + pendingNewPage = true; } }); ClientEventBus.on('AGENT_CONTROL', () => { if (current && planActive()) renderAsk(); }); diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index 36688f8a..e83eda63 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -83,7 +83,7 @@

Take a quick look

-
thinking through the next step…
+
working through the next step…
@@ -648,6 +649,40 @@

Properties

+ + +
@@ -748,6 +783,7 @@

Properties

+ diff --git a/screenshots/occupancy3d-demo.png b/screenshots/occupancy3d-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..d76885724a4a54650e4e917f0f7359c5ec71be5b GIT binary patch literal 244754 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 TrFVG4Cmh;Z274avatQi=n=0Ej literal 0 HcmV?d00001 From 91ec6d2f5befd489b007c35ddbca8660c8412afe Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 06:59:57 +0530 Subject: [PATCH 25/34] docs: UX v2 interaction-flow / IA audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flow/IA audit (not visual) from a live click-audit with the agent on, plus code cross-check. Prioritized findings + fixes: P0 loading-state legibility (stream thinking summary — set thinking display:summarized + handle thinking_delta), first-character truncation in the plan feed, control/auth wall hidden in chat, double ask-mount, ASK_CLEARED never emitted, no path back to welcome, non-stateful routing, resume hard-reload, and the workspace-IA placement of the 3D optical-space view. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/ux-v2-flow-audit.md | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/ux-v2-flow-audit.md diff --git a/docs/ux-v2-flow-audit.md b/docs/ux-v2-flow-audit.md new file mode 100644 index 00000000..00bba3e7 --- /dev/null +++ b/docs/ux-v2-flow-audit.md @@ -0,0 +1,106 @@ +# UX v2 — interaction-flow / IA audit + +**Branch:** `feature/ux-v2` (now includes the 3D optical-space view). +**Scope:** the *flow* of the agent-first UI — clicks, how each step renders, how the +workspace is unveiled, moving back/forth between views, resume — **not** the visual look +(the look is fine). Plus where the 3D optical-space view belongs in the new workspace IA. + +**Method:** live click-audit driven through a real browser as a *dev biologist* would use +it, with the agent **live** (Opus 4.8, `--offline` hardware, `GENTLY_NO_AUTH=1` single +controller), cross-checked against the code. Screenshots from the run are in `screenshots/audit-*.png`. + +> Correction to an earlier automated pass: the plan-wizard helpers +> (`buildAskCard`/`answerChoice`/`togglePanel`) are **not** missing — `agent-chat.js` +> exports them and the module loads; the plan wizard works. The real issues are below. + +--- + +## What works (keep it) + +- **The forward path is good.** Entry → one calm choice (Plan / Quick look / "just tell me") + → overlay dismisses to reveal the workspace → grouped rail (NOW / LIBRARY / SYSTEM) drives + everything through one chokepoint (`app.js switchTab`). The welcome→workspace unveil is genuinely nice. +- **The agent-driven plan wizard is strong.** Live, it asked a well-framed scientific question + ("What's the core scientific question this run should capture?") with real C. elegans options, + ran a `query_lab_history` tool with visible provenance, and **assembled THE PLAN panel as each + answer landed** (strain → wavelengths, etc.). The "plan builds as you answer" feel is excellent. +- **The dual-render** (ask shows in the plan stage *and* the chat transcript) is implemented. + +--- + +## Findings (prioritized) + +| # | Pri | Symptom (felt) | Root cause / evidence | Fix | +|---|-----|----------------|------------------------|-----| +| 1 | **P0** | First plan step sat on "working through the next step…" for **~90s** with a static spinner — feels hung. | The wait is the model *thinking*. The streaming call requests **no thinking config** and the stream loop reads only text deltas. `conversation.py:272-275` (only `output_config.effort`), `conversation.py:654-657` (only `event.delta.text`). | Set `thinking={"type":"adaptive","display":"summarized"}` on the stream (`conversation.py:552`); handle `thinking_delta` in the loop (`:654`) and emit as a `thinking` activity; render it live + add an elapsed timer. See §1. | +| 2 | **P1** | Agent's first line renders as **"'d love to help…"** — leading "I" dropped. | Plan-feed streaming path drops the first character of the turn's first text block; the chat transcript renders it correctly (`12_41` vs `12_3` in the run). Plan feed: `landing.js applyActivity` `'text'` case (`:269`). | Most likely the first `AGENT_ACTIVITY`/`text` delta is missed by `landing.js`'s listener (subscribed after the first delta) or coalesced wrong. Confirm with a 1-line repro; the transcript path is the reference. | +| 3 | **P1** | Clicked the primary "Plan an experiment" → plan stage spun forever; the *real* blocker ("Viewing only — control is held by another client / sign in to control") was **hidden in the chat panel**. | Control/auth state isn't surfaced on the landing/plan surface — only in the chat dock. A viewer can enter the plan flow and dead-end. | Surface control/sign-in state on the landing **before** the primary CTA; gate or relabel "Plan an experiment" when `!hasControl`; show the wall on the plan stage, not just chat. | +| 4 | **P1** | (Structural) The same ask renders in **two** stage mounts plus the transcript. | `#v2-plan-ask` **and** `#ask-stage` both render the ask (the overlay covers the workspace copy, so only cosmetic/perf today). Two live regions seen in the run (`12_10` + `12_24`). | One stage mount at a time — suppress `#ask-stage` while the landing overlay owns the ask. | +| 5 | **P1** | Cross-surface clear can desync. | `ASK_CLEARED` is **listened for but emitted nowhere** (`landing.js:624`, `ask-stage.js:43` listen; no emit in repo). Answering works locally because `renderAsk.onPick` clears directly, but stage↔transcript sync relies on the missing signal. | Emit `ASK_CLEARED` the instant a `choice_response` is sent (per the migration plan's Phase-1 blocker), plus on cancel/control-loss/socket-close. | +| 6 | **P1** | **No way back.** Once the landing dismisses, there's no path back to welcome / "start a new plan" from the workspace — must reload. | `dismiss()` is one-way (`landing.js:42-54`); `V2Landing.show()` exists but is never called from the workspace. | Add a "New plan" / "Talk to Gently" entry in the rail or header that re-summons the welcome/plan surface. | +| 7 | **P2** | Browser **Back / refresh don't mean anything**; refresh mid-plan loses state and may re-show the landing. | Entry hash is consumed (`app.js` → `replaceState('/')`, ~`:650-662`); no deliberate URL/state sync; in-memory plan state (`planKickedOff`, feed pages) resets on reload. | Real routing: sync screen/tab to URL/History so Back/forward/refresh resolve; persist or re-hydrate plan progress. | +| 8 | **P2** | **Resume = full page reload** — jarring, re-shows landing, drops chat position. | `session_changed` → `window.location.href='/'` (`websocket.js:147`; `review.js resumeSession ~:101-116`). Flagged in the migration plan. | In-place re-hydration on `session_changed` instead of a hard reload. | +| 9 | **P1 (IA)** | The **3D optical-space view is buried**: SYSTEM → Devices → (Map / Details / **3D**) — a sub-sub-toggle. | It was integrated into the *legacy* Devices tab structure; the ux-v2 grouped rail doesn't surface it. | Promote "the scope in space" to a first-class run-time surface (NOW tier), reconciled with the grouped rail. See §2. | +| 10 | **P2** | Offline / agent-silent dead-ends the wizard at "working…". | `startPlan` campaign fetch falls through silently if offline (`landing.js ~:502-508`); no error path. | Timeout + inline error/retry on the plan stage. | + +--- + +## §1 — Make the loading state legible (P0, the one the user wants first) + +The 90s "working…" is the agent reasoning. The Claude streaming API exposes this on three +channels; gently currently surfaces none of the reasoning: + +- **Thinking** — `content_block_delta` → `thinking_delta`. **Opus 4.8 defaults to + `display:"omitted"` (empty thinking text)**, and gently doesn't set the thinking config at + all on the stream, so there's nothing to show. Unlock: `thinking={"type":"adaptive","display":"summarized"}`. +- **Tool activity** — `input_json_delta` + tool start/stop. **Already flowing** — the plan feed + renders tool cards (saw the `query_lab_history` card with input/result). +- **Text** — `text_delta`. Already flowing (this is the path with the bug #2 truncation). + +**Backend (`gently/harness/conversation.py`):** +1. `:552` `self.claude.messages.stream(...)` — add `thinking={"type":"adaptive","display":"summarized"}` + (keep `output_config.effort`). +2. `:654` event loop — currently only `if hasattr(event.delta, "text")`. Add a branch for + `event.delta.type == "thinking_delta"` → `yield {"type":"thinking","text": event.delta.thinking}`. + +**Frontend (`gently/ui/web/static/js/landing.js`):** `applyActivity` already has a `thinking` +case (`:266`) that only sets a static label — render the streamed thinking text instead, and add +an elapsed timer to `#v2-plan-thinking` so a long think reads as progress, not a hang. + +Net: the reasoning summary + current tool + a timer fill the wait. Only the backend `display` +flag is a new capability; the rest is surfacing data gently already receives. + +--- + +## §2 — Workspace organization & where the 3D view belongs (P1, IA) + +The ux-v2 workspace is organized differently from the old flat tab bar: a **grouped rail** +(NOW: Home/Experiment/Embryos · LIBRARY: Plans/Sessions · SYSTEM: Devices/Calibration/Logs), +a **session-context strip**, and the **AGENT'S VIEW** surface. The 3D optical-space view, +however, lives in the *legacy* Devices structure (`devices.js switchView`, VIEWS = +`['map','details','optical3d']`; `index.html` devices-content Map/Details/3D switcher). + +During an actual run, "where the scope is in space" + the live experiment + the agent's view are +**NOW-tier** concerns, not a System utility three clicks deep. Proposal (to design next): +- Promote the 3D optical-space + live experiment to a first-class run-time surface in the rail + (or make it the default workspace view while a run is active). +- Keep the Devices Map/Details as the System-tier hardware utility; the 3D "scope in space" + graduates out of that toggle. + +--- + +## Recommended sequencing + +1. **P0 loading state** (§1) — highest felt value, mostly surfacing existing data. +2. **P1 quick correctness**: #2 truncation, #3 control-wall surfacing, #4 single ask mount, #5 `ASK_CLEARED` emit. +3. **P1 reachability**: #6 "new plan"/back entry; then #9 the workspace-IA / 3D-placement redesign (its own design pass). +4. **P2 navigation**: #7 real routing, #8 resume re-hydration, #10 offline error path. + +--- + +## Notes / housekeeping + +- Findings 1–5, 10 verified live with the agent on; 6–9 verified from code + the live rail. +- `screenshots/audit-*.png` (live run) and `screenshots/uxv2-*.png` are local evidence (untracked). +- The earlier visual-design exploration (`docs/superpowers/mockups/`, `screenshots/dir-*.png`) is + superseded — the look is staying as-is — and can be deleted. From 9edb766255720b0e50c3d1e63d97d77955b18da3 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 07:20:37 +0530 Subject: [PATCH 26/34] UX v2 P0: live-stream the agent turn + show reasoning during the wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streaming path collected the entire turn before yielding anything, so the plan wizard sat on a static "working…" spinner for the whole turn (~90s). And the streamed call requested no thinking, so there was no reasoning to show even if it had streamed. - conversation.py: rewrite call_claude_stream to stream live — a worker thread drains the SDK stream and pushes events onto an asyncio queue as they arrive; the coroutine yields text/thinking deltas in real time. Enable adaptive thinking with display="summarized" (+ effort=medium) and emit thinking_delta as {"type":"thinking"} chunks. Full assistant content (incl. thinking blocks) is still replayed from final_message, so the tool loop stays valid. Retry / 400-fallback / refusal-fallback preserved (clean while nothing's been yielded). - agent-chat.js: forward the thinking text on the 'thinking' activity. - landing.js: render streamed reasoning as a dim block in the plan feed and add an elapsed-time counter to the thinking indicator so a long think reads as progress, not a hang. Verified live (Opus 4.8): reasoning + prose stream into the plan feed during the turn with a ticking timer; no console errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/conversation.py | 190 +++++++++++++++++--------- gently/ui/web/static/js/agent-chat.js | Bin 63820 -> 63846 bytes gently/ui/web/static/js/landing.js | 63 ++++++++- 3 files changed, 178 insertions(+), 75 deletions(-) diff --git a/gently/harness/conversation.py b/gently/harness/conversation.py index 6ba26262..067e2f85 100644 --- a/gently/harness/conversation.py +++ b/gently/harness/conversation.py @@ -546,80 +546,142 @@ async def call_claude_stream(self, system_prompt, tools, tool_label_fn, auto_sav """ from anthropic import APIStatusError, BadRequestError - def stream_and_collect(model): - def _run(m): - events = [] - with self.claude.messages.stream( - model=m, - system=system_prompt, - messages=self.conversation_history, - tools=tools, - max_tokens=4096, - ) as stream: - for event in stream: - events.append(event) - return events, stream.get_final_message() - - try: - return _run(model) - except BadRequestError: - # Fable 5 under <30-day org data retention (or unavailable) rejects - # with a 400 — fall back to Opus 4.8 so the turn still streams. - fb = settings.models.refusal_fallback - if not fb or fb == model: - raise - logger.warning( - "Stream model %s rejected the request (400); falling back to %s", model, fb - ) - return _run(fb) + # Live streaming: a worker thread drains the SDK's (blocking) stream and + # pushes each event onto an asyncio queue as it arrives, so this coroutine + # can yield text/thinking deltas in real time instead of collecting the + # whole turn first (which left the UI on a blank spinner for the entire + # turn). thinking=summarized surfaces the model's reasoning during the + # wait. The full assistant content (incl. thinking blocks) is replayed from + # final_message below, so the tool-loop continuation stays valid. + _DONE = object() + + async def _stream_live(model, sink): + """Stream one attempt live: yield delta dicts as they arrive; record + events / final_message / error / full_text into `sink`.""" + loop = asyncio.get_running_loop() + queue: asyncio.Queue = asyncio.Queue() + state: dict = {} + + def worker(): + try: + with self.claude.messages.stream( + model=model, + system=system_prompt, + messages=self.conversation_history, + tools=tools, + max_tokens=16000, + # Adaptive thinking with a streamed, human-readable summary — + # this is what fills the "working…" wait. Opus 4.8 defaults to + # display="omitted" (empty thinking text), so it must be set. + thinking={"type": "adaptive", "display": "summarized"}, + output_config={"effort": "medium"}, + ) as stream: + for event in stream: + loop.call_soon_threadsafe(queue.put_nowait, event) + state["final"] = stream.get_final_message() + except BaseException as exc: # noqa: BLE001 — re-raised to caller below + state["error"] = exc + finally: + loop.call_soon_threadsafe(queue.put_nowait, _DONE) + + task = asyncio.create_task(asyncio.to_thread(worker)) + events: list = [] + full_text: list = [] + while True: + item = await queue.get() + if item is _DONE: + break + events.append(item) + if item.type != "content_block_delta": + continue + delta = item.delta + dtype = getattr(delta, "type", None) + if dtype == "thinking_delta": + chunk = getattr(delta, "thinking", "") or "" + if chunk: + yield {"type": "thinking", "text": chunk} + elif dtype == "text_delta" or hasattr(delta, "text"): + chunk = getattr(delta, "text", "") or "" + if chunk: + full_text.append(chunk) + yield {"type": "text", "text": chunk} + await task + sink["events"] = events + sink["full_text"] = full_text + sink["final"] = state.get("final") + sink["error"] = state.get("error") - # Run streaming in thread with retry logic max_retries = 3 retry_delay = 1.0 + fb = settings.models.refusal_fallback + model_in_use = self.model + sink: dict = {} for attempt in range(max_retries): - try: - events, final_message = await asyncio.to_thread(stream_and_collect, self.model) - self._track_token_usage(final_message) + sink = {} + yielded_any = False + async for chunk in _stream_live(model_in_use, sink): + yielded_any = True + yield chunk + err = sink.get("error") + if err is None: break - except APIStatusError as e: - error_type = getattr(e, "body", {}) + # Fable 5 under <30-day data retention (or unavailable) rejects with a + # 400 — fall back to Opus 4.8. Only safe before any partial was streamed. + if isinstance(err, BadRequestError) and fb and fb != model_in_use and not yielded_any: + logger.warning( + "Stream model %s rejected the request (400); falling back to %s", + model_in_use, + fb, + ) + model_in_use = fb + continue + if isinstance(err, APIStatusError): + error_type = getattr(err, "body", {}) if isinstance(error_type, dict): error_type = error_type.get("error", {}).get("type", "") - - if ( + overloaded = ( error_type in ("overloaded_error", "rate_limit_error") - or "overloaded" in str(e).lower() - ): - if attempt < max_retries - 1: - wait_time = retry_delay * (2**attempt) - logger.warning( - f"API overloaded, retrying in {wait_time:.1f}s " - f"(attempt {attempt + 1}/{max_retries})" - ) - yield { - "type": "text", - "text": f"\n*[API busy, retrying in {wait_time:.0f}s...]*\n", - } - await asyncio.sleep(wait_time) - continue - raise + or "overloaded" in str(err).lower() + ) + if overloaded and attempt < max_retries - 1 and not yielded_any: + wait_time = retry_delay * (2**attempt) + logger.warning( + "API overloaded, retrying in %.1fs (attempt %d/%d)", + wait_time, + attempt + 1, + max_retries, + ) + yield { + "type": "text", + "text": f"\n*[API busy, retrying in {wait_time:.0f}s...]*\n", + } + await asyncio.sleep(wait_time) + continue + raise err else: raise RuntimeError("API overloaded after multiple retries") - # Refusal → retry the whole streamed turn on the fallback model (Opus 4.8) - # before giving up. The original partial output is discarded (we re-collect - # and only yield the fallback's events below). - fb = settings.models.refusal_fallback - if final_message.stop_reason == "refusal" and fb and fb != self.model: - logger.warning("Model %s declined the streamed turn; retrying on %s", self.model, fb) - events, final_message = await asyncio.to_thread(stream_and_collect, fb) + final_message = sink["final"] + full_text = sink["full_text"] + self._track_token_usage(final_message) + + # Refusal → retry on the fallback model. Re-streaming live is only safe when + # the refusal came before any visible output (pre-output refusals carry empty + # content, so nothing was yielded); otherwise we keep the partial we showed. + if final_message.stop_reason == "refusal" and fb and model_in_use != fb and not full_text: + logger.warning("Model %s declined the streamed turn; retrying on %s", model_in_use, fb) + sink = {} + async for chunk in _stream_live(fb, sink): + yield chunk + model_in_use = fb + final_message = sink["final"] + full_text = sink["full_text"] self._track_token_usage(final_message) - # Last resort: if even the fallback declined, surface it and stop — - # discard any partial, don't iterate empty content or process tools. + # Last resort: if even the fallback declined, surface it and stop. if final_message.stop_reason == "refusal": - logger.warning("Claude declined the request (model=%s)", self.model) + logger.warning("Claude declined the request (model=%s)", model_in_use) yield { "type": "text", "text": "(The request was declined by the model's safety system. Try rephrasing.)", @@ -639,7 +701,7 @@ def _run(m): len(final_message.content), tool_block_count, len(tools), - self.model, + model_in_use, ) if tool_block_count > 0 and final_message.stop_reason != "tool_use": logger.error( @@ -648,14 +710,6 @@ def _run(m): final_message.stop_reason, ) - # Process events and yield text - full_text = [] - for event in events: - if event.type == "content_block_delta": - if hasattr(event.delta, "text"): - full_text.append(event.delta.text) - yield {"type": "text", "text": event.delta.text} - # Detect fake XML tool calls in text (Claude writing tool_use as text) joined_text = "".join(full_text) if "" in joined_text or "" in joined_text: diff --git a/gently/ui/web/static/js/agent-chat.js b/gently/ui/web/static/js/agent-chat.js index 0e8514df3e603df1220675ba2cff1c2ce566e7d4..58be6e5c13b417fd95d0f16b42ae953a0a0c3467 100644 GIT binary patch delta 22 ecmX@}iTT+l<_#;uCr^kpnY=HDck_ktnuP$3atpoy delta 22 ecmaF%iTTVY<_#;uCp)bWnH&%-x%oqQ*+Kw_+6z7a diff --git a/gently/ui/web/static/js/landing.js b/gently/ui/web/static/js/landing.js index bb4a0923..4f0f2917 100644 --- a/gently/ui/web/static/js/landing.js +++ b/gently/ui/web/static/js/landing.js @@ -18,6 +18,7 @@ const V2Landing = (() => { let el = null; let current = null; // the ask currently in #v2-plan-ask let feedTextEl = null; // current accumulating prose paragraph in the feed + let feedThinkingEl = null; // current accumulating reasoning (thinking) block let runningTools = {}; // tool name -> stack of running card elements let feedHadContent = false; // did this turn surface anything in the feed? let capturedCampaignId = null; // best-effort id scraped from tool results @@ -58,10 +59,39 @@ const V2Landing = (() => { const l = document.querySelector('#v2-plan-thinking .v2-plan-thinking-label'); if (l && text) l.textContent = text; } + // Elapsed-time counter so a long think reads as progress, not a hang. Starts + // when the thinking indicator first shows and runs until it's hidden (turn end). + let _thinkTimer = null; + let _thinkStart = 0; + function _thinkTick() { + const t = $('v2-plan-thinking'); + if (!t) return; + let el = t.querySelector('.v2-plan-elapsed'); + if (!el) { + el = document.createElement('span'); + el.className = 'v2-plan-elapsed'; + el.style.cssText = 'margin-left:6px;opacity:.55;font-variant-numeric:tabular-nums;'; + t.appendChild(el); + } + const s = Math.round((Date.now() - _thinkStart) / 1000); + el.textContent = s > 0 ? s + 's' : ''; + } function showThinking(on, label) { const t = $('v2-plan-thinking'); if (t) t.classList.toggle('hidden', !on); if (on && label) setThinkingLabel(label); + if (on) { + if (!_thinkTimer) { + _thinkStart = Date.now(); + _thinkTick(); + _thinkTimer = setInterval(_thinkTick, 1000); + } + } else if (_thinkTimer) { + clearInterval(_thinkTimer); + _thinkTimer = null; + const el = t && t.querySelector('.v2-plan-elapsed'); + if (el) el.textContent = ''; + } } // Human-readable "what's happening right now" from a tool activity event, // so the status line names the live operation instead of a static string. @@ -106,7 +136,7 @@ const V2Landing = (() => { '

'; } feedPages = []; feedPage = 0; curPageEl = null; pendingNewPage = false; - feedTextEl = null; runningTools = {}; feedHadContent = false; + feedTextEl = null; feedThinkingEl = null; runningTools = {}; feedHadContent = false; capturedCampaignId = null; hidePlanError(); } @@ -260,15 +290,34 @@ const V2Landing = (() => { const f = feedEl(); if (!f) return; switch (act.kind) { case 'turn_start': - feedTextEl = null; pendingNewPage = true; hidePlanError(); clearFallback(); + feedTextEl = null; feedThinkingEl = null; pendingNewPage = true; hidePlanError(); clearFallback(); showThinking(true, 'reviewing your campaign and plan…'); break; - case 'thinking': - showThinking(true, 'thinking through the next step…'); + case 'thinking': { + // Stream the model's reasoning summary live into the feed as a dim + // block, so the wait shows what the agent is actually considering. + showThinking(true); + const chunk = act.text || ''; + if (!chunk) { setThinkingLabel('thinking through the next step…'); break; } + if (!feedThinkingEl) { + feedThinkingEl = document.createElement('div'); + feedThinkingEl.className = 'v2-act-think'; + feedThinkingEl.style.cssText = + 'font-style:italic;opacity:.7;white-space:pre-wrap;margin:2px 0 8px;font-size:12.5px;line-height:1.5;'; + feedThinkingEl._raw = ''; + feedTarget().appendChild(feedThinkingEl); + } + feedThinkingEl._raw += chunk; + feedThinkingEl.textContent = feedThinkingEl._raw; + feedHadContent = true; + setThinkingLabel('reasoning…'); + scrollFeedIfNearBottom(); break; + } case 'text': { const chunk = act.text || ''; if (!chunk) break; + feedThinkingEl = null; // reasoning block ends when prose begins if (!feedTextEl) { feedTextEl = document.createElement('div'); feedTextEl.className = 'v2-act-text'; @@ -284,7 +333,7 @@ const V2Landing = (() => { // ask_user_choice IS the active question (rendered separately in // #v2-plan-ask) — don't also show it as a feed card. if (act.name === 'ask_user_choice') break; - feedTextEl = null; + feedTextEl = null; feedThinkingEl = null; const card = buildToolCard(act, false); feedTarget().appendChild(card); (runningTools[act.name] = runningTools[act.name] || []).push(card); @@ -296,7 +345,7 @@ const V2Landing = (() => { captureCampaignId(act.full); if (PLAN_TOOLS.has(act.name)) schedulePlanRefresh(); if (act.name === 'ask_user_choice') break; - feedTextEl = null; + feedTextEl = null; feedThinkingEl = null; const arr = runningTools[act.name] || []; const card = arr.pop(); if (card) updateToolCard(card, act); @@ -305,7 +354,7 @@ const V2Landing = (() => { break; } case 'turn_end': - showThinking(false); feedTextEl = null; + showThinking(false); feedTextEl = null; feedThinkingEl = null; refreshPlanPanel(); if (!current && !feedHadContent) showFallback(); break; From f209874a23bd97731ac5c42f222794a6a45fc069 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 07:31:02 +0530 Subject: [PATCH 27/34] Fix create/update_plan_item crash when spec/references passed as JSON strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The model often serializes nested tool args (spec, references) as JSON strings instead of objects. create_plan_item stored the raw string, and read-back via _dict_to_plan_item did spec_data.items() on a str → AttributeError, leaving a malformed, unreadable plan item persisted. - planning.py: _coerce_plan_args() parses string spec/references (and int-casts estimated_days) in both create_plan_item and update_plan_item before storing. - file_store.py: _dict_to_plan_item tolerates spec/references persisted as JSON strings (parse on read; fall back cleanly on garbage), so existing bad items load instead of crashing. Verified: string spec/refs hydrate to ImagingSpec/list; malformed spec → None (no crash). Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/memory/file_store.py | 13 +++++++++++ gently/harness/plan_mode/tools/planning.py | 27 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/gently/harness/memory/file_store.py b/gently/harness/memory/file_store.py index 6a8c5eb3..451e7df5 100644 --- a/gently/harness/memory/file_store.py +++ b/gently/harness/memory/file_store.py @@ -2538,6 +2538,14 @@ def _dict_to_plan_item(d: dict) -> PlanItem: imaging_spec = None bench_spec = None + # Tolerate specs persisted as JSON strings (older tool calls that passed + # spec as a string instead of an object) so read-back never crashes. + if isinstance(spec_data, str): + try: + spec_data = json.loads(spec_data) + except (json.JSONDecodeError, TypeError): + spec_data = None + if spec_data: if item_type == PlanItemType.IMAGING: valid = {f.name for f in dataclasses.fields(ImagingSpec)} @@ -2547,6 +2555,11 @@ def _dict_to_plan_item(d: dict) -> PlanItem: bench_spec = BenchSpec(**{k: v for k, v in spec_data.items() if k in valid}) references = d.get("references") or [] + if isinstance(references, str): + try: + references = json.loads(references) or [] + except (json.JSONDecodeError, TypeError): + references = [] return PlanItem( id=d["id"], diff --git a/gently/harness/plan_mode/tools/planning.py b/gently/harness/plan_mode/tools/planning.py index 34785e5f..09e529d3 100644 --- a/gently/harness/plan_mode/tools/planning.py +++ b/gently/harness/plan_mode/tools/planning.py @@ -7,9 +7,34 @@ """ import dataclasses +import json from ...tools.registry import ToolCategory, ToolExample, tool + +def _coerce_plan_args(spec, references, estimated_days): + """The model often serializes nested args (spec/references) as JSON strings + instead of objects — accept either so plan-item creation doesn't store a raw + string (which later breaks ImagingSpec/BenchSpec hydration). Returns the + normalized (spec, references, estimated_days).""" + if isinstance(spec, str): + try: + spec = json.loads(spec) + except (json.JSONDecodeError, TypeError): + spec = None + if isinstance(references, str): + try: + references = json.loads(references) + except (json.JSONDecodeError, TypeError): + references = None + if isinstance(estimated_days, str): + try: + estimated_days = int(estimated_days) + except (ValueError, TypeError): + estimated_days = None + return spec, references, estimated_days + + # --------------------------------------------------------------------------- # Campaign / Phase Management # --------------------------------------------------------------------------- @@ -132,6 +157,7 @@ async def create_plan_item( return "Error: Context store not available" store = agent.context_store + spec, references, estimated_days = _coerce_plan_args(spec, references, estimated_days) # Resolve phase_number → subcampaign ID target_campaign_id = campaign_id @@ -226,6 +252,7 @@ async def update_plan_item( from gently.harness.memory.model import PlanItemStatus status_enum = PlanItemStatus(status) if status else None + spec, references, estimated_days = _coerce_plan_args(spec, references, estimated_days) store.update_plan_item( item_id=resolved_id, status=status_enum, From 144d8ddda5d1287fb8530324076e59ecda35a50b Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 08:56:41 +0530 Subject: [PATCH 28/34] UX v2: add a concision/communication-style section to the plan-mode prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plan-mode prompt specified what to design in detail but nothing about how to write to the user, so (with Opus 4.8's stronger narration) the agent produced dense questions, paragraph-long ask_user_choice options, and over-explained prose — cognitively heavy for a working biologist. Add an explicit communication-style section: lead with the ask/finding, short questions + short options, plain words over process jargon, one-clause rationale (full reasoning goes to provenance/references), one idea per message. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/plan_mode/prompt.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/gently/harness/plan_mode/prompt.py b/gently/harness/plan_mode/prompt.py index 62df77d7..782bd7af 100644 --- a/gently/harness/plan_mode/prompt.py +++ b/gently/harness/plan_mode/prompt.py @@ -79,6 +79,29 @@ IMPORTANT: ALWAYS use ask_user_choice when asking the researcher questions. Never present options as text lists. +## Communication style — keep it light to read + +You're talking to a working biologist, not a software user. Optimize every +user-facing message for fast reading, not completeness: + +- **Lead with the ask or the finding.** The first sentence should be the question, + the decision, or what you found — supporting detail comes after, and only when it + changes what they'd do next. +- **Short questions, short options.** Keep an ask_user_choice question to one line, + and each option to a few-word label plus at most a one-line rationale — never a + paragraph. Trust the biologist to know the domain; don't re-explain standard + concepts (what a histone marker is, why controls matter). +- **Plain words, not process jargon.** Use the field's real terms (strain names, + stages, wavelengths) but drop software/workflow jargon and hedging. +- **Give the short "why", not the survey.** One clause of rationale beats an + exhaustive list of everything you weighed. Put the full reasoning in the spec's + provenance and references, not in the message. +- **One idea per message.** Don't stack caveats, alternatives, and next steps into + one dense block. If something is optional, say so briefly or leave it out. + +Readability and brevity are different — choose readability, but get there by +saying less, not by compressing into fragments or abbreviations. + ## Reading Papers Use read_paper to retrieve and read scientific papers. It accepts: From d061e9d8d1ab65f23e5dae565fb021066ae6451c Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 09:14:25 +0530 Subject: [PATCH 29/34] UX v2: run independent read-only tool calls concurrently A turn's tool calls were always awaited one-by-one, so several independent lookups (e.g. search_strains for dlg-1 then ajm-1) ran serially even when the model could issue them together. Add a concurrency fast-path: when EVERY tool in a turn is non-hardware (requires_microscope=False) and non-interactive (not ask_user_choice), fire their tool_start events, asyncio.gather the executions, then emit results. Any microscope action or ask_user_choice in the batch falls back to the existing serial path, so hardware is never raced and interactive prompts/stateful ordering are preserved. Also nudge the plan-mode prompt to batch independent lookups into one turn so the model actually produces parallelizable tool calls. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/conversation.py | 67 ++++++++++++++++++++++++++++++ gently/harness/plan_mode/prompt.py | 4 ++ 2 files changed, 71 insertions(+) diff --git a/gently/harness/conversation.py b/gently/harness/conversation.py index 067e2f85..8f6b8b11 100644 --- a/gently/harness/conversation.py +++ b/gently/harness/conversation.py @@ -730,7 +730,74 @@ def worker(): await asyncio.sleep(0.05) tool_results = [] + + # Concurrency fast-path: run a turn's tool calls in parallel when ALL of + # them are non-hardware and non-interactive (e.g. several strain / paper / + # lab-history lookups). Any microscope action or ask_user_choice in the + # batch falls back to the serial path below, so we never race hardware or + # an interactive prompt, and ordering of stateful ops is preserved. + tool_blocks = [b for b in response_content if getattr(b, "type", None) == "tool_use"] + _interactive = {"ask_user_choice"} + + def _parallel_safe(b): + td = self._tool_registry.get(b.name) + return td is not None and not td.requires_microscope and b.name not in _interactive + + handled_parallel = False + if len(tool_blocks) > 1 and all(_parallel_safe(b) for b in tool_blocks): + handled_parallel = True + starts = {b.id: time.time() for b in tool_blocks} + for b in tool_blocks: + yield { + "type": "tool_start", + "tool_name": b.name, + "tool_input": b.input, + "tool_label": tool_label_fn(b.name, b.input), + } + gathered = await asyncio.gather( + *[self._execute_single_tool(b.name, b.input) for b in tool_blocks], + return_exceptions=True, + ) + for b, res in zip(tool_blocks, gathered, strict=True): + if isinstance(res, BaseException): + is_error_flag = True + result_text = f"Error: {res}" + tool_results.append( + { + "type": "tool_result", + "tool_use_id": b.id, + "content": result_text, + "is_error": True, + } + ) + else: + is_error_flag = False + result_text = res if isinstance(res, str) else str(res) + tool_results.append( + {"type": "tool_result", "tool_use_id": b.id, "content": res} + ) + result_summary = next( + (ln.strip() for ln in (result_text or "").splitlines() if ln.strip()), + "", + ) + if len(result_summary) > 140: + result_summary = result_summary[:139] + "…" + result_full = result_text or "" + if len(result_full) > 4000: + result_full = result_full[:4000] + "\n…(truncated)" + yield { + "type": "tool_call", + "tool_name": b.name, + "tool_input": b.input, + "duration": time.time() - starts[b.id], + "result_summary": result_summary, + "result_full": result_full, + "is_error": is_error_flag, + } + for block in response_content: + if handled_parallel: + break if hasattr(block, "type") and block.type == "tool_use": start_time = time.time() diff --git a/gently/harness/plan_mode/prompt.py b/gently/harness/plan_mode/prompt.py index 782bd7af..7aefbe3f 100644 --- a/gently/harness/plan_mode/prompt.py +++ b/gently/harness/plan_mode/prompt.py @@ -176,6 +176,10 @@ items, search to confirm strain availability, check the literature for recent protocols, and attach references. Your built-in knowledge is a great starting point for brainstorming — the databases are where you confirm before finalizing. +11. **Batch independent lookups**: When you need several independent reads — multiple + strains, several papers, or a few lab-history queries — request them together in + one turn so they run in parallel. Don't fetch one, wait for it, then fetch the + next; that's slow. (The system runs same-turn read-only lookups concurrently.) """ From a779901bb8f5147c7ddb39a96546bc11e37b3cfd Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 09:29:51 +0530 Subject: [PATCH 30/34] Fix create_plan_item crash when phase_number is a string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit phase_number arrived as "1" (stringified, like spec/references), so get_nth_subcampaign did `1 <= "1"` → TypeError. Coerce phase_number/phase_order to int in the create tool, and make get_nth_subcampaign tolerant of a numeric string. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/memory/file_store.py | 5 +++++ gently/harness/plan_mode/tools/planning.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/gently/harness/memory/file_store.py b/gently/harness/memory/file_store.py index 451e7df5..c19f785b 100644 --- a/gently/harness/memory/file_store.py +++ b/gently/harness/memory/file_store.py @@ -582,6 +582,11 @@ def get_subcampaigns(self, campaign_id: str) -> list[Campaign]: return children def get_nth_subcampaign(self, parent_id: str, n: int) -> Campaign | None: + # Tolerate n arriving as a numeric string (tool args are often stringified). + try: + n = int(n) + except (ValueError, TypeError): + return None phases = self.get_subcampaigns(parent_id) if 1 <= n <= len(phases): return phases[n - 1] diff --git a/gently/harness/plan_mode/tools/planning.py b/gently/harness/plan_mode/tools/planning.py index 09e529d3..87715acd 100644 --- a/gently/harness/plan_mode/tools/planning.py +++ b/gently/harness/plan_mode/tools/planning.py @@ -158,6 +158,16 @@ async def create_plan_item( store = agent.context_store spec, references, estimated_days = _coerce_plan_args(spec, references, estimated_days) + if isinstance(phase_number, str): + try: + phase_number = int(phase_number) + except (ValueError, TypeError): + phase_number = None + if isinstance(phase_order, str): + try: + phase_order = int(phase_order) + except (ValueError, TypeError): + phase_order = -1 # Resolve phase_number → subcampaign ID target_campaign_id = campaign_id From 06741eb8b99ca6a0296058eda9ab573fcf9c4381 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 09:29:51 +0530 Subject: [PATCH 31/34] Restrict tool concurrency to read-only tools; nudge batched plan-item creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The concurrency fast-path gated only on requires_microscope=False, which would also have run mutating tools (create_/update_/delete_plan_item) in parallel — they share a campaign's plan file and are order-dependent, so concurrent runs could corrupt it. Tighten the gate to genuinely read-only tools (search_/read_/ query_/get_/list_/recall_/find_/fetch_/lookup_ prefixes). Mutating creates stay serial. The latency win for plan construction is fewer model turns, not parallel writes: add a prompt nudge to emit several create_plan_item calls in one turn (executed serially, safely) instead of one item per turn. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/conversation.py | 22 +++++++++++++++++++++- gently/harness/plan_mode/prompt.py | 4 ++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/gently/harness/conversation.py b/gently/harness/conversation.py index 8f6b8b11..a3955df0 100644 --- a/gently/harness/conversation.py +++ b/gently/harness/conversation.py @@ -738,10 +738,30 @@ def worker(): # an interactive prompt, and ordering of stateful ops is preserved. tool_blocks = [b for b in response_content if getattr(b, "type", None) == "tool_use"] _interactive = {"ask_user_choice"} + # Only parallelize genuinely read-only tools (independent lookups). Mutating + # tools (create_/update_/delete_/set_…) must stay serial — they share state + # (e.g. a campaign's plan file) and are order-dependent, so concurrent runs + # could race or corrupt it. + _readonly_prefixes = ( + "search_", + "read_", + "query_", + "get_", + "list_", + "recall_", + "find_", + "fetch_", + "lookup_", + ) def _parallel_safe(b): td = self._tool_registry.get(b.name) - return td is not None and not td.requires_microscope and b.name not in _interactive + return ( + td is not None + and not td.requires_microscope + and b.name not in _interactive + and b.name.startswith(_readonly_prefixes) + ) handled_parallel = False if len(tool_blocks) > 1 and all(_parallel_safe(b) for b in tool_blocks): diff --git a/gently/harness/plan_mode/prompt.py b/gently/harness/plan_mode/prompt.py index 7aefbe3f..df30bbf9 100644 --- a/gently/harness/plan_mode/prompt.py +++ b/gently/harness/plan_mode/prompt.py @@ -180,6 +180,10 @@ strains, several papers, or a few lab-history queries — request them together in one turn so they run in parallel. Don't fetch one, wait for it, then fetch the next; that's slow. (The system runs same-turn read-only lookups concurrently.) +12. **Build the plan in few turns**: Each turn is a model round-trip, so creating one + item per turn makes plan construction crawl. When writing a phase's items, emit + several create_plan_item calls in a single turn (then set any dependencies in a + follow-up). Fewer turns = a much faster plan. """ From faee1819003eed0ee8e682845ab46880ce74d5da Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 10:12:26 +0530 Subject: [PATCH 32/34] UX v2: plan-done state + restructured THE PLAN panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two flow/IA fixes to the plan wizard, both surfaced in a live click-audit: 1. Plan-ready completion state. When propose_plan lands and the turn settles with no pending question, the wizard now flips to a "ready" state instead of leaving the user parked on the last feed step: the header reads "Your plan is ready — N items across M phases" (counted from the real plan tree), the eyebrow/orb turn green, and the footer link is promoted to a filled primary "Open the workspace ›" button. Resets on the next turn_start so refining drops the banner cleanly. 2. THE PLAN panel readability. Items were a flat gray bullet list with phases labeled by machine shorthand ("nrp-p1"). Each item is now a structured row — phase-relative number (1.1), a type-colored marker (imaging=blue, genetics=purple, analysis=green, bench=amber, decision_point=rose gate-square), the title, and an optional duration. Phases read by their human name ("Phase 1 — Reporter validation"). Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/ui/web/static/css/landing.css | 49 ++++++++++++++-- gently/ui/web/static/js/landing.js | 88 ++++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 11 deletions(-) diff --git a/gently/ui/web/static/css/landing.css b/gently/ui/web/static/css/landing.css index dad23d0b..49d27d9e 100644 --- a/gently/ui/web/static/css/landing.css +++ b/gently/ui/web/static/css/landing.css @@ -279,6 +279,21 @@ body.ux-v2 .home-hero { display: none; } } .v2-plan-skip:hover { color: var(--text-secondary, #475569); } +/* ── Plan-ready state: the design is done, signpost the finish line ───────── */ +.v2-screen-plan.ready .v2-plan-orb { + background: radial-gradient(closest-side at 38% 34%, #ffffff, #c8f0d4 40%, var(--accent-green, #16a34a) 100%); +} +.v2-screen-plan.ready .v2-plan-who { color: var(--accent-green, #16a34a); } +.v2-screen-plan.ready .v2-plan-foot { border-top-color: color-mix(in srgb, var(--accent-green) 35%, var(--border)); } +/* promote "open the workspace" from a quiet skip link to the primary action */ +.v2-screen-plan.ready #v2-plan-continue { + background: var(--accent-green, #16a34a); color: #fff; + border-radius: 999px; padding: 11px 20px; font-weight: 600; +} +.v2-screen-plan.ready #v2-plan-continue:hover { + color: #fff; background: color-mix(in srgb, var(--accent-green) 88%, #000); +} + /* ── Agent-activity feed: claude.ai-style collapsible tool cards ──────────── */ /* Both screens share the .v2-landing-inner top anchor (no per-screen align flip — that was the welcome→plan lurch). The feed is a fixed-height viewport (height @@ -369,13 +384,37 @@ body.ux-v2[data-theme="dark"] .v2-plan-error { /* plan-panel: phases + tasks (real plan), and a free-text-answer row variant */ .v2-plan-phase { margin-top: 12px; } .v2-plan-phase:first-child { margin-top: 0; } -.v2-plan-phase-h { font-size: 13px; font-weight: 600; letter-spacing: -.01em; color: var(--text, #0f172a); margin-bottom: 5px; } -.v2-plan-task { display: flex; gap: 8px; font-size: var(--v2-fs-cap); color: var(--text-secondary, #475569); padding: 2px 0; } -.v2-plan-task::before { content: "·"; color: var(--text-muted, #94a3b8); } +.v2-plan-phase-h { + font-size: var(--v2-fs-eyebrow); font-weight: 700; letter-spacing: .06em; + text-transform: uppercase; color: var(--text-secondary, #475569); margin-bottom: 6px; +} +/* a plan item: "P.I" number · type-color dot · title · optional duration. + the type dot encodes the item kind (imaging/genetics/analysis/…) at a glance. */ +.v2-plan-task { + display: grid; grid-template-columns: auto 8px 1fr auto; align-items: baseline; + gap: 9px; font-size: var(--v2-fs-cap); color: var(--text-secondary, #475569); + padding: 6px 0; border-top: 1px solid color-mix(in srgb, var(--border, #e4e9f0) 55%, transparent); +} +.v2-plan-phase .v2-plan-task:first-child { border-top: 0; } +.v2-task-num { + font: 600 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; + color: var(--text-muted, #94a3b8); font-variant-numeric: tabular-nums; +} +.v2-task-dot { width: 8px; height: 8px; border-radius: 50%; align-self: center; background: var(--text-muted, #94a3b8); } +.v2-task-ttl { min-width: 0; color: var(--text, #0f172a); line-height: 1.45; } +.v2-task-days { + font-size: 10.5px; font-weight: 600; color: var(--text-muted, #94a3b8); + font-variant-numeric: tabular-nums; white-space: nowrap; +} +.v2-plan-task.type-imaging .v2-task-dot { background: var(--accent, #2f6df6); } +.v2-plan-task.type-genetics .v2-task-dot { background: #8b5cf6; } +.v2-plan-task.type-analysis .v2-task-dot { background: var(--accent-green, #16a34a); } +.v2-plan-task.type-bench .v2-task-dot { background: #d97706; } +/* decision points read as gates — a rotated square, not a round bead */ +.v2-plan-task.type-decision_point .v2-task-dot { background: #e11d48; border-radius: 1px; transform: rotate(45deg); } .v2-plan-title-row { font-size: var(--v2-fs-sm); font-weight: 600; letter-spacing: -.01em; color: var(--text, #0f172a); margin-bottom: 8px; } .v2-plan-row.v2-plan-row-freetext .v { font-style: italic; } -.v2-plan-task-empty { color: var(--text-muted, #94a3b8); font-style: italic; } -.v2-plan-task-empty::before { color: transparent; } +.v2-plan-task-empty { grid-column: 1 / -1; color: var(--text-muted, #94a3b8); font-style: italic; } /* THE PLAN pager: ‹ Prev · "Phase · i of N" · Next › + dots, one phase per page */ .v2-plan-pager { display: flex; align-items: center; gap: 8px; margin: 2px 0 12px; } diff --git a/gently/ui/web/static/js/landing.js b/gently/ui/web/static/js/landing.js index 4f0f2917..623576fc 100644 --- a/gently/ui/web/static/js/landing.js +++ b/gently/ui/web/static/js/landing.js @@ -22,6 +22,7 @@ const V2Landing = (() => { let runningTools = {}; // tool name -> stack of running card elements let feedHadContent = false; // did this turn surface anything in the feed? let capturedCampaignId = null; // best-effort id scraped from tool results + let planProposed = false; // propose_plan ran → plan is ready to commit const $ = (id) => document.getElementById(id); @@ -137,7 +138,8 @@ const V2Landing = (() => { } feedPages = []; feedPage = 0; curPageEl = null; pendingNewPage = false; feedTextEl = null; feedThinkingEl = null; runningTools = {}; feedHadContent = false; - capturedCampaignId = null; + capturedCampaignId = null; planProposed = false; + clearPlanReady(); hidePlanError(); } function newFeedPage() { @@ -291,6 +293,7 @@ const V2Landing = (() => { switch (act.kind) { case 'turn_start': feedTextEl = null; feedThinkingEl = null; pendingNewPage = true; hidePlanError(); clearFallback(); + clearPlanReady(); // new work in flight — drop any "ready" state showThinking(true, 'reviewing your campaign and plan…'); break; case 'thinking': { @@ -344,6 +347,7 @@ const V2Landing = (() => { captureCampaignId(act.summary); captureCampaignId(act.full); if (PLAN_TOOLS.has(act.name)) schedulePlanRefresh(); + if (act.name === 'propose_plan' && !act.is_error) planProposed = true; if (act.name === 'ask_user_choice') break; feedTextEl = null; feedThinkingEl = null; const arr = runningTools[act.name] || []; @@ -357,6 +361,10 @@ const V2Landing = (() => { showThinking(false); feedTextEl = null; feedThinkingEl = null; refreshPlanPanel(); if (!current && !feedHadContent) showFallback(); + // Plan proposed and the agent has settled (no pending question) → + // the design is done. Surface a clear "ready" state instead of + // leaving the user parked on the last wizard step. + if (planProposed && !current) showPlanReady(); break; case 'turn_error': showPlanError(act.error || 'Something went wrong — open the conversation for detail.'); @@ -373,6 +381,48 @@ const V2Landing = (() => { feedTarget().appendChild(d); } + // ── plan-ready state ─────────────────────────────────────────────── + // Once the agent has proposed the plan and gone quiet, the wizard is done. + // Mark the screen "ready": rename the header, count phases/items from the + // panel, and promote "open workspace" to the obvious primary action — so the + // finish line is signposted instead of looking like one more wizard step. + function planCounts() { + let phases = 0, items = 0; + planPages.forEach(p => { + if (p.name !== 'Tasks') phases++; + items += (p.items || []).length; + }); + return { phases, items }; + } + function showPlanReady() { + const sec = document.querySelector('.v2-screen-plan'); + if (!sec) return; + sec.classList.add('ready'); + showThinking(false); + const who = sec.querySelector('.v2-plan-who'); + const title = sec.querySelector('.v2-plan-title'); + if (who) who.textContent = 'Gently · plan ready'; + if (title) { + const { phases, items } = planCounts(); + title.textContent = items + ? `Your plan is ready — ${items} item${items === 1 ? '' : 's'} across ${phases} phase${phases === 1 ? '' : 's'}` + : 'Your plan is ready'; + } + const cont = $('v2-plan-continue'); + if (cont) cont.textContent = 'Open the workspace ›'; + } + function clearPlanReady() { + const sec = document.querySelector('.v2-screen-plan'); + if (!sec || !sec.classList.contains('ready')) return; + sec.classList.remove('ready'); + const who = sec.querySelector('.v2-plan-who'); + const title = sec.querySelector('.v2-plan-title'); + if (who) who.textContent = 'Gently · planning'; + if (title) title.textContent = "Let's design your run"; + const cont = $('v2-plan-continue'); + if (cont) cont.textContent = 'Continue in workspace ›'; + } + // ── THE PLAN panel: mirror the real campaign tree ────────────────── async function refreshPlanPanel() { try { @@ -392,6 +442,12 @@ const V2Landing = (() => { c = c || {}; return c.shorthand || c.display_name || c.description || 'Plan'; } + // Phases read better by their human name ("Phase 1 — Reporter validation") + // than by their code shorthand ("nrp-p1"), which looks like a machine id. + function phaseName(c) { + c = c || {}; + return c.display_name || c.description || c.shorthand || 'Phase'; + } // THE PLAN renders as a pager — one phase per page with ‹ Prev / Next ›, // a position label, and dots — instead of one long scroll. planPage is held // across re-renders (the panel refetches on every plan-writing tool) so the @@ -409,7 +465,7 @@ const V2Landing = (() => { if (rootItems.length) pages.push({ name: 'Tasks', items: rootItems }); phases.forEach(phase => { if (!phase) return; - pages.push({ name: planName(phase.campaign), items: phase.items || [] }); + pages.push({ name: phaseName(phase.campaign), items: phase.items || [] }); }); planPages = pages; planTitleText = planName(tree.campaign); @@ -442,7 +498,8 @@ const V2Landing = (() => { prev.addEventListener('click', () => { if (planPage > 0) { planPage--; drawPlanPage(); } }); const pos = document.createElement('span'); pos.className = 'v2-plan-pager-pos'; - pos.textContent = `${page.name} · ${i + 1} of ${n}`; + pos.textContent = page.name; // position shown by the dots below + pos.title = `${page.name} · ${i + 1} of ${n}`; const next = document.createElement('button'); next.className = 'v2-plan-pager-btn'; next.type = 'button'; next.textContent = 'Next ›'; next.disabled = i === n - 1; @@ -459,11 +516,30 @@ const V2Landing = (() => { const tasks = document.createElement('div'); tasks.className = 'v2-plan-phase'; const items = page.items || []; + // phase ordinal (1-based) for "P.I" numbering; the rootItems "Tasks" page + // isn't a phase, so it numbers items bare (1, 2, …). + const phaseOrd = pages.slice(0, i + 1).filter(p => p.name !== 'Tasks').length; if (items.length) { - items.forEach(it => { + items.forEach((it, idx) => { + const type = String(it.type || '').toLowerCase(); const t = document.createElement('div'); - t.className = 'v2-plan-task'; - t.textContent = it.title || it.shorthand || '(task)'; + t.className = 'v2-plan-task type-' + (type || 'other'); + const num = document.createElement('span'); + num.className = 'v2-task-num'; + num.textContent = phaseOrd ? `${phaseOrd}.${idx + 1}` : `${idx + 1}`; + const dot = document.createElement('span'); + dot.className = 'v2-task-dot'; + dot.title = type || 'task'; + const ttl = document.createElement('span'); + ttl.className = 'v2-task-ttl'; + ttl.textContent = it.title || it.shorthand || '(task)'; + t.append(num, dot, ttl); + if (it.estimated_days) { + const d = document.createElement('span'); + d.className = 'v2-task-days'; + d.textContent = `${it.estimated_days}d`; + t.append(d); + } tasks.appendChild(t); }); } else { From f6f2ba310c99b1e626d728e386f2d082cf9fa559 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 10:19:08 +0530 Subject: [PATCH 33/34] UX v2: drop wrap-up reasoning litter from the plan feed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streamed thinking summary fills the wait, but the trailing "wrap-up" reasoning ("let me wrap this up concisely and offer to export…") was left in the feed as a dim block right next to the answer it precedes — reading as leaked narration. It's a thinking block, not the agent's text output. When the spoken answer begins, remove the immediately-preceding thinking block from the DOM instead of only nulling the pointer. Thinking that precedes a TOOL is kept (it's the rationale for that action); only the reasoning that precedes the answer text is dropped. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/ui/web/static/js/landing.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gently/ui/web/static/js/landing.js b/gently/ui/web/static/js/landing.js index 623576fc..a8cc751c 100644 --- a/gently/ui/web/static/js/landing.js +++ b/gently/ui/web/static/js/landing.js @@ -320,7 +320,13 @@ const V2Landing = (() => { case 'text': { const chunk = act.text || ''; if (!chunk) break; - feedThinkingEl = null; // reasoning block ends when prose begins + // The reasoning that immediately precedes the spoken answer is + // wrap-up meta ("let me wrap this up concisely and offer to + // export…") — drop the block entirely so the feed keeps the + // answer, not the narration of getting there. Reasoning that + // precedes a TOOL is left in place (tool_start only nulls the + // pointer) as the rationale for that action. + if (feedThinkingEl) { feedThinkingEl.remove(); feedThinkingEl = null; } if (!feedTextEl) { feedTextEl = document.createElement('div'); feedTextEl.className = 'v2-act-text'; From b8df61074cdd89f5a3675d562540de2a3af9f4e7 Mon Sep 17 00:00:00 2001 From: P S Kesavan Date: Tue, 16 Jun 2026 11:12:46 +0530 Subject: [PATCH 34/34] UX v2: export-plan button replaces the end-of-plan prose upsell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The agent used to close a finished plan by asking "want me to export this as markdown, or save it as a template?" — a question the plan wizard can't answer (no text input there). Replace it with a real action. - Plan-ready footer gains a quiet "↓ Export plan" button. It fetches the existing /api/campaigns/{id}/export tree and renders a shareable markdown doc client-side (title, goal, phases, numbered items with type, specs, references), then downloads it as {shorthand}.md. Revealed in the ready state, hidden again if a new turn starts. - Plan-mode prompt: after propose_plan, close on the summary and do NOT offer to export / save-as-template / ask "what next?" — the interface owns those now. Co-Authored-By: Claude Opus 4.8 (1M context) --- gently/harness/plan_mode/prompt.py | 6 ++ gently/ui/web/static/css/landing.css | 8 +++ gently/ui/web/static/js/landing.js | 87 ++++++++++++++++++++++++++++ gently/ui/web/templates/index.html | 1 + 4 files changed, 102 insertions(+) diff --git a/gently/harness/plan_mode/prompt.py b/gently/harness/plan_mode/prompt.py index df30bbf9..055d0df2 100644 --- a/gently/harness/plan_mode/prompt.py +++ b/gently/harness/plan_mode/prompt.py @@ -76,6 +76,12 @@ 3. Set dependencies between items 4. Present the full plan for review with propose_plan +After propose_plan, close with a short confirmation of what the plan contains +(item/phase count, the critical path, anything notable) and stop there. Do NOT +offer to export it, save it as a template, or ask "what would you like to do +next?" — exporting and opening the workspace are handled by the interface, not +this conversation. End on the summary, not an upsell. + IMPORTANT: ALWAYS use ask_user_choice when asking the researcher questions. Never present options as text lists. diff --git a/gently/ui/web/static/css/landing.css b/gently/ui/web/static/css/landing.css index 49d27d9e..046675de 100644 --- a/gently/ui/web/static/css/landing.css +++ b/gently/ui/web/static/css/landing.css @@ -278,6 +278,14 @@ body.ux-v2 .home-hero { display: none; } color: var(--text-muted, #94a3b8); padding: 11px 10px; border-radius: 8px; transition: color .2s; } .v2-plan-skip:hover { color: var(--text-secondary, #475569); } +.v2-plan-export { + background: none; border: 1px solid var(--border, #e4e9f0); color: var(--text-secondary, #475569); + border-radius: 999px; padding: 11px 16px; font: inherit; font-size: var(--v2-fs-sm); + font-weight: 600; cursor: pointer; transition: border-color .2s, color .2s, background .2s; +} +.v2-plan-export:hover { border-color: var(--border-strong); color: var(--text, #0f172a); background: var(--bg-hover); } +.v2-plan-export:disabled { opacity: .6; cursor: default; } +.v2-plan-export[hidden] { display: none; } /* ── Plan-ready state: the design is done, signpost the finish line ───────── */ .v2-screen-plan.ready .v2-plan-orb { diff --git a/gently/ui/web/static/js/landing.js b/gently/ui/web/static/js/landing.js index a8cc751c..6d058ebe 100644 --- a/gently/ui/web/static/js/landing.js +++ b/gently/ui/web/static/js/landing.js @@ -416,6 +416,8 @@ const V2Landing = (() => { } const cont = $('v2-plan-continue'); if (cont) cont.textContent = 'Open the workspace ›'; + const exp = $('v2-plan-export'); + if (exp) exp.hidden = false; // the plan is final → offer the download } function clearPlanReady() { const sec = document.querySelector('.v2-screen-plan'); @@ -427,6 +429,89 @@ const V2Landing = (() => { if (title) title.textContent = "Let's design your run"; const cont = $('v2-plan-continue'); if (cont) cont.textContent = 'Continue in workspace ›'; + const exp = $('v2-plan-export'); + if (exp) exp.hidden = true; + } + + // ── export the finished plan as a shareable markdown doc ──────────── + // Replaces the agent's end-of-plan "want me to export this?" prose with a + // real action: pull the enriched plan tree (/export) and render it to + // markdown client-side so the biologist can drop it in a doc or share it. + function specToLines(spec) { + let s = spec; + if (typeof s === 'string') { try { s = JSON.parse(s); } catch { return s ? ['- ' + s] : []; } } + if (!s || typeof s !== 'object') return []; + const out = []; + const fmt = (v) => Array.isArray(v) ? v.join(', ') : (typeof v === 'object' ? JSON.stringify(v) : String(v)); + const pick = (k, label) => { if (s[k] != null && s[k] !== '') out.push(`- **${label}:** ${fmt(s[k])}`); }; + pick('strain', 'Strain'); pick('goal', 'Goal'); + if (Array.isArray(s.channels) && s.channels.length) { + out.push('- **Channels:** ' + s.channels.map(c => `${c.name || '?'} (${c.excitation_nm || '?'} nm${c.exposure_ms ? `, ${c.exposure_ms} ms` : ''})`).join(', ')); + } + pick('num_slices', 'Slices'); pick('interval_s', 'Interval (s)'); pick('temperature_c', 'Temperature (°C)'); + pick('num_embryos', 'Embryos'); pick('start_stage', 'Start stage'); pick('stop_condition', 'Stop condition'); + pick('criteria', 'Criteria'); pick('success_criteria', 'Success criteria'); + return out; + } + function buildPlanMarkdown(tree) { + const L = []; + L.push(`# ${tree.description || tree.shorthand || 'Experimental plan'}`, ''); + if (tree.target) L.push(`**Goal:** ${tree.target}`, ''); + if (tree.shorthand) L.push(`**Plan ID:** \`${tree.shorthand}\``, ''); + L.push(`_Exported from Gently — ${new Date().toLocaleString()}_`, ''); + const renderItems = (items, prefix) => { + (items || []).slice().sort((a, b) => (a.phase_order || 0) - (b.phase_order || 0)).forEach((it, idx) => { + L.push(`### ${prefix}${idx + 1} ${it.title || '(task)'} \`${it.type || 'task'}\``, ''); + if (it.description) L.push(it.description, ''); + const sl = specToLines(it.spec); + if (sl.length) L.push(...sl, ''); + const refs = it.references || []; + if (refs.length) { + L.push('**References:**'); + refs.forEach((r, i) => L.push(`${i + 1}. ${r.citation || r.id || ''}${r.source ? ` _(${r.source})_` : ''}`)); + L.push(''); + } + }); + }; + if ((tree.items || []).length) { L.push('## Tasks', ''); renderItems(tree.items, ''); } + (tree.children || []).forEach((ph, pi) => { + if (!ph) return; + L.push(`## ${ph.display_name || ph.description || ph.shorthand || `Phase ${pi + 1}`}`, ''); + if (ph.target) L.push(ph.target, ''); + renderItems(ph.items, `${pi + 1}.`); + }); + return L.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'; + } + async function resolveCampaignId() { + if (capturedCampaignId) return capturedCampaignId; + try { + const r = await fetch('/api/campaigns'); + if (r.ok) { const d = await r.json(); const t = (d.campaigns || [])[0]; return (t && t.campaign && t.campaign.id) || null; } + } catch (e) { /* offline */ } + return null; + } + async function exportPlan() { + const btn = $('v2-plan-export'); + const id = await resolveCampaignId(); + if (!id) { showPlanError('No plan to export yet.'); return; } + if (btn) { btn.disabled = true; btn.textContent = '↓ Exporting…'; } + try { + const r = await fetch(`/api/campaigns/${encodeURIComponent(id)}/export`); + if (!r.ok) throw new Error(`export ${r.status}`); + const tree = await r.json(); + const md = buildPlanMarkdown(tree); + const blob = new Blob([md], { type: 'text/markdown' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${(tree.shorthand || 'plan').replace(/[^\w.-]+/g, '_')}.md`; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + } catch (e) { + showPlanError('Could not export the plan — open the conversation to export it manually.'); + } finally { + if (btn) { btn.disabled = false; btn.textContent = '↓ Export plan'; } + } } // ── THE PLAN panel: mirror the real campaign tree ────────────────── @@ -744,6 +829,8 @@ const V2Landing = (() => { if (planChat) planChat.addEventListener('click', openChat); const cont = $('v2-plan-continue'); if (cont) cont.addEventListener('click', dismiss); + const exp = $('v2-plan-export'); + if (exp) exp.addEventListener('click', exportPlan); // The agent's questions + work render in the plan stage while it's active; // once we've receded into the workspace, AskStage (#ask-stage) takes over. diff --git a/gently/ui/web/templates/index.html b/gently/ui/web/templates/index.html index 3e704938..cd0b1fba 100644 --- a/gently/ui/web/templates/index.html +++ b/gently/ui/web/templates/index.html @@ -96,6 +96,7 @@

Take a quick look

+