Files
gooseek/backend/internal/labs/generator.go
home 06fe57c765 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
2026-02-27 04:15:32 +03:00

760 lines
18 KiB
Go

package labs
import (
"context"
"encoding/json"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/gooseek/backend/internal/llm"
"github.com/google/uuid"
)
type Generator struct {
llm llm.Client
}
func NewGenerator(llmClient llm.Client) *Generator {
return &Generator{llm: llmClient}
}
type GenerateOptions struct {
Query string
Data interface{}
PreferredTypes []VisualizationType
Theme string
Locale string
MaxVisualizations int
}
func (g *Generator) GenerateReport(ctx context.Context, opts GenerateOptions) (*Report, error) {
analysisPrompt := fmt.Sprintf(`Analyze this data and query to determine the best visualizations.
Query: %s
Data: %v
Determine:
1. What visualizations would best represent this data?
2. How should the data be structured for each visualization?
3. What insights can be highlighted?
Respond in JSON format:
{
"title": "Report title",
"sections": [
{
"title": "Section title",
"visualizations": [
{
"type": "chart_type",
"title": "Viz title",
"dataMapping": { "how to map the data" },
"insight": "Key insight"
}
]
}
]
}
Available visualization types: bar_chart, line_chart, pie_chart, donut_chart, table, stat_cards, kpi, comparison, timeline, progress, heatmap, code_block, markdown, collapsible, tabs, accordion`, opts.Query, opts.Data)
result, err := g.llm.GenerateText(ctx, llm.StreamRequest{
Messages: []llm.Message{{Role: "user", Content: analysisPrompt}},
})
if err != nil {
return nil, err
}
var analysis struct {
Title string `json:"title"`
Sections []struct {
Title string `json:"title"`
Visualizations []struct {
Type string `json:"type"`
Title string `json:"title"`
DataMapping map[string]interface{} `json:"dataMapping"`
Insight string `json:"insight"`
} `json:"visualizations"`
} `json:"sections"`
}
jsonStr := extractJSON(result)
if err := json.Unmarshal([]byte(jsonStr), &analysis); err != nil {
return g.createDefaultReport(opts)
}
report := &Report{
ID: uuid.New().String(),
Title: analysis.Title,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Theme: opts.Theme,
Sections: make([]ReportSection, 0),
}
for _, sec := range analysis.Sections {
section := ReportSection{
ID: uuid.New().String(),
Title: sec.Title,
Visualizations: make([]Visualization, 0),
}
for _, viz := range sec.Visualizations {
visualization := g.createVisualization(VisualizationType(viz.Type), viz.Title, opts.Data, viz.DataMapping)
if visualization != nil {
section.Visualizations = append(section.Visualizations, *visualization)
}
}
if len(section.Visualizations) > 0 {
report.Sections = append(report.Sections, section)
}
}
return report, nil
}
func (g *Generator) createDefaultReport(opts GenerateOptions) (*Report, error) {
report := &Report{
ID: uuid.New().String(),
Title: "Анализ данных",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Sections: []ReportSection{
{
ID: uuid.New().String(),
Title: "Обзор",
Visualizations: []Visualization{
g.CreateMarkdown("", formatDataAsMarkdown(opts.Data)),
},
},
},
}
return report, nil
}
func (g *Generator) createVisualization(vizType VisualizationType, title string, data interface{}, mapping map[string]interface{}) *Visualization {
switch vizType {
case VizBarChart, VizLineChart, VizAreaChart:
return g.createChartVisualization(vizType, title, data, mapping)
case VizPieChart, VizDonutChart:
return g.createPieVisualization(vizType, title, data, mapping)
case VizTable:
return g.createTableVisualization(title, data, mapping)
case VizStatCards:
return g.createStatCardsVisualization(title, data, mapping)
case VizKPI:
return g.createKPIVisualization(title, data, mapping)
case VizTimeline:
return g.createTimelineVisualization(title, data, mapping)
case VizComparison:
return g.createComparisonVisualization(title, data, mapping)
case VizProgress:
return g.createProgressVisualization(title, data, mapping)
case VizMarkdown:
content := extractStringFromData(data, mapping, "content")
viz := g.CreateMarkdown(title, content)
return &viz
default:
viz := g.CreateMarkdown(title, formatDataAsMarkdown(data))
return &viz
}
}
func (g *Generator) createChartVisualization(vizType VisualizationType, title string, data interface{}, mapping map[string]interface{}) *Visualization {
chartData := &ChartData{
Labels: make([]string, 0),
Datasets: make([]ChartDataset, 0),
}
if dataMap, ok := data.(map[string]interface{}); ok {
labels := make([]string, 0)
values := make([]float64, 0)
for k, v := range dataMap {
labels = append(labels, k)
values = append(values, toFloat64(v))
}
chartData.Labels = labels
chartData.Datasets = append(chartData.Datasets, ChartDataset{
Label: title,
Data: values,
})
}
if dataSlice, ok := data.([]interface{}); ok {
for _, item := range dataSlice {
if itemMap, ok := item.(map[string]interface{}); ok {
if label, ok := itemMap["label"].(string); ok {
chartData.Labels = append(chartData.Labels, label)
}
if value, ok := itemMap["value"]; ok {
if len(chartData.Datasets) == 0 {
chartData.Datasets = append(chartData.Datasets, ChartDataset{Label: title, Data: []float64{}})
}
chartData.Datasets[0].Data = append(chartData.Datasets[0].Data, toFloat64(value))
}
}
}
}
return &Visualization{
ID: uuid.New().String(),
Type: vizType,
Title: title,
Data: chartData,
Config: VisualizationConfig{
ShowLegend: true,
ShowTooltip: true,
ShowGrid: true,
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) createPieVisualization(vizType VisualizationType, title string, data interface{}, mapping map[string]interface{}) *Visualization {
chartData := &ChartData{
Labels: make([]string, 0),
Datasets: make([]ChartDataset, 0),
}
dataset := ChartDataset{Label: title, Data: []float64{}}
if dataMap, ok := data.(map[string]interface{}); ok {
for k, v := range dataMap {
chartData.Labels = append(chartData.Labels, k)
dataset.Data = append(dataset.Data, toFloat64(v))
}
}
chartData.Datasets = append(chartData.Datasets, dataset)
return &Visualization{
ID: uuid.New().String(),
Type: vizType,
Title: title,
Data: chartData,
Config: VisualizationConfig{
ShowLegend: true,
ShowTooltip: true,
ShowValues: true,
Animated: true,
},
Style: VisualizationStyle{
Height: "300px",
},
Responsive: true,
}
}
func (g *Generator) createTableVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
tableData := &TableData{
Columns: make([]TableColumn, 0),
Rows: make([]TableRow, 0),
}
if dataSlice, ok := data.([]interface{}); ok && len(dataSlice) > 0 {
if firstRow, ok := dataSlice[0].(map[string]interface{}); ok {
for key := range firstRow {
tableData.Columns = append(tableData.Columns, TableColumn{
Key: key,
Label: formatColumnLabel(key),
Sortable: true,
})
}
}
for _, item := range dataSlice {
if rowMap, ok := item.(map[string]interface{}); ok {
tableData.Rows = append(tableData.Rows, TableRow(rowMap))
}
}
}
if dataMap, ok := data.(map[string]interface{}); ok {
tableData.Columns = []TableColumn{
{Key: "key", Label: "Параметр", Sortable: true},
{Key: "value", Label: "Значение", Sortable: true},
}
for k, v := range dataMap {
tableData.Rows = append(tableData.Rows, TableRow{
"key": k,
"value": v,
})
}
}
tableData.Summary = &TableSummary{
TotalRows: len(tableData.Rows),
}
return &Visualization{
ID: uuid.New().String(),
Type: VizTable,
Title: title,
Data: tableData,
Config: VisualizationConfig{
Sortable: true,
Searchable: true,
Paginated: len(tableData.Rows) > 10,
PageSize: 10,
},
Responsive: true,
}
}
func (g *Generator) createStatCardsVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
cardsData := &StatCardsData{
Cards: make([]StatCard, 0),
}
colors := []string{"#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6", "#EC4899"}
colorIdx := 0
if dataMap, ok := data.(map[string]interface{}); ok {
for k, v := range dataMap {
card := StatCard{
ID: uuid.New().String(),
Title: formatColumnLabel(k),
Value: v,
Color: colors[colorIdx%len(colors)],
}
cardsData.Cards = append(cardsData.Cards, card)
colorIdx++
}
}
return &Visualization{
ID: uuid.New().String(),
Type: VizStatCards,
Title: title,
Data: cardsData,
Config: VisualizationConfig{
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) createKPIVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
kpiData := &KPIData{
Value: data,
}
if dataMap, ok := data.(map[string]interface{}); ok {
if v, ok := dataMap["value"]; ok {
kpiData.Value = v
}
if v, ok := dataMap["change"].(float64); ok {
kpiData.Change = v
if v >= 0 {
kpiData.ChangeType = "increase"
} else {
kpiData.ChangeType = "decrease"
}
}
if v, ok := dataMap["target"]; ok {
kpiData.Target = v
}
if v, ok := dataMap["unit"].(string); ok {
kpiData.Unit = v
}
}
return &Visualization{
ID: uuid.New().String(),
Type: VizKPI,
Title: title,
Data: kpiData,
Config: VisualizationConfig{
Animated: true,
ShowValues: true,
},
Style: VisualizationStyle{
MinHeight: "150px",
},
Responsive: true,
}
}
func (g *Generator) createTimelineVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
timelineData := &TimelineData{
Events: make([]TimelineEvent, 0),
}
if dataSlice, ok := data.([]interface{}); ok {
for _, item := range dataSlice {
if itemMap, ok := item.(map[string]interface{}); ok {
event := TimelineEvent{
ID: uuid.New().String(),
}
if v, ok := itemMap["date"].(string); ok {
event.Date, _ = time.Parse(time.RFC3339, v)
}
if v, ok := itemMap["title"].(string); ok {
event.Title = v
}
if v, ok := itemMap["description"].(string); ok {
event.Description = v
}
timelineData.Events = append(timelineData.Events, event)
}
}
}
sort.Slice(timelineData.Events, func(i, j int) bool {
return timelineData.Events[i].Date.Before(timelineData.Events[j].Date)
})
return &Visualization{
ID: uuid.New().String(),
Type: VizTimeline,
Title: title,
Data: timelineData,
Config: VisualizationConfig{
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) createComparisonVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
compData := &ComparisonData{
Items: make([]ComparisonItem, 0),
Categories: make([]string, 0),
}
if dataSlice, ok := data.([]interface{}); ok && len(dataSlice) > 0 {
if firstItem, ok := dataSlice[0].(map[string]interface{}); ok {
for k := range firstItem {
if k != "name" && k != "id" && k != "image" {
compData.Categories = append(compData.Categories, k)
}
}
}
for _, item := range dataSlice {
if itemMap, ok := item.(map[string]interface{}); ok {
compItem := ComparisonItem{
ID: uuid.New().String(),
Values: make(map[string]interface{}),
}
if v, ok := itemMap["name"].(string); ok {
compItem.Name = v
}
if v, ok := itemMap["image"].(string); ok {
compItem.Image = v
}
for _, cat := range compData.Categories {
if v, ok := itemMap[cat]; ok {
compItem.Values[cat] = v
}
}
compData.Items = append(compData.Items, compItem)
}
}
}
return &Visualization{
ID: uuid.New().String(),
Type: VizComparison,
Title: title,
Data: compData,
Config: VisualizationConfig{
ShowLabels: true,
},
Responsive: true,
}
}
func (g *Generator) createProgressVisualization(title string, data interface{}, mapping map[string]interface{}) *Visualization {
progressData := &ProgressData{
Current: 0,
Total: 100,
ShowValue: true,
Animated: true,
}
if dataMap, ok := data.(map[string]interface{}); ok {
if v, ok := dataMap["current"]; ok {
progressData.Current = toFloat64(v)
}
if v, ok := dataMap["total"]; ok {
progressData.Total = toFloat64(v)
}
if v, ok := dataMap["label"].(string); ok {
progressData.Label = v
}
}
if v, ok := data.(float64); ok {
progressData.Current = v
}
return &Visualization{
ID: uuid.New().String(),
Type: VizProgress,
Title: title,
Data: progressData,
Config: VisualizationConfig{
Animated: true,
ShowValues: true,
},
Responsive: true,
}
}
func (g *Generator) CreateBarChart(title string, labels []string, values []float64) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizBarChart,
Title: title,
Data: &ChartData{
Labels: labels,
Datasets: []ChartDataset{
{Label: title, Data: values},
},
},
Config: VisualizationConfig{
ShowLegend: true,
ShowTooltip: true,
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) CreateLineChart(title string, labels []string, datasets []ChartDataset) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizLineChart,
Title: title,
Data: &ChartData{
Labels: labels,
Datasets: datasets,
},
Config: VisualizationConfig{
ShowLegend: true,
ShowTooltip: true,
ShowGrid: true,
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) CreatePieChart(title string, labels []string, values []float64) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizPieChart,
Title: title,
Data: &ChartData{
Labels: labels,
Datasets: []ChartDataset{
{Label: title, Data: values},
},
},
Config: VisualizationConfig{
ShowLegend: true,
ShowTooltip: true,
ShowValues: true,
},
Responsive: true,
}
}
func (g *Generator) CreateTable(title string, columns []TableColumn, rows []TableRow) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizTable,
Title: title,
Data: &TableData{
Columns: columns,
Rows: rows,
Summary: &TableSummary{TotalRows: len(rows)},
},
Config: VisualizationConfig{
Sortable: true,
Searchable: true,
Paginated: len(rows) > 10,
PageSize: 10,
},
Responsive: true,
}
}
func (g *Generator) CreateStatCards(title string, cards []StatCard) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizStatCards,
Title: title,
Data: &StatCardsData{Cards: cards},
Config: VisualizationConfig{
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) CreateKPI(title string, value interface{}, change float64, unit string) Visualization {
changeType := "neutral"
if change > 0 {
changeType = "increase"
} else if change < 0 {
changeType = "decrease"
}
return Visualization{
ID: uuid.New().String(),
Type: VizKPI,
Title: title,
Data: &KPIData{
Value: value,
Change: change,
ChangeType: changeType,
Unit: unit,
},
Config: VisualizationConfig{
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) CreateMarkdown(title string, content string) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizMarkdown,
Title: title,
Data: &MarkdownData{Content: content},
Responsive: true,
}
}
func (g *Generator) CreateCodeBlock(title, code, language string) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizCodeBlock,
Title: title,
Data: &CodeBlockData{
Code: code,
Language: language,
ShowLineNum: true,
Copyable: true,
},
Responsive: true,
}
}
func (g *Generator) CreateTabs(title string, tabs []TabItem) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizTabs,
Title: title,
Data: &TabsData{Tabs: tabs},
Responsive: true,
}
}
func (g *Generator) CreateAccordion(title string, items []AccordionItem) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizAccordion,
Title: title,
Data: &AccordionData{Items: items},
Config: VisualizationConfig{
Animated: true,
},
Responsive: true,
}
}
func (g *Generator) CreateHeatmap(title string, xLabels, yLabels []string, values [][]float64) Visualization {
return Visualization{
ID: uuid.New().String(),
Type: VizHeatmap,
Title: title,
Data: &HeatmapData{
XLabels: xLabels,
YLabels: yLabels,
Values: values,
},
Config: VisualizationConfig{
ShowTooltip: true,
ShowLabels: true,
},
Responsive: true,
}
}
func extractJSON(text string) string {
re := regexp.MustCompile(`(?s)\{.*\}`)
match := re.FindString(text)
if match != "" {
return match
}
return "{}"
}
func toFloat64(v interface{}) float64 {
switch val := v.(type) {
case float64:
return val
case float32:
return float64(val)
case int:
return float64(val)
case int64:
return float64(val)
case string:
f, _ := strconv.ParseFloat(val, 64)
return f
default:
return 0
}
}
func formatColumnLabel(key string) string {
key = strings.ReplaceAll(key, "_", " ")
key = strings.ReplaceAll(key, "-", " ")
words := strings.Fields(key)
for i, word := range words {
if len(word) > 0 {
words[i] = strings.ToUpper(string(word[0])) + strings.ToLower(word[1:])
}
}
return strings.Join(words, " ")
}
func extractStringFromData(data interface{}, mapping map[string]interface{}, key string) string {
if dataMap, ok := data.(map[string]interface{}); ok {
if v, ok := dataMap[key].(string); ok {
return v
}
}
return fmt.Sprintf("%v", data)
}
func formatDataAsMarkdown(data interface{}) string {
jsonBytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Sprintf("%v", data)
}
return "```json\n" + string(jsonBytes) + "\n```"
}