feat: Go backend, enhanced search, new widgets, Docker deploy

Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
This commit is contained in:
home
2026-02-27 04:15:32 +03:00
parent 328d968f3f
commit 06fe57c765
285 changed files with 53132 additions and 1871 deletions

View File

@@ -0,0 +1,701 @@
package learning
import (
"context"
"encoding/json"
"fmt"
"regexp"
"time"
"github.com/gooseek/backend/internal/llm"
"github.com/google/uuid"
)
type LearningMode string
const (
ModeExplain LearningMode = "explain"
ModeGuided LearningMode = "guided"
ModeInteractive LearningMode = "interactive"
ModePractice LearningMode = "practice"
ModeQuiz LearningMode = "quiz"
)
type DifficultyLevel string
const (
DifficultyBeginner DifficultyLevel = "beginner"
DifficultyIntermediate DifficultyLevel = "intermediate"
DifficultyAdvanced DifficultyLevel = "advanced"
DifficultyExpert DifficultyLevel = "expert"
)
type StepByStepLesson struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Topic string `json:"topic"`
Difficulty DifficultyLevel `json:"difficulty"`
Mode LearningMode `json:"mode"`
Steps []LearningStep `json:"steps"`
Prerequisites []string `json:"prerequisites,omitempty"`
LearningGoals []string `json:"learningGoals"`
EstimatedTime int `json:"estimatedTimeMinutes"`
Progress LessonProgress `json:"progress"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type LearningStep struct {
ID string `json:"id"`
Number int `json:"number"`
Title string `json:"title"`
Type StepType `json:"type"`
Content StepContent `json:"content"`
Interaction *StepInteraction `json:"interaction,omitempty"`
Hints []string `json:"hints,omitempty"`
Examples []Example `json:"examples,omitempty"`
Practice *PracticeExercise `json:"practice,omitempty"`
Quiz *QuizQuestion `json:"quiz,omitempty"`
Duration int `json:"durationSeconds,omitempty"`
Status StepStatus `json:"status"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type StepType string
const (
StepExplanation StepType = "explanation"
StepVisualization StepType = "visualization"
StepCode StepType = "code"
StepInteractive StepType = "interactive"
StepPractice StepType = "practice"
StepQuiz StepType = "quiz"
StepSummary StepType = "summary"
StepCheckpoint StepType = "checkpoint"
)
type StepStatus string
const (
StatusLocked StepStatus = "locked"
StatusAvailable StepStatus = "available"
StatusInProgress StepStatus = "in_progress"
StatusCompleted StepStatus = "completed"
StatusSkipped StepStatus = "skipped"
)
type StepContent struct {
Text string `json:"text"`
Markdown string `json:"markdown,omitempty"`
HTML string `json:"html,omitempty"`
Code *CodeContent `json:"code,omitempty"`
Visualization *VisualizationContent `json:"visualization,omitempty"`
Media *MediaContent `json:"media,omitempty"`
Formula string `json:"formula,omitempty"`
Highlights []TextHighlight `json:"highlights,omitempty"`
}
type CodeContent struct {
Language string `json:"language"`
Code string `json:"code"`
Filename string `json:"filename,omitempty"`
Runnable bool `json:"runnable"`
Editable bool `json:"editable"`
Highlights []int `json:"highlights,omitempty"`
Annotations []CodeAnnotation `json:"annotations,omitempty"`
}
type CodeAnnotation struct {
Line int `json:"line"`
Text string `json:"text"`
Type string `json:"type"`
}
type VisualizationContent struct {
Type string `json:"type"`
Data interface{} `json:"data"`
Config map[string]interface{} `json:"config,omitempty"`
}
type MediaContent struct {
Type string `json:"type"`
URL string `json:"url"`
Caption string `json:"caption,omitempty"`
Duration int `json:"duration,omitempty"`
}
type TextHighlight struct {
Start int `json:"start"`
End int `json:"end"`
Text string `json:"text"`
Type string `json:"type"`
Note string `json:"note,omitempty"`
}
type StepInteraction struct {
Type string `json:"type"`
Prompt string `json:"prompt"`
Options []Option `json:"options,omitempty"`
Validation *Validation `json:"validation,omitempty"`
Feedback *Feedback `json:"feedback,omitempty"`
}
type Option struct {
ID string `json:"id"`
Text string `json:"text"`
IsCorrect bool `json:"isCorrect,omitempty"`
Feedback string `json:"feedback,omitempty"`
}
type Validation struct {
Type string `json:"type"`
Pattern string `json:"pattern,omitempty"`
Expected string `json:"expected,omitempty"`
Keywords []string `json:"keywords,omitempty"`
}
type Feedback struct {
Correct string `json:"correct"`
Incorrect string `json:"incorrect"`
Partial string `json:"partial,omitempty"`
}
type Example struct {
Title string `json:"title"`
Description string `json:"description"`
Input string `json:"input,omitempty"`
Output string `json:"output,omitempty"`
Code string `json:"code,omitempty"`
Language string `json:"language,omitempty"`
}
type PracticeExercise struct {
Prompt string `json:"prompt"`
Instructions string `json:"instructions"`
Starter string `json:"starter,omitempty"`
Solution string `json:"solution,omitempty"`
TestCases []TestCase `json:"testCases,omitempty"`
Hints []string `json:"hints,omitempty"`
}
type TestCase struct {
Input string `json:"input"`
Expected string `json:"expected"`
Hidden bool `json:"hidden,omitempty"`
}
type QuizQuestion struct {
Question string `json:"question"`
Type string `json:"type"`
Options []Option `json:"options,omitempty"`
CorrectIndex []int `json:"correctIndex,omitempty"`
Explanation string `json:"explanation"`
Points int `json:"points"`
}
type LessonProgress struct {
CurrentStep int `json:"currentStep"`
CompletedSteps []int `json:"completedSteps"`
Score int `json:"score"`
MaxScore int `json:"maxScore"`
TimeSpent int `json:"timeSpentSeconds"`
StartedAt time.Time `json:"startedAt"`
LastAccessed time.Time `json:"lastAccessed"`
Completed bool `json:"completed"`
CompletedAt *time.Time `json:"completedAt,omitempty"`
}
type LearningGenerator struct {
llm llm.Client
}
func NewLearningGenerator(llmClient llm.Client) *LearningGenerator {
return &LearningGenerator{llm: llmClient}
}
type GenerateLessonOptions struct {
Topic string
Query string
Difficulty DifficultyLevel
Mode LearningMode
MaxSteps int
Locale string
IncludeCode bool
IncludeQuiz bool
}
func (g *LearningGenerator) GenerateLesson(ctx context.Context, opts GenerateLessonOptions) (*StepByStepLesson, error) {
if opts.MaxSteps == 0 {
opts.MaxSteps = 10
}
if opts.Difficulty == "" {
opts.Difficulty = DifficultyBeginner
}
if opts.Mode == "" {
opts.Mode = ModeExplain
}
langInstruction := ""
if opts.Locale == "ru" {
langInstruction = "Generate all content in Russian language."
}
prompt := fmt.Sprintf(`Create a step-by-step educational lesson on the following topic.
Topic: %s
Query: %s
Difficulty: %s
Mode: %s
Max Steps: %d
Include Code Examples: %v
Include Quiz: %v
%s
Generate a structured lesson with these requirements:
1. Break down the concept into clear, digestible steps
2. Each step should build on the previous one
3. Include explanations, examples, and visualizations where helpful
4. For code topics, include runnable code snippets
5. Add practice exercises for interactive learning
6. Include quiz questions to test understanding
Respond in this JSON format:
{
"title": "Lesson title",
"description": "Brief description",
"learningGoals": ["Goal 1", "Goal 2"],
"estimatedTimeMinutes": 15,
"steps": [
{
"title": "Step title",
"type": "explanation|code|interactive|practice|quiz|summary",
"content": {
"text": "Main explanation",
"markdown": "## Formatted content",
"code": {"language": "python", "code": "example", "runnable": true},
"formula": "optional LaTeX formula"
},
"hints": ["Hint 1"],
"examples": [{"title": "Example", "description": "...", "code": "..."}],
"quiz": {
"question": "...",
"type": "multiple_choice",
"options": [{"id": "a", "text": "Option A", "isCorrect": false}],
"explanation": "..."
}
}
]
}`, opts.Topic, opts.Query, opts.Difficulty, opts.Mode, opts.MaxSteps, opts.IncludeCode, opts.IncludeQuiz, langInstruction)
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSON(result)
var parsed struct {
Title string `json:"title"`
Description string `json:"description"`
LearningGoals []string `json:"learningGoals"`
EstimatedTimeMinutes int `json:"estimatedTimeMinutes"`
Steps []struct {
Title string `json:"title"`
Type string `json:"type"`
Content struct {
Text string `json:"text"`
Markdown string `json:"markdown"`
Code *struct {
Language string `json:"language"`
Code string `json:"code"`
Runnable bool `json:"runnable"`
} `json:"code"`
Formula string `json:"formula"`
} `json:"content"`
Hints []string `json:"hints"`
Examples []struct {
Title string `json:"title"`
Description string `json:"description"`
Code string `json:"code"`
} `json:"examples"`
Quiz *struct {
Question string `json:"question"`
Type string `json:"type"`
Options []struct {
ID string `json:"id"`
Text string `json:"text"`
IsCorrect bool `json:"isCorrect"`
} `json:"options"`
Explanation string `json:"explanation"`
} `json:"quiz"`
} `json:"steps"`
}
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
return g.createDefaultLesson(opts)
}
lesson := &StepByStepLesson{
ID: uuid.New().String(),
Title: parsed.Title,
Description: parsed.Description,
Topic: opts.Topic,
Difficulty: opts.Difficulty,
Mode: opts.Mode,
LearningGoals: parsed.LearningGoals,
EstimatedTime: parsed.EstimatedTimeMinutes,
Steps: make([]LearningStep, 0),
Progress: LessonProgress{
CurrentStep: 0,
CompletedSteps: []int{},
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
for i, s := range parsed.Steps {
step := LearningStep{
ID: uuid.New().String(),
Number: i + 1,
Title: s.Title,
Type: StepType(s.Type),
Content: StepContent{
Text: s.Content.Text,
Markdown: s.Content.Markdown,
Formula: s.Content.Formula,
},
Hints: s.Hints,
Status: StatusAvailable,
}
if i > 0 {
step.Status = StatusLocked
}
if s.Content.Code != nil {
step.Content.Code = &CodeContent{
Language: s.Content.Code.Language,
Code: s.Content.Code.Code,
Runnable: s.Content.Code.Runnable,
Editable: true,
}
}
for _, ex := range s.Examples {
step.Examples = append(step.Examples, Example{
Title: ex.Title,
Description: ex.Description,
Code: ex.Code,
})
}
if s.Quiz != nil {
quiz := &QuizQuestion{
Question: s.Quiz.Question,
Type: s.Quiz.Type,
Explanation: s.Quiz.Explanation,
Points: 10,
}
for _, opt := range s.Quiz.Options {
quiz.Options = append(quiz.Options, Option{
ID: opt.ID,
Text: opt.Text,
IsCorrect: opt.IsCorrect,
})
}
step.Quiz = quiz
}
lesson.Steps = append(lesson.Steps, step)
}
lesson.Progress.MaxScore = len(lesson.Steps) * 10
return lesson, nil
}
func (g *LearningGenerator) createDefaultLesson(opts GenerateLessonOptions) (*StepByStepLesson, error) {
return &StepByStepLesson{
ID: uuid.New().String(),
Title: fmt.Sprintf("Learn: %s", opts.Topic),
Description: opts.Query,
Topic: opts.Topic,
Difficulty: opts.Difficulty,
Mode: opts.Mode,
LearningGoals: []string{"Understand the basics"},
EstimatedTime: 10,
Steps: []LearningStep{
{
ID: uuid.New().String(),
Number: 1,
Title: "Introduction",
Type: StepExplanation,
Content: StepContent{
Text: opts.Query,
Markdown: fmt.Sprintf("# %s\n\n%s", opts.Topic, opts.Query),
},
Status: StatusAvailable,
},
},
Progress: LessonProgress{
CurrentStep: 0,
CompletedSteps: []int{},
MaxScore: 10,
},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}
func (g *LearningGenerator) GenerateExplanation(ctx context.Context, topic string, difficulty DifficultyLevel, locale string) (*LearningStep, error) {
langInstruction := ""
if locale == "ru" {
langInstruction = "Respond in Russian."
}
prompt := fmt.Sprintf(`Explain this topic step by step for a %s level learner.
Topic: %s
%s
Format your response with clear sections:
1. Start with a simple definition
2. Explain key concepts
3. Provide a real-world analogy
4. Give a concrete example
Use markdown formatting.`, difficulty, topic, langInstruction)
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
return &LearningStep{
ID: uuid.New().String(),
Number: 1,
Title: topic,
Type: StepExplanation,
Content: StepContent{
Markdown: result,
},
Status: StatusAvailable,
}, nil
}
func (g *LearningGenerator) GenerateQuiz(ctx context.Context, topic string, numQuestions int, difficulty DifficultyLevel, locale string) ([]QuizQuestion, error) {
langInstruction := ""
if locale == "ru" {
langInstruction = "Generate all questions and answers in Russian."
}
prompt := fmt.Sprintf(`Generate %d multiple choice quiz questions about: %s
Difficulty level: %s
%s
Respond in JSON format:
{
"questions": [
{
"question": "Question text",
"options": [
{"id": "a", "text": "Option A", "isCorrect": false},
{"id": "b", "text": "Option B", "isCorrect": true},
{"id": "c", "text": "Option C", "isCorrect": false},
{"id": "d", "text": "Option D", "isCorrect": false}
],
"explanation": "Why the correct answer is correct"
}
]
}`, numQuestions, topic, difficulty, langInstruction)
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSON(result)
var parsed struct {
Questions []struct {
Question string `json:"question"`
Options []struct {
ID string `json:"id"`
Text string `json:"text"`
IsCorrect bool `json:"isCorrect"`
} `json:"options"`
Explanation string `json:"explanation"`
} `json:"questions"`
}
if err := json.Unmarshal([]byte(jsonStr), &parsed); err != nil {
return nil, err
}
questions := make([]QuizQuestion, 0)
for _, q := range parsed.Questions {
quiz := QuizQuestion{
Question: q.Question,
Type: "multiple_choice",
Explanation: q.Explanation,
Points: 10,
}
for _, opt := range q.Options {
quiz.Options = append(quiz.Options, Option{
ID: opt.ID,
Text: opt.Text,
IsCorrect: opt.IsCorrect,
})
}
questions = append(questions, quiz)
}
return questions, nil
}
func (g *LearningGenerator) GeneratePracticeExercise(ctx context.Context, topic, language string, difficulty DifficultyLevel, locale string) (*PracticeExercise, error) {
langInstruction := ""
if locale == "ru" {
langInstruction = "Write instructions and explanations in Russian."
}
prompt := fmt.Sprintf(`Create a coding practice exercise for: %s
Programming language: %s
Difficulty: %s
%s
Generate:
1. A clear problem statement
2. Step-by-step instructions
3. Starter code template
4. Solution code
5. Test cases
Respond in JSON:
{
"prompt": "Problem statement",
"instructions": "Step-by-step instructions",
"starter": "// Starter code",
"solution": "// Solution code",
"testCases": [
{"input": "test input", "expected": "expected output"}
],
"hints": ["Hint 1", "Hint 2"]
}`, topic, language, difficulty, langInstruction)
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSON(result)
var exercise PracticeExercise
if err := json.Unmarshal([]byte(jsonStr), &exercise); err != nil {
return &PracticeExercise{
Prompt: topic,
Instructions: "Practice this concept",
}, nil
}
return &exercise, nil
}
func (l *StepByStepLesson) CompleteStep(stepIndex int) {
if stepIndex < 0 || stepIndex >= len(l.Steps) {
return
}
l.Steps[stepIndex].Status = StatusCompleted
alreadyCompleted := false
for _, idx := range l.Progress.CompletedSteps {
if idx == stepIndex {
alreadyCompleted = true
break
}
}
if !alreadyCompleted {
l.Progress.CompletedSteps = append(l.Progress.CompletedSteps, stepIndex)
l.Progress.Score += 10
}
if stepIndex+1 < len(l.Steps) {
l.Steps[stepIndex+1].Status = StatusAvailable
l.Progress.CurrentStep = stepIndex + 1
}
if len(l.Progress.CompletedSteps) == len(l.Steps) {
l.Progress.Completed = true
now := time.Now()
l.Progress.CompletedAt = &now
}
l.UpdatedAt = time.Now()
l.Progress.LastAccessed = time.Now()
}
func (l *StepByStepLesson) SubmitQuizAnswer(stepIndex int, selectedOptions []string) (bool, string) {
if stepIndex < 0 || stepIndex >= len(l.Steps) {
return false, "Invalid step"
}
step := &l.Steps[stepIndex]
if step.Quiz == nil {
return false, "No quiz in this step"
}
correctCount := 0
totalCorrect := 0
for _, opt := range step.Quiz.Options {
if opt.IsCorrect {
totalCorrect++
}
}
for _, selected := range selectedOptions {
for _, opt := range step.Quiz.Options {
if opt.ID == selected && opt.IsCorrect {
correctCount++
}
}
}
isCorrect := correctCount == totalCorrect && len(selectedOptions) == totalCorrect
if isCorrect {
return true, step.Quiz.Explanation
}
return false, step.Quiz.Explanation
}
func extractJSON(text string) string {
re := regexp.MustCompile(`(?s)\{.*\}`)
match := re.FindString(text)
if match != "" {
return match
}
return "{}"
}
func (l *StepByStepLesson) ToJSON() ([]byte, error) {
return json.Marshal(l)
}
func ParseLesson(data []byte) (*StepByStepLesson, error) {
var lesson StepByStepLesson
if err := json.Unmarshal(data, &lesson); err != nil {
return nil, err
}
return &lesson, nil
}