DocDoeAI / src /lib /api /tutor.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
4.94 kB
import { askDocDoe } from "@/lib/api/brain";
import { getPublicDemoMode } from "@/lib/env";
import { hasLiveSession } from "@/lib/api/mode";
import type { BrainAskMode, BrainAskResponse, DocumentChunk, StudentLevel } from "@/lib/api/types";
export type DocDoeAnswer = {
title: string;
summary: string;
points: string[];
examTip: string;
citations: string[];
evidenceLabel?: string | null;
};
type AskDocDoeInput = {
documentId: string;
sourceTitle: string;
sourceIds?: string[];
sourceTitles?: string[];
question: string;
language?: string;
level?: StudentLevel;
mode?: string;
};
function createFallbackAnswer(input: AskDocDoeInput, chunks: DocumentChunk[]): DocDoeAnswer {
const bestChunk = chunks[0]?.chunk_text;
return {
title: `Study insight: ${input.question.slice(0, 42)}${input.question.length > 42 ? "..." : ""}`,
summary: bestChunk
? `From ${input.sourceTitle}: ${bestChunk}`
: `Choose a ready source and DocDoe will answer from your material. For now, focus on the core idea and turn it into short exam points.`,
points: chunks.slice(0, 3).map((chunk) => chunk.heading || chunk.chunk_text).filter(Boolean),
examTip: "Exam tip: write the definition first, then add keywords, then finish with one clean example or diagram if needed.",
citations: chunks.length > 0 ? chunks.map((chunk) => chunk.heading || input.sourceTitle).slice(0, 3) : [input.sourceTitle],
};
}
function isExamPrompt(question: string) {
const text = question.toLowerCase();
return text.includes("mark") || text.includes("exam") || text.includes("answer") || text.includes("pyq");
}
function askModeFromPrompt(question: string, explicitMode?: string): BrainAskMode {
if (explicitMode === "explain_simple" || explicitMode === "exam_answer" || explicitMode === "pyq_pattern" || explicitMode === "summarize" || explicitMode === "diagram_help") {
return explicitMode;
}
const text = question.toLowerCase();
if (text.includes("pyq") || text.includes("pattern")) return "pyq_pattern";
if (isExamPrompt(question)) return "exam_answer";
if (text.includes("summar") || text.includes("notes") || text.includes("points")) return "summarize";
if (text.includes("diagram")) return "diagram_help";
return "explain_simple";
}
function brainAnswerToDocDoeAnswer(input: AskDocDoeInput, response: BrainAskResponse): DocDoeAnswer {
const sectionPoints = response.sections
.flatMap((section) => section.body.split(/\n+/).map((line) => line.replace(/^[-*\d.\s]+/, "").trim()))
.filter(Boolean);
const sourceTitleById = new Map<string, string>();
input.sourceIds?.forEach((id, index) => {
const title = input.sourceTitles?.[index];
if (title) sourceTitleById.set(id, title);
});
const citations = response.citations
.map((citation) => citation.source_title || (citation.source_id ? sourceTitleById.get(citation.source_id) : null) || citation.heading || citation.snippet)
.filter((citation): citation is string => Boolean(citation));
return {
title: response.sections[0]?.title || `DocDoe answer: ${input.question.slice(0, 42)}${input.question.length > 42 ? "..." : ""}`,
summary: response.answer,
points: sectionPoints.length > 0 ? sectionPoints.slice(0, 6) : response.suggested_actions.slice(0, 4),
examTip: response.trust_note || "Use keywords from your source and keep the answer point-wise.",
citations: citations.length > 0 ? Array.from(new Set(citations)).slice(0, 6) : input.sourceTitles?.slice(0, 5) ?? [input.sourceTitle],
evidenceLabel: response.evidence_label ?? null,
};
}
export async function askDocDoeFromSource(input: AskDocDoeInput): Promise<DocDoeAnswer> {
const sourceIds = input.sourceIds?.length ? input.sourceIds : [input.documentId];
if (getPublicDemoMode() === "live" && !hasLiveSession()) {
// Only block if no session at all — local sources still work via fallback
if (sourceIds.length === 0) {
throw new Error("Sign in to use real DocDoe answers.");
}
}
if (input.question.toLowerCase().includes("pyq") && !input.sourceTitle.toLowerCase().includes("pyq")) {
const fallback = createFallbackAnswer(input, []);
return {
...fallback,
title: `Likely questions from this material`,
summary: "No question papers uploaded yet. Here are exam-style questions from your uploaded material instead.",
points: [
"Upload a previous question paper to unlock PYQ pattern analysis.",
...fallback.points.slice(0, 3),
],
evidenceLabel: "Based on uploaded material only",
};
}
const response = await askDocDoe({
question: input.question,
mode: askModeFromPrompt(input.question, input.mode),
source_id: input.documentId,
source_ids: sourceIds,
level: input.level ?? "intermediate",
language_preference: input.language ?? "English",
});
return brainAnswerToDocDoeAnswer(input, response);
}