a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
163 lines
3.8 KiB
Go
163 lines
3.8 KiB
Go
package eval
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
mcpserver "github.com/mark3labs/mcp-go/server"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/server"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Handler extends server.Handler with eval-specific endpoints.
|
|
// It inherits every /v1/* route from the base handler and adds
|
|
// POST /v1/augment for the eval augment pipeline.
|
|
type Handler struct {
|
|
*server.Handler
|
|
}
|
|
|
|
// NewHandler creates an eval HTTP handler that dispatches to MCP tools.
|
|
// It provides all server /v1/* endpoints plus POST /v1/augment.
|
|
func NewHandler(mcpServer *mcpserver.MCPServer, g *graph.Graph, version string, logger *zap.Logger) *Handler {
|
|
base := server.NewHandler(mcpServer, g, version, logger)
|
|
h := &Handler{Handler: base}
|
|
base.Mux().HandleFunc("POST /v1/augment", h.handleAugment)
|
|
return h
|
|
}
|
|
|
|
// --- /augment (eval-specific) ---
|
|
|
|
type augmentRequest struct {
|
|
Pattern string `json:"pattern"`
|
|
}
|
|
|
|
type augmentResponse struct {
|
|
Pattern string `json:"pattern"`
|
|
Symbols []augmentSymbol `json:"symbols"`
|
|
}
|
|
|
|
type augmentSymbol struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
File string `json:"file"`
|
|
Kind string `json:"kind"`
|
|
Callers []string `json:"callers,omitempty"`
|
|
Callees []string `json:"callees,omitempty"`
|
|
CallChain []string `json:"call_chain,omitempty"`
|
|
}
|
|
|
|
func (h *Handler) handleAugment(w http.ResponseWriter, r *http.Request) {
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
|
if err != nil {
|
|
server.WriteJSONError(w, http.StatusBadRequest, "failed to read request body")
|
|
return
|
|
}
|
|
|
|
var req augmentRequest
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
server.WriteJSONError(w, http.StatusBadRequest, "malformed JSON: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if req.Pattern == "" {
|
|
server.WriteJSONError(w, http.StatusBadRequest, "missing 'pattern' field")
|
|
return
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
searchResults := h.CallTool(ctx, "search_symbols", map[string]any{
|
|
"query": req.Pattern,
|
|
"compact": true,
|
|
})
|
|
|
|
symbolIDs := extractSymbolIDs(searchResults)
|
|
|
|
var symbols []augmentSymbol
|
|
for _, id := range symbolIDs {
|
|
sym := augmentSymbol{ID: id}
|
|
|
|
if parts := strings.SplitN(id, "::", 2); len(parts) == 2 {
|
|
sym.File = parts[0]
|
|
sym.Name = parts[1]
|
|
} else {
|
|
sym.Name = id
|
|
}
|
|
|
|
if node := h.Graph().GetNode(id); node != nil {
|
|
sym.Kind = string(node.Kind)
|
|
}
|
|
|
|
usageResults := h.CallTool(ctx, "find_usages", map[string]any{
|
|
"id": id,
|
|
"compact": true,
|
|
})
|
|
sym.Callers = extractLines(usageResults)
|
|
|
|
chainResults := h.CallTool(ctx, "get_call_chain", map[string]any{
|
|
"function_id": id,
|
|
"compact": true,
|
|
"depth": float64(2),
|
|
})
|
|
sym.CallChain = extractLines(chainResults)
|
|
|
|
symbols = append(symbols, sym)
|
|
}
|
|
|
|
resp := augmentResponse{
|
|
Pattern: req.Pattern,
|
|
Symbols: symbols,
|
|
}
|
|
server.WriteJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
// extractSymbolIDs parses symbol IDs from compact search_symbols output.
|
|
func extractSymbolIDs(text string) []string {
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
var ids []string
|
|
seen := make(map[string]bool)
|
|
for _, line := range strings.Split(text, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 3 {
|
|
name := parts[1]
|
|
fileLine := parts[2]
|
|
file := fileLine
|
|
if idx := strings.LastIndex(fileLine, ":"); idx > 0 {
|
|
file = fileLine[:idx]
|
|
}
|
|
id := file + "::" + name
|
|
if !seen[id] {
|
|
seen[id] = true
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
// extractLines splits text into non-empty trimmed lines.
|
|
func extractLines(text string) []string {
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
var lines []string
|
|
for _, line := range strings.Split(text, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
return lines
|
|
}
|