Files
home ab48a0632b
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
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
2026-03-02 20:25:44 +03:00

254 lines
6.6 KiB
Go

package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gooseek/backend/internal/agent"
"github.com/gooseek/backend/internal/llm"
"github.com/gooseek/backend/internal/search"
"github.com/gooseek/backend/internal/session"
"github.com/gooseek/backend/pkg/config"
"github.com/gooseek/backend/pkg/middleware"
"github.com/gooseek/backend/pkg/ndjson"
"github.com/gooseek/backend/pkg/storage"
)
type SearchRequest struct {
Message struct {
MessageID string `json:"messageId"`
ChatID string `json:"chatId"`
Content string `json:"content"`
} `json:"message"`
OptimizationMode string `json:"optimizationMode"`
Sources []string `json:"sources"`
History [][]string `json:"history"`
Files []string `json:"files"`
ChatModel ChatModel `json:"chatModel"`
SystemInstructions string `json:"systemInstructions"`
Locale string `json:"locale"`
AnswerMode string `json:"answerMode"`
ResponsePrefs *struct {
Format string `json:"format"`
Length string `json:"length"`
Tone string `json:"tone"`
} `json:"responsePrefs"`
LearningMode bool `json:"learningMode"`
}
type ChatModel struct {
ProviderID string `json:"providerId"`
Key string `json:"key"`
}
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatal("Failed to load config:", err)
}
searchClient := search.NewSearXNGClient(cfg)
var photoCache *agent.PhotoCacheService
if cfg.MinioEndpoint != "" {
minioStorage, err := storage.NewMinioStorage(storage.MinioConfig{
Endpoint: cfg.MinioEndpoint,
AccessKey: cfg.MinioAccessKey,
SecretKey: cfg.MinioSecretKey,
Bucket: cfg.MinioBucket,
UseSSL: cfg.MinioUseSSL,
PublicURL: cfg.MinioPublicURL,
})
if err != nil {
log.Printf("Warning: MinIO init failed (photo cache disabled): %v", err)
} else {
photoCache = agent.NewPhotoCacheService(minioStorage)
log.Printf("Photo cache enabled: MinIO at %s, bucket=%s, publicURL=%s", cfg.MinioEndpoint, cfg.MinioBucket, cfg.MinioPublicURL)
}
}
app := fiber.New(fiber.Config{
StreamRequestBody: true,
BodyLimit: 10 * 1024 * 1024,
ReadTimeout: time.Minute,
WriteTimeout: 5 * time.Minute,
IdleTimeout: 2 * time.Minute,
})
app.Use(logger.New())
app.Use(cors.New())
app.Get("/health", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"})
})
agents := app.Group("/api/v1/agents", middleware.JWT(middleware.JWTConfig{
Secret: cfg.JWTSecret,
AuthSvcURL: cfg.AuthSvcURL,
AllowGuest: true,
}))
agents.Post("/search", func(c *fiber.Ctx) error {
var req SearchRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
}
if req.Message.Content == "" {
return c.Status(400).JSON(fiber.Map{"error": "Message content required"})
}
providerID := req.ChatModel.ProviderID
modelKey := req.ChatModel.Key
if providerID == "" && cfg.TimewebAPIKey != "" {
providerID = "timeweb"
modelKey = "gpt-4o"
} else if providerID == "" {
providerID = "ollama"
modelKey = cfg.OllamaModelKey
}
baseURL := cfg.TimewebAPIBaseURL
if providerID == "ollama" {
baseURL = cfg.OllamaBaseURL
if modelKey == "" {
modelKey = cfg.OllamaModelKey
}
}
llmClient, err := llm.NewClient(llm.ProviderConfig{
ProviderID: providerID,
ModelKey: modelKey,
APIKey: getAPIKey(cfg, providerID),
BaseURL: baseURL,
AgentAccessID: cfg.TimewebAgentAccessID,
})
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to create LLM client: " + err.Error()})
}
chatHistory := make([]llm.Message, 0, len(req.History))
for _, h := range req.History {
if len(h) >= 2 {
role := llm.RoleUser
if h[0] == "ai" || h[0] == "assistant" {
role = llm.RoleAssistant
}
chatHistory = append(chatHistory, llm.Message{
Role: role,
Content: h[1],
})
}
}
mode := agent.ModeBalanced
switch req.OptimizationMode {
case "speed":
mode = agent.ModeSpeed
case "quality":
mode = agent.ModeQuality
}
var responsePrefs *agent.ResponsePrefs
if req.ResponsePrefs != nil {
responsePrefs = &agent.ResponsePrefs{
Format: req.ResponsePrefs.Format,
Length: req.ResponsePrefs.Length,
Tone: req.ResponsePrefs.Tone,
}
}
input := agent.OrchestratorInput{
ChatHistory: chatHistory,
FollowUp: req.Message.Content,
Config: agent.OrchestratorConfig{
LLM: llmClient,
SearchClient: searchClient,
Mode: mode,
Sources: req.Sources,
FileIDs: req.Files,
SystemInstructions: req.SystemInstructions,
Locale: req.Locale,
AnswerMode: req.AnswerMode,
ResponsePrefs: responsePrefs,
LearningMode: req.LearningMode,
DiscoverSvcURL: cfg.DiscoverSvcURL,
Crawl4AIURL: cfg.Crawl4AIURL,
TravelSvcURL: cfg.TravelSvcURL,
TravelPayoutsToken: cfg.TravelPayoutsToken,
TravelPayoutsMarker: cfg.TravelPayoutsMarker,
PhotoCache: photoCache,
},
}
sess := session.NewSession()
c.Set("Content-Type", "application/x-ndjson")
c.Set("Cache-Control", "no-cache")
c.Set("Transfer-Encoding", "chunked")
c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
writer := ndjson.NewWriter(w)
unsubscribe := sess.Subscribe(func(eventType session.EventType, data interface{}) {
if eventType == session.EventData {
if dataMap, ok := data.(map[string]interface{}); ok {
writer.Write(dataMap)
w.Flush()
}
}
})
defer unsubscribe()
err := agent.RunOrchestrator(ctx, sess, input)
if err != nil {
ndjson.WriteError(writer, err)
}
})
return nil
})
agents.Get("/status", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ready", "user": middleware.GetUserID(c)})
})
port := cfg.AgentSvcPort
log.Printf("agent-svc listening on :%d", port)
log.Fatal(app.Listen(fmt.Sprintf(":%d", port)))
}
func getAPIKey(cfg *config.Config, providerID string) string {
switch providerID {
case "ollama":
return ""
case "timeweb":
return cfg.TimewebAPIKey
case "openai":
return cfg.OpenAIAPIKey
case "anthropic":
return cfg.AnthropicAPIKey
case "gemini", "google":
return cfg.GeminiAPIKey
default:
return ""
}
}
func init() {
if os.Getenv("PORT") == "" {
os.Setenv("PORT", "3018")
}
}