217 lines
5.9 KiB
Go
217 lines
5.9 KiB
Go
package engine
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/local/glpi-neural-brain/internal/model"
|
|
)
|
|
|
|
const uncategorizedFilter = "__uncategorized__"
|
|
|
|
type RuntimeSettings struct {
|
|
LearningEnabled bool `json:"learning_enabled"`
|
|
ThinkingEnabled bool `json:"thinking_enabled"`
|
|
LearningCategories []string `json:"learning_categories"`
|
|
DisplayCategories []string `json:"display_categories"`
|
|
ThinkingCategories []string `json:"thinking_categories"`
|
|
ViewMode string `json:"view_mode"`
|
|
}
|
|
|
|
type CategoryInfo struct {
|
|
Name string `json:"name"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
func (e *Engine) defaultRuntimeSettings() RuntimeSettings {
|
|
return normalizeRuntimeSettings(RuntimeSettings{
|
|
LearningEnabled: e.Cfg.LearningEnabled,
|
|
ThinkingEnabled: e.Cfg.ThinkingEnabled,
|
|
LearningCategories: append([]string(nil), e.Cfg.LearningCategories...),
|
|
DisplayCategories: append([]string(nil), e.Cfg.DisplayCategories...),
|
|
ThinkingCategories: append([]string(nil), e.Cfg.ThinkingCategories...),
|
|
ViewMode: e.Cfg.DefaultView,
|
|
})
|
|
}
|
|
|
|
func normalizeRuntimeSettings(in RuntimeSettings) RuntimeSettings {
|
|
in.LearningCategories = normalizeCategories(in.LearningCategories)
|
|
in.DisplayCategories = normalizeCategories(in.DisplayCategories)
|
|
in.ThinkingCategories = normalizeCategories(in.ThinkingCategories)
|
|
in.ViewMode = strings.ToLower(strings.TrimSpace(in.ViewMode))
|
|
if in.ViewMode == "" {
|
|
in.ViewMode = "neural"
|
|
}
|
|
if in.ViewMode != "neural" && in.ViewMode != "honeycomb" {
|
|
in.ViewMode = "neural"
|
|
}
|
|
return in
|
|
}
|
|
|
|
func normalizeCategories(values []string) []string {
|
|
seen := map[string]string{}
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(value)
|
|
if _, exists := seen[key]; !exists {
|
|
seen[key] = value
|
|
}
|
|
}
|
|
out := make([]string, 0, len(seen))
|
|
for _, value := range seen {
|
|
out = append(out, value)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i]) < strings.ToLower(out[j]) })
|
|
return out
|
|
}
|
|
|
|
func (e *Engine) loadRuntimeSettings() {
|
|
settings := e.defaultRuntimeSettings()
|
|
if strings.TrimSpace(e.runtimePath) != "" {
|
|
if data, err := os.ReadFile(e.runtimePath); err == nil {
|
|
var stored RuntimeSettings
|
|
if json.Unmarshal(data, &stored) == nil {
|
|
settings = normalizeRuntimeSettings(stored)
|
|
}
|
|
}
|
|
}
|
|
e.runtimeMu.Lock()
|
|
e.runtime = settings
|
|
e.runtimeMu.Unlock()
|
|
}
|
|
|
|
func (e *Engine) RuntimeSettings() RuntimeSettings {
|
|
e.runtimeMu.RLock()
|
|
settings := e.runtime
|
|
e.runtimeMu.RUnlock()
|
|
settings.LearningCategories = append([]string{}, settings.LearningCategories...)
|
|
settings.DisplayCategories = append([]string{}, settings.DisplayCategories...)
|
|
settings.ThinkingCategories = append([]string{}, settings.ThinkingCategories...)
|
|
return settings
|
|
}
|
|
|
|
func (e *Engine) SetRuntimeSettings(settings RuntimeSettings) (RuntimeSettings, error) {
|
|
settings = normalizeRuntimeSettings(settings)
|
|
e.runtimeMu.Lock()
|
|
previous := e.runtime
|
|
e.runtime = settings
|
|
e.runtimeMu.Unlock()
|
|
|
|
if !settings.ThinkingEnabled {
|
|
e.stateMu.Lock()
|
|
if !e.enrichRunning && e.enrichResult == "queued" {
|
|
e.enrichResult = "disabled"
|
|
}
|
|
e.stateMu.Unlock()
|
|
}
|
|
|
|
if e.Persistence != nil && strings.TrimSpace(e.runtimePath) != "" {
|
|
data, err := json.MarshalIndent(settings, "", " ")
|
|
if err != nil {
|
|
return previous, err
|
|
}
|
|
if _, err := e.Persistence.QueueFile(e.runtimePath, append(data, '\n'), 0o640); err != nil {
|
|
e.runtimeMu.Lock()
|
|
e.runtime = previous
|
|
e.runtimeMu.Unlock()
|
|
return previous, err
|
|
}
|
|
}
|
|
|
|
e.Broker.Publish(model.Activity{
|
|
Type: "runtime.settings.updated",
|
|
Source: "ui",
|
|
Phase: "control",
|
|
Message: "Lern-, Anzeige- und Thinking-Einstellungen wurden aktualisiert",
|
|
Strength: .32,
|
|
Metadata: map[string]any{
|
|
"learning_enabled": settings.LearningEnabled,
|
|
"thinking_enabled": settings.ThinkingEnabled,
|
|
"learning_categories": len(settings.LearningCategories),
|
|
"display_categories": len(settings.DisplayCategories),
|
|
"thinking_categories": len(settings.ThinkingCategories),
|
|
"view_mode": settings.ViewMode,
|
|
},
|
|
})
|
|
return settings, nil
|
|
}
|
|
|
|
func (e *Engine) LearningEnabled() bool {
|
|
e.runtimeMu.RLock()
|
|
enabled := e.runtime.LearningEnabled
|
|
e.runtimeMu.RUnlock()
|
|
return enabled
|
|
}
|
|
|
|
func (e *Engine) ThinkingEnabled() bool {
|
|
e.runtimeMu.RLock()
|
|
enabled := e.runtime.ThinkingEnabled
|
|
e.runtimeMu.RUnlock()
|
|
return enabled
|
|
}
|
|
|
|
func (e *Engine) learningCategories() []string {
|
|
e.runtimeMu.RLock()
|
|
out := append([]string(nil), e.runtime.LearningCategories...)
|
|
e.runtimeMu.RUnlock()
|
|
return out
|
|
}
|
|
|
|
func (e *Engine) thinkingCategories() []string {
|
|
e.runtimeMu.RLock()
|
|
out := append([]string(nil), e.runtime.ThinkingCategories...)
|
|
e.runtimeMu.RUnlock()
|
|
return out
|
|
}
|
|
|
|
func (e *Engine) Categories() []CategoryInfo {
|
|
snapshot := e.Graph.Snapshot()
|
|
counts := map[string]int{}
|
|
names := map[string]string{}
|
|
uncategorized := 0
|
|
for _, node := range snapshot.Nodes {
|
|
if node.Kind != "knowledge" && node.Kind != "ai-think" && node.Kind != "external" {
|
|
continue
|
|
}
|
|
if len(node.Categories) == 0 {
|
|
uncategorized++
|
|
continue
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, category := range node.Categories {
|
|
category = strings.TrimSpace(category)
|
|
if category == "" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(category)
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
seen[key] = true
|
|
if _, ok := names[key]; !ok {
|
|
names[key] = category
|
|
}
|
|
counts[key]++
|
|
}
|
|
}
|
|
out := make([]CategoryInfo, 0, len(counts)+1)
|
|
for key, count := range counts {
|
|
out = append(out, CategoryInfo{Name: names[key], Count: count})
|
|
}
|
|
if uncategorized > 0 {
|
|
out = append(out, CategoryInfo{Name: uncategorizedFilter, Count: uncategorized})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].Count == out[j].Count {
|
|
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
|
|
}
|
|
return out[i].Count > out[j].Count
|
|
})
|
|
return out
|
|
}
|