feat(graph): GraphRAG augment brain_answer with top-hit subgraph
All checks were successful
CI / Lint / Test / Vet (push) Successful in 12s
CI / Mirror to GitHub (push) Successful in 3s

Commit 4 of Track A — the no-shelfware close-out the grill demanded.
brain_answer now folds the 1-hop outgoing neighbourhood of its top
BM25/rerank hit into the LLM's context as a <related> block when
BRAIN_GRAPH_ENABLED is on. With the flag off the prompt is byte-for-
byte identical to the pre-Track-A behaviour, so existing tests still
pass without modification.

The hop list contains slug, edge_type, doc_path — no extra retrieval
pass, no second LLM call, no file reads. The model can ignore the
block when irrelevant; when it adds signal we get GraphRAG for free.

Refs: docs/superpowers/specs/2026-05-homelab-training-graph-next-step.md
in infra repo + grill addendum item "Track A: GraphRAG wiring into
brain_answer is mandatory in same commit chain (no shelfware risk)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathias
2026-05-23 15:24:45 +02:00
parent 2148565ee6
commit 153ef6ccac

View File

@@ -96,6 +96,29 @@ func (s *Server) brainAnswer(ctx context.Context, args json.RawMessage) (json.Ra
sources = append(sources, r.Path) sources = append(sources, r.Path)
} }
// GraphRAG augmentation: when the graph is wired, attach the 1-hop
// outgoing neighbourhood of the top BM25/rerank hit as an extra
// context block. The LLM can ignore it when irrelevant; when the
// neighbour adds signal we don't need a second retrieval pass.
// Failures are silently skipped — graph is augmentation, not
// correctness.
if reader, ok := s.graph.(graphReader); ok && len(results) > 0 {
topSlug := slugFromPath(results[0].Path)
if topSlug != "" {
if ns, gerr := reader.Subgraph(ctx, topSlug, 1); gerr == nil && len(ns) > 0 {
sb.WriteString("<related>\n")
for _, n := range ns {
label := n.Title
if label == "" {
label = n.Slug
}
fmt.Fprintf(&sb, "- %s (%s) at %s\n", label, n.EdgeType, n.DocPath)
}
sb.WriteString("</related>\n\n")
}
}
}
answer, err := s.answerLLM(ctx, answerSystemPrompt, sb.String()+"Question: "+a.Query) answer, err := s.answerLLM(ctx, answerSystemPrompt, sb.String()+"Question: "+a.Query)
if err != nil { if err != nil {
return nil, fmt.Errorf("llm: %w", err) return nil, fmt.Errorf("llm: %w", err)
@@ -107,6 +130,25 @@ func (s *Server) brainAnswer(ctx context.Context, args json.RawMessage) (json.Ra
}) })
} }
// slugFromPath converts "wiki/concepts/foo.md" → "foo".
// Returns "" when path has no .md suffix or empty basename.
func slugFromPath(path string) string {
if path == "" {
return ""
}
// strip directory
for i := len(path) - 1; i >= 0; i-- {
if path[i] == '/' {
path = path[i+1:]
break
}
}
if !strings.HasSuffix(path, ".md") {
return ""
}
return strings.TrimSuffix(path, ".md")
}
type brainClassifyArgs struct { type brainClassifyArgs struct {
Text string `json:"text"` Text string `json:"text"`
} }