Files
gooseek/backend/internal/learning/profile_builder.go
home ab48a0632b
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
feat: CI/CD pipeline + Learning/Medicine/Travel services
- 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
2026-03-02 20:25:44 +03:00

204 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package learning
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/gooseek/backend/internal/llm"
)
type UserProfile struct {
Name string `json:"name"`
ExperienceYears float64 `json:"experience_years"`
CurrentRole string `json:"current_role"`
Skills []string `json:"skills"`
ProgrammingLangs []string `json:"programming_languages"`
Frameworks []string `json:"frameworks"`
Education string `json:"education"`
Industries []string `json:"industries"`
Strengths []string `json:"strengths"`
GrowthAreas []string `json:"growth_areas"`
CareerGoals string `json:"career_goals"`
RecommendedTracks []string `json:"recommended_tracks"`
Level string `json:"level"`
Summary string `json:"summary"`
}
func BuildProfileFromResume(ctx context.Context, llmClient llm.Client, extractedText string) (json.RawMessage, error) {
if strings.TrimSpace(extractedText) == "" {
return json.RawMessage("{}"), fmt.Errorf("empty resume text")
}
if len(extractedText) > 12000 {
extractedText = extractedText[:12000]
}
prompt := `Ты — senior HR-аналитик с 15-летним опытом в IT-рекрутинге в РФ. Проанализируй резюме и создай детальный профиль.
Резюме:
` + extractedText + `
ЗАДАЧА: Извлеки максимум информации. Определи реальный уровень кандидата (не завышай).
Ответь строго JSON (без markdown, без комментариев):
{
"name": "Имя Фамилия",
"experience_years": 3.5,
"current_role": "текущая должность или последняя",
"skills": ["навык1", "навык2", "навык3"],
"programming_languages": ["Go", "Python"],
"frameworks": ["React", "Fiber"],
"education": "образование кратко",
"industries": ["fintech", "ecommerce"],
"strengths": ["сильная сторона 1", "сильная сторона 2"],
"growth_areas": ["зона роста 1", "зона роста 2"],
"career_goals": "предположительные цели на основе опыта",
"recommended_tracks": ["рекомендуемый трек 1", "трек 2"],
"level": "junior|middle|senior|lead|expert",
"summary": "Краткая характеристика кандидата в 2-3 предложения"
}`
var profile UserProfile
err := generateAndParse(ctx, llmClient, prompt, &profile, 2)
if err != nil {
return json.RawMessage("{}"), fmt.Errorf("profile extraction failed: %w", err)
}
if profile.Level == "" {
profile.Level = inferLevel(profile.ExperienceYears)
}
if profile.Summary == "" {
profile.Summary = fmt.Sprintf("%s, %s, опыт %.0f лет", profile.Name, profile.CurrentRole, profile.ExperienceYears)
}
result, err := json.Marshal(profile)
if err != nil {
return json.RawMessage("{}"), err
}
return result, nil
}
func BuildProfileFromOnboarding(ctx context.Context, llmClient llm.Client, answers map[string]string) (json.RawMessage, error) {
answersJSON, _ := json.Marshal(answers)
prompt := `Ты — методолог обучения. На основе ответов пользователя на онбординг-вопросы, построй профиль.
Ответы пользователя:
` + string(answersJSON) + `
Ответь строго JSON:
{
"name": "",
"experience_years": 0,
"current_role": "",
"skills": [],
"programming_languages": [],
"frameworks": [],
"education": "",
"industries": [],
"strengths": [],
"growth_areas": [],
"career_goals": "",
"recommended_tracks": [],
"level": "beginner|junior|middle|senior",
"summary": "Краткая характеристика"
}`
var profile UserProfile
if err := generateAndParse(ctx, llmClient, prompt, &profile, 2); err != nil {
return json.RawMessage("{}"), err
}
if profile.Level == "" {
profile.Level = "beginner"
}
result, _ := json.Marshal(profile)
return result, nil
}
func inferLevel(years float64) string {
switch {
case years < 1:
return "beginner"
case years < 3:
return "junior"
case years < 5:
return "middle"
case years < 8:
return "senior"
default:
return "lead"
}
}
func generateAndParse(ctx context.Context, llmClient llm.Client, prompt string, target interface{}, maxRetries int) error {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
attemptCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
result, err := llmClient.GenerateText(attemptCtx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: prompt}},
})
cancel()
if err != nil {
lastErr = err
continue
}
jsonStr := extractJSONBlock(result)
if err := json.Unmarshal([]byte(jsonStr), target); err != nil {
lastErr = fmt.Errorf("attempt %d: JSON parse error: %w", attempt, err)
continue
}
return nil
}
return fmt.Errorf("all %d attempts failed: %w", maxRetries+1, lastErr)
}
func extractJSONBlock(text string) string {
if strings.Contains(text, "```json") {
start := strings.Index(text, "```json") + 7
end := strings.Index(text[start:], "```")
if end > 0 {
return strings.TrimSpace(text[start : start+end])
}
}
if strings.Contains(text, "```") {
start := strings.Index(text, "```") + 3
if nl := strings.Index(text[start:], "\n"); nl >= 0 {
start += nl + 1
}
end := strings.Index(text[start:], "```")
if end > 0 {
candidate := strings.TrimSpace(text[start : start+end])
if len(candidate) > 2 && candidate[0] == '{' {
return candidate
}
}
}
depth := 0
startIdx := -1
for i, ch := range text {
if ch == '{' {
if depth == 0 {
startIdx = i
}
depth++
} else if ch == '}' {
depth--
if depth == 0 && startIdx >= 0 {
return text[startIdx : i+1]
}
}
}
return "{}"
}