Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions by Darkest-Teddy · Pull Request #334 · SaplingLearn/Sapling · GitHub
Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions by Darkest-Teddy · Pull Request #334 · SaplingLearn/Sapling · GitHub
Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions by Darkest-Teddy · Pull Request #334 · SaplingLearn/Sapling · GitHub
Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions by Darkest-Teddy · Pull Request #334 · SaplingLearn/Sapling · GitHub
Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions by Darkest-Teddy · Pull Request #334 · SaplingLearn/Sapling · GitHub
Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions by Darkest-Teddy · Pull Request #334 · SaplingLearn/Sapling · GitHub
Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions by Darkest-Teddy · Pull Request #334 · SaplingLearn/Sapling · GitHub
Skip to content

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions - #334

Merged
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail
Jul 15, 2026
Merged

feat(learn): Tutor knowledge-map rail overhaul + AI concept descriptions#334
Jose-Gael-Cruz-Lopez merged 4 commits into
stagingfrom
feat/tutor-knowledge-map-rail

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the Tutor active-session right rail into a working navigator over
the course knowledge map, plus supporting graph and backend changes.

Frontend

  • Rail matches the Tutor Session design: Knowledge-map header (course code +
    name), graph on a transparent radial-glow surface, tier legend, Focused-
    concept card, "In this branch" list, "Elsewhere in course" chips.
  • Graph is scoped to the focused course's tree only.
  • Focus is decoupled from the chat: clicking a node/list item focuses it in
    the rail without touching the conversation; Resume/Start (or double-click)
    switches the session — resume existing session for that concept, else new.
  • Focus card anchors on the course when no concept is focused.
  • Per-concept descriptions: stored → lazily-fetched (cached) → generic
    fallback. Fetch is skipped in local mode (no AI).
  • Add a concept (links to the focused node / course root) or remove the
    focused one.

Graph

  • Clamp pan/zoom and node-drag to the viewport so the graph can't leave its
    box. Force-simulation params untouched.

Backend

  • concept_describe Pydantic AI agent (tool-less, one-sentence output) on
    the flash-lite tier, exposed via POST /api/graph/{user_id}/concept-description.

Notes

  • Local dataset gains real course_id + codes, richer concept lists, and
    per-concept descriptions.
  • The AI-description path compiles/imports/lints but was not exercised
    end-to-end
    (no backend + Gemini key in the dev environment) — worth a
    live check.

Summary by CodeRabbit

  • New Features

    • Added AI-generated descriptions for concepts, optionally tailored to a course.
    • Enhanced the knowledge map with focused concepts, branch and course navigation, mastery tiers, and double-click session shortcuts.
    • Added support for creating and removing concepts directly from the map.
    • Added richer local course and concept data, including descriptions and course codes.
  • Improvements

    • Prevented panning, zooming, and dragging the knowledge map into empty space.
    • Concept descriptions now load automatically when available.

Darkest-Teddyand others added 3 commits July 10, 2026 17:30
Constrain the KnowledgeGraph2D view transform and node dragging so the
graph can't be flung into empty space or clipped outside its box:
- clampView() keeps the content's bounding box inside the window (or
covering it when larger), applied on pan, wheel-zoom, and both zoom
buttons
- node drag is clamped to the visible bounds mapped back through the
current transform
Force-simulation parameters are untouched — only the camera/drag bounds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a tool-less Pydantic AI agent that returns a one-sentence,
student-facing description for a concept, plus the route that drives it:
- agents/concept_describe.py: concept_describe_agent (typed
ConceptDescription output) + build_message helper
- agents/_providers.py: register the concept_describe task on the
gemini-2.5-flash-lite tier (short single-shot generation)
- routes/graph.py: POST /api/graph/{user_id}/concept-description
Backs the Tutor knowledge-map rail's focus card for concepts that lack a
stored description (e.g. manually-added ones).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the active-session rail to the Tutor Session design and make it
a working navigator over the course map:
- rail content matches the mockup: Knowledge-map header (course code +
name), graph on a transparent radial-glow surface, tier legend, Focused
concept card, "In this branch" list, "Elsewhere in course" chips
- graph is filtered to the focused course's tree only (not the full
multi-course graph)
- focus is decoupled from the chat: clicking a node (or a list item)
focuses it in the rail without touching the conversation; the focus
card's Resume/Start button — or a double-click — switches the session
(resume existing session for that concept, else start fresh)
- focus card anchors on the course when no concept is focused
- per-concept descriptions: shown from stored data, else lazily fetched
from the concept-description endpoint and cached (skipped in local mode,
which has no AI, falling back to the connected-concepts line)
- manually add a concept (links to the focused node / course root) or
remove the focused concept; remove also hits deleteGraphNode on real
backends
- local dataset: real course_id + course codes on nodes, richer concept
lists, and one-line descriptions per concept
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-stagingadcd11dCommit Preview URL

Branch Preview URL
Jul 14 2026, 04:53 PM

@coderabbitai

coderabbitaiBot commented Jul 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 480a6536-951e-482c-b5d7-6a5e87083b16

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tutor-knowledge-map-rail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/screens/Learn.tsx (1)

214-237: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

beginSession/switchToConcept never pass the focused concept's course_id — new sessions can be created under the wrong or missing course.

cardCourseId (derived from topicNode?.course_id) can diverge from the selectedCourseId state — e.g. when the active topic doesn't match a concept name but a later-focused rail node does, or the session was started with no course selected. beginSession always uses selectedCourseId, and switchToConcept/handleNodeClick's double-click path have no way to override it. The "Start session" button on the focus card (switchToConcept(focusConcept.name), Line 902) will therefore call startSession with a stale/empty course context instead of focusConcept.course_id. Separately, both switchToConcept's existing-session lookup (Lines 423-425) and focusHasSession (Lines 580-582) match sessions by topic name only, so two courses sharing a concept name can resume the wrong session.

🐛 Suggested fix
- const beginSession = async (t: string) => {+ const beginSession = async (t: string, courseId?: string) => {
const topicName = t.trim();
if (!topicName || !userId) return;
setFocusedNodeId(null);
setTopic(topicName);
setTopicDraft(topicName);
setMessages([{ id: msgId(), role: "assistant", content: "", loading: true }]);
setStarting(true);
try {
- const res = await startSession(userId, topicName, mode, selectedCourseId || undefined, sharedCtx, modelPref);+ const res = await startSession(userId, topicName, mode, (courseId ?? selectedCourseId) || undefined, sharedCtx, modelPref);
...
- const switchToConcept = (name: string) => {- const existing = recentSessions.find(- s => s.topic.trim().toLowerCase() === name.trim().toLowerCase(),- );+ const switchToConcept = (name: string, courseId?: string) => {+ const existing = recentSessions.find(+ s => s.topic.trim().toLowerCase() === name.trim().toLowerCase()+ && (!courseId || s.course_id === courseId),+ );
if (existing) {
setFocusedNodeId(null);
handleResume(existing);
} else {
- beginSession(name);+ beginSession(name, courseId);
}
};

And update call sites: switchToConcept(n.name, n.course_id) in handleNodeClick, and switchToConcept(focusConcept.name, focusConcept.course_id) on the focus-card button.

Also applies to: 419-446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 214 - 237, Update
beginSession to accept an optional course ID override and pass it to
startSession, falling back to selectedCourseId only when no override is
provided. Update switchToConcept and its callers, including handleNodeClick and
the focus-card button, to pass each concept’s course_id. Include course_id in
switchToConcept’s existing-session lookup and focusHasSession matching so
sessions are scoped by both topic name and course.
🧹 Nitpick comments (2)
backend/routes/graph.py (1)

50-54: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No length bounds on concept/course_label before hitting a paid LLM call.

Unbounded strings increase per-call cost/latency risk and widen the attack surface for abuse against an external API. Consider adding max_length (e.g. via Field) on both fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 50 - 54, Add maximum length validation
to both fields in ConceptDescriptionBody, using Pydantic Field constraints for
concept and optional course_label before the request reaches the LLM call.
Choose appropriate bounded limits and preserve course_label’s optional default.
frontend/src/components/screens/Learn.tsx (1)

633-640: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Backend delete failure is silently swallowed — UI and persisted graph can drift out of sync.

deleteGraphNode(...).catch(() => {}) means a failed backend delete leaves the node removed locally but still present server-side; a later refetch (e.g. page reload) will make it reappear with no explanation. Surface the failure so the user can retry.

♻️ Suggested fix
- if (!IS_LOCAL_MODE && userId) deleteGraphNode(userId, nodeId).catch(() => {});+ if (!IS_LOCAL_MODE && userId) {+ deleteGraphNode(userId, nodeId).catch(() => {+ toast.error("Couldn't remove concept on the server — it may reappear after reload.");+ });+ }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/screens/Learn.tsx` around lines 633 - 640, Update
removeConcept to handle deleteGraphNode failures instead of silently swallowing
them: preserve or restore the removed node and its edges as appropriate, and
surface a clear user-facing error with an option to retry. Keep the optimistic
local removal and focused-node clearing behavior for successful deletes, using
the existing notification/error-handling patterns in Learn.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/routes/graph.py`:
- Around line 104-129: In describe_concept, limit or truncate the normalized
concept and course_label values before passing them to build_message. Wrap the
concept_describe_agent.run invocation executed by run_agent_sync in targeted
exception handling for model, transport, and validation failures, translating
those errors into HTTPException(status_code=502) while preserving unexpected
exceptions for the generic handler.
---
Outside diff comments:
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 214-237: Update beginSession to accept an optional course ID
override and pass it to startSession, falling back to selectedCourseId only when
no override is provided. Update switchToConcept and its callers, including
handleNodeClick and the focus-card button, to pass each concept’s course_id.
Include course_id in switchToConcept’s existing-session lookup and
focusHasSession matching so sessions are scoped by both topic name and course.
---
Nitpick comments:
In `@backend/routes/graph.py`:
- Around line 50-54: Add maximum length validation to both fields in
ConceptDescriptionBody, using Pydantic Field constraints for concept and
optional course_label before the request reaches the LLM call. Choose
appropriate bounded limits and preserve course_label’s optional default.
In `@frontend/src/components/screens/Learn.tsx`:
- Around line 633-640: Update removeConcept to handle deleteGraphNode failures
instead of silently swallowing them: preserve or restore the removed node and
its edges as appropriate, and surface a clear user-facing error with an option
to retry. Keep the optimistic local removal and focused-node clearing behavior
for successful deletes, using the existing notification/error-handling patterns
in Learn.tsx.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4267cc9e-0428-4f93-abce-16ea44370928

📥 Commits

Reviewing files that changed from the base of the PR and between 8bce5b0 and 8381e07.

📒 Files selected for processing (9)
  • backend/agents/_providers.py
  • backend/agents/concept_describe.py
  • backend/routes/graph.py
  • frontend/src/components/KnowledgeGraph2D.tsx
  • frontend/src/components/screens/Learn.tsx
  • frontend/src/lib/api.ts
  • frontend/src/lib/data.ts
  • frontend/src/lib/localData.ts
  • frontend/src/lib/types.ts

Comment on lines +104 to +129
@router.post("/{user_id}/concept-description")
def describe_concept(user_id: str, body: ConceptDescriptionBody, request: Request):
"""Generate a one-sentence, student-facing description for a concept.

Backs the knowledge-map rail's focus card for concepts without a stored
description. Tool-less LLM call — the concept name and course label are
handed straight to the agent.
"""
require_self(user_id, request)
concept = body.concept.strip()
if not concept:
raise HTTPException(status_code=400, detail="concept is required")
deps = SaplingDeps(
user_id=user_id,
course_id=None,
supabase=None,
request_id=current_request_id() or str(uuid.uuid4()),
)
result = run_agent_sync(
concept_describe_agent.run(
build_message(concept, body.course_label),
deps=deps,
usage_limits=WORKER_LIMITS,
)
)
return {"description": result.output.description}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bashset -euo pipefail
printf'\n== Files ==\n'
git ls-files | rg 'backend/(routes/graph.py|agents/.*concept|.*concept.*describe|.*concept.*description|agents/|schemas/)'||trueprintf'\n== Search symbols ==\n'
rg -n "concept_describe_agent|ConceptDescription|describe_concept|build_message|run_agent_sync|WORKER_LIMITS" backend -S
printf'\n== Outline candidate files ==\n'forfin backend/routes/graph.py backend/agents/concept_describe.py backend/agents/*concept* backend/agents/*.py;do
[ -f"$f" ] && { echo"--- $f"; wc -l "$f"; ast-grep outline "$f" --view expanded ||true; }
done

Repository: SaplingLearn/Sapling

Length of output: 17293


🏁 Script executed:

#!/bin/bashset -euo pipefail
echo'== backend/agents/concept_describe.py =='
cat -n backend/agents/concept_describe.py
echoecho'== backend/agents/_run.py =='
cat -n backend/agents/_run.py
echoecho'== backend/routes/graph.py (100-130) =='
sed -n '100,130p' backend/routes/graph.py
echoecho'== exception handlers in backend/main.py =='
rg -n "exception_handler|HTTPException|RequestValidationError|except Exception|traceback" backend/main.py backend/routes -S

Repository: SaplingLearn/Sapling

Length of output: 30158


Guard the concept-description agent call. Wrap run_agent_sync(concept_describe_agent.run(...)) and return a 502 for model/transport/validation failures; otherwise this route falls through to the generic 500 handler. Add a length cap or truncation for concept and course_label before building the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/routes/graph.py` around lines 104 - 129, In describe_concept, limit
or truncate the normalized concept and course_label values before passing them
to build_message. Wrap the concept_describe_agent.run invocation executed by
run_agent_sync in targeted exception handling for model, transport, and
validation failures, translating those errors into
HTTPException(status_code=502) while preserving unexpected exceptions for the
generic handler.

The knowledge-map rail's /concept-description endpoint handed the concept
name and course label straight to the LLM with no length bound, and ran the
agent with no error handling — so an oversized payload could bloat the prompt
and any model/transport/validation failure surfaced as an opaque 500.
- Truncate concept (200) and course_label (120) before build_message.
- Wrap the agent run: (AgentRunError, httpx.HTTPError, ValidationError) -> 502,
leaving unexpected exceptions to the generic 500 handler.
- Add route tests: happy path, truncation, 502 translation, 400 empty concept,
and unexpected-exception-falls-through-to-500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for freeto 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

@Darkest-Teddy@Jose-Gael-Cruz-Lopez