Honeycomb und Control-Patch

This commit is contained in:
2026-08-04 05:28:51 +02:00
parent 23339a8be3
commit 3af51bf888
21 changed files with 1222 additions and 79 deletions
+52 -10
View File
@@ -28,7 +28,11 @@ import (
"github.com/local/glpi-neural-brain/internal/research"
)
var ErrNoCandidate = errors.New("no enrichment candidate")
var (
ErrNoCandidate = errors.New("no enrichment candidate")
ErrLearningDisabled = errors.New("learning is disabled")
ErrThinkingDisabled = errors.New("thinking is disabled")
)
type EnrichOutcome struct {
Result string
@@ -63,9 +67,17 @@ type Engine struct {
enrichCreated uint64
enrichRejected uint64
enrichRequests chan string
runtimeMu sync.RWMutex
runtime RuntimeSettings
runtimePath string
}
func New(cfg config.Config, g *graph.Store, b *activity.Broker) *Engine {
if !cfg.RuntimeDefaultsConfigured {
cfg.LearningEnabled = true
cfg.ThinkingEnabled = true
cfg.DefaultView = "neural"
}
if cfg.EnrichBatchSize < 1 {
cfg.EnrichBatchSize = 1
}
@@ -96,13 +108,14 @@ func New(cfg config.Config, g *graph.Store, b *activity.Broker) *Engine {
RequireEmbeddingModel: cfg.OllamaRequireEmbeddingModel,
}, cfg.ChatModel, cfg.EmbeddingModel)
persistence := persist.New(g, b, cfg.PersistInterval)
e := &Engine{Cfg: cfg, Graph: g, Broker: b, Ollama: pool, Persistence: persistence, Scanner: &ingest.KnowledgeScanner{Graph: g, ProductionDirs: cfg.KnowledgeDirs, StagingDirs: cfg.StagingDirs}, enrichRequests: make(chan string, 1)}
e := &Engine{Cfg: cfg, Graph: g, Broker: b, Ollama: pool, Persistence: persistence, Scanner: &ingest.KnowledgeScanner{Graph: g, ProductionDirs: cfg.KnowledgeDirs, StagingDirs: cfg.StagingDirs}, enrichRequests: make(chan string, 1), runtimePath: filepath.Join(cfg.DataDir, "runtime-settings.json")}
e.loadRuntimeSettings()
if cfg.SearXNGURL != "" {
e.Research = research.New(cfg.SearXNGURL)
}
if cfg.GLPIKBEnabled {
client := glpi.New(cfg.GLPIURL, cfg.GLPIAPIVersion, cfg.GLPIClientID, cfg.GLPIClientSecret, cfg.GLPIUsername, cfg.GLPIPassword, cfg.GLPITimeout)
e.GLPIKB = ingest.NewGLPIKBSyncer(ingest.GLPIKBConfig{Enabled: true, Path: cfg.GLPIKBPath, Filter: cfg.GLPIKBFilter, Limit: cfg.GLPIKBLimit, SyncInterval: cfg.GLPIKBSyncInterval, Source: cfg.GLPIKBSource, CachePath: filepath.Join(cfg.DataDir, "glpi-kb-cache.json")}, client, g, b, persistence)
e.GLPIKB = ingest.NewGLPIKBSyncer(ingest.GLPIKBConfig{Enabled: true, Path: cfg.GLPIKBPath, Filter: cfg.GLPIKBFilter, Limit: cfg.GLPIKBLimit, SyncInterval: cfg.GLPIKBSyncInterval, Source: cfg.GLPIKBSource, CachePath: filepath.Join(cfg.DataDir, "glpi-kb-cache.json"), ShouldSync: e.LearningEnabled}, client, g, b, persistence)
}
return e
}
@@ -113,8 +126,10 @@ func (e *Engine) Start(ctx context.Context) {
e.GLPIKB.Start(ctx)
}
go func() {
if err := e.Scan(ctx); err != nil {
slog.Error("initial brain scan failed", "error", err)
if e.LearningEnabled() {
if err := e.Scan(ctx); err != nil && !errors.Is(err, ErrLearningDisabled) {
slog.Error("initial brain scan failed", "error", err)
}
}
ticker := time.NewTicker(e.Cfg.ScanInterval)
defer ticker.Stop()
@@ -123,7 +138,10 @@ func (e *Engine) Start(ctx context.Context) {
case <-ctx.Done():
return
case <-ticker.C:
if err := e.Scan(ctx); err != nil {
if !e.LearningEnabled() {
continue
}
if err := e.Scan(ctx); err != nil && !errors.Is(err, ErrLearningDisabled) {
slog.Error("brain scan failed", "error", err)
}
}
@@ -169,6 +187,13 @@ func (e *Engine) enrichmentWorker(ctx context.Context) {
}
func (e *Engine) RequestEnrich(trigger string) bool {
if !e.ThinkingEnabled() {
e.stateMu.Lock()
e.enrichResult = "disabled"
e.enrichError = ErrThinkingDisabled.Error()
e.stateMu.Unlock()
return false
}
if strings.TrimSpace(trigger) == "" {
trigger = "manual"
}
@@ -213,6 +238,10 @@ func (e *Engine) runEnrichmentCycle(ctx context.Context, trigger string) {
result := "completed"
var cycleErr error
for step := 0; step < e.Cfg.EnrichBatchSize; step++ {
if !e.ThinkingEnabled() {
result = "disabled"
break
}
outcome, err := e.enrichOne(ctx, trigger)
if err != nil {
cycleErr = err
@@ -306,6 +335,9 @@ func (e *Engine) idle(ctx context.Context) {
}
}
func (e *Engine) Scan(ctx context.Context) error {
if !e.LearningEnabled() {
return ErrLearningDisabled
}
e.mu.Lock()
defer e.mu.Unlock()
beforeVersion := e.Graph.Version()
@@ -313,7 +345,7 @@ func (e *Engine) Scan(ctx context.Context) error {
if err != nil {
return err
}
pendingEmbeddings := len(e.Graph.NodesForEmbedding())
pendingEmbeddings := len(e.Graph.NodesForEmbeddingFiltered(e.learningCategories()))
if e.Graph.Version() != beforeVersion || pendingEmbeddings > 0 {
e.Broker.Publish(model.Activity{Type: "scan.started", Source: "brain", Phase: "ingest", Message: "Neue oder geänderte Wissenselemente werden verarbeitet", Strength: .45, Metadata: map[string]any{"pending_embeddings": pendingEmbeddings}})
}
@@ -346,7 +378,7 @@ func (e *Engine) Scan(ctx context.Context) error {
return nil
}
func (e *Engine) ensureEmbeddings(ctx context.Context) error {
pending := e.Graph.NodesForEmbedding()
pending := e.Graph.NodesForEmbeddingFiltered(e.learningCategories())
if len(pending) == 0 {
return nil
}
@@ -377,7 +409,7 @@ func (e *Engine) ensureEmbeddings(ctx context.Context) error {
return nil
}
func (e *Engine) ensureFallbackEmbeddings() {
for _, n := range e.Graph.NodesForEmbedding() {
for _, n := range e.Graph.NodesForEmbeddingFiltered(e.learningCategories()) {
e.Graph.SetVector(n.ID, hashEmbedding(embeddingText(n), 256))
}
}
@@ -490,11 +522,17 @@ func (e *Engine) fallbackAnswer(q string, hits []model.Hit) string {
}
func (e *Engine) EnrichOne(ctx context.Context) error {
if !e.ThinkingEnabled() {
return ErrThinkingDisabled
}
_, err := e.enrichOne(ctx, "direct")
return err
}
func (e *Engine) enrichOne(ctx context.Context, trigger string) (EnrichOutcome, error) {
if !e.ThinkingEnabled() {
return EnrichOutcome{Result: "disabled"}, ErrThinkingDisabled
}
e.mu.Lock()
defer e.mu.Unlock()
@@ -509,7 +547,7 @@ func (e *Engine) enrichOne(ctx context.Context, trigger string) (EnrichOutcome,
e.setOllamaOK(true)
}
a, b, sim, ok, comparisons := e.Graph.NextPair(e.Cfg.SimilarityThreshold, e.Cfg.EnrichAnchors)
a, b, sim, ok, comparisons := e.Graph.NextPairFiltered(e.Cfg.SimilarityThreshold, e.Cfg.EnrichAnchors, e.thinkingCategories())
if !ok {
e.stateMu.Lock()
e.lastAttempt = time.Now().UTC()
@@ -631,6 +669,7 @@ func (e *Engine) Status() map[string]any {
"enrich_batch_size": e.Cfg.EnrichBatchSize, "enrich_anchors": e.Cfg.EnrichAnchors,
"research_enabled": e.Cfg.ResearchEnabled, "chat_model": e.Cfg.ChatModel, "embedding_model": e.Cfg.EmbeddingModel,
"ollama_pool": e.Ollama.PoolStatus(), "persistence": e.Persistence.Status(),
"runtime_settings": e.RuntimeSettings(),
}
if e.GLPIKB != nil {
status["glpi_kb"] = e.GLPIKB.Status()
@@ -646,6 +685,9 @@ func (e *Engine) Flush(ctx context.Context) error {
}
func (e *Engine) SyncGLPIKB(ctx context.Context) error {
if !e.LearningEnabled() {
return ErrLearningDisabled
}
if e.GLPIKB == nil {
return fmt.Errorf("GLPI knowledge-base integration is disabled")
}