package learning import ( "context" "encoding/json" "fmt" "github.com/gooseek/backend/internal/db" "github.com/gooseek/backend/internal/llm" ) type PersonalPlan struct { Modules []PlanModule `json:"modules"` TotalHours int `json:"total_hours"` DifficultyAdjusted string `json:"difficulty_adjusted"` PersonalizationNote string `json:"personalization_notes"` MilestoneProject string `json:"milestone_project"` } type PlanModule struct { Index int `json:"index"` Title string `json:"title"` Description string `json:"description"` Skills []string `json:"skills"` EstimatedHrs int `json:"estimated_hours"` PracticeFocus string `json:"practice_focus"` TaskCount int `json:"task_count"` IsCheckpoint bool `json:"is_checkpoint"` } func BuildPersonalPlan(ctx context.Context, llmClient llm.Client, course *db.LearningCourse, profileJSON string) (json.RawMessage, error) { profileInfo := "Профиль неизвестен — план по умолчанию." if profileJSON != "" && profileJSON != "{}" { profileInfo = "Профиль ученика:\n" + truncateStr(profileJSON, 2000) } outlineStr := string(course.BaseOutline) if len(outlineStr) > 4000 { outlineStr = outlineStr[:4000] } prompt := fmt.Sprintf(`Ты — ведущий методолог обучения в IT с 10-летним опытом. Адаптируй базовый план курса под конкретного ученика. Курс: %s Описание: %s Сложность: %s Длительность: %d часов Базовый план: %s %s ТРЕБОВАНИЯ: 1. Минимум теории, максимум боевой практики (как на реальных проектах в РФ) 2. Каждый модуль = конкретное практическое задание из реального проекта 3. Прогрессия: от простого к сложному, учитывая текущий уровень ученика 4. Каждый 3-й модуль — checkpoint (мини-проект для проверки навыков) 5. Финальный milestone project — полноценный проект для портфолио 6. Учитывай стек и опыт ученика — не повторяй то, что он уже знает Ответь строго JSON: { "modules": [ { "index": 0, "title": "Название модуля", "description": "Что изучаем и делаем", "skills": ["навык1"], "estimated_hours": 4, "practice_focus": "Конкретная практическая задача из реального проекта", "task_count": 3, "is_checkpoint": false } ], "total_hours": 40, "difficulty_adjusted": "intermediate", "personalization_notes": "Как план адаптирован под ученика", "milestone_project": "Описание финального проекта для портфолио" }`, course.Title, course.ShortDescription, course.Difficulty, course.DurationHours, outlineStr, profileInfo) var plan PersonalPlan if err := generateAndParse(ctx, llmClient, prompt, &plan, 2); err != nil { return course.BaseOutline, fmt.Errorf("plan generation failed, using base outline: %w", err) } if len(plan.Modules) == 0 { return course.BaseOutline, nil } for i := range plan.Modules { plan.Modules[i].Index = i if plan.Modules[i].TaskCount == 0 { plan.Modules[i].TaskCount = 2 } } if plan.TotalHours == 0 { total := 0 for _, m := range plan.Modules { total += m.EstimatedHrs } plan.TotalHours = total } result, err := json.Marshal(plan) if err != nil { return course.BaseOutline, err } return result, nil } func truncateStr(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." }