feat(graph): GraphRAG augment brain_answer with top-hit subgraph
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:
@@ -96,6 +96,29 @@ func (s *Server) brainAnswer(ctx context.Context, args json.RawMessage) (json.Ra
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user