diff --git a/ingestion/internal/mcp/tools_answer.go b/ingestion/internal/mcp/tools_answer.go index e8d15cc..9a72084 100644 --- a/ingestion/internal/mcp/tools_answer.go +++ b/ingestion/internal/mcp/tools_answer.go @@ -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("\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("\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"` }