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
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user