feat: Go backend, enhanced search, new widgets, Docker deploy
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
This commit is contained in:
235
backend/cmd/chat-svc/main.go
Normal file
235
backend/cmd/chat-svc/main.go
Normal file
@@ -0,0 +1,235 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user