Files
glpi-neural-brain/internal/ingest/agent.go
2026-08-04 03:43:12 +02:00

148 lines
3.2 KiB
Go

package ingest
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
"github.com/local/glpi-neural-brain/internal/activity"
"github.com/local/glpi-neural-brain/internal/graph"
"github.com/local/glpi-neural-brain/internal/model"
)
type AgentWatcher struct {
Files []string
Graph *graph.Store
Broker *activity.Broker
mu sync.Mutex
offsets map[string]int64
}
func NewAgentWatcher(files []string, g *graph.Store, b *activity.Broker) *AgentWatcher {
return &AgentWatcher{Files: files, Graph: g, Broker: b, offsets: map[string]int64{}}
}
func (w *AgentWatcher) Start(ctx context.Context) {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
w.poll()
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}()
}
func (w *AgentWatcher) poll() {
for _, path := range w.Files {
_ = w.readNew(path)
}
}
func (w *AgentWatcher) readNew(path string) error {
f, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
defer f.Close()
w.mu.Lock()
off := w.offsets[path]
w.mu.Unlock()
st, err := f.Stat()
if err != nil {
return err
}
if st.Size() < off {
off = 0
}
if _, err = f.Seek(off, io.SeekStart); err != nil {
return err
}
sc := bufio.NewScanner(f)
buf := make([]byte, 64*1024)
sc.Buffer(buf, 8<<20)
for sc.Scan() {
line := append([]byte(nil), sc.Bytes()...)
off += int64(len(sc.Bytes()) + 1)
w.process(line)
}
if err := sc.Err(); err != nil {
return err
}
w.mu.Lock()
w.offsets[path] = off
w.mu.Unlock()
return nil
}
func (w *AgentWatcher) process(line []byte) {
var run map[string]any
if json.Unmarshal(line, &run) != nil {
return
}
runID := str(run["run_id"])
ticket := str(run["ticket_id"])
trigger := str(run["trigger"])
outcome := str(run["outcome"])
analyses, _ := run["analyses"].([]any)
nodeSet := map[string]bool{}
var phases []string
for _, raw := range analyses {
a, _ := raw.(map[string]any)
if a == nil {
continue
}
phases = append(phases, str(a["analysis_type"]))
collectExternalIDs(a, func(id string) {
if n, ok := w.Graph.LookupExternal(id); ok {
nodeSet[n.ID] = true
}
})
}
var nodeIDs []string
for id := range nodeSet {
nodeIDs = append(nodeIDs, id)
}
edgeIDs := w.Graph.ConnectingEdges(nodeIDs)
msg := fmt.Sprintf("Agent-Lauf %s · Ticket %s · %s", runID, ticket, outcome)
if len(phases) > 0 {
msg += " · " + strings.Join(phases, " → ")
}
w.Broker.Publish(model.Activity{Type: "agent.run", Source: "agent", Phase: trigger, Message: msg, NodeIDs: nodeIDs, EdgeIDs: edgeIDs, Strength: .9, Metadata: map[string]any{"run_id": runID, "ticket_id": ticket, "outcome": outcome}})
}
func collectExternalIDs(v any, fn func(string)) {
switch x := v.(type) {
case map[string]any:
for k, v := range x {
lk := strings.ToLower(k)
if strings.Contains(lk, "knowledge_id") || lk == "id" {
s := str(v)
if len(s) > 2 {
fn(s)
}
}
collectExternalIDs(v, fn)
}
case []any:
for _, v := range x {
collectExternalIDs(v, fn)
}
}
}
func str(v any) string {
if v == nil {
return ""
}
return strings.TrimSpace(fmt.Sprint(v))
}