All checks were successful
release-tag / release-image (push) Successful in 2m32s
135 lines
6.4 KiB
Go
135 lines
6.4 KiB
Go
package engine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/local/glpi-neural-brain/internal/graph"
|
|
"github.com/local/glpi-neural-brain/internal/model"
|
|
)
|
|
|
|
func (e *Engine) effectiveVectorLayoutMode() string {
|
|
if e.Cfg.VectorGraphLayout {
|
|
return "full"
|
|
}
|
|
if e.Cfg.VectorGraphRelaxLayout {
|
|
return "relax"
|
|
}
|
|
return "off"
|
|
}
|
|
|
|
func (e *Engine) vectorMaintenanceDue(now time.Time) (reevaluate bool, layout bool) {
|
|
if !e.Cfg.VectorGraphEnabled {
|
|
return false, false
|
|
}
|
|
e.stateMu.RLock()
|
|
lastGraph := e.lastVectorGraph
|
|
lastLayout := e.lastVectorLayout
|
|
maintenanceStartedAt := e.vectorMaintenanceStartedAt
|
|
e.stateMu.RUnlock()
|
|
reevaluate = !e.Graph.HasEdgesByOrigin(graph.VectorMathOrigin) || lastGraph.IsZero() || now.Sub(lastGraph) >= e.Cfg.VectorGraphReevaluateInterval
|
|
// Full layout is intentionally coupled to semantic reevaluation. Relax mode
|
|
// may run on its own slower/faster cadence without enabling the hard layout.
|
|
if !e.Cfg.VectorGraphLayout && e.Cfg.VectorGraphRelaxLayout {
|
|
if lastLayout.IsZero() {
|
|
// Do not fake last_vector_layout merely to delay the first relaxation.
|
|
// Keep telemetry truthful and use the process-start marker as a separate
|
|
// not-before timestamp.
|
|
layout = maintenanceStartedAt.IsZero() || now.Sub(maintenanceStartedAt) >= e.Cfg.VectorGraphLayoutRelaxInterval
|
|
} else {
|
|
layout = now.Sub(lastLayout) >= e.Cfg.VectorGraphLayoutRelaxInterval
|
|
}
|
|
}
|
|
return reevaluate, layout
|
|
}
|
|
|
|
func (e *Engine) vectorMaintenanceLoop(ctx context.Context) {
|
|
// Give bootstrap-owned scans a short head start. If Learning is disabled the
|
|
// loop still runs and can maintain an already persisted vector layer.
|
|
initialDelay := 5 * time.Second
|
|
if e.SpeedModeEnabled() {
|
|
initialDelay = 0
|
|
}
|
|
initial := time.NewTimer(initialDelay)
|
|
defer initial.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-initial.C:
|
|
}
|
|
if err := e.runVectorMaintenance(ctx, "startup-maintenance"); err != nil {
|
|
slog.Warn("vector maintenance startup run failed", "error", err)
|
|
}
|
|
ticker := time.NewTicker(time.Minute)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
if err := e.runVectorMaintenance(ctx, "scheduled-maintenance"); err != nil {
|
|
slog.Warn("vector maintenance run failed", "error", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *Engine) runVectorMaintenance(ctx context.Context, trigger string) error {
|
|
reevaluateDue, layoutDue := e.vectorMaintenanceDue(time.Now().UTC())
|
|
if !reevaluateDue && !layoutDue {
|
|
return nil
|
|
}
|
|
// Serialize with Learning Scan. Both operations may replace vector-math
|
|
// edges/positions and must never race against each other.
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
reevaluateDue, layoutDue = e.vectorMaintenanceDue(time.Now().UTC())
|
|
if !reevaluateDue && !layoutDue {
|
|
return nil
|
|
}
|
|
if e.Graph.CountVectorsByDimension(256) != 0 {
|
|
if e.Broker != nil {
|
|
e.Broker.Publish(model.Activity{Type: "vector.graph.maintenance.skipped", Source: "brain", Phase: "semantic-linking", Message: "Vector-Maintenance wartet auf echte Embeddings; 256D-Fallback-Vektoren werden nicht mit dem Produktionsraum gemischt", Strength: .28, Metadata: map[string]any{"trigger": trigger, "reason": "fallback_vectors_present"}})
|
|
}
|
|
return nil
|
|
}
|
|
entries := e.Graph.VectorSemanticEntries(e.effectiveLearningFilter())
|
|
if len(entries) < 2 {
|
|
return nil
|
|
}
|
|
started := time.Now().UTC()
|
|
runID := fmt.Sprintf("vector-maintenance-%d", started.UnixNano())
|
|
if e.Broker != nil {
|
|
e.Broker.Publish(model.Activity{Type: "vector.graph.maintenance.started", Source: "brain", Phase: "semantic-linking", Message: "Periodische mathematische Nähepflege wurde gestartet", Strength: .4, Metadata: map[string]any{
|
|
"run_id": runID, "trigger": trigger, "reevaluate_due": reevaluateDue, "layout_due": layoutDue, "effective_layout_mode": e.effectiveVectorLayoutMode(), "learning_enabled": e.LearningEnabled(),
|
|
}})
|
|
}
|
|
execution, err := e.rebuildVectorSemanticLayer(ctx, runID, e.effectiveLearningFilter())
|
|
if err != nil {
|
|
if e.Broker != nil {
|
|
e.Broker.Publish(model.Activity{Type: "vector.graph.maintenance.failed", Source: "brain", Phase: "semantic-linking", Message: "Periodische mathematische Nähepflege ist fehlgeschlagen", Strength: .32, Metadata: map[string]any{"run_id": runID, "trigger": trigger, "error": err.Error(), "agent_required": e.Cfg.VectorGraphAgentRequired}})
|
|
}
|
|
return err
|
|
}
|
|
now := time.Now().UTC()
|
|
e.stateMu.Lock()
|
|
e.lastVectorGraph = now
|
|
e.stateMu.Unlock()
|
|
stats := execution.Stats
|
|
if e.Broker != nil {
|
|
meta := withRunMutations(map[string]any{
|
|
"run_id": runID, "trigger": trigger, "algorithm": "mutual-knn-local-scaling-v1", "orphan_algorithm": "orphan-knn-local-scaling-v1", "no_model_call": true,
|
|
"indexed": stats.Indexed, "links": stats.Links, "reciprocal_links": stats.ReciprocalLinks, "candidate_pairs": stats.CandidatePairs, "exact_comparisons": stats.ExactComparisons,
|
|
"orphan_pass_enabled": e.Cfg.VectorGraphOrphanPass, "orphan_focus": stats.OrphanFocus, "orphan_links": stats.OrphanLinks, "orphan_exact_comparisons": stats.OrphanStats.ExactComparisons, "orphan_candidate_pairs": stats.OrphanStats.CandidatePairs,
|
|
"position_updates": stats.PositionUpdates, "layout_mode": execution.LayoutMode, "layout_applied": execution.LayoutDue && stats.PositionUpdates > 0, "periodic_refresh": reevaluateDue, "layout_refresh": layoutDue,
|
|
"reevaluate_interval": e.Cfg.VectorGraphReevaluateInterval.String(), "layout_relax_interval": e.Cfg.VectorGraphLayoutRelaxInterval.String(), "learning_enabled": e.LearningEnabled(),
|
|
"agent_offloaded": execution.Offloaded, "agent_id": execution.AgentID, "agent_compute_ms": execution.ComputeMS, "agent_fallback_reason": execution.FallbackReason, "speed_mode": e.SpeedModeEnabled(), "cpu_workers": e.vectorPrimaryConfig(execution.LayoutDue).Workers, "duration_ms": time.Since(started).Milliseconds(),
|
|
}, execution.Mutations)
|
|
e.Broker.Publish(model.Activity{Type: "vector.graph.rebuilt", Source: "brain", Phase: "semantic-linking", Message: fmt.Sprintf("Periodische Vektorpflege: %d Primär- + %d Orphan-Kanten aus %d Embeddings", stats.Links, stats.OrphanLinks, stats.Indexed), Strength: .58, Metadata: meta})
|
|
e.Broker.Publish(model.Activity{Type: "vector.graph.maintenance.completed", Source: "brain", Phase: "semantic-linking", Message: fmt.Sprintf("Mathematische Nähepflege abgeschlossen · Layoutmodus %s · %d Positionsupdates", execution.LayoutMode, stats.PositionUpdates), Strength: .5, Metadata: meta})
|
|
}
|
|
return nil
|
|
}
|