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

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"regexp"
"strings"
"sync"
@@ -103,7 +104,11 @@ func CollectPOIsEnriched(ctx context.Context, cfg TravelOrchestratorConfig, brie
}
// Phase 4: Fallback geocoding for POIs without coordinates
allPOIs = geocodePOIs(ctx, cfg, allPOIs)
allPOIs = geocodePOIs(ctx, cfg, brief, allPOIs)
// Hard filter: drop POIs that are far away from any destination center.
// This prevents ambiguous geocoding from pulling in other cities/countries.
allPOIs = filterPOIsNearDestinations(allPOIs, destinations, 250)
allPOIs = deduplicatePOIs(allPOIs)
@@ -453,6 +458,14 @@ func enrichPOIPhotos(ctx context.Context, cfg TravelOrchestratorConfig, brief *T
}
if len(photos) > 0 {
if cfg.PhotoCache != nil {
citySlug := dest
if citySlug == "" {
citySlug = "unknown"
}
photos = cfg.PhotoCache.CachePhotoBatch(ctx, citySlug, photos)
}
mu.Lock()
pois[idx].Photos = photos
mu.Unlock()
@@ -463,12 +476,18 @@ func enrichPOIPhotos(ctx context.Context, cfg TravelOrchestratorConfig, brief *T
wg.Wait()
photosFound := 0
cachedCount := 0
for _, p := range pois {
if len(p.Photos) > 0 {
photosFound++
for _, ph := range p.Photos {
if strings.Contains(ph, "storage.gooseek") || strings.Contains(ph, "minio") {
cachedCount++
}
}
}
}
log.Printf("[travel-poi] enriched %d/%d POIs with photos", photosFound, len(pois))
log.Printf("[travel-poi] enriched %d/%d POIs with photos (%d cached in MinIO)", photosFound, len(pois), cachedCount)
return pois
}
@@ -636,19 +655,27 @@ func extractPOIsWithLLM(ctx context.Context, llmClient llm.Client, brief *TripBr
return pois
}
func geocodePOIs(ctx context.Context, cfg TravelOrchestratorConfig, pois []POICard) []POICard {
func geocodePOIs(ctx context.Context, cfg TravelOrchestratorConfig, brief *TripBrief, pois []POICard) []POICard {
destSuffix := strings.Join(brief.Destinations, ", ")
for i := range pois {
if pois[i].Lat != 0 && pois[i].Lng != 0 {
continue
}
// Try geocoding by address first, then by name + city
// Try geocoding by address first, then by name+destination.
queries := []string{}
if pois[i].Address != "" {
queries = append(queries, pois[i].Address)
if destSuffix != "" && !strings.Contains(strings.ToLower(pois[i].Address), strings.ToLower(destSuffix)) {
queries = append(queries, fmt.Sprintf("%s, %s", pois[i].Address, destSuffix))
}
}
if pois[i].Name != "" {
queries = append(queries, pois[i].Name)
if destSuffix != "" {
queries = append(queries, fmt.Sprintf("%s, %s", pois[i].Name, destSuffix))
} else {
queries = append(queries, pois[i].Name)
}
}
for _, query := range queries {
@@ -674,6 +701,46 @@ func geocodePOIs(ctx context.Context, cfg TravelOrchestratorConfig, pois []POICa
return pois
}
func distanceKm(lat1, lng1, lat2, lng2 float64) float64 {
const earthRadiusKm = 6371.0
toRad := func(d float64) float64 { return d * math.Pi / 180 }
lat1r := toRad(lat1)
lat2r := toRad(lat2)
dLat := toRad(lat2 - lat1)
dLng := toRad(lng2 - lng1)
a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(lat1r)*math.Cos(lat2r)*math.Sin(dLng/2)*math.Sin(dLng/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return earthRadiusKm * c
}
func filterPOIsNearDestinations(pois []POICard, destinations []destGeoEntry, maxKm float64) []POICard {
if len(destinations) == 0 {
return pois
}
filtered := make([]POICard, 0, len(pois))
for _, p := range pois {
if p.Lat == 0 && p.Lng == 0 {
continue
}
minD := math.MaxFloat64
for _, d := range destinations {
if d.Lat == 0 && d.Lng == 0 {
continue
}
dd := distanceKm(p.Lat, p.Lng, d.Lat, d.Lng)
if dd < minD {
minD = dd
}
}
if minD <= maxKm {
filtered = append(filtered, p)
} else {
log.Printf("[travel-poi] dropped far POI '%s' (%.0fkm from destinations)", p.Name, minD)
}
}
return filtered
}
func deduplicatePOIs(pois []POICard) []POICard {
seen := make(map[string]bool)
var unique []POICard