Some checks failed
release-tag / Resolve release metadata (push) Successful in 30s
release-tag / Build knowledge (push) Failing after 4m51s
release-tag / Build control (push) Failing after 5m0s
release-tag / Build agent (push) Failing after 5m0s
release-tag / Build agent-data-init (push) Failing after 5m5s
release-tag / Build neuroforge-worker (push) Failing after 5m7s
release-tag / Build neuroforge (push) Failing after 5m9s
257 lines
9.7 KiB
Go
257 lines
9.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"neuroforge/internal/core"
|
|
)
|
|
|
|
type integrationGraphNode struct {
|
|
ID string `json:"id"`
|
|
Kind string `json:"kind"`
|
|
Label string `json:"label"`
|
|
Group string `json:"group,omitempty"`
|
|
Community string `json:"community,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Score float64 `json:"score,omitempty"`
|
|
Meta map[string]any `json:"meta,omitempty"`
|
|
}
|
|
|
|
type integrationGraphEdge struct {
|
|
ID string `json:"id"`
|
|
From string `json:"from"`
|
|
To string `json:"to"`
|
|
Kind string `json:"kind"`
|
|
Label string `json:"label,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Weight float64 `json:"weight,omitempty"`
|
|
Meta map[string]any `json:"meta,omitempty"`
|
|
}
|
|
|
|
type integrationGraphPayload struct {
|
|
Scope string `json:"scope"`
|
|
Title string `json:"title"`
|
|
Nodes []integrationGraphNode `json:"nodes"`
|
|
Edges []integrationGraphEdge `json:"edges"`
|
|
Meta map[string]any `json:"meta,omitempty"`
|
|
}
|
|
|
|
// integrationResearchGraph exposes only bounded research provenance metadata.
|
|
// Full source bodies and prompts stay behind their existing dedicated APIs.
|
|
func (s *Server) integrationResearchGraph(w http.ResponseWriter, r *http.Request) {
|
|
limit := graphBoundedInt(r.URL.Query().Get("runs"), 6, 1, 20)
|
|
maxEvents := graphBoundedInt(r.URL.Query().Get("max_events"), 320, 20, 800)
|
|
runs := s.store.ResearchRunsSnapshot("", limit)
|
|
g := integrationGraphPayload{Scope: "research", Title: "Research Provenance", Meta: map[string]any{"runs": len(runs), "max_events": maxEvents}}
|
|
seen := map[string]bool{}
|
|
addNode := func(n integrationGraphNode) {
|
|
if n.ID == "" || seen[n.ID] {
|
|
return
|
|
}
|
|
seen[n.ID] = true
|
|
g.Nodes = append(g.Nodes, n)
|
|
}
|
|
addEdge := func(e integrationGraphEdge) {
|
|
if e.ID == "" {
|
|
e.ID = e.From + "->" + e.To + ":" + e.Kind
|
|
}
|
|
g.Edges = append(g.Edges, e)
|
|
}
|
|
eventsLeft := maxEvents
|
|
for _, run := range runs {
|
|
goalID := "goal:" + run.GoalID
|
|
runID := "research-run:" + run.ID
|
|
addNode(integrationGraphNode{ID: goalID, Kind: "research_goal", Label: graphCompact(firstGraphNonEmpty(run.GoalTitle, run.GoalID), 90), Group: "research", Community: "goal", Status: "goal"})
|
|
addNode(integrationGraphNode{ID: runID, Kind: "research_run", Label: graphCompact(firstGraphNonEmpty(run.GoalTitle, run.ID), 90), Group: "research", Community: "run", Status: run.Status, Meta: map[string]any{"started_at": run.StartedAt, "completed_at": run.CompletedAt, "stats": run.Stats, "last_error": graphCompact(run.LastError, 180)}})
|
|
addEdge(integrationGraphEdge{From: goalID, To: runID, Kind: "research_cycle", Status: run.Status})
|
|
for _, q := range run.Queries {
|
|
qid := "query:" + run.ID + ":" + shortGraphHash(q)
|
|
addNode(integrationGraphNode{ID: qid, Kind: "query", Label: graphCompact(q, 100), Group: "research", Community: "search", Status: "planned"})
|
|
addEdge(integrationGraphEdge{From: runID, To: qid, Kind: "planned_query"})
|
|
}
|
|
for _, ev := range run.Events {
|
|
if eventsLeft <= 0 {
|
|
break
|
|
}
|
|
eventsLeft--
|
|
qid := ""
|
|
if strings.TrimSpace(ev.Query) != "" {
|
|
qid = "query:" + run.ID + ":" + shortGraphHash(ev.Query)
|
|
addNode(integrationGraphNode{ID: qid, Kind: "query", Label: graphCompact(ev.Query, 100), Group: "research", Community: "search"})
|
|
}
|
|
sourceID := ""
|
|
if ev.SourceID != "" {
|
|
sourceID = "source:" + ev.SourceID
|
|
} else if ev.URL != "" {
|
|
sourceID = "url:" + shortGraphHash(ev.URL)
|
|
}
|
|
if sourceID != "" {
|
|
status := ev.Status
|
|
if status == "" {
|
|
status = "seen"
|
|
}
|
|
addNode(integrationGraphNode{ID: sourceID, Kind: "source", Label: graphCompact(firstGraphNonEmpty(ev.Title, ev.URL, ev.SourceID), 100), Group: "source", Community: "research-source", Status: status, Score: ev.Score, Meta: map[string]any{"url": ev.URL, "source_id": ev.SourceID, "phase": ev.Phase, "engine": ev.Metadata["engine"], "mimetype": ev.Metadata["mimetype"]}})
|
|
from := runID
|
|
if qid != "" {
|
|
from = qid
|
|
}
|
|
addEdge(integrationGraphEdge{From: from, To: sourceID, Kind: graphResearchEdgeKind(ev.Type), Label: ev.Type, Status: ev.Status, Weight: ev.Score})
|
|
}
|
|
if ev.Type == "claim.extracted" {
|
|
cid := "claim:" + run.ID + ":" + strconv.FormatUint(ev.Seq, 10)
|
|
addNode(integrationGraphNode{ID: cid, Kind: "claim", Label: graphCompact(ev.Preview, 120), Group: "evidence", Community: "claim", Status: ev.Status, Score: ev.Confidence, Meta: map[string]any{"phase": ev.Phase, "message": graphCompact(ev.Message, 160)}})
|
|
from := runID
|
|
if sourceID != "" {
|
|
from = sourceID
|
|
}
|
|
addEdge(integrationGraphEdge{From: from, To: cid, Kind: "claim_extracted", Status: ev.Status})
|
|
}
|
|
if ev.MemoryID != "" {
|
|
mid := "memory:" + ev.MemoryID
|
|
status := ev.Status
|
|
if strings.Contains(ev.Type, "corroborated") {
|
|
status = "corroborated"
|
|
}
|
|
if strings.Contains(ev.Type, "duplicate") {
|
|
status = "duplicate"
|
|
}
|
|
addNode(integrationGraphNode{ID: mid, Kind: "memory", Label: graphCompact(firstGraphNonEmpty(ev.Preview, ev.Message, ev.MemoryID), 120), Group: "brain", Community: "evidence", Status: status, Score: firstGraphScore(ev.Confidence, ev.Similarity), Meta: map[string]any{"memory_id": ev.MemoryID, "event": ev.Type, "similarity": ev.Similarity, "confidence": ev.Confidence}})
|
|
from := runID
|
|
if sourceID != "" {
|
|
from = sourceID
|
|
}
|
|
kind := "learned_as"
|
|
if strings.Contains(ev.Type, "corroborated") {
|
|
kind = "corroborates"
|
|
} else if strings.Contains(ev.Type, "duplicate") {
|
|
kind = "matches_existing"
|
|
}
|
|
addEdge(integrationGraphEdge{From: from, To: mid, Kind: kind, Label: ev.Type, Status: ev.Status, Weight: firstGraphScore(ev.Confidence, ev.Similarity)})
|
|
}
|
|
}
|
|
}
|
|
s.json(w, http.StatusOK, g)
|
|
}
|
|
|
|
// integrationBrainGraph is a bounded, redacted operational graph. It is not a
|
|
// memory export: vectors and full text are omitted, and the caller controls only
|
|
// the visualization window size.
|
|
func (s *Server) integrationBrainGraph(w http.ResponseWriter, r *http.Request) {
|
|
maxNodes := graphBoundedInt(r.URL.Query().Get("max_nodes"), 320, 50, 700)
|
|
memories := s.store.MemoriesSnapshot()
|
|
sort.SliceStable(memories, func(i, j int) bool {
|
|
a, b := memoryGraphPriority(memories[i]), memoryGraphPriority(memories[j])
|
|
if a == b {
|
|
return memories[i].CreatedAt.After(memories[j].CreatedAt)
|
|
}
|
|
return a > b
|
|
})
|
|
if len(memories) > maxNodes {
|
|
memories = memories[:maxNodes]
|
|
}
|
|
g := integrationGraphPayload{Scope: "brain", Title: "NeuroForge Brain", Meta: map[string]any{"nodes_budget": maxNodes, "total_memories": len(s.store.MemoriesSnapshot())}}
|
|
seen := map[string]core.Memory{}
|
|
for _, m := range memories {
|
|
seen[m.ID] = m
|
|
label := graphCompact(firstGraphNonEmpty(m.Provenance.SourceTitle, m.TruthKey, m.Text, m.ID), 110)
|
|
community := m.Provenance.Source
|
|
if community == "" {
|
|
community = m.MemoryType
|
|
}
|
|
g.Nodes = append(g.Nodes, integrationGraphNode{ID: "memory:" + m.ID, Kind: "memory_" + m.MemoryType, Label: label, Group: "brain", Community: graphCompact(community, 48), Status: firstGraphNonEmpty(m.Status, core.MemoryActive), Score: m.Salience, Meta: map[string]any{"memory_id": m.ID, "kind": m.Kind, "source": m.Provenance.Source, "confidence": m.Confidence, "reward": m.Reward, "salience": m.Salience, "access_count": m.AccessCount, "created_at": m.CreatedAt, "source_id": m.Provenance.SourceMemoryID}})
|
|
}
|
|
for _, syn := range s.store.SynapsesSnapshot() {
|
|
_, aok := seen[syn.A]
|
|
_, bok := seen[syn.B]
|
|
if !aok || !bok {
|
|
continue
|
|
}
|
|
g.Edges = append(g.Edges, integrationGraphEdge{ID: "syn:" + syn.A + ":" + syn.B, From: "memory:" + syn.A, To: "memory:" + syn.B, Kind: "synapse", Weight: syn.Weight, Meta: map[string]any{"similarity": syn.Similarity, "activations": syn.Activations}})
|
|
}
|
|
for _, m := range memories {
|
|
for _, old := range m.Supersedes {
|
|
if _, ok := seen[old]; ok {
|
|
g.Edges = append(g.Edges, integrationGraphEdge{From: "memory:" + m.ID, To: "memory:" + old, Kind: "supersedes", Status: "active", Weight: 1})
|
|
}
|
|
}
|
|
for _, old := range m.ConsolidatedFrom {
|
|
if _, ok := seen[old]; ok {
|
|
g.Edges = append(g.Edges, integrationGraphEdge{From: "memory:" + old, To: "memory:" + m.ID, Kind: "consolidated_into", Weight: 1})
|
|
}
|
|
}
|
|
}
|
|
s.json(w, http.StatusOK, g)
|
|
}
|
|
|
|
func graphBoundedInt(raw string, def, min, max int) int {
|
|
n, err := strconv.Atoi(strings.TrimSpace(raw))
|
|
if err != nil || n < min {
|
|
return def
|
|
}
|
|
if n > max {
|
|
return max
|
|
}
|
|
return n
|
|
}
|
|
func graphCompact(v string, n int) string {
|
|
v = strings.Join(strings.Fields(strings.TrimSpace(v)), " ")
|
|
rr := []rune(v)
|
|
if n > 0 && len(rr) > n {
|
|
return string(rr[:n]) + "…"
|
|
}
|
|
return v
|
|
}
|
|
func firstGraphNonEmpty(xs ...string) string {
|
|
for _, x := range xs {
|
|
if strings.TrimSpace(x) != "" {
|
|
return strings.TrimSpace(x)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
func firstGraphScore(xs ...float64) float64 {
|
|
for _, x := range xs {
|
|
if x != 0 {
|
|
return x
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
func shortGraphHash(s string) string {
|
|
var h uint64 = 1469598103934665603
|
|
for _, b := range []byte(s) {
|
|
h ^= uint64(b)
|
|
h *= 1099511628211
|
|
}
|
|
return fmt.Sprintf("%x", h)
|
|
}
|
|
func graphResearchEdgeKind(t string) string {
|
|
if strings.HasPrefix(t, "search.") {
|
|
return "search_result"
|
|
}
|
|
if strings.HasPrefix(t, "download.") {
|
|
return "fetched"
|
|
}
|
|
if strings.HasPrefix(t, "source.") {
|
|
return "source_event"
|
|
}
|
|
return "research_event"
|
|
}
|
|
func memoryGraphPriority(m core.Memory) float64 {
|
|
p := m.Salience + m.Confidence*.5 + float64(m.AccessCount)*.01
|
|
if m.Status == core.MemoryActive {
|
|
p += .5
|
|
}
|
|
if strings.HasPrefix(m.Provenance.Source, "glpi.outcome.") {
|
|
p += 1
|
|
}
|
|
if strings.HasPrefix(m.Provenance.Source, "integration:") {
|
|
p += .4
|
|
}
|
|
return p
|
|
}
|