148 lines
3.2 KiB
Go
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))
|
|
}
|