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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user