Skip to content

refactor: sage app foundations with real chat, KB UI, generation endpoints, audio, history#37

Closed
spencerwozniak wants to merge 11 commits into
zeapsu:mainfrom
spencerwozniak:feat/sage-app-foundations
Closed

refactor: sage app foundations with real chat, KB UI, generation endpoints, audio, history#37
spencerwozniak wants to merge 11 commits into
zeapsu:mainfrom
spencerwozniak:feat/sage-app-foundations

Conversation

@spencerwozniak

@spencerwozniak spencerwozniak commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This branch replaces every placeholder fixture in the Sage app with a real backend round-trip and adds the foundational endpoints, UI surfaces, and developer tooling that the rest of the product can build on. The original SAMPLE_* arrays for flashcards, quizzes, chat, history, and audio are gone — every view now fetches live, KB-grounded data from the FastAPI backend.

There is one feature doc per area under docs/, and each commit message corresponds to one of those docs.

image

What changed

Backend (FastAPI)

  • Generation endpointsPOST /api/generate/{flashcards,quiz,report}. Each retrieves excerpts from the user's KB (semantic search when a topic is provided, random sample otherwise — optionally scoped to a tome), calls the configured LLM with response_format=json_object on OpenAI-compatible providers, and validates the parsed payload before responding. Empty KB → 400 with a hint to upload documents.
  • Audio narration + TTSPOST /api/generate/audio returns a spoken-style narration script with sentence-level segments and word-rate-estimated timings. When OPENAI_API_KEY is set, it also synthesizes an MP3 via tts-1 and caches it; GET /api/audio/{id}.mp3 streams it back with a regex guard against directory traversal.
  • Chat sessions endpointGET /api/chat/sessions backed by a new KnowledgeStore.list_sessions() that joins sessions × messages × tomes in one SQL pass and returns derived titles, tome name, message count, and last-activity timestamp.
  • Capability-aware system prompt — chat SYSTEM_PROMPT now lists every routing keyword and the view it opens, so the model can describe its own routed features when asked "what can you do?" and stays silent when the frontend has already routed away.

Frontend (Next.js / Tauri)

  • Knowledge-base UI — new UploadModal (paste / drag-drop / file-picker for text-like files, deduped server-side by content hash) and KnowledgeBaseWidget (two-pane list + detail browser with per-row delete). Reachable via a new circular button next to the command-bar pill and via the verb router (knowledge, kb, docs, …).
  • Live viewsFlashcardsView, QuizView, AudioView, ReportView are all real fetch wrappers around the matching /api/generate/* endpoint. Each renders loading / error-with-retry / content states and a "Grounded in: …" source footer.
  • Real audio playerAudioPlayerWidget now supports two playback modes: a hidden <audio> element when the backend returned an MP3 (with a Download link), and a SpeechSynthesis fallback when no key is configured (with a "Browser TTS" badge). Transcript scroll-lock works in both modes.
  • Real chat round-tripChatWidget calls sendChat(), tracks sessionId across turns, surfaces backend errors inline, and accepts initialQuery so the user's first command-bar prompt seeds the conversation.
  • Shared in-chat command routingdetectView() is lifted into page.tsx and exposed to ChatWidget via onCommand, so typing "make flashcards on attention" from inside the chat view routes to the flashcards view instead of being answered as prose. The model badge in CommandBar and ChatWidget is driven by getRuntimeConfig() instead of being hard-coded.
  • History panelHistoryPanel now reads GET /api/chat/sessions, filters out empty stub sessions, derives titles from the first user message, and groups rows into Today / Yesterday / Earlier this week / Older.
  • Sage API clientsrc/lib/sage-api.ts gets typed helpers for every new endpoint (knowledge, generation, audio, history) plus a resolveAudioUrl() that works from both the Next.js dev server and the Tauri sidecar.

Tooling

  • Agentation dev panelagentation added as a devDependency; a small <AgentationDev /> wrapper mounted at the end of <body> renders only when NODE_ENV === "development", so the in-app feedback overlay never ships in a production build.

Docs

Per-feature reference notes under docs/ — one file per feature area, each with a "Files touched" section so reviewers can map features to diffs quickly:

  • README.md (index)
  • agentation.md
  • chat-backend-integration.md
  • knowledge-base-ui.md
  • in-chat-command-routing.md
  • generation-endpoints.md
  • audio-generation.md
  • history-panel.md

Test plan

  • cd backend && uvicorn main:app boots without import errors.
  • GET / returns OK; GET /api/config returns provider/model.
  • Upload a .md file via the round button → it appears in KnowledgeBaseWidget with the expected chunk count and content hash.
  • Re-upload the same file → the response says "already in knowledge base".
  • Type flashcards on attention in the command bar with a non-empty KB → real cards appear with a "Grounded in: …" footer; empty KB → an error card pointing at upload.
  • Same for quiz on transformers, report on attention, audio on backpropagation.
  • With OPENAI_API_KEY set, the audio view streams an MP3 and exposes a Download link; without one, the SpeechSynthesis fallback runs and the badge reads "Browser TTS".
  • Type flashcards inside the chat view → it routes to the flashcards view, no chat reply is generated.
  • Ask the chat "what can you do?" → it lists the routed features accurately from the new capability prompt section.
  • HistoryPanel shows real conversations, grouped by Today / Yesterday / Earlier this week / Older; search filters by title and tome name.
  • npm run build in frontend/ succeeds; npx tsc --noEmit is clean.
  • Production build does not include the Agentation overlay.

Known leftovers / explicitly out of scope

  • One unrelated local edit to frontend/src-tauri/tauri.conf.json (removal of identifier) is not included — it would break Tauri builds, so it's kept in the working tree for the author to triage.
  • No persistence for generated quizzes / flashcards / audio / reports yet — they live in React state. History therefore only contains chat rows; the HistoryItem["type"] union and ICON map are pre-shaped to extend.
  • PDF/DOCX/image upload is still TODO — the ingest endpoint takes a string. The cleanest path is a new multipart/form-data endpoint that calls the existing PDFService.extract_text and forwards into the ingest pipeline.

Where to go next

All the groundwork is in place to take Sage from "real, working scaffold" to a genuinely exceptional product. The two highest-leverage follow-ups:

1. Deploy on Vercel

Make an account on https://vercel.com/ and connect it to this repository so that you can host the web app publicly. (This is free!)

2. Make Tomes a real, first-class feature

The schema, store methods, and tome-aware retrieval already exist (create_tome, link_to_tome, get_tome_documents, get_tome_document_ids, tome_id on SearchDocsSkill, etc.), and every /api/generate/* endpoint already accepts a tome_id. What's missing is the application layer on top:

  • Active-tome state at the page level, threaded through CommandBar, ChatWidget, UploadModal, and all *View components, so uploads and generations are scoped by default instead of polluting the global KB.
  • Tome management UI — a richer TomeSelector with create / rename / delete / set-active, drag-to-link documents from the KB browser, and an "active tome" pill in the header.
  • Per-tome dashboards — a tome view that shows linked documents, recent sessions in that tome (the list_sessions query already filters by tome_id), and one-click "make a quiz / flashcards / report from this tome".
  • Persistence for generated artifacts scoped to a tome — once quizzes, flashcards, audio, and reports are stored, the History panel can show them filtered by tome, and the unioned HistoryItem["type"] will start carrying real diversity.
  • Tome-scoped chat sessions surfaced in History — already supported by the store; UI just needs to surface the tome name and let users resume a session in its original tome.

3. Begin integrating more advanced ML models

The provider abstraction (providers/factory.py, providers/base.py) and the skill registry (skills/base.py, SearchDocsSkill) are deliberately model-agnostic. That makes the next ML upgrades incremental, not architectural:

  • Smarter retrieval — swap the current embedding model for a stronger one (e.g., text-embedding-3-large or bge-large-en-v1.5), add a re-ranker (bge-reranker or cohere/rerank) between SearchDocsSkill and the LLM, and add hybrid BM25 + dense scoring on top of the existing chunk table.
  • Better grounding — span-level citations instead of document-level ones. Every /api/generate/* endpoint already passes chunk_index through to the response; the next step is to have the LLM emit [doc:chunk] markers and render real footnotes in FlashcardsView / QuizView / ReportView.
  • Native PDF / DOCX / image ingest — wire PDFService.extract_text (already used by the arXiv flow) into a multipart/form-data endpoint, then layer in OCR (Tesseract or a vision LLM) and table-aware parsers (unstructured, pdfplumber).
  • Long-context / summarization upgrades — the report generator is currently single-shot. Plug in a map-reduce summarizer (or just route to a long-context model for tomes above a chunk threshold) for materially better study guides.
  • Agentic skill expansion — the SkillRegistry is the right place to add web_fetch, arxiv_search, wolfram_alpha, etc. The chat orchestrator already supports tool-calls; new skills are additive.
  • Eval harness — with everything grounded in KnowledgeStore, it's now cheap to wire a small eval set per skill (retrieval recall@k, quiz answer accuracy, report faithfulness) so each model upgrade can be measured rather than vibed.

spencerwozniak and others added 11 commits May 11, 2026 20:38
Provides a pip-installable dependency list alongside the uv-managed
pyproject.toml for environments that don't use uv.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds POST /api/generate/{flashcards,quiz,report} on a new router. Each
endpoint pulls excerpts from the knowledge base (semantic search when a
topic is provided, random sample otherwise — optionally scoped to a
tome), prompts the configured LLM with response_format=json_object on
OpenAI-compatible providers, and validates the parsed payload before
responding. An empty KB returns 400 with a hint to upload documents.

Also exposes an llm_text(...) helper used by the audio endpoint for
plain-text completions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/generate/audio retrieves KB context (or randomly samples it),
prompts the configured provider for a spoken-style narration script,
and segments it into sentence-level chunks with word-rate-estimated
timings for transcript scroll-lock.

When OPENAI_API_KEY is configured, the script is also synthesized to
MP3 via OpenAI's tts-1 voice (default: alloy) and cached to disk; the
response includes audio_url. Without a key, audio_url is null and the
frontend falls back to the browser's SpeechSynthesis voice.

GET /api/audio/{audio_id}.mp3 streams the cached file with a regex
guard against directory traversal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Adds KnowledgeStore.list_sessions(): one SQL pass joining sessions,
  messages, and tomes that returns derived title (first user message),
  tome name, message count, and last activity timestamp. Ordered by
  most-recent activity.
- Adds GET /api/chat/sessions?limit=<n> as a thin wrapper for the
  History panel.
- Extends the chat SYSTEM_PROMPT with an "App capabilities you should
  know about" section so the model can describe its own routed
  features (quiz / flashcards / audio / report / history / tomes /
  knowledge base / upload) accurately when the user asks "what can
  you do?", and skips replying when the frontend has already routed
  away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds typed helpers covering every new backend surface:

- Knowledge base: listDocuments, getDocument, deleteDocument plus
  DocumentSummary / DocumentDetail.
- Generation: generateFlashcards, generateQuiz, generateReport,
  generateAudio with shared GenerateOpts and GeneratedSource.
- Audio: AudioGeneration / AudioSegment plus resolveAudioUrl() that
  joins a relative audio path against the active API base (so it
  works from both the Next.js dev server and the Tauri sidecar).
- History: listChatSessions and ChatSessionSummary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new components reachable from the round button next to the
command-bar pill:

- UploadModal: paste / drag-drop / file-picker ingest of text-like
  files (.txt, .md, .markdown, .rst, .csv, .json, .log). Backend
  dedupes by content hash so re-uploads are safe.
- KnowledgeBaseWidget: two-pane list+detail browser over every
  ingested document. Lists titles with doc_type/source/timestamp,
  shows chunk count, content hash, linked tomes, and a 200-char
  preview. Per-row delete. An "Add document" button in the header
  opens UploadModal and refreshes the list on close.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hard-coded SAMPLE_* placeholders that the original
FlashcardWidget / QuizWidget / AudioPlayerWidget / ReportViewWidget
rendered with real, KB-grounded views that POST to the new
/api/generate/* endpoints.

Each view shares the same shape: loading card, error card with retry,
content card, and a "Grounded in: ..." source footer listing the
distinct source documents.

AudioPlayerWidget gains two new props — audioUrl and script — and
switches between two playback modes:

- <audio> element when the backend synthesized an MP3 (OpenAI TTS).
  Timeupdate events drive currentTime; a Download link appears.
- SpeechSynthesis fallback when no audio_url. Seek bar restarts
  speech from the nearest segment; a "Browser TTS" badge appears
  in place of Download.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ChatWidget now POSTs to /api/chat via sendChat(), tracks sessionId
  across turns, surfaces backend errors inline as assistant-style
  messages, and accepts an initialQuery prop so the first command-bar
  prompt seeds the conversation.
- The verb router used to live only in the command bar. detectView()
  is now lifted to page.tsx and shared with ChatWidget through an
  onCommand callback, so typing "make flashcards on attention" from
  inside the chat view routes correctly instead of being answered as
  prose. Supported verbs: quiz / test / flashcards / audio /
  podcast / listen / report / study guide / summary / history /
  tome / collection / library / knowledge / kb / docs.
- CommandBar gets a circular knowledge-base button (icon: library_add)
  to the right of the pill that opens the KB browser; the model badge
  is now driven by getRuntimeConfig() instead of being hard-coded.
- page.tsx threads generationPrompt through the routed views so the
  user's full command text becomes the topic for /api/generate/*.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the SAMPLE_HISTORY array with a fetch against
GET /api/chat/sessions. Filters out stub sessions (no user message
or zero messages), derives a row title from the first user message
(first line, 90 char cap), and groups rows into Today / Yesterday /
Earlier this week / Older buckets using a shared formatTimestamp
helper that handles both `datetime('now')` and ISO timestamp formats.

Search input filters by title or tome name client-side. Loading,
error, and empty states are inline. The filter pills (chat / quiz /
flashcard / etc.) are removed for now — only chat rows exist until
the other generations gain server-side persistence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Installs the agentation npm package as a devDependency and mounts a
small <AgentationDev /> wrapper at the end of <body> in the root
layout. The wrapper only renders when NODE_ENV === "development", so
the overlay never ships in a production build.

The optional MCP server (which forwards feedback into a CLI agent
session) is set up out-of-tree via `npx agentation-mcp init` — no
config is committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One markdown file per feature area introduced on this branch:

- README.md — index
- agentation.md — dev-only feedback overlay
- chat-backend-integration.md — ChatWidget ↔ /api/chat
- knowledge-base-ui.md — upload modal + KB browser
- in-chat-command-routing.md — shared detectView() and the
  capability section of the chat system prompt
- generation-endpoints.md — /api/generate/{flashcards,quiz}
- audio-generation.md — /api/generate/audio + TTS playback
- history-panel.md — live session list

Each doc has a "Files touched" section so reviewers can map features
to diffs quickly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@spencerwozniak spencerwozniak changed the title Sage app foundations: real chat, KB UI, generation endpoints, audio, history refactor: sage app foundations with real chat, KB UI, generation endpoints, audio, history May 12, 2026
@zeapsu

zeapsu commented May 12, 2026

Copy link
Copy Markdown
Owner

Great Sage review has entered the chat. 🧙‍♂️

First off, thank you for the thoughtful work here. The direction is very much aligned with where Sage is going: local-first knowledge base, uploaded sources, grounded generated artifacts, chat/history, and richer study views. I also tested the branch locally in an isolated checkout:

Backend:
cd backend && HOME=$(mktemp -d) uv run --group dev pytest -q
Result: 41 passed

Frontend:
cd frontend && npm ci && npm run build
Result: build succeeded

So this is not a "this does not work" review. It is more that the branch is too large and overlaps with recent work on main.

The current main branch already has related implementations for several areas this PR touches:

  • frontend/src/components/ChatWidget.tsx
  • frontend/src/components/HistoryPanel.tsx
  • frontend/src/components/AudioPlayerWidget.tsx
  • frontend/src/components/FlashcardWidget.tsx
  • frontend/src/components/QuizWidget.tsx
  • frontend/src/components/ReportViewWidget.tsx
  • frontend/src/components/TomeSelector.tsx
  • frontend/src/lib/sage-api.ts
  • backend/api/chat.py
  • backend/api/knowledge.py
  • backend/api/tomes.py
  • backend/store/db.py

This PR also adds useful new pieces that are not currently on main, including:

  • backend/api/generate.py
  • backend/api/audio.py
  • frontend/src/components/KnowledgeBaseWidget.tsx
  • frontend/src/components/UploadModal.tsx
  • frontend/src/components/FlashcardsView.tsx
  • frontend/src/components/QuizView.tsx
  • frontend/src/components/ReportView.tsx
  • frontend/src/components/AudioView.tsx
  • feature docs under docs/

Those are worth considering, but I do not think we should merge this whole branch as one PR because it mixes several architectural and product decisions at once.

Specific things I would like to avoid merging as-is:

  • backend/requirements.txt, since the project has moved to uv and pyproject.toml
  • broad rewrites of components that already exist on main
  • Agentation as a dependency without a separate decision
  • OpenAI TTS and /tmp/sage_audio behavior without a focused review
  • DB/schema changes without an explicit migration path for existing local Sage databases

I also hit a local existing-DB issue while testing related branches:

sqlite3.OperationalError: no such column: content_hash

That suggests we should add a small migration/schema-hardening step before relying on newer fields against existing local user databases.

My recommendation: let’s split or cherry-pick this branch into smaller PRs. Suggested chunks:

  1. DB migration / schema hardening for existing local DBs
  2. KB upload and browser UI
  3. Generation endpoints for flashcards, quiz, report
  4. Live Flashcards/Quiz/Report views wired to those endpoints
  5. Audio generation endpoint and audio view
  6. History/session improvements, only where they improve on current main
  7. Agentation dev panel as a separate optional tooling PR

There is good work here. I just want to preserve the current shape of Sage and bring the best pieces in deliberately instead of merging one large parallel implementation.

@spencerwozniak

spencerwozniak commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of a 4-PR split:

Why this split and not a finer one

A finer chunking was considered along these lines:

  1. DB migration / schema hardening for existing local DBs
  2. KB upload and browser UI
  3. Generation endpoints for flashcards, quiz, report
  4. Live Flashcards/Quiz/Report views wired to those endpoints
  5. Audio generation endpoint and audio view
  6. History/session improvements, only where they improve on current main
  7. Agentation dev panel as a separate optional tooling PR

Rejected because it doesn't match the actual diff on this branch:

  1. There is no DB migration / schema hardening in this PR. The only backend/store/db.py change is an additive list_sessions() read query — no CREATE, ALTER, or migration logic. That chunk would be empty.
  2. The frontend chunks share a single dependencyfrontend/src/lib/sage-api.ts (~216 lines of typed helpers). Every KB / generation / audio / history view imports from it. Splitting by feature means either duplicating the client across PRs or carving the client itself into per-feature slices, which fights how the file is written.
  3. One commit ships all four views together (live flashcards / quiz / audio / report views). Separating "Flashcards/Quiz/Report views" from "audio view" requires rewriting that commit by hand.
  4. History improvements don't overlap with main — both the backend list_sessions query and the rewritten HistoryPanel.tsx are net-new. There is nothing to filter to "only where they improve on current main."
  5. The verb router and docs were missing from the finer listdetectView() lifted to page.tsx is what makes "make flashcards on X" route correctly from inside the chat view, and the 8 markdown files document the whole branch. Both need a home.

The 4-PR split keeps each PR self-contained, lets the backend / chores / docs merge in any order, and only one PR (#43, frontend) carries the full feature payload — which is unavoidable given how tightly the frontend commits coordinate on page.tsx, CommandBar, and the shared API client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants