DocDoeAI / src /lib /api /tuition-brain.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
4.57 kB
import { apiPost } from "@/lib/api/client";
import { shouldUseDemoData } from "@/lib/api/mode";
import {
deterministicTuitionPlan,
type TuitionBrainMission,
type TuitionBrainPlan,
type TuitionBrainProfileInput,
} from "@/components/tuition/chapter-dashboard/tuitionBrainPlan";
/**
* The real tuition brain client.
*
* `planTodaysTuition` is what the UI calls: it asks the backend LLM to reason
* over the student's profile + their REAL curated missions (restate what they
* need, pick today's focus, why), and falls back to the deterministic plan when
* logged out or the backend/LLM is unavailable. Either way the recommendation
* is always a real mission — the backend validates the LLM pick against the
* candidates, and the fallback picks from them directly.
*/
type BrainMissionWire = {
chapter_id: string;
chapter_title: string;
mission_id: string;
mission_title: string;
completed: boolean;
importance: number;
};
type BrainPlanWire = {
restatement: string;
focus_reason: string;
recommended_chapter_id: string | null;
recommended_mission_id: string | null;
study_tip: string;
grounded: boolean;
is_fallback: boolean;
};
type BrainRequestWire = {
profile: {
class_level: string;
board: string;
subject: string;
goal: string | null;
daily_minutes: number | null;
exam_date: string | null;
weak_topics: string[];
low_mastery_topics: string[];
recent_mistakes: string[];
unfinished_mission_keys: string[];
revision_due_mission_keys: string[];
days_until_exam: number | null;
language: string;
};
candidate_missions: BrainMissionWire[];
};
function toWireMissions(missions: TuitionBrainMission[]): BrainMissionWire[] {
return missions.slice(0, 80).map((m) => ({
chapter_id: m.chapterId,
chapter_title: m.chapterTitle,
mission_id: m.missionId,
mission_title: m.missionTitle,
completed: m.completed,
importance: m.importance ?? 1,
}));
}
function fromWirePlan(wire: BrainPlanWire): TuitionBrainPlan {
return {
restatement: wire.restatement,
focusReason: wire.focus_reason,
recommendedChapterId: wire.recommended_chapter_id,
recommendedMissionId: wire.recommended_mission_id,
studyTip: wire.study_tip,
grounded: wire.grounded,
isFallback: wire.is_fallback,
};
}
/**
* Best-effort remote call. Returns null on failure (backend down, offline,
* over budget) so the caller falls back to the deterministic plan.
*
* No session gate: the /tuition-brain/plan endpoint serves anonymous beta
* students too (bounded by the backend's daily provider budget), so the
* login-less beta flow can still reach the real brain.
*/
export async function requestTuitionPlanRemote(
profile: TuitionBrainProfileInput,
missions: TuitionBrainMission[],
): Promise<TuitionBrainPlan | null> {
if (shouldUseDemoData()) return null;
try {
const body: BrainRequestWire = {
profile: {
class_level: profile.classLevel,
board: profile.board,
subject: profile.subject,
goal: profile.goal ?? null,
daily_minutes: profile.dailyMinutes ?? null,
exam_date: profile.examDate ?? null,
weak_topics: profile.weakTopics,
low_mastery_topics: profile.lowMasteryTopics ?? [],
recent_mistakes: profile.recentMistakes ?? [],
unfinished_mission_keys: profile.unfinishedMissionKeys ?? [],
revision_due_mission_keys: profile.revisionDueMissionKeys ?? [],
days_until_exam: profile.daysUntilExam ?? null,
language: profile.language ?? "English",
},
candidate_missions: toWireMissions(missions),
};
const wire = await apiPost<BrainPlanWire, BrainRequestWire>("/tuition-brain/plan", body);
// Guard: never trust a recommendation that isn't one of the missions we sent.
if (wire.recommended_mission_id) {
const isReal = missions.some(
(m) => m.chapterId === wire.recommended_chapter_id && m.missionId === wire.recommended_mission_id,
);
if (!isReal) return null;
}
return fromWirePlan(wire);
} catch {
return null;
}
}
/** The UI entry point: real brain when possible, deterministic otherwise. */
export async function planTodaysTuition(
profile: TuitionBrainProfileInput,
missions: TuitionBrainMission[],
): Promise<TuitionBrainPlan> {
const remote = await requestTuitionPlanRemote(profile, missions);
return remote ?? deterministicTuitionPlan(profile, missions);
}
export type { TuitionBrainMission, TuitionBrainPlan, TuitionBrainProfileInput };