# ET-LLM-004: Integración con Módulo Educativo

**Épica:** OQI-007 - LLM Strategy Agent
**Versión:** 1.0
**Fecha:** 2025-12-05
**Estado:** Planificado
**Prioridad:** P1 - Alto

---

## Resumen

Esta especificación define cómo el agente LLM se integra con el módulo educativo (OQI-002) para proporcionar asistencia personalizada, explicaciones contextuales y recomendaciones de aprendizaje.

---

## Arquitectura de Integración

```
┌─────────────────────────────────────────────────────────────────────────┐
│                     EDUCATION INTEGRATION LAYER                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  ┌────────────────────┐     ┌────────────────────┐                      │
│  │   OQI-002          │     │   OQI-007          │                      │
│  │   Education Module │     │   LLM Agent        │                      │
│  │                    │     │                    │                      │
│  │  ┌──────────────┐  │     │  ┌──────────────┐  │                      │
│  │  │   Courses    │◄─┼─────┼─►│  Education   │  │                      │
│  │  │   Service    │  │     │  │  Assistant   │  │                      │
│  │  └──────────────┘  │     │  │  Agent       │  │                      │
│  │                    │     │  └──────────────┘  │                      │
│  │  ┌──────────────┐  │     │         │         │                      │
│  │  │   Progress   │◄─┼─────┼─────────┘         │                      │
│  │  │   Tracker    │  │     │                    │                      │
│  │  └──────────────┘  │     │  ┌──────────────┐  │                      │
│  │                    │     │  │   Learning   │  │                      │
│  │  ┌──────────────┐  │     │  │  Recommender │  │                      │
│  │  │   Content    │◄─┼─────┼─►│              │  │                      │
│  │  │   Library    │  │     │  └──────────────┘  │                      │
│  │  └──────────────┘  │     │                    │                      │
│  └────────────────────┘     └────────────────────┘                      │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## Componentes

### 1. Education Context Builder

```typescript
// src/modules/copilot/education/context-builder.ts

interface EducationContext {
  currentCourse: CourseInfo | null;
  currentLesson: LessonInfo | null;
  overallProgress: ProgressSummary;
  recentQuizzes: QuizResult[];
  strugglingTopics: Topic[];
  masteredTopics: Topic[];
  suggestedNextSteps: Suggestion[];
}

interface CourseInfo {
  id: string;
  title: string;
  level: 'beginner' | 'intermediate' | 'advanced';
  progress: number;
  currentModule: string;
}

interface LessonInfo {
  id: string;
  title: string;
  type: 'video' | 'reading' | 'interactive';
  concepts: string[];
  duration: number;
  completed: boolean;
}

@Injectable()
export class EducationContextBuilder {
  constructor(
    private readonly courseService: CourseService,
    private readonly progressService: ProgressService,
  ) {}

  async build(userId: string): Promise<EducationContext> {
    const [
      enrollments,
      progress,
      quizResults,
      analytics,
    ] = await Promise.all([
      this.courseService.getUserEnrollments(userId),
      this.progressService.getUserProgress(userId),
      this.courseService.getRecentQuizzes(userId, 10),
      this.progressService.getLearningAnalytics(userId),
    ]);

    // Find current course (most recently accessed)
    const currentEnrollment = enrollments
      .filter(e => e.status === 'in_progress')
      .sort((a, b) => b.lastAccessedAt.getTime() - a.lastAccessedAt.getTime())[0];

    let currentCourse: CourseInfo | null = null;
    let currentLesson: LessonInfo | null = null;

    if (currentEnrollment) {
      const course = await this.courseService.getCourse(currentEnrollment.courseId);
      const lesson = await this.courseService.getCurrentLesson(
        userId,
        currentEnrollment.courseId,
      );

      currentCourse = {
        id: course.id,
        title: course.title,
        level: course.level,
        progress: currentEnrollment.progress,
        currentModule: lesson?.moduleName || 'N/A',
      };

      if (lesson) {
        currentLesson = {
          id: lesson.id,
          title: lesson.title,
          type: lesson.type,
          concepts: lesson.concepts,
          duration: lesson.duration,
          completed: lesson.completed,
        };
      }
    }

    // Analyze struggling topics
    const strugglingTopics = this.identifyStrugglingTopics(quizResults, analytics);
    const masteredTopics = this.identifyMasteredTopics(quizResults, analytics);

    // Generate suggestions
    const suggestedNextSteps = await this.generateSuggestions(
      userId,
      strugglingTopics,
      progress,
    );

    return {
      currentCourse,
      currentLesson,
      overallProgress: progress.summary,
      recentQuizzes: quizResults.slice(0, 5),
      strugglingTopics,
      masteredTopics,
      suggestedNextSteps,
    };
  }

  private identifyStrugglingTopics(
    quizResults: QuizResult[],
    analytics: LearningAnalytics,
  ): Topic[] {
    const topicScores = new Map<string, number[]>();

    // Aggregate quiz scores by topic
    for (const result of quizResults) {
      for (const topic of result.topics) {
        const scores = topicScores.get(topic) || [];
        scores.push(result.topicScores[topic] || result.score);
        topicScores.set(topic, scores);
      }
    }

    // Identify topics with consistently low scores
    const struggling: Topic[] = [];
    for (const [topic, scores] of topicScores) {
      const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;
      if (avgScore < 70 && scores.length >= 2) {
        struggling.push({
          name: topic,
          averageScore: avgScore,
          attempts: scores.length,
          lastAttempt: this.findLastAttemptDate(quizResults, topic),
        });
      }
    }

    return struggling.sort((a, b) => a.averageScore - b.averageScore);
  }

  private async generateSuggestions(
    userId: string,
    strugglingTopics: Topic[],
    progress: UserProgress,
  ): Promise<Suggestion[]> {
    const suggestions: Suggestion[] = [];

    // Suggest review for struggling topics
    for (const topic of strugglingTopics.slice(0, 3)) {
      const relatedLesson = await this.courseService.findLessonByTopic(topic.name);
      if (relatedLesson) {
        suggestions.push({
          type: 'review',
          priority: 'high',
          title: `Revisar: ${topic.name}`,
          description: `Tu puntuación promedio es ${topic.averageScore}%. Te recomiendo repasar este tema.`,
          action: {
            type: 'goto_lesson',
            lessonId: relatedLesson.id,
            lessonTitle: relatedLesson.title,
          },
        });
      }
    }

    // Suggest next lesson if current course in progress
    const nextLesson = await this.progressService.getNextLesson(userId);
    if (nextLesson) {
      suggestions.push({
        type: 'continue',
        priority: 'medium',
        title: `Continuar: ${nextLesson.title}`,
        description: `Estás en el ${progress.summary.currentCourseProgress}% del curso.`,
        action: {
          type: 'goto_lesson',
          lessonId: nextLesson.id,
          lessonTitle: nextLesson.title,
        },
      });
    }

    // Suggest practice if no recent activity
    if (progress.summary.daysSinceLastActivity > 3) {
      suggestions.push({
        type: 'practice',
        priority: 'medium',
        title: 'Práctica recomendada',
        description: 'Han pasado algunos días. ¿Qué tal un poco de práctica?',
        action: {
          type: 'start_practice',
          topics: strugglingTopics.map(t => t.name),
        },
      });
    }

    return suggestions;
  }
}
```

### 2. Education Assistant Agent

```typescript
// src/modules/copilot/education/assistant-agent.ts

const EDUCATION_SYSTEM_PROMPT = `
# Education Assistant - OrbiQuant Academy

## Tu Rol
Eres un tutor personalizado de trading, especializado en ayudar a los usuarios
a aprender conceptos de trading e inversión. Tu objetivo es hacer que el aprendizaje
sea accesible, interesante y aplicable al mundo real.

## Contexto del Usuario
{user_education_context}

## Principios de Enseñanza

1. **Adapta al nivel:** Ajusta la complejidad de tus explicaciones al nivel del usuario
2. **Usa ejemplos reales:** Conecta conceptos con situaciones de mercado actuales
3. **Fomenta la práctica:** Sugiere ejercicios en paper trading
4. **Sé paciente:** Si el usuario no entiende, intenta explicar de forma diferente
5. **Celebra el progreso:** Reconoce los logros del usuario

## Reglas Importantes

1. NUNCA des respuestas directas a quizzes o evaluaciones
2. Si detectas confusión persistente, sugiere revisar lecciones específicas
3. Siempre relaciona la teoría con la práctica
4. No hagas promesas de rentabilidad
5. Usa analogías simples para conceptos complejos
6. Si no conoces un tema específico del curso, admítelo

## Herramientas Disponibles

- get_lesson_content(lessonId): Obtener contenido de una lección
- get_user_progress(): Ver progreso del usuario
- find_related_lessons(topic): Buscar lecciones relacionadas a un tema
- get_practice_questions(topic): Obtener preguntas de práctica
- get_market_example(concept): Buscar ejemplo real en mercado actual

## Formato de Respuesta para Explicaciones

### Explicación de Concepto
1. Definición simple (1-2 oraciones)
2. Analogía o metáfora
3. Ejemplo con mercado real
4. Cómo aplicarlo en trading
5. Recurso adicional (lección relacionada)

### Asistencia en Quiz (sin dar respuestas)
1. Clarificar qué se pregunta
2. Recordar conceptos relevantes
3. Dar pista indirecta
4. Sugerir revisar sección específica
`;

@Injectable()
export class EducationAssistantAgent {
  constructor(
    private readonly llmProvider: LLMProviderService,
    private readonly contextBuilder: EducationContextBuilder,
    private readonly lessonService: LessonService,
    private readonly marketDataService: MarketDataService,
  ) {}

  async processEducationQuery(
    userId: string,
    query: string,
    conversationHistory: Message[],
  ): Promise<AsyncGenerator<StreamChunk>> {
    // Build education context
    const educationContext = await this.contextBuilder.build(userId);

    // Format context for system prompt
    const formattedContext = this.formatEducationContext(educationContext);

    // Detect query intent
    const intent = await this.detectIntent(query);

    // Build tools based on intent
    const tools = this.getToolsForIntent(intent);

    // Create completion
    const systemPrompt = EDUCATION_SYSTEM_PROMPT.replace(
      '{user_education_context}',
      formattedContext,
    );

    return this.llmProvider.createChatCompletion({
      model: 'gpt-4o',
      messages: [
        { role: 'system', content: systemPrompt },
        ...conversationHistory,
        { role: 'user', content: query },
      ],
      tools,
      stream: true,
    });
  }

  private formatEducationContext(context: EducationContext): string {
    let formatted = '';

    if (context.currentCourse) {
      formatted += `## Curso Actual\n`;
      formatted += `- **Curso:** ${context.currentCourse.title}\n`;
      formatted += `- **Nivel:** ${context.currentCourse.level}\n`;
      formatted += `- **Progreso:** ${context.currentCourse.progress}%\n`;
      formatted += `- **Módulo actual:** ${context.currentCourse.currentModule}\n\n`;
    }

    if (context.currentLesson) {
      formatted += `## Lección Actual\n`;
      formatted += `- **Lección:** ${context.currentLesson.title}\n`;
      formatted += `- **Conceptos:** ${context.currentLesson.concepts.join(', ')}\n`;
      formatted += `- **Estado:** ${context.currentLesson.completed ? 'Completada' : 'En progreso'}\n\n`;
    }

    if (context.strugglingTopics.length > 0) {
      formatted += `## Temas con Dificultad\n`;
      for (const topic of context.strugglingTopics) {
        formatted += `- ${topic.name} (promedio: ${topic.averageScore}%)\n`;
      }
      formatted += '\n';
    }

    if (context.masteredTopics.length > 0) {
      formatted += `## Temas Dominados\n`;
      formatted += context.masteredTopics.map(t => t.name).join(', ') + '\n\n';
    }

    if (context.suggestedNextSteps.length > 0) {
      formatted += `## Sugerencias de Siguiente Paso\n`;
      for (const suggestion of context.suggestedNextSteps) {
        formatted += `- [${suggestion.priority}] ${suggestion.title}\n`;
      }
    }

    return formatted;
  }

  private async detectIntent(query: string): Promise<EducationIntent> {
    // Simple keyword-based intent detection
    const lowerQuery = query.toLowerCase();

    if (lowerQuery.includes('quiz') || lowerQuery.includes('examen') ||
        lowerQuery.includes('respuesta')) {
      return 'quiz_help';
    }

    if (lowerQuery.includes('qué es') || lowerQuery.includes('explica') ||
        lowerQuery.includes('cómo funciona')) {
      return 'concept_explanation';
    }

    if (lowerQuery.includes('ejemplo') || lowerQuery.includes('práctica')) {
      return 'practical_example';
    }

    if (lowerQuery.includes('siguiente') || lowerQuery.includes('qué debo')) {
      return 'learning_path';
    }

    return 'general_help';
  }

  private getToolsForIntent(intent: EducationIntent): Tool[] {
    const baseTools = [
      {
        type: 'function',
        function: {
          name: 'get_lesson_content',
          description: 'Obtiene el contenido de una lección específica',
          parameters: {
            type: 'object',
            properties: {
              lessonId: { type: 'string' },
            },
            required: ['lessonId'],
          },
        },
      },
      {
        type: 'function',
        function: {
          name: 'find_related_lessons',
          description: 'Busca lecciones relacionadas a un tema',
          parameters: {
            type: 'object',
            properties: {
              topic: { type: 'string' },
            },
            required: ['topic'],
          },
        },
      },
    ];

    if (intent === 'practical_example') {
      baseTools.push({
        type: 'function',
        function: {
          name: 'get_market_example',
          description: 'Busca un ejemplo real en el mercado actual',
          parameters: {
            type: 'object',
            properties: {
              concept: { type: 'string' },
            },
            required: ['concept'],
          },
        },
      });
    }

    if (intent === 'quiz_help') {
      baseTools.push({
        type: 'function',
        function: {
          name: 'get_practice_questions',
          description: 'Obtiene preguntas de práctica (no del quiz oficial)',
          parameters: {
            type: 'object',
            properties: {
              topic: { type: 'string' },
              difficulty: { type: 'string', enum: ['easy', 'medium', 'hard'] },
            },
            required: ['topic'],
          },
        },
      });
    }

    return baseTools;
  }
}
```

### 3. Learning Recommender

```typescript
// src/modules/copilot/education/learning-recommender.ts

interface LearningRecommendation {
  type: 'course' | 'lesson' | 'practice' | 'review';
  item: CourseItem | LessonItem | PracticeItem;
  reason: string;
  priority: number;
  estimatedTime: number; // minutes
}

@Injectable()
export class LearningRecommender {
  constructor(
    private readonly courseService: CourseService,
    private readonly progressService: ProgressService,
    private readonly analyticsService: LearningAnalyticsService,
  ) {}

  async getRecommendations(
    userId: string,
    limit: number = 5,
  ): Promise<LearningRecommendation[]> {
    const [
      progress,
      analytics,
      availableCourses,
      completedCourses,
    ] = await Promise.all([
      this.progressService.getUserProgress(userId),
      this.analyticsService.getUserAnalytics(userId),
      this.courseService.getAvailableCourses(userId),
      this.courseService.getCompletedCourses(userId),
    ]);

    const recommendations: LearningRecommendation[] = [];

    // 1. Continue current course
    if (progress.currentCourse) {
      const nextLesson = await this.progressService.getNextLesson(userId);
      if (nextLesson) {
        recommendations.push({
          type: 'lesson',
          item: nextLesson,
          reason: 'Continúa donde lo dejaste',
          priority: 100,
          estimatedTime: nextLesson.duration,
        });
      }
    }

    // 2. Review struggling topics
    const strugglingTopics = analytics.strugglingTopics || [];
    for (const topic of strugglingTopics.slice(0, 2)) {
      const reviewLesson = await this.courseService.findReviewMaterial(topic.name);
      if (reviewLesson) {
        recommendations.push({
          type: 'review',
          item: reviewLesson,
          reason: `Refuerza tu comprensión de ${topic.name}`,
          priority: 90 - topic.averageScore,
          estimatedTime: 15,
        });
      }
    }

    // 3. Practice recommendations
    if (analytics.daysSinceLastPractice > 2) {
      const practiceTopics = this.selectPracticeTopics(analytics);
      recommendations.push({
        type: 'practice',
        item: {
          topics: practiceTopics,
          questionCount: 10,
        },
        reason: 'La práctica hace al maestro',
        priority: 70,
        estimatedTime: 20,
      });
    }

    // 4. Next course recommendation
    if (progress.currentCourse?.progress > 80) {
      const nextCourse = this.selectNextCourse(
        completedCourses,
        availableCourses,
        analytics,
      );
      if (nextCourse) {
        recommendations.push({
          type: 'course',
          item: nextCourse,
          reason: 'Siguiente paso en tu aprendizaje',
          priority: 50,
          estimatedTime: nextCourse.totalDuration,
        });
      }
    }

    // Sort by priority and limit
    return recommendations
      .sort((a, b) => b.priority - a.priority)
      .slice(0, limit);
  }

  private selectPracticeTopics(analytics: LearningAnalytics): string[] {
    const topics: string[] = [];

    // Mix of struggling and mastered topics (70/30 ratio)
    const struggling = (analytics.strugglingTopics || []).slice(0, 3);
    const mastered = (analytics.masteredTopics || []).slice(0, 1);

    topics.push(...struggling.map(t => t.name));
    topics.push(...mastered.map(t => t.name));

    return topics;
  }

  private selectNextCourse(
    completed: Course[],
    available: Course[],
    analytics: LearningAnalytics,
  ): Course | null {
    // Filter out completed courses
    const completedIds = new Set(completed.map(c => c.id));
    const candidates = available.filter(c => !completedIds.has(c.id));

    if (candidates.length === 0) return null;

    // Score candidates based on prerequisites and user level
    const scored = candidates.map(course => {
      let score = 0;

      // Check prerequisites
      const prereqsMet = course.prerequisites.every(p => completedIds.has(p));
      if (!prereqsMet) return { course, score: -1 };

      // Score based on level progression
      const levelOrder = ['beginner', 'intermediate', 'advanced'];
      const userLevel = analytics.estimatedLevel || 'beginner';
      const courseLevel = course.level;

      const userLevelIndex = levelOrder.indexOf(userLevel);
      const courseLevelIndex = levelOrder.indexOf(courseLevel);

      if (courseLevelIndex === userLevelIndex) score += 50;
      if (courseLevelIndex === userLevelIndex + 1) score += 30;

      // Score based on topic interest
      const userInterests = analytics.topicInterests || [];
      const matchingTopics = course.topics.filter(t =>
        userInterests.includes(t)
      ).length;
      score += matchingTopics * 10;

      return { course, score };
    });

    const best = scored
      .filter(s => s.score >= 0)
      .sort((a, b) => b.score - a.score)[0];

    return best?.course || null;
  }
}
```

---

## Tools para el Agente

### get_lesson_content

```typescript
async function getLessonContent(lessonId: string): Promise<LessonContent> {
  const lesson = await lessonService.getLesson(lessonId);

  return {
    id: lesson.id,
    title: lesson.title,
    summary: lesson.summary,
    keyPoints: lesson.keyPoints,
    concepts: lesson.concepts,
    examples: lesson.examples,
    relatedLessons: lesson.relatedLessons.map(l => ({
      id: l.id,
      title: l.title,
    })),
  };
}
```

### get_market_example

```typescript
async function getMarketExample(concept: string): Promise<MarketExample> {
  // Map concepts to market patterns
  const conceptPatterns: Record<string, string[]> = {
    'soporte': ['support_bounce', 'support_break'],
    'resistencia': ['resistance_rejection', 'breakout'],
    'rsi': ['oversold_bounce', 'overbought_reversal'],
    'macd': ['bullish_crossover', 'bearish_crossover'],
    'tendencia': ['uptrend', 'downtrend'],
  };

  const patterns = conceptPatterns[concept.toLowerCase()] || [];

  // Find recent example in market
  const example = await marketDataService.findPatternExample(patterns, {
    lookbackDays: 30,
    preferLiquid: true,
  });

  if (!example) {
    return {
      found: false,
      message: `No encontré un ejemplo reciente de ${concept} en el mercado.`,
    };
  }

  return {
    found: true,
    symbol: example.symbol,
    pattern: example.pattern,
    date: example.date,
    description: example.description,
    chartUrl: `/charts/${example.symbol}?highlight=${example.date}`,
  };
}
```

---

## Dependencias

### Módulos Internos
- CourseService (OQI-002)
- ProgressService (OQI-002)
- LessonService (OQI-002)
- MarketDataService (OQI-003)

### Base de Datos
- courses
- lessons
- enrollments
- lesson_progress
- quiz_results

---

## Referencias

- [RF-LLM-004: Asistencia Educativa](../requerimientos/RF-LLM-004-educational-assistance.md)
- [OQI-002: Módulo Educativo](../../OQI-002-education/)

---

*Especificación técnica - Sistema NEXUS*
*OrbiQuant IA Trading Platform*
