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

@@ -22,13 +22,25 @@ type TravelContext struct {
BestTimeInfo string `json:"bestTimeInfo,omitempty"`
}
type DailyForecast struct {
Date string `json:"date"`
TempMin float64 `json:"tempMin"`
TempMax float64 `json:"tempMax"`
Conditions string `json:"conditions"`
Icon string `json:"icon"`
RainChance string `json:"rainChance"`
Wind string `json:"wind,omitempty"`
Tip string `json:"tip,omitempty"`
}
type WeatherAssessment struct {
Summary string `json:"summary"`
TempMin float64 `json:"tempMin"`
TempMax float64 `json:"tempMax"`
Conditions string `json:"conditions"`
Clothing string `json:"clothing"`
RainChance string `json:"rainChance"`
Summary string `json:"summary"`
TempMin float64 `json:"tempMin"`
TempMax float64 `json:"tempMax"`
Conditions string `json:"conditions"`
Clothing string `json:"clothing"`
RainChance string `json:"rainChance"`
DailyForecast []DailyForecast `json:"dailyForecast,omitempty"`
}
type SafetyAssessment struct {
@@ -87,7 +99,6 @@ func searchForContext(ctx context.Context, client *search.SearXNGClient, brief *
dest := strings.Join(brief.Destinations, ", ")
currentYear := time.Now().Format("2006")
currentMonth := time.Now().Format("01")
monthNames := map[string]string{
"01": "январь", "02": "февраль", "03": "март",
@@ -95,10 +106,25 @@ func searchForContext(ctx context.Context, client *search.SearXNGClient, brief *
"07": "июль", "08": "август", "09": "сентябрь",
"10": "октябрь", "11": "ноябрь", "12": "декабрь",
}
month := monthNames[currentMonth]
tripMonth := time.Now().Format("01")
if brief.StartDate != "" {
if t, err := time.Parse("2006-01-02", brief.StartDate); err == nil {
tripMonth = t.Format("01")
}
}
month := monthNames[tripMonth]
dateRange := ""
if brief.StartDate != "" && brief.EndDate != "" {
dateRange = fmt.Sprintf("%s — %s", brief.StartDate, brief.EndDate)
} else if brief.StartDate != "" {
dateRange = brief.StartDate
}
queries := []string{
fmt.Sprintf("погода %s %s %s прогноз", dest, month, currentYear),
fmt.Sprintf("погода %s %s %s прогноз по дням", dest, month, currentYear),
fmt.Sprintf("прогноз погоды %s %s на 14 дней", dest, dateRange),
fmt.Sprintf("безопасность туристов %s %s", dest, currentYear),
fmt.Sprintf("ограничения %s туризм %s", dest, currentYear),
fmt.Sprintf("что нужно знать туристу %s %s", dest, currentYear),
@@ -154,20 +180,40 @@ func extractContextWithLLM(ctx context.Context, llmClient llm.Client, brief *Tri
dest := strings.Join(brief.Destinations, ", ")
currentDate := time.Now().Format("2006-01-02")
prompt := fmt.Sprintf(`Ты — эксперт по путешествиям. Оцени текущую обстановку в %s для поездки %s — %s.
Сегодня: %s.
tripDays := computeTripDays(brief.StartDate, brief.EndDate)
dailyForecastNote := ""
if tripDays > 0 {
dailyForecastNote = fmt.Sprintf(`
ВАЖНО: Поездка длится %d дней (%s — %s). Составь прогноз погоды НА КАЖДЫЙ ДЕНЬ поездки.
В "dailyForecast" должно быть ровно %d элементов — по одному на каждый день.`, tripDays, brief.StartDate, brief.EndDate, tripDays)
}
prompt := fmt.Sprintf(`Ты — эксперт по путешествиям. Оцени обстановку в %s для поездки %s — %s.
Сегодня: %s.
%s
%s
Верни ТОЛЬКО JSON (без текста):
{
"weather": {
"summary": "Краткое описание погоды на период поездки",
"tempMin": число_градусов_минимум,
"tempMax": число_градусов_максимум,
"conditions": "солнечно/облачно/дождливо/снежно",
"clothing": "Что надеть: конкретные рекомендации",
"rainChance": "низкая/средняя/высокая"
"summary": "Общее описание погоды на весь период поездки",
"tempMin": число_минимумаесь_период,
"tempMax": числоаксимумаесь_период,
"conditions": "преобладающие условия: солнечно/облачно/переменная облачность/дождливо/снежно",
"clothing": "Что надеть: конкретные рекомендации по одежде",
"rainChance": "низкая/средняя/высокая",
"dailyForecast": [
{
"date": "YYYY-MM-DD",
"tempMin": число,
"tempMax": число,
"conditions": "солнечно/облачно/дождь/гроза/снег/туман/переменная облачность",
"icon": "sun/cloud/cloud-sun/rain/storm/snow/fog/wind",
"rainChance": "низкая/средняя/высокая",
"wind": "слабый/умеренный/сильный",
"tip": "Краткий совет на этот день (необязательно, только если есть что сказать)"
}
]
},
"safety": {
"level": "safe/caution/warning/danger",
@@ -190,27 +236,35 @@ func extractContextWithLLM(ctx context.Context, llmClient llm.Client, brief *Tri
}
Правила:
- Используй ТОЛЬКО актуальные данные %s года
- weather: реальный прогноз на период поездки, не среднегодовые значения
- safety: объективная оценка, не преувеличивай опасности
- Используй актуальные данные %s года и данные из поиска
- dailyForecast: прогноз НА КАЖДЫЙ ДЕНЬ поездки с конкретными температурами и условиями
- Если точный прогноз недоступен — используй климатические данные для этого периода, но старайся варьировать по дням реалистично
- icon: одно из значений sun/cloud/cloud-sun/rain/storm/snow/fog/wind
- weather.summary: общее описание, упомяни если ожидаются дождливые дни
- safety: объективная оценка, не преувеличивай
- restrictions: визовые требования, медицинские ограничения, локальные правила
- tips: 3-5 практичных советов для туриста
- Если данных нет — используй свои знания о регионе, но отмечай это
- tips: 3-5 практичных советов
- Температуры в градусах Цельсия`,
dest,
brief.StartDate,
brief.EndDate,
currentDate,
dailyForecastNote,
contextBuilder.String(),
time.Now().Format("2006"),
)
llmCtx, cancel := context.WithTimeout(ctx, 25*time.Second)
llmCtx, cancel := context.WithTimeout(ctx, 35*time.Second)
defer cancel()
maxTokens := 3000
if tripDays > 5 {
maxTokens = 4000
}
response, err := llmClient.GenerateText(llmCtx, llm.StreamRequest{
Messages: []llm.Message{{Role: llm.RoleUser, Content: prompt}},
Options: llm.StreamOptions{MaxTokens: 2000, Temperature: 0.2},
Options: llm.StreamOptions{MaxTokens: maxTokens, Temperature: 0.3},
})
if err != nil {
log.Printf("[travel-context] LLM extraction failed: %v", err)
@@ -233,9 +287,31 @@ func extractContextWithLLM(ctx context.Context, llmClient llm.Client, brief *Tri
travelCtx.Safety.EmergencyNo = "112"
}
log.Printf("[travel-context] extracted context: weather=%s, safety=%s, restrictions=%d, tips=%d",
travelCtx.Weather.Conditions, travelCtx.Safety.Level,
len(travelCtx.Restrictions), len(travelCtx.Tips))
log.Printf("[travel-context] extracted context: weather=%s (%d daily), safety=%s, restrictions=%d, tips=%d",
travelCtx.Weather.Conditions, len(travelCtx.Weather.DailyForecast),
travelCtx.Safety.Level, len(travelCtx.Restrictions), len(travelCtx.Tips))
return &travelCtx
}
func computeTripDays(startDate, endDate string) int {
if startDate == "" || endDate == "" {
return 0
}
start, err := time.Parse("2006-01-02", startDate)
if err != nil {
return 0
}
end, err := time.Parse("2006-01-02", endDate)
if err != nil {
return 0
}
days := int(end.Sub(start).Hours()/24) + 1
if days < 1 {
return 1
}
if days > 30 {
return 30
}
return days
}