Files
studyos/server/systemPromptBuilder.js
renato97 4ff4302a8c feat: implement 33 nice-to-have features + fix 37 code review bugs
5 SDD batches archived:
- Batch 1: UI Polish (10 features, 14 tasks)
- Batch 2: Study System (8 features, 23 tasks)
- Batch 3: Infrastructure (5 features, 22 tasks)
- Batch 4: AI Advanced (5 features, 30 tasks) — RAG with @xenova/transformers
- Batch 5: Core Features (5 features, 19 tasks)

37 bugs fixed from comprehensive code review (11 CRITICAL, 12 HIGH, 14 MEDIUM/LOW):
- SSE streaming now works (event.token check)
- API keys no longer exposed via GET /api/models
- FTS5 injection sanitized
- DB backup/restore with admin auth
- Buddy mode wired (buddy_meta column)
- Exam auto-submit stale closure fixed
- CSS variables aligned with design tokens
- Progress data corruption fixed
- WebSocket protocol auto-detection
- Tests infrastructure completed (vitest + node:test)
2026-06-08 18:18:47 -03:00

144 lines
5.6 KiB
JavaScript

/**
* Builds the system prompt for a conversation based on its type, user progress,
* available PDFs, and any attachment texts.
*/
function buildSystemPrompt(conversation, progressRows = [], pdfContents = [], attachmentTexts = [], ragChunks = [], difficulty = 'normal') {
if (conversation.type === 'main') {
return buildMainPrompt(progressRows, pdfContents, attachmentTexts, ragChunks, difficulty, conversation);
}
if (conversation.type === 'fork') {
return buildForkPrompt(conversation);
}
return '';
}
function buildMainPrompt(progressRows, pdfContents, attachmentTexts, ragChunks, difficulty, conversation) {
let prompt = `Sos un tutor de estudio personal especializado. Tu objetivo es ayudar al usuario a aprender de forma eficiente y con seguimiento real de su progreso.
PROGRESO ACTUAL DEL USUARIO:
${formatProgressRows(progressRows)}
REGLAS PARA PARCIALES SIMULADOS:
- Temas marcados como DOMINADO (>=80%): incluir máximo 1-2 ejercicios simples de repaso.
- Temas en progreso o sin práctica: incluir proporcionalmente más ejercicios.
`;
if (pdfContents.length > 0) {
prompt += `
PDFS DISPONIBLES (en orden de prioridad del usuario):
${formatPDFList(pdfContents)}
Cuando el usuario pida contenido de un PDF, incluir el markdown relevante en tu respuesta.
`;
const MAX_CONTENT_LENGTH = 30000;
let pdfContentBlocks = [];
let totalLength = 0;
for (const pdf of pdfContents) {
if (!pdf.content_markdown) continue;
const content = pdf.content_markdown;
totalLength += content.length;
pdfContentBlocks.push({ name: pdf.original_name, content });
}
if (totalLength > MAX_CONTENT_LENGTH) {
const ratio = MAX_CONTENT_LENGTH / totalLength;
pdfContentBlocks = pdfContentBlocks.map((b) => ({
name: b.name,
content:
b.content.substring(0, Math.floor(b.content.length * ratio)) +
'\n\n[Contenido truncado por límites de contexto]',
}));
}
if (pdfContentBlocks.length > 0) {
prompt += pdfContentBlocks
.map((b) => `\n--- CONTENIDO DE "${b.name}" ---\n${b.content}`)
.join('\n');
}
}
if (attachmentTexts.length > 0) {
prompt += `
ARCHIVOS ADJUNTOS EN ESTA CONSULTA:
${attachmentTexts.map((t, i) => `--- Adjunto ${i + 1} ---\n${t}`).join('\n\n')}
`;
}
if (ragChunks && ragChunks.length > 0) {
prompt += `
REFERENCE CONTEXT (fragmentos relevantes de PDFs):
${ragChunks.map((c, i) => `[${i + 1}] (PDF ${c.pdf_id}, chunk ${c.chunk_index})\n${c.content}`).join('\n\n')}
`;
}
if (difficulty && difficulty !== 'normal') {
const levelLabel = difficulty === 'easy' || difficulty === 'facil' ? 'FÁCIL' : difficulty === 'hard' || difficulty === 'dificil' ? 'DIFÍCIL' : 'NORMAL';
prompt += `
NIVEL: ${levelLabel} — adaptá la profundidad y el lenguaje al nivel.
`;
}
if (conversation && conversation.buddy_meta) {
const meta = typeof conversation.buddy_meta === 'string' ? JSON.parse(conversation.buddy_meta) : conversation.buddy_meta;
const a = meta.role_a || 'Estudiante A';
const b = meta.role_b || 'Estudiante B';
prompt += `
MODO COMPAÑERO DE ESTUDIO: hay 2 usuarios llamados "${a}" y "${b}". Dirigite a ambos.
`;
}
prompt += `
CAPACIDADES:
- Generar exámenes simulados adaptados al progreso
- Crear ejercicios graduados por dificultad
- Dar explicaciones paso a paso
- Señalar errores recurrentes y sugerir correcciones
- Mantener roadmap de estudio personalizado
FORMATO DE RESPUESTA:
- Para fórmulas matemáticas, usar SIEMPRE LaTeX inline con $...$ y bloques con $$...$$. Ejemplo: $d = \\sqrt{(x_2-x_1)^2 + (y_2-y_1)^2}$
- Para gráficos de coordenadas, planos, rectas, o puntos, usar un bloque de código \`\`\`graph seguido de un JSON con el formato: {"points":[[x1,y1],[x2,y2]], "segments":[[x1,y1,x2,y2]], "grid":[xMin,xMax,yMin,yMax]}. Ejemplo para graficar A(1,2) y B(4,6):
\`\`\`graph
{"points":[[1,2],[4,6]], "segments":[[1,2,4,6]], "grid":[-1,6,-1,7]}
\`\`\`
- Para código o comandos, usar bloques de código con el lenguaje correspondiente
FORMATO DE EJERCICIOS: Cuando el usuario resuelva un ejercicio, al final de tu respuesta incluí exactamente este JSON (invisible para el usuario, solo para tracking):
{"exercise_logged": {"topic": "nombre_del_topic", "correct": true/false}}
`;
return prompt;
}
function buildForkPrompt(conversation) {
return `Sos un tutor especializado EXCLUSIVAMENTE en el tema: ${conversation.title}.
Este es un micro-chat derivado de la sesión principal de estudio.
REGLA ESTRICTA: No salgas del tema asignado. Si el usuario pregunta algo fuera de scope, redirigilo amablemente al tema.
Cuando el usuario termine, el contexto de esta sesión se va a integrar automáticamente al chat principal.
Podés usar ejercicios, ejemplos, preguntas y explicaciones paso a paso sobre ${conversation.title}.`;
}
function formatProgressRows(rows) {
if (!rows || rows.length === 0) {
return '(Sin datos de progreso aún. Empezá practicando cualquier tema.)';
}
return rows.map(r => {
const pct = r.exercises_done > 0 ? Math.round((r.exercises_correct / r.exercises_done) * 100) : 0;
let status;
if (pct >= 80) status = '✓ DOMINADO';
else if (pct >= 50) status = '→ en progreso';
else status = '✗ necesita práctica';
return `- ${r.topic}: ${r.exercises_done} ejercicios, ${pct}% correctos ${status}`;
}).join('\n');
}
function formatPDFList(pdfs) {
return pdfs
.sort((a, b) => a.reorder_index - b.reorder_index)
.map((p, i) => `${i + 1}. ${p.original_name}${p.content_markdown ? ' [contenido extraído]' : ' [pendiente de procesar]'}`)
.join('\n');
}
module.exports = { buildSystemPrompt };