package mcp import ( "context" "encoding/json" "fmt" "strings" "github.com/mathiasbq/hyperguild/ingestion/internal/search" ) const ( answerSystemPrompt = `You are a knowledge assistant. Answer the question using ONLY the provided sources. Cite source file paths inline when referencing specific content. If the context does not contain enough information to answer, say so clearly.` classifySystemPrompt = `Classify the document. Respond with JSON only, no markdown fences. {"type":"...","title":"...","tags":["..."]} Valid types: spec, plan, decision, note, wiki, log, code, unknown.` ) type brainAnswerArgs struct { Query string `json:"query"` } func (s *Server) brainAnswer(ctx context.Context, args json.RawMessage) (json.RawMessage, error) { if s.answerLLM == nil { return nil, fmt.Errorf("answer LLM not configured: set BRAIN_LLM_PRIMARY_URL") } var a brainAnswerArgs if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("parse args: %w", err) } if a.Query == "" { return nil, fmt.Errorf("query is required") } results, err := search.Query(s.brainDir, a.Query, 10) if err != nil { return nil, fmt.Errorf("search: %w", err) } if len(results) == 0 { return json.Marshal(map[string]any{ "answer": "No relevant content found in brain.", "sources": []string{}, }) } var sb strings.Builder sources := make([]string, 0, len(results)) for _, r := range results { fmt.Fprintf(&sb, "\n%s\n\n\n", r.Path, r.Excerpt) sources = append(sources, r.Path) } answer, err := s.answerLLM(ctx, answerSystemPrompt, sb.String()+"Question: "+a.Query) if err != nil { return nil, fmt.Errorf("llm: %w", err) } return json.Marshal(map[string]any{ "answer": answer, "sources": sources, }) } type brainClassifyArgs struct { Text string `json:"text"` } type classifyResult struct { Type string `json:"type"` Title string `json:"title"` Tags []string `json:"tags"` } func (s *Server) brainClassify(ctx context.Context, args json.RawMessage) (json.RawMessage, error) { if s.answerLLM == nil { return nil, fmt.Errorf("answer LLM not configured: set BRAIN_LLM_PRIMARY_URL") } var a brainClassifyArgs if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("parse args: %w", err) } if a.Text == "" { return nil, fmt.Errorf("text is required") } text := a.Text if len(text) > 3000 { text = text[:3000] } raw, err := s.answerLLM(ctx, classifySystemPrompt, text) if err != nil { return nil, fmt.Errorf("llm: %w", err) } // Strip markdown fences if model adds them despite the instruction. raw = strings.TrimSpace(raw) raw = strings.TrimPrefix(raw, "```json") raw = strings.TrimPrefix(raw, "```") raw = strings.TrimSuffix(raw, "```") raw = strings.TrimSpace(raw) var cr classifyResult if err := json.Unmarshal([]byte(raw), &cr); err != nil { return nil, fmt.Errorf("parse classify response %q: %w", raw, err) } if cr.Tags == nil { cr.Tags = []string{} } return json.Marshal(cr) }