Major changes: - Add Go backend (backend/) with microservices architecture - Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler, proxy-manager, media-search, fastClassifier, language detection - New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard, UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions - Improved discover-svc with discover-db integration - Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md) - Library-svc: project_id schema migration - Remove deprecated finance-svc and travel-svc - Localization improvements across services Made-with: Cursor
236 lines
6.0 KiB
Go
236 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
|
"github.com/gooseek/backend/pkg/config"
|
|
)
|
|
|
|
type ChatRequest 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"`
|
|
EmbeddingModel ChatModel `json:"embeddingModel"`
|
|
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)
|
|
}
|
|
|
|
app := fiber.New(fiber.Config{
|
|
StreamRequestBody: true,
|
|
BodyLimit: 50 * 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"})
|
|
})
|
|
|
|
app.Get("/ready", func(c *fiber.Ctx) error {
|
|
return c.JSON(fiber.Map{"status": "ready"})
|
|
})
|
|
|
|
app.Get("/metrics", func(c *fiber.Ctx) error {
|
|
c.Set("Content-Type", "text/plain; charset=utf-8")
|
|
return c.SendString(
|
|
"# HELP gooseek_up Service is up (1) or down (0)\n" +
|
|
"# TYPE gooseek_up gauge\n" +
|
|
"gooseek_up 1\n",
|
|
)
|
|
})
|
|
|
|
app.Get("/api/v1/config", func(c *fiber.Ctx) error {
|
|
return c.JSON(fiber.Map{
|
|
"values": fiber.Map{
|
|
"version": 1,
|
|
"setupComplete": true,
|
|
"preferences": fiber.Map{},
|
|
},
|
|
"fields": fiber.Map{},
|
|
"modelProviders": []interface{}{},
|
|
"envOnlyMode": true,
|
|
})
|
|
})
|
|
|
|
app.Post("/api/v1/chat", func(c *fiber.Ctx) error {
|
|
var req ChatRequest
|
|
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"})
|
|
}
|
|
|
|
isDiscoverSummary := strings.HasPrefix(req.Message.Content, "Summary: ") && len(req.Message.Content) > 9
|
|
var summaryURL string
|
|
if isDiscoverSummary {
|
|
summaryURL = strings.TrimSpace(strings.TrimPrefix(req.Message.Content, "Summary: "))
|
|
}
|
|
|
|
agentURL := strings.TrimSuffix(cfg.AgentSvcURL, "/") + "/api/v1/agents/search"
|
|
|
|
httpReq, err := http.NewRequest("POST", agentURL, bytes.NewReader(c.Body()))
|
|
if err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
if auth := c.Get("Authorization"); auth != "" {
|
|
httpReq.Header.Set("Authorization", auth)
|
|
}
|
|
|
|
client := &http.Client{Timeout: 5 * time.Minute}
|
|
resp, err := client.Do(httpReq)
|
|
if err != nil {
|
|
log.Printf("Agent service error: %v", err)
|
|
return c.Status(503).JSON(fiber.Map{"error": "Agent service unavailable"})
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
return c.Status(resp.StatusCode).Send(body)
|
|
}
|
|
|
|
c.Set("Content-Type", "application/x-ndjson")
|
|
c.Set("Cache-Control", "no-cache")
|
|
c.Set("Connection", "keep-alive")
|
|
|
|
if isDiscoverSummary && summaryURL != "" && cfg.DiscoverSvcURL != "" {
|
|
collected := make([]string, 0)
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
scanner.Buffer(make([]byte, 1024*1024), 10*1024*1024)
|
|
|
|
var result bytes.Buffer
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.TrimSpace(line) != "" {
|
|
collected = append(collected, line)
|
|
result.WriteString(line + "\n")
|
|
}
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if len(collected) > 0 {
|
|
go saveArticleSummary(cfg.DiscoverSvcURL, summaryURL, collected)
|
|
}
|
|
return c.SendString(result.String())
|
|
}
|
|
|
|
return c.SendStream(resp.Body)
|
|
})
|
|
|
|
port := cfg.ChatSvcPort
|
|
log.Printf("chat-svc listening on :%d", port)
|
|
log.Fatal(app.Listen(fmt.Sprintf(":%d", port)))
|
|
}
|
|
|
|
func init() {
|
|
if os.Getenv("PORT") == "" {
|
|
os.Setenv("PORT", "3005")
|
|
}
|
|
}
|
|
|
|
func saveArticleSummary(discoverSvcURL, articleURL string, events []string) {
|
|
if discoverSvcURL == "" || articleURL == "" || len(events) == 0 {
|
|
return
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"url": articleURL,
|
|
"events": events,
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
log.Printf("article-summary marshal error: %v", err)
|
|
return
|
|
}
|
|
|
|
url := strings.TrimSuffix(discoverSvcURL, "/") + "/api/v1/discover/article-summary"
|
|
|
|
maxRetries := 5
|
|
retryDelay := 2 * time.Second
|
|
|
|
for attempt := 1; attempt <= maxRetries; attempt++ {
|
|
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
|
if err != nil {
|
|
log.Printf("article-summary request error (attempt %d): %v", attempt, err)
|
|
continue
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 2 * time.Minute}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
log.Printf("article-summary save error (attempt %d): %v", attempt, err)
|
|
if attempt < maxRetries {
|
|
time.Sleep(retryDelay)
|
|
}
|
|
continue
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
log.Printf("article-summary saved: %s", articleURL[:min(60, len(articleURL))])
|
|
return
|
|
}
|
|
|
|
log.Printf("article-summary save failed (attempt %d): status %d", attempt, resp.StatusCode)
|
|
if attempt < maxRetries {
|
|
time.Sleep(retryDelay)
|
|
}
|
|
}
|
|
|
|
log.Printf("article-summary save failed after %d retries: %s", maxRetries, articleURL[:min(60, len(articleURL))])
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|