feat: enrichers, panel de ingest y menu contextual en el grafo
- Añade enricher.go + directorio enrichers/ para enriquecer entidades con fuentes externas. - Nuevos componentes frontend: IngestPanel (panel de ingesta de datos) y NodeContextMenu (menu contextual sobre nodos del grafo). - Retira SearchBar y lib/utils.ts; la busqueda se integra dentro de los paneles existentes. - Ajusta tipos (types.go, types.ts, wailsjs/go) y theming (postcss + app.css + Mantine). - Actualiza app.go y wails.json para exponer las nuevas capacidades. - Añade directorio projects/ con estado inicial. - Rebuild del frontend (dist actualizado).
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
@@ -580,20 +581,210 @@ func (a *App) GetRelationPresets() []string {
|
||||
return relationPresets
|
||||
}
|
||||
|
||||
// --- Enrichers ---
|
||||
|
||||
func (a *App) GetEnrichers() []EnricherDef {
|
||||
log.Printf("[GetEnrichers] returning %d enrichers", len(enricherRegistry))
|
||||
return enricherRegistry
|
||||
}
|
||||
|
||||
func (a *App) GetEnrichersForEntity(entityID string) ([]EnricherDef, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
return nil, fmt.Errorf("no project selected")
|
||||
}
|
||||
entity, err := a.db.GetEntity(entityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entity == nil {
|
||||
return nil, fmt.Errorf("entity %s not found", entityID)
|
||||
}
|
||||
result := enrichersForType(entity.TypeRef)
|
||||
log.Printf("[GetEnrichersForEntity] entityID=%s typeRef=%s -> %d enrichers", entityID, entity.TypeRef, len(result))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *App) RunEnricher(enricherID, entityID string) (GraphData, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return GraphData{}, fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
log.Printf("[RunEnricher] enricherID=%s entityID=%s", enricherID, entityID)
|
||||
|
||||
enricher := findEnricher(enricherID)
|
||||
if enricher == nil {
|
||||
return GraphData{}, fmt.Errorf("enricher %s not found", enricherID)
|
||||
}
|
||||
|
||||
entity, err := a.db.GetEntity(entityID)
|
||||
if err != nil {
|
||||
return GraphData{}, err
|
||||
}
|
||||
if entity == nil {
|
||||
return GraphData{}, fmt.Errorf("entity %s not found", entityID)
|
||||
}
|
||||
|
||||
// Serialize entity to JSON for the Python script
|
||||
entityJSON, err := json.Marshal(map[string]any{
|
||||
"id": entity.ID,
|
||||
"name": entity.Name,
|
||||
"type_ref": entity.TypeRef,
|
||||
"metadata": entity.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return GraphData{}, fmt.Errorf("marshaling entity: %w", err)
|
||||
}
|
||||
|
||||
// Run enricher
|
||||
enrichersDir := filepath.Join(filepath.Dir(os.Args[0]), "enrichers")
|
||||
// Fallback: try relative to working directory
|
||||
if _, err := os.Stat(enrichersDir); err != nil {
|
||||
enrichersDir = "enrichers"
|
||||
}
|
||||
// Fallback: try relative to project dir
|
||||
if _, err := os.Stat(filepath.Join(enrichersDir, enricher.Script)); err != nil {
|
||||
// Try from the app source directory
|
||||
if exePath, err2 := os.Executable(); err2 == nil {
|
||||
enrichersDir = filepath.Join(filepath.Dir(exePath), "enrichers")
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[RunEnricher] executing %s in %s", enricher.Script, enrichersDir)
|
||||
result, err := runEnricherScript(a.registryRoot, enrichersDir, enricher.Script, entityJSON)
|
||||
if err != nil {
|
||||
log.Printf("[RunEnricher] ERROR: %v", err)
|
||||
return GraphData{}, err
|
||||
}
|
||||
|
||||
log.Printf("[RunEnricher] result: %d entities, %d relations", len(result.Entities), len(result.Relations))
|
||||
|
||||
// Insert results into operations.db
|
||||
if err := a.insertEnricherResults(result, entityID); err != nil {
|
||||
log.Printf("[RunEnricher] ERROR inserting results: %v", err)
|
||||
return GraphData{}, err
|
||||
}
|
||||
|
||||
// Return full graph
|
||||
data, err := buildGraphData(a.db)
|
||||
if err != nil {
|
||||
return GraphData{}, err
|
||||
}
|
||||
log.Printf("[RunEnricher] OK: graph now has %d nodes, %d edges", len(data.Nodes), len(data.Edges))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// --- Ingest ---
|
||||
|
||||
func (a *App) IngestURL(url string) (string, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return "", fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
return "", fmt.Errorf("URL cannot be empty")
|
||||
}
|
||||
|
||||
log.Printf("[IngestURL] url=%s", url)
|
||||
|
||||
id := makeEntityID(url, "url")
|
||||
now := time.Now()
|
||||
e := &ops.Entity{
|
||||
ID: id,
|
||||
Name: url,
|
||||
TypeRef: "url",
|
||||
Status: ops.StatusActive,
|
||||
Description: "Ingested URL",
|
||||
Domain: "fuzzygraph",
|
||||
Tags: []string{"ingested"},
|
||||
Source: "fuzzygraph",
|
||||
Metadata: map[string]any{"url": url},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if a.registryDB != nil {
|
||||
if err := ops.InsertEntityWithSnapshot(a.db, a.registryDB, e); err != nil {
|
||||
if err2 := a.db.InsertEntity(e); err2 != nil {
|
||||
return "", err2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := a.db.InsertEntity(e); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[IngestURL] OK: %s", id)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (a *App) IngestFile(filePath string) (string, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return "", fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
filePath = strings.TrimSpace(filePath)
|
||||
if filePath == "" {
|
||||
return "", fmt.Errorf("file path cannot be empty")
|
||||
}
|
||||
|
||||
log.Printf("[IngestFile] path=%s", filePath)
|
||||
|
||||
name := filepath.Base(filePath)
|
||||
ext := strings.TrimPrefix(filepath.Ext(filePath), ".")
|
||||
id := makeEntityID(name, "document")
|
||||
now := time.Now()
|
||||
|
||||
e := &ops.Entity{
|
||||
ID: id,
|
||||
Name: name,
|
||||
TypeRef: "document",
|
||||
Status: ops.StatusActive,
|
||||
Description: fmt.Sprintf("Ingested document (%s)", ext),
|
||||
Domain: "fuzzygraph",
|
||||
Tags: []string{"ingested"},
|
||||
Source: "fuzzygraph",
|
||||
Metadata: map[string]any{"file_path": filePath, "format": ext},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if a.registryDB != nil {
|
||||
if err := ops.InsertEntityWithSnapshot(a.db, a.registryDB, e); err != nil {
|
||||
if err2 := a.db.InsertEntity(e); err2 != nil {
|
||||
return "", err2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := a.db.InsertEntity(e); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[IngestFile] OK: %s", id)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func makeEntityID(name, typeRef string) string {
|
||||
clean := strings.ToLower(strings.TrimSpace(name))
|
||||
clean = strings.ReplaceAll(clean, " ", "_")
|
||||
clean = strings.ReplaceAll(clean, "-", "_")
|
||||
|
||||
parts := strings.Split(typeRef, "_")
|
||||
shortType := typeRef
|
||||
if len(parts) >= 2 {
|
||||
shortType = parts[1]
|
||||
// Truncate long names (URLs etc.)
|
||||
if len(clean) > 60 {
|
||||
clean = clean[:60]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s_%s", clean, shortType)
|
||||
return fmt.Sprintf("%s_%s", clean, typeRef)
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
|
||||
Reference in New Issue
Block a user