feat: CI/CD pipeline + Learning/Medicine/Travel services
Some checks failed
Build and Deploy GooSeek / build-backend (push) Failing after 1m4s
Build and Deploy GooSeek / build-webui (push) Failing after 1m2s
Build and Deploy GooSeek / deploy (push) Has been skipped

- 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
This commit is contained in:
home
2026-03-02 20:25:44 +03:00
parent 08bd41e75c
commit ab48a0632b
92 changed files with 15562 additions and 2198 deletions

View File

@@ -0,0 +1,836 @@
package agent
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/gooseek/backend/internal/llm"
"github.com/gooseek/backend/internal/session"
"github.com/gooseek/backend/internal/types"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
)
type LearningIntent string
const (
IntentTaskGenerate LearningIntent = "task_generate"
IntentVerify LearningIntent = "verify"
IntentPlan LearningIntent = "plan"
IntentQuiz LearningIntent = "quiz"
IntentExplain LearningIntent = "explain"
IntentQuestion LearningIntent = "question"
IntentOnboarding LearningIntent = "onboarding"
IntentProgress LearningIntent = "progress"
)
type LearningBrief struct {
Intent LearningIntent `json:"intent"`
Topic string `json:"topic"`
Difficulty string `json:"difficulty"`
Language string `json:"language"`
TaskType string `json:"task_type"`
SpecificRequest string `json:"specific_request"`
CodeSubmitted string `json:"code_submitted"`
NeedsContext bool `json:"needs_context"`
}
type LearningDraft struct {
Brief *LearningBrief
ProfileContext string
CourseContext string
PlanContext string
TaskContext string
GeneratedTask *GeneratedTask
GeneratedQuiz *GeneratedQuiz
GeneratedPlan *GeneratedPlan
Evaluation *TaskEvaluation
Explanation string
Phase string
}
type GeneratedTask struct {
Title string `json:"title"`
Difficulty string `json:"difficulty"`
EstimatedMin int `json:"estimated_minutes"`
Description string `json:"description"`
Requirements []string `json:"requirements"`
Acceptance []string `json:"acceptance_criteria"`
Hints []string `json:"hints"`
SandboxSetup string `json:"sandbox_setup"`
VerifyCmd string `json:"verify_command"`
StarterCode string `json:"starter_code"`
TestCode string `json:"test_code"`
SkillsTrained []string `json:"skills_trained"`
}
type GeneratedQuiz struct {
Title string `json:"title"`
Questions []QuizQuestion `json:"questions"`
}
type QuizQuestion struct {
Question string `json:"question"`
Options []string `json:"options"`
Correct int `json:"correct_index"`
Explain string `json:"explanation"`
}
type GeneratedPlan struct {
Modules []PlanModule `json:"modules"`
TotalHours int `json:"total_hours"`
DifficultyAdjusted string `json:"difficulty_adjusted"`
PersonalizationNote string `json:"personalization_notes"`
}
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"`
}
type TaskEvaluation struct {
Score int `json:"score"`
MaxScore int `json:"max_score"`
Passed bool `json:"passed"`
Strengths []string `json:"strengths"`
Issues []string `json:"issues"`
Suggestions []string `json:"suggestions"`
CodeQuality string `json:"code_quality"`
}
// RunLearningOrchestrator is the multi-agent pipeline for learning chat.
// Flow: Intent Classifier → Context Collector → Specialized Agent → Widget Emission.
func RunLearningOrchestrator(ctx context.Context, sess *session.Session, input OrchestratorInput) error {
researchBlockID := uuid.New().String()
sess.EmitBlock(types.NewResearchBlock(researchBlockID))
emitPhase(sess, researchBlockID, "reasoning", "Анализирую запрос...")
// --- Phase 1: Intent Classification via LLM ---
brief, err := runLearningPlanner(ctx, input)
if err != nil {
log.Printf("[learning] planner error: %v, falling back to keyword", err)
brief = fallbackClassify(input.FollowUp)
}
log.Printf("[learning] intent=%s topic=%q difficulty=%s", brief.Intent, brief.Topic, brief.Difficulty)
draft := &LearningDraft{
Brief: brief,
Phase: "collecting",
}
// --- Phase 2: Parallel Context Collection ---
emitPhase(sess, researchBlockID, "searching", phaseSearchLabel(brief.Intent))
var mu sync.Mutex
g, gctx := errgroup.WithContext(ctx)
g.Go(func() error {
profile := extractProfileContext(input)
mu.Lock()
draft.ProfileContext = profile
mu.Unlock()
return nil
})
g.Go(func() error {
course := extractCourseContext(input)
mu.Lock()
draft.CourseContext = course
mu.Unlock()
return nil
})
g.Go(func() error {
plan := extractPlanContext(input)
mu.Lock()
draft.PlanContext = plan
mu.Unlock()
return nil
})
_ = g.Wait()
// --- Phase 3: Specialized Agent based on intent ---
emitPhase(sess, researchBlockID, "reasoning", phaseAgentLabel(brief.Intent))
switch brief.Intent {
case IntentTaskGenerate:
task, err := runTaskGeneratorAgent(gctx, input, draft)
if err != nil {
log.Printf("[learning] task generator error: %v", err)
} else {
draft.GeneratedTask = task
}
case IntentVerify:
eval, err := runCodeReviewAgent(gctx, input, draft)
if err != nil {
log.Printf("[learning] code review error: %v", err)
} else {
draft.Evaluation = eval
}
case IntentQuiz:
quiz, err := runQuizGeneratorAgent(gctx, input, draft)
if err != nil {
log.Printf("[learning] quiz generator error: %v", err)
} else {
draft.GeneratedQuiz = quiz
}
case IntentPlan:
plan, err := runPlanBuilderAgent(gctx, input, draft)
if err != nil {
log.Printf("[learning] plan builder error: %v", err)
} else {
draft.GeneratedPlan = plan
}
}
sess.EmitResearchComplete()
// --- Phase 4: Generate response text + emit widgets ---
emitLearningResponse(ctx, sess, input, draft)
sess.EmitEnd()
return nil
}
// --- Phase 1: LLM-based Intent Classification ---
func runLearningPlanner(ctx context.Context, input OrchestratorInput) (*LearningBrief, error) {
plannerCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
prompt := fmt.Sprintf(`Ты — классификатор запросов в образовательной платформе. Определи намерение ученика.
Сообщение ученика: "%s"
Контекст (если есть): %s
Определи intent и параметры. Ответь строго JSON:
{
"intent": "task_generate|verify|plan|quiz|explain|question|onboarding|progress",
"topic": "тема если определена",
"difficulty": "beginner|intermediate|advanced|expert",
"language": "язык программирования если есть",
"task_type": "code|test|review|deploy|debug|refactor|design",
"specific_request": "что конкретно просит",
"code_submitted": "код если ученик прислал код для проверки",
"needs_context": true
}
Правила:
- "задание/задачу/практику/упражнение" → task_generate
- "проверь/оцени/ревью/посмотри код" → verify
- "план/программу/roadmap/что учить" → plan
- "тест/квиз/экзамен" → quiz
- "объясни/расскажи/как работает" → explain
- "прогресс/результаты/статистика" → progress
- Если код в сообщении и просьба проверить → verify + code_submitted
- По умолчанию → question`, input.FollowUp, truncate(input.Config.SystemInstructions, 500))
result, err := input.Config.LLM.GenerateText(plannerCtx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSONFromLLM(result)
var brief LearningBrief
if err := json.Unmarshal([]byte(jsonStr), &brief); err != nil {
return nil, fmt.Errorf("parse brief: %w", err)
}
if brief.Intent == "" {
brief.Intent = IntentQuestion
}
return &brief, nil
}
func fallbackClassify(query string) *LearningBrief {
q := strings.ToLower(query)
brief := &LearningBrief{Intent: IntentQuestion, NeedsContext: true}
switch {
case containsAny(q, "задание", "задачу", "практик", "упражнение", "тренир", "дай задач"):
brief.Intent = IntentTaskGenerate
case containsAny(q, "провер", "оцени", "ревью", "посмотри код", "правильно ли", "code review"):
brief.Intent = IntentVerify
case containsAny(q, "план", "програм", "roadmap", "чему учить", "маршрут обучения"):
brief.Intent = IntentPlan
case containsAny(q, "тест", "экзамен", "квиз", "проверочн"):
brief.Intent = IntentQuiz
case containsAny(q, "объясни", "расскажи", "как работает", "что такое", "зачем нужн"):
brief.Intent = IntentExplain
case containsAny(q, "прогресс", "результат", "статистик", "сколько сделал"):
brief.Intent = IntentProgress
}
return brief
}
// --- Phase 2: Context Extraction ---
func extractProfileContext(input OrchestratorInput) string {
if input.Config.UserMemory != "" {
return input.Config.UserMemory
}
return ""
}
func extractCourseContext(input OrchestratorInput) string {
si := input.Config.SystemInstructions
if idx := strings.Index(si, "Текущий курс:"); idx >= 0 {
end := strings.Index(si[idx:], "\n")
if end > 0 {
return si[idx : idx+end]
}
return si[idx:]
}
return ""
}
func extractPlanContext(input OrchestratorInput) string {
si := input.Config.SystemInstructions
if idx := strings.Index(si, "План обучения:"); idx >= 0 {
return si[idx:]
}
return ""
}
// --- Phase 3: Specialized Agents ---
func runTaskGeneratorAgent(ctx context.Context, input OrchestratorInput, draft *LearningDraft) (*GeneratedTask, error) {
agentCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
defer cancel()
contextBlock := buildTaskGenContext(draft)
prompt := fmt.Sprintf(`Ты — ведущий разработчик в крупной IT-компании в РФ. Генерируй боевое практическое задание.
%s
Тема: %s
Сложность: %s
Язык: %s
Тип: %s
ТРЕБОВАНИЯ К ЗАДАНИЮ:
1. Задание должно быть РЕАЛЬНЫМ — как задача из production проекта в российской IT-компании
2. Чёткая постановка: что сделать, какие входные данные, что на выходе
3. Обязательно: тесты, обработка ошибок, edge cases
4. Код-стайл: линтеры, форматирование, документация
5. Если backend: REST API, middleware, валидация, логирование
6. Если frontend: компоненты, стейт-менеджмент, адаптивность
7. Starter code должен компилироваться/запускаться
8. Verify command должен реально проверять решение
Ответь строго JSON:
{
"title": "Название задания",
"difficulty": "beginner|intermediate|advanced",
"estimated_minutes": 30,
"description": "Подробное описание задачи в markdown",
"requirements": ["Требование 1", "Требование 2"],
"acceptance_criteria": ["Критерий приёмки 1", "Критерий приёмки 2"],
"hints": ["Подсказка 1 (скрытая)"],
"sandbox_setup": "команды для подготовки окружения (apt install, npm init, etc.)",
"verify_command": "команда для проверки решения (go test ./... или npm test)",
"starter_code": "начальный код проекта",
"test_code": "код тестов для автопроверки",
"skills_trained": ["навык1", "навык2"]
}`, contextBlock, orDefault(draft.Brief.Topic, "по текущему курсу"),
orDefault(draft.Brief.Difficulty, "intermediate"),
orDefault(draft.Brief.Language, "go"),
orDefault(draft.Brief.TaskType, "code"))
result, err := input.Config.LLM.GenerateText(agentCtx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSONFromLLM(result)
var task GeneratedTask
if err := json.Unmarshal([]byte(jsonStr), &task); err != nil {
return nil, fmt.Errorf("parse task: %w", err)
}
return &task, nil
}
func runCodeReviewAgent(ctx context.Context, input OrchestratorInput, draft *LearningDraft) (*TaskEvaluation, error) {
agentCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
codeToReview := draft.Brief.CodeSubmitted
if codeToReview == "" {
codeToReview = input.Config.FileContext
}
if codeToReview == "" {
for _, m := range input.ChatHistory {
if m.Role == "user" && (strings.Contains(m.Content, "```") || strings.Contains(m.Content, "func ") || strings.Contains(m.Content, "function ")) {
codeToReview = m.Content
}
}
}
if codeToReview == "" {
return &TaskEvaluation{
Score: 0,
MaxScore: 100,
Issues: []string{"Код для проверки не найден. Пришлите код в сообщении или загрузите файл."},
}, nil
}
prompt := fmt.Sprintf(`Ты — senior code reviewer в крупной IT-компании. Проведи строгое ревью кода.
Код для проверки:
%s
Контекст задания: %s
Оцени по критериям:
1. Корректность (работает ли код правильно)
2. Код-стайл (форматирование, именование, идиоматичность)
3. Обработка ошибок (edge cases, panic recovery, валидация)
4. Тесты (есть ли, покрытие, качество)
5. Безопасность (SQL injection, XSS, утечки данных)
6. Производительность (O-нотация, утечки памяти, N+1)
Ответь строго JSON:
{
"score": 75,
"max_score": 100,
"passed": true,
"strengths": ["Что хорошо"],
"issues": ["Что исправить"],
"suggestions": ["Рекомендации по улучшению"],
"code_quality": "good|acceptable|needs_work|poor"
}`, truncate(codeToReview, 6000), truncate(draft.TaskContext, 1000))
result, err := input.Config.LLM.GenerateText(agentCtx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSONFromLLM(result)
var eval TaskEvaluation
if err := json.Unmarshal([]byte(jsonStr), &eval); err != nil {
return nil, fmt.Errorf("parse eval: %w", err)
}
if eval.MaxScore == 0 {
eval.MaxScore = 100
}
return &eval, nil
}
func runQuizGeneratorAgent(ctx context.Context, input OrchestratorInput, draft *LearningDraft) (*GeneratedQuiz, error) {
agentCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
prompt := fmt.Sprintf(`Ты — методолог обучения. Создай тест для проверки знаний.
Тема: %s
Сложность: %s
Контекст курса: %s
Создай 5 вопросов. Вопросы должны проверять ПОНИМАНИЕ, а не зубрёжку.
Включи: практические сценарии, код-сниппеты, архитектурные решения.
Ответь строго JSON:
{
"title": "Название теста",
"questions": [
{
"question": "Текст вопроса (может содержать код в markdown)",
"options": ["Вариант A", "Вариант B", "Вариант C", "Вариант D"],
"correct_index": 0,
"explanation": "Почему этот ответ правильный"
}
]
}`, orDefault(draft.Brief.Topic, "текущий модуль"),
orDefault(draft.Brief.Difficulty, "intermediate"),
truncate(draft.CourseContext, 1000))
result, err := input.Config.LLM.GenerateText(agentCtx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSONFromLLM(result)
var quiz GeneratedQuiz
if err := json.Unmarshal([]byte(jsonStr), &quiz); err != nil {
return nil, fmt.Errorf("parse quiz: %w", err)
}
return &quiz, nil
}
func runPlanBuilderAgent(ctx context.Context, input OrchestratorInput, draft *LearningDraft) (*GeneratedPlan, error) {
agentCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
prompt := fmt.Sprintf(`Ты — ведущий методолог обучения в IT. Построй персональный план обучения.
Профиль ученика: %s
Текущий курс: %s
Текущий прогресс: %s
Требования:
1. Минимум теории, максимум боевой практики
2. Каждый модуль = практическое задание из реального проекта
3. Прогрессия сложности от текущего уровня ученика
4. Учитывай уже пройденные темы
5. Задания как в российских IT-компаниях
Ответь строго JSON:
{
"modules": [
{
"index": 0,
"title": "Название модуля",
"description": "Что изучаем и делаем",
"skills": ["навык1"],
"estimated_hours": 4,
"practice_focus": "Конкретная практическая задача",
"task_count": 3
}
],
"total_hours": 40,
"difficulty_adjusted": "intermediate",
"personalization_notes": "Как план адаптирован под ученика"
}`, truncate(draft.ProfileContext, 1500),
truncate(draft.CourseContext, 1000),
truncate(draft.PlanContext, 1000))
result, err := input.Config.LLM.GenerateText(agentCtx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
if err != nil {
return nil, err
}
jsonStr := extractJSONFromLLM(result)
var plan GeneratedPlan
if err := json.Unmarshal([]byte(jsonStr), &plan); err != nil {
return nil, fmt.Errorf("parse plan: %w", err)
}
return &plan, nil
}
// --- Phase 4: Response Generation + Widget Emission ---
func emitLearningResponse(ctx context.Context, sess *session.Session, input OrchestratorInput, draft *LearningDraft) {
// Emit structured widgets first
emitLearningWidgets(sess, draft)
// Then stream the conversational response
textBlockID := uuid.New().String()
sess.EmitBlock(types.NewTextBlock(textBlockID, ""))
systemPrompt := buildLearningResponsePrompt(input, draft)
messages := make([]llm.Message, 0, len(input.ChatHistory)+3)
messages = append(messages, llm.Message{Role: "system", Content: systemPrompt})
for _, m := range input.ChatHistory {
messages = append(messages, m)
}
messages = append(messages, llm.Message{Role: "user", Content: input.FollowUp})
streamCtx, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()
ch, err := input.Config.LLM.StreamText(streamCtx, llm.StreamRequest{Messages: messages})
if err != nil {
sess.UpdateBlock(textBlockID, []session.Patch{
{Op: "replace", Path: "/data", Value: fmt.Sprintf("Ошибка: %v", err)},
})
return
}
var fullContent strings.Builder
for chunk := range ch {
if chunk.ContentChunk == "" {
continue
}
fullContent.WriteString(chunk.ContentChunk)
sess.UpdateBlock(textBlockID, []session.Patch{
{Op: "replace", Path: "/data", Value: fullContent.String()},
})
}
}
func emitLearningWidgets(sess *session.Session, draft *LearningDraft) {
if draft.GeneratedTask != nil {
sess.EmitBlock(types.NewWidgetBlock(uuid.New().String(), "learning_task", map[string]interface{}{
"status": "ready",
"title": draft.GeneratedTask.Title,
"difficulty": draft.GeneratedTask.Difficulty,
"estimated": draft.GeneratedTask.EstimatedMin,
"requirements": draft.GeneratedTask.Requirements,
"acceptance": draft.GeneratedTask.Acceptance,
"hints": draft.GeneratedTask.Hints,
"verify_cmd": draft.GeneratedTask.VerifyCmd,
"starter_code": draft.GeneratedTask.StarterCode,
"test_code": draft.GeneratedTask.TestCode,
"skills": draft.GeneratedTask.SkillsTrained,
}))
}
if draft.Evaluation != nil {
sess.EmitBlock(types.NewWidgetBlock(uuid.New().String(), "learning_evaluation", map[string]interface{}{
"score": draft.Evaluation.Score,
"max_score": draft.Evaluation.MaxScore,
"passed": draft.Evaluation.Passed,
"strengths": draft.Evaluation.Strengths,
"issues": draft.Evaluation.Issues,
"suggestions": draft.Evaluation.Suggestions,
"quality": draft.Evaluation.CodeQuality,
}))
}
if draft.GeneratedQuiz != nil {
sess.EmitBlock(types.NewWidgetBlock(uuid.New().String(), "learning_quiz", map[string]interface{}{
"title": draft.GeneratedQuiz.Title,
"questions": draft.GeneratedQuiz.Questions,
"count": len(draft.GeneratedQuiz.Questions),
}))
}
if draft.GeneratedPlan != nil {
sess.EmitBlock(types.NewWidgetBlock(uuid.New().String(), "learning_plan", map[string]interface{}{
"modules": draft.GeneratedPlan.Modules,
"total_hours": draft.GeneratedPlan.TotalHours,
"difficulty": draft.GeneratedPlan.DifficultyAdjusted,
"notes": draft.GeneratedPlan.PersonalizationNote,
}))
}
sess.EmitBlock(types.NewWidgetBlock(uuid.New().String(), "learning_progress", map[string]interface{}{
"phase": "idle",
"intent": string(draft.Brief.Intent),
"timestamp": time.Now().Format(time.RFC3339),
}))
}
func buildLearningResponsePrompt(input OrchestratorInput, draft *LearningDraft) string {
var sb strings.Builder
sb.WriteString(`Ты — AI-наставник на платформе GooSeek Education. Ведёшь обучение через чат.
СТИЛЬ:
- Минимум теории, максимум практики. Объясняй кратко, по делу.
- Задания «боевые» — как на реальных проектах в российских IT-компаниях.
- Конструктивная обратная связь: что хорошо + что улучшить.
- Адаптируй сложность под ученика.
- Русский язык. Markdown для форматирования.
- Будь строгим но справедливым ментором, не «добрым учителем».
`)
switch draft.Brief.Intent {
case IntentTaskGenerate:
if draft.GeneratedTask != nil {
taskJSON, _ := json.Marshal(draft.GeneratedTask)
sb.WriteString("СГЕНЕРИРОВАННОЕ ЗАДАНИЕ (уже отправлено как виджет, не дублируй полностью):\n")
sb.WriteString(string(taskJSON))
sb.WriteString("\n\nПредставь задание ученику: кратко опиши суть, мотивируй, дай контекст зачем это нужно в реальной работе. НЕ копируй JSON — виджет уже показан.\n\n")
} else {
sb.WriteString("Задание не удалось сгенерировать. Предложи ученику уточнить тему или сложность.\n\n")
}
case IntentVerify:
if draft.Evaluation != nil {
evalJSON, _ := json.Marshal(draft.Evaluation)
sb.WriteString("РЕЗУЛЬТАТ РЕВЬЮ (уже отправлен как виджет):\n")
sb.WriteString(string(evalJSON))
sb.WriteString("\n\nДай развёрнутую обратную связь: разбери каждый issue, объясни почему это проблема, покажи как исправить с примерами кода. Похвали за strengths.\n\n")
}
case IntentQuiz:
if draft.GeneratedQuiz != nil {
sb.WriteString("Тест сгенерирован и показан как виджет. Кратко представь тест, объясни что проверяется.\n\n")
}
case IntentPlan:
if draft.GeneratedPlan != nil {
planJSON, _ := json.Marshal(draft.GeneratedPlan)
sb.WriteString("ПЛАН ОБУЧЕНИЯ (уже отправлен как виджет):\n")
sb.WriteString(string(planJSON))
sb.WriteString("\n\nПредставь план: объясни логику прогрессии, почему именно такие модули, что ученик получит в итоге.\n\n")
}
case IntentProgress:
sb.WriteString("Покажи прогресс ученика на основе контекста. Если данных нет — предложи начать с задания или теста.\n\n")
}
if draft.ProfileContext != "" {
sb.WriteString("ПРОФИЛЬ УЧЕНИКА:\n")
sb.WriteString(truncate(draft.ProfileContext, 1500))
sb.WriteString("\n\n")
}
if draft.CourseContext != "" {
sb.WriteString("ТЕКУЩИЙ КУРС:\n")
sb.WriteString(draft.CourseContext)
sb.WriteString("\n\n")
}
if draft.PlanContext != "" {
sb.WriteString("ПЛАН ОБУЧЕНИЯ:\n")
sb.WriteString(truncate(draft.PlanContext, 2000))
sb.WriteString("\n\n")
}
if input.Config.FileContext != "" {
sb.WriteString("КОД/ФАЙЛЫ УЧЕНИКА:\n")
sb.WriteString(truncate(input.Config.FileContext, 4000))
sb.WriteString("\n\n")
}
return sb.String()
}
// --- Helpers ---
func emitPhase(sess *session.Session, blockID, stepType, text string) {
step := types.ResearchSubStep{ID: uuid.New().String(), Type: stepType}
switch stepType {
case "reasoning":
step.Reasoning = text
case "searching":
step.Searching = []string{text}
}
sess.UpdateBlock(blockID, []session.Patch{
{Op: "replace", Path: "/data/subSteps", Value: []types.ResearchSubStep{step}},
})
}
func phaseSearchLabel(intent LearningIntent) string {
switch intent {
case IntentTaskGenerate:
return "Проектирую практическое задание..."
case IntentVerify:
return "Анализирую код..."
case IntentPlan:
return "Строю план обучения..."
case IntentQuiz:
return "Создаю тест..."
case IntentExplain:
return "Готовлю объяснение..."
case IntentProgress:
return "Собираю статистику..."
default:
return "Готовлю ответ..."
}
}
func phaseAgentLabel(intent LearningIntent) string {
switch intent {
case IntentTaskGenerate:
return "Генерирую боевое задание..."
case IntentVerify:
return "Провожу code review..."
case IntentPlan:
return "Адаптирую план под профиль..."
case IntentQuiz:
return "Составляю вопросы..."
default:
return "Формирую ответ..."
}
}
func buildTaskGenContext(draft *LearningDraft) string {
var parts []string
if draft.ProfileContext != "" {
parts = append(parts, "Профиль ученика: "+truncate(draft.ProfileContext, 800))
}
if draft.CourseContext != "" {
parts = append(parts, "Курс: "+draft.CourseContext)
}
if draft.PlanContext != "" {
parts = append(parts, "План: "+truncate(draft.PlanContext, 800))
}
if len(parts) == 0 {
return ""
}
return strings.Join(parts, "\n\n")
}
func extractJSONFromLLM(response string) string {
if strings.Contains(response, "```json") {
start := strings.Index(response, "```json") + 7
end := strings.Index(response[start:], "```")
if end > 0 {
return strings.TrimSpace(response[start : start+end])
}
}
if strings.Contains(response, "```") {
start := strings.Index(response, "```") + 3
if nl := strings.Index(response[start:], "\n"); nl >= 0 {
start += nl + 1
}
end := strings.Index(response[start:], "```")
if end > 0 {
candidate := strings.TrimSpace(response[start : start+end])
if len(candidate) > 2 && candidate[0] == '{' {
return candidate
}
}
}
depth := 0
startIdx := -1
for i, ch := range response {
if ch == '{' {
if depth == 0 {
startIdx = i
}
depth++
} else if ch == '}' {
depth--
if depth == 0 && startIdx >= 0 {
candidate := response[startIdx : i+1]
if len(candidate) > 10 {
return candidate
}
}
}
}
return "{}"
}
func containsAny(s string, substrs ...string) bool {
for _, sub := range substrs {
if strings.Contains(s, sub) {
return true
}
}
return false
}
func orDefault(val, def string) string {
if val == "" {
return def
}
return val
}