feat: Progress Trail + Topic Appetizer for latency reduction#136
Open
YishaiGlasner wants to merge 38 commits into
Open
feat: Progress Trail + Topic Appetizer for latency reduction#136YishaiGlasner wants to merge 38 commits into
YishaiGlasner wants to merge 38 commits into
Conversation
…uring streaming When the agent completes a source-fetching tool call (get_text, get_english_translations), a collapsible yellow box appears so the user can start reading the reference on Sefaria while Claude finishes its response. The box expands during streaming and collapses to one line once the full reply arrives. - Add tool_input to tool_end SSE progress events (tool_runtime.py) - New SourceSuggestion.svelte component (book icon, header, Sefaria link) - LCChatbot: track firstSourcePreview state, SOURCE_PROVIDING_TOOLS set, onProgress capture logic, streaming + post-response render slots - CSS via :global() rules in root component (shadow DOM requirement) - i18n: source.readWhileWaiting, source.readOnSefaria keys (en.json)
|
The preview deployment for sefaria/ai-chatbot:server is ready. 🟢 Open app | Open Build Logs | Open Application Logs Last updated at: 2026-05-27 15:06:23 CET |
📊 Code Quality Score: 12/100
Scored by GitVelocity · How are scores calculated? |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR surfaces a “read while you wait” source preview when the streaming agent finishes fetching a specific source, then pins a collapsible source link to the completed assistant response.
Changes:
- Adds
toolInputtotool_endprogress events so the client can identify the fetched reference. - Introduces
SourceSuggestionand client state to display the first successful source-providing tool result during and after streaming. - Adds English i18n strings and parent-level styles for the source suggestion panel.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
server/chat/V2/agent/tool_runtime.py |
Includes tool input on streamed tool_end progress updates. |
src/components/LCChatbot.svelte |
Tracks the first fetched source and renders the live/collapsed source suggestion UI. |
src/components/SourceSuggestion.svelte |
Adds the stateless source suggestion panel component. |
src/i18n/locales/en.json |
Adds labels used by the source suggestion UI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Adds SefariaClient.search_topics() which calls api/name/{query}?type=topic
and returns [{title, slug}, ...] filtered to Topic completions only.
Creates server/chat/V2/appetizer/ package with three passing unit tests.
Also fixes pre-existing B905 ruff warning (zip without strict=).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nd tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Renders a growing list of tool-call/status progress entries with
collapse/expand toggle (collapsed=true shows "Show thinking (N steps)",
collapsed=false streams all entries live).
SVG icons reuse paths from LCChatbot.svelte for visual consistency.
NOTE: requires two i18n keys in en.json (a later task):
"progress.showThinking": "Show thinking ({count} steps)"
"progress.hideThinking": "Hide thinking ({count} steps)"
Until added, svelte-i18n will fall back to the raw key strings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace single thinking bubble with scrolling progress trail showing all tool calls and status events. Trail collapses to toggle after answer arrives. Adds i18n keys and shadow DOM CSS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…er event Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show a Sefaria topic link in a yellow box within 5 seconds via the parallel Haiku appetizer pipeline. Includes collapse/expand, fade-in animation, and appetizer_click custom event for analytics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Fix executor leak: shut down appetizer_executor in finally block 2. Fix tool_end matching: use findLastIndex by toolName instead of array position — prevents stuck spinners when status events interleave with tool calls 3. Fix stuck status entries: mark all running trail entries as complete before persisting to assistantMessage 4. Fix empty state: show "Thinking..." fallback before first SSE progress event arrives 5. Fix NONE check: strip trailing punctuation before comparing Haiku's concept extraction response 6. Fix multi-instance: use $host() instead of document.querySelector for appetizer click tracking 7. Remove dead currentProgress variable (no longer read in template) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tier 1: regex strips prompt wrappers, sends keywords to Sefaria name API (<500ms). Covers most prompts. Tier 2: Haiku extracts concept for abstract prompts (2-4s). Both run inside 5-second timeout. No Haiku cost for common queries.
The Sefaria api/name endpoint returns the Shabbat tractate (type=ref)
before the Shabbat topic. Now search_topics tries a direct slug lookup
via api/v2/topics/{slug} when the name API returns zero Topic-type
results. Also adds debug logging to the appetizer thread.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The chat.appetizer logger was not registered in Django's LOGGING config, causing appetizer logs to be silently dropped and making it difficult to debug appetizer functionality. Added chat.appetizer logger entry with handlers=['chat_console'], level='INFO', propagate=False to match other chat module loggers.
Test results from May 25, 2026 after fixing chat.appetizer logging: - TA-1: PASS — appetizer appeared at ~12.8s for 'find me sources about Shabbat' - TA-2: PASS — link href=https://www.sefaria.org/topics/shabbat, text='Shabbat →' - TA-3: PASS — appetizer fully collapsible: clicking header toggles aria-expanded and removes/adds .appetizer-body - TA-4: PASS — translation prompt ('translate Genesis 1:1') does not generate appetizer, only discovery prompts do - TA-5: PASS — appetizer persists across multiple messages, remains collapsed initially Root cause of TA-1 regression was missing 'chat.appetizer' logger config in Django LOGGING dict.
Move appetizer thread start from inside generate_sse() to right after authentication — before session creation, summary loading, and message saving. This eliminates the 2-5s setup overhead and delivers the appetizer event within ~450ms of the request. Fix TopicAppetizer link click by using window.open() explicitly instead of relying on shadow DOM <a> default navigation which was unreliable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Record timing for each appetizer pipeline step (classification, tier-1 search, tier-2 Haiku extraction, tier-2 search) and attach as metadata.appetizer on the Braintrust trace span. Non-intrusive — metrics are collected passively and attached after the agent completes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
window.location.href navigates away from the page and destroys the chatbot. Revert to window.open(_blank) which safely opens the topic in a new tab. Same-page SPA navigation requires Sefaria host-side changes to intercept the appetizer_click custom event. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…very Many reverse proxies (nginx, gunicorn, cloud LBs) buffer the first 4-8KB before forwarding to the client. This caused SSE events (including the appetizer) to be delayed ~25 seconds on production. Send a 4KB SSE comment at the start of the stream to flush these buffers. The comment is ignored by EventSource but forces the proxy to start forwarding immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Log timestamps at each stage: appetizer thread start, SSE generator start, proxy flush, first event yield. All relative to request start time for easy correlation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove target="_blank", window.open, and e.preventDefault. Use a plain <a href> so the browser's native click event propagates up through the shadow DOM. Sefaria's React app intercepts same-origin clicks and does client-side routing — the chatbot stays alive and the topic page loads in the same tab. Verified on production: clicking the topic link navigates to the topic page while the chatbot remains visible with conversation intact. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…andle /topics/ Sefaria's sefaria:bootstrap-url event handles text references (/Genesis.1.1) but silently ignores topic URLs (/topics/shabbat). Detect /topics/ paths in handleMessageLinkClick and fall back to window.open so topic links actually navigate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…action The old tier-1 used English-only regexes + direct name API lookup. Hebrew prompts like 'תן לי מקורות על סיוון כ'' sent the raw Hebrew to the name API, where 'תן' matched 'תניא' (Tanya) — wrong topic. Now Haiku is the single entry point: it extracts the core Jewish concept from any prompt language, then the name API resolves the slug. Expected latency: ~1.5–2.5s (well under the 5s timeout). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs: 1. Haiku prompt too narrow: 'if not about Jewish texts, return NONE' filtered out Sefaria topics like parenting, money, relationships. New prompt scopes to 'topics that could appear in the Sefaria library' which covers universal topics through a Jewish-text lens. 2. Appetizer never dismissed: once shown it persisted forever, even after the answer arrived — cluttering the loading experience. Now cleared immediately after the answer message is committed to state (the topic is still saved on the message object for history). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pic card Previous commit incorrectly cleared appetizerData (the topic card). The intended behavior: clear firstSourcePreview so the source mention disappears once the answer is shown, reducing loading-experience clutter. The TopicAppetizer persists as intended. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deleted SourceSuggestion.svelte and all references: - import, SOURCE_PROVIDING_TOOLS constant, firstSourcePreview + sourcePreviewMessageId state, tool_end population logic, both render sites (streaming + history), and all CSS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in trail Appetizer improvements: - Haiku now returns up to 3 ranked candidates (comma-separated) instead of a single concept. Tries each until one matches — fixes cases like 'Herod the Great' where the first candidate fails but 'Herod' hits. Multiple candidates also add natural think-time (~2-4s total). - Canonical short names prompted: 'Herod' not 'Herod the Great'. - search_topics now accepts PersonTopic and AuthorTopic in addition to Topic — 'Herod' was filtered out because it's type PersonTopic. ProgressTrail: - Tool entry descriptions with single-quoted refs (e.g. 'Pesachim 119b') are now rendered as clickable sefaria.org links. SourceSuggestion: - File restored (kept for future re-enablement, currently unused). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e double quotes)
Refs in tool descriptions use double quotes e.g. "Mishnah Shabbat 7:2".
Previous regex only matched single quotes. Updated to use backreference
so both 'Pesachim 119b' and "Mishnah Shabbat 7:2" are linkified.
Non-ref quoted strings ("both", "en") still pass through unchanged since
refToUrl returns null for them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Upgrade appetizer model from Haiku to Sonnet for smarter topic extraction - Return up to 3 matched topics (exhaustive search, deduplicated by slug) - Bump timeout from 5s to 8s for Sonnet inference + sequential API calls - SSE payload now sends topics array instead of single flat object - Frontend renders comma-separated topic links (no arrow, no hint) - New header: "Discover sources connected to related topics..." - Same-page navigation via sefaria:bootstrap-url event (no new tab) - Migration guard for old flat appetizerData format in localStorage - ProgressTrail refs styled bold with external-link icon - Refined prompt: prefer specific topics, distinct candidates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sonnet generates full markdown responses with search XML tags instead of comma-separated topic names. Haiku follows the system prompt correctly and returns quality multi-topic results in ~1.5-2.5s. Keep 8s timeout for headroom. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ction Sonnet ignores system prompts and enters "assistant mode" with free-form text. Tool-forcing via tool_choice + extract_topics tool constrains output to structured data — physically impossible to produce markdown. Returns 3 quality topics in ~2-3.5s. Based on CandleKeep research: Anthropic Prompting Best Practices recommends structured output tools with tool_choice as the strongest format enforcement for Claude 4.x models (prefilling is deprecated/errors on 4.x). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ation <a> tags with href always navigate even with e.preventDefault() in shadow DOM context. Switched to <button> elements styled as links — clicking dispatches sefaria:bootstrap-url for in-page navigation without any default browser navigation to intercept. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lsewhere Svelte 5 delegated onclick doesn't reliably preventDefault on <a> tags in shadow DOM. Fix: use Svelte action (use:attachClickHandler) with direct addEventListener for reliable preventDefault. On sefaria.org: dispatches sefaria:bootstrap-url for in-page navigation. Off sefaria.org: window.open as fallback (opens topic in new tab). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Header was truncating in narrow panel. Shortened to "Discover sources on related topics". Added margin-right: 4px on comma separators so topic names don't run together. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…page navigation Previously, message-body links to /topics/* were opening in a new tab. This removes the early return so they dispatch the sefaria:bootstrap-url event, enabling in-page navigation on Sefaria just like ref links. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Filter external-domain links: open in new tab instead of dispatching sefaria:bootstrap-url with a foreign path - Filter off-Sefaria embeds: open topic/ref on sefaria.org in new tab when the host page isn't sefaria.org (mirrors handleAppetizerClick) This makes handleMessageLinkClick consistent with handleAppetizerClick, which already has the onSefaria hostname guard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two features to reduce perceived latency in the Library Assistant, based on the team review meeting (2026-05-25):
Track A: Progress Trail
Track B: Topic Appetizer (parallel Haiku pipeline)
api/namesearch → topic foundSpeed Architecture
The appetizer thread fires immediately after authentication, before session creation/summary loading/message saving. A 4KB SSE comment at stream start flushes reverse proxy buffers (nginx/gunicorn).
Production Results
Observability
metadata.appetizeron Braintrust traceFiles Changed
server/chat/V2/views.py— appetizer thread, proxy flush, Braintrust metrics, SSE timing logsserver/chat/V2/appetizer/appetizer_service.py— two-tier service with per-step timingserver/chat/V2/appetizer/test_appetizer_service.py— 16 unit testsserver/chat/V2/agent/sefaria_client.py—search_topics()with slug fallbackserver/chat/V2/agent/contracts.py—appetizer_datafieldsrc/components/TopicAppetizer.svelte— yellow box UIsrc/components/ProgressTrail.svelte— thinking trail UIsrc/components/LCChatbot.svelte— wiring both componentssrc/i18n/locales/en.json— i18n stringsTest Plan
🤖 Generated with Claude Code