- Add Gitea Actions workflow for automated build & deploy - Add K8s manifests: webui, travel-svc, medicine-svc, sandbox-svc - Update kustomization for localhost:5000 registry - Add ingress for gooseek.ru and api.gooseek.ru - Learning cabinet with onboarding, courses, sandbox integration - Medicine service with symptom analysis and doctor matching - Travel service with itinerary planning - Server setup scripts (NVIDIA/CUDA, K3s, Gitea runner) Made-with: Cursor
119 lines
3.9 KiB
Go
119 lines
3.9 KiB
Go
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] + "..."
|
||
}
|