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:
@@ -628,6 +628,141 @@ func (s *Service) BuildRouteFromPoints(ctx context.Context, trip *Trip) (*RouteD
|
||||
return s.openRoute.GetDirections(ctx, points, "driving-car")
|
||||
}
|
||||
|
||||
// ValidateItineraryRequest is the input for itinerary validation.
|
||||
type ValidateItineraryRequest struct {
|
||||
Days []ValidateDay `json:"days"`
|
||||
POIs []ValidatePOI `json:"pois,omitempty"`
|
||||
Events []ValidateEvent `json:"events,omitempty"`
|
||||
}
|
||||
|
||||
type ValidateDay struct {
|
||||
Date string `json:"date"`
|
||||
Items []ValidateItem `json:"items"`
|
||||
}
|
||||
|
||||
type ValidateItem struct {
|
||||
RefType string `json:"refType"`
|
||||
RefID string `json:"refId"`
|
||||
Title string `json:"title"`
|
||||
StartTime string `json:"startTime,omitempty"`
|
||||
EndTime string `json:"endTime,omitempty"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
Note string `json:"note,omitempty"`
|
||||
Cost float64 `json:"cost,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type ValidatePOI struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Schedule map[string]string `json:"schedule,omitempty"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
}
|
||||
|
||||
type ValidateEvent struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
DateStart string `json:"dateStart,omitempty"`
|
||||
DateEnd string `json:"dateEnd,omitempty"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
}
|
||||
|
||||
type ValidationWarning struct {
|
||||
DayIdx int `json:"dayIdx"`
|
||||
ItemIdx int `json:"itemIdx,omitempty"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ValidateItineraryResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Warnings []ValidationWarning `json:"warnings"`
|
||||
Suggestions []ValidationWarning `json:"suggestions"`
|
||||
}
|
||||
|
||||
func (s *Service) ValidateItinerary(ctx context.Context, req ValidateItineraryRequest) (*ValidateItineraryResponse, error) {
|
||||
if s.llmClient == nil {
|
||||
return &ValidateItineraryResponse{Valid: true, Warnings: []ValidationWarning{}, Suggestions: []ValidationWarning{}}, nil
|
||||
}
|
||||
|
||||
daysJSON, _ := json.Marshal(req.Days)
|
||||
poisJSON, _ := json.Marshal(req.POIs)
|
||||
eventsJSON, _ := json.Marshal(req.Events)
|
||||
|
||||
prompt := fmt.Sprintf(`Проверь маршрут путешествия на логистические ошибки и предложи улучшения.
|
||||
|
||||
Маршрут по дням: %s
|
||||
|
||||
Доступные POI (с расписанием): %s
|
||||
|
||||
Доступные события (с датами): %s
|
||||
|
||||
Проверь:
|
||||
1. Логистику: нет ли точек в разных концах города подряд без достаточного времени на переезд
|
||||
2. Расписание: если POI имеет schedule и стоит в день когда закрыт — это ошибка
|
||||
3. Даты событий: если событие стоит в день вне его dateStart-dateEnd — это ошибка
|
||||
4. Реалистичность: не слишком ли много активностей в день (>6 основных)
|
||||
5. Время: нет ли пересечений по времени
|
||||
|
||||
Верни ТОЛЬКО JSON:
|
||||
{
|
||||
"valid": true/false,
|
||||
"warnings": [{"dayIdx": 0, "itemIdx": 2, "message": "причина"}],
|
||||
"suggestions": [{"dayIdx": 0, "message": "рекомендация"}]
|
||||
}
|
||||
|
||||
Если всё хорошо — warnings пустой массив, valid=true. Suggestions — необязательные рекомендации.`, string(daysJSON), string(poisJSON), string(eventsJSON))
|
||||
|
||||
var fullResponse strings.Builder
|
||||
err := s.llmClient.StreamChat(ctx, []ChatMessage{
|
||||
{Role: "user", Content: prompt},
|
||||
}, func(chunk string) {
|
||||
fullResponse.WriteString(chunk)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM validation failed: %w", err)
|
||||
}
|
||||
|
||||
responseText := fullResponse.String()
|
||||
|
||||
jsonStart := strings.Index(responseText, "{")
|
||||
jsonEnd := strings.LastIndex(responseText, "}")
|
||||
if jsonStart < 0 || jsonEnd < 0 || jsonEnd <= jsonStart {
|
||||
return &ValidateItineraryResponse{
|
||||
Valid: false,
|
||||
Warnings: []ValidationWarning{{
|
||||
DayIdx: 0,
|
||||
Message: "Не удалось проверить маршрут — повторите попытку",
|
||||
}},
|
||||
Suggestions: []ValidationWarning{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var result ValidateItineraryResponse
|
||||
if err := json.Unmarshal([]byte(responseText[jsonStart:jsonEnd+1]), &result); err != nil {
|
||||
return &ValidateItineraryResponse{
|
||||
Valid: false,
|
||||
Warnings: []ValidationWarning{{
|
||||
DayIdx: 0,
|
||||
Message: "Ошибка анализа маршрута — попробуйте ещё раз",
|
||||
}},
|
||||
Suggestions: []ValidationWarning{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if result.Warnings == nil {
|
||||
result.Warnings = []ValidationWarning{}
|
||||
}
|
||||
if result.Suggestions == nil {
|
||||
result.Suggestions = []ValidationWarning{}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *Service) EnrichTripWithAI(ctx context.Context, trip *Trip) error {
|
||||
if len(trip.Route) == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user