package main import ( "context" "fmt" "log" "os" "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gooseek/backend/internal/learning" "github.com/gooseek/backend/internal/llm" "github.com/gooseek/backend/pkg/config" ) type LessonStore struct { lessons map[string]*learning.StepByStepLesson } func NewLessonStore() *LessonStore { return &LessonStore{ lessons: make(map[string]*learning.StepByStepLesson), } } func (s *LessonStore) Save(lesson *learning.StepByStepLesson) { s.lessons[lesson.ID] = lesson } func (s *LessonStore) Get(id string) *learning.StepByStepLesson { return s.lessons[id] } func (s *LessonStore) List(limit, offset int) []*learning.StepByStepLesson { result := make([]*learning.StepByStepLesson, 0) i := 0 for _, l := range s.lessons { if i >= offset && len(result) < limit { result = append(result, l) } i++ } return result } func (s *LessonStore) Delete(id string) bool { if _, ok := s.lessons[id]; ok { delete(s.lessons, id) return true } return false } func main() { cfg, err := config.Load() if err != nil { log.Fatal("Failed to load config:", err) } var llmClient llm.Client // Priority 1: Timeweb Cloud AI (recommended for production) if cfg.TimewebAgentAccessID != "" && cfg.TimewebAPIKey != "" { client, err := llm.NewTimewebClient(llm.TimewebConfig{ ProviderID: "timeweb", BaseURL: cfg.TimewebAPIBaseURL, AgentAccessID: cfg.TimewebAgentAccessID, APIKey: cfg.TimewebAPIKey, ModelKey: cfg.DefaultLLMModel, ProxySource: cfg.TimewebProxySource, }) if err != nil { log.Printf("Warning: Failed to create Timeweb client: %v", err) } else { llmClient = client log.Println("Using Timeweb Cloud AI as LLM provider") } } // Priority 2: Anthropic if llmClient == nil && cfg.AnthropicAPIKey != "" && !isJWT(cfg.AnthropicAPIKey) { client, err := llm.NewAnthropicClient(llm.ProviderConfig{ ProviderID: "anthropic", APIKey: cfg.AnthropicAPIKey, ModelKey: "claude-3-5-sonnet-20241022", }) if err != nil { log.Printf("Warning: Failed to create Anthropic client: %v", err) } else { llmClient = client log.Println("Using Anthropic as LLM provider") } } // Priority 3: OpenAI (only if it's a real OpenAI key, not Timeweb JWT) if llmClient == nil && cfg.OpenAIAPIKey != "" && !isJWT(cfg.OpenAIAPIKey) { client, err := llm.NewOpenAIClient(llm.ProviderConfig{ ProviderID: "openai", APIKey: cfg.OpenAIAPIKey, ModelKey: "gpt-4o-mini", }) if err != nil { log.Printf("Warning: Failed to create OpenAI client: %v", err) } else { llmClient = client log.Println("Using OpenAI as LLM provider") } } if llmClient == nil { log.Fatal("No LLM provider configured. Please set TIMEWEB_AGENT_ACCESS_ID + TIMEWEB_API_KEY, or OPENAI_API_KEY, or ANTHROPIC_API_KEY") } generator := learning.NewLearningGenerator(llmClient) store := NewLessonStore() app := fiber.New(fiber.Config{ BodyLimit: 50 * 1024 * 1024, ReadTimeout: 120 * time.Second, WriteTimeout: 120 * time.Second, }) app.Use(logger.New()) app.Use(cors.New()) app.Get("/health", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"status": "ok"}) }) app.Post("/api/v1/learning/lesson", func(c *fiber.Ctx) error { var req struct { Topic string `json:"topic"` Query string `json:"query"` Difficulty string `json:"difficulty"` Mode string `json:"mode"` MaxSteps int `json:"maxSteps"` Locale string `json:"locale"` IncludeCode bool `json:"includeCode"` IncludeQuiz bool `json:"includeQuiz"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) defer cancel() difficulty := learning.DifficultyBeginner switch req.Difficulty { case "intermediate": difficulty = learning.DifficultyIntermediate case "advanced": difficulty = learning.DifficultyAdvanced case "expert": difficulty = learning.DifficultyExpert } mode := learning.ModeExplain switch req.Mode { case "guided": mode = learning.ModeGuided case "interactive": mode = learning.ModeInteractive case "practice": mode = learning.ModePractice case "quiz": mode = learning.ModeQuiz } lesson, err := generator.GenerateLesson(ctx, learning.GenerateLessonOptions{ Topic: req.Topic, Query: req.Query, Difficulty: difficulty, Mode: mode, MaxSteps: req.MaxSteps, Locale: req.Locale, IncludeCode: req.IncludeCode, IncludeQuiz: req.IncludeQuiz, }) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } store.Save(lesson) return c.JSON(lesson) }) app.Post("/api/v1/learning/explain", func(c *fiber.Ctx) error { var req struct { Topic string `json:"topic"` Difficulty string `json:"difficulty"` Locale string `json:"locale"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() difficulty := learning.DifficultyBeginner switch req.Difficulty { case "intermediate": difficulty = learning.DifficultyIntermediate case "advanced": difficulty = learning.DifficultyAdvanced case "expert": difficulty = learning.DifficultyExpert } step, err := generator.GenerateExplanation(ctx, req.Topic, difficulty, req.Locale) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(step) }) app.Post("/api/v1/learning/quiz", func(c *fiber.Ctx) error { var req struct { Topic string `json:"topic"` NumQuestions int `json:"numQuestions"` Difficulty string `json:"difficulty"` Locale string `json:"locale"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } if req.NumQuestions == 0 { req.NumQuestions = 5 } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() difficulty := learning.DifficultyBeginner switch req.Difficulty { case "intermediate": difficulty = learning.DifficultyIntermediate case "advanced": difficulty = learning.DifficultyAdvanced case "expert": difficulty = learning.DifficultyExpert } questions, err := generator.GenerateQuiz(ctx, req.Topic, req.NumQuestions, difficulty, req.Locale) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"questions": questions}) }) app.Post("/api/v1/learning/practice", func(c *fiber.Ctx) error { var req struct { Topic string `json:"topic"` Language string `json:"language"` Difficulty string `json:"difficulty"` Locale string `json:"locale"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() difficulty := learning.DifficultyBeginner switch req.Difficulty { case "intermediate": difficulty = learning.DifficultyIntermediate case "advanced": difficulty = learning.DifficultyAdvanced case "expert": difficulty = learning.DifficultyExpert } exercise, err := generator.GeneratePracticeExercise(ctx, req.Topic, req.Language, difficulty, req.Locale) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(exercise) }) app.Get("/api/v1/learning/lessons", func(c *fiber.Ctx) error { limit := c.QueryInt("limit", 20) offset := c.QueryInt("offset", 0) lessons := store.List(limit, offset) summaries := make([]map[string]interface{}, 0) for _, l := range lessons { summaries = append(summaries, map[string]interface{}{ "id": l.ID, "title": l.Title, "topic": l.Topic, "difficulty": l.Difficulty, "mode": l.Mode, "stepsCount": len(l.Steps), "estimatedTime": l.EstimatedTime, "progress": l.Progress, "createdAt": l.CreatedAt, }) } return c.JSON(fiber.Map{"lessons": summaries, "count": len(summaries)}) }) app.Get("/api/v1/learning/lessons/:id", func(c *fiber.Ctx) error { id := c.Params("id") lesson := store.Get(id) if lesson == nil { return c.Status(404).JSON(fiber.Map{"error": "Lesson not found"}) } return c.JSON(lesson) }) app.Post("/api/v1/learning/lessons/:id/complete-step", func(c *fiber.Ctx) error { id := c.Params("id") lesson := store.Get(id) if lesson == nil { return c.Status(404).JSON(fiber.Map{"error": "Lesson not found"}) } var req struct { StepIndex int `json:"stepIndex"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } lesson.CompleteStep(req.StepIndex) return c.JSON(fiber.Map{ "success": true, "progress": lesson.Progress, }) }) app.Post("/api/v1/learning/lessons/:id/submit-answer", func(c *fiber.Ctx) error { id := c.Params("id") lesson := store.Get(id) if lesson == nil { return c.Status(404).JSON(fiber.Map{"error": "Lesson not found"}) } var req struct { StepIndex int `json:"stepIndex"` SelectedOptions []string `json:"selectedOptions"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request"}) } correct, explanation := lesson.SubmitQuizAnswer(req.StepIndex, req.SelectedOptions) if correct { lesson.CompleteStep(req.StepIndex) } return c.JSON(fiber.Map{ "correct": correct, "explanation": explanation, "progress": lesson.Progress, }) }) app.Delete("/api/v1/learning/lessons/:id", func(c *fiber.Ctx) error { id := c.Params("id") if store.Delete(id) { return c.JSON(fiber.Map{"success": true}) } return c.Status(404).JSON(fiber.Map{"error": "Lesson not found"}) }) port := getEnvInt("LEARNING_SVC_PORT", 3034) log.Printf("learning-svc listening on :%d", port) log.Fatal(app.Listen(fmt.Sprintf(":%d", port))) } func getEnvInt(key string, defaultValue int) int { if val := os.Getenv(key); val != "" { var result int if _, err := fmt.Sscanf(val, "%d", &result); err == nil { return result } } return defaultValue } func isJWT(s string) bool { return len(s) > 10 && s[:3] == "eyJ" }