Files
explorer-monorepo/backend/api/rest/ai_docs.go

137 lines
3.8 KiB
Go
Raw Normal View History

refactor(ai): split the 1180-line ai.go into focused files Decomposes backend/api/rest/ai.go (which the review flagged at 1180 lines and which was the largest file in the repo by a wide margin) into six purpose-built files inside the same package, so no import paths change for any caller and *Server receivers keep working: ai.go 198 handlers + feature flags + exported AI* DTOs ai_context.go 381 buildAIContext + indexed-DB queries (stats / tx / address / block) + regex patterns + extractBlockReference ai_routes.go 139 queryAIRoutes + filterAIRouteMatches + routeMatchesQuery + normalizeHexString ai_docs.go 136 loadAIDocSnippets + findAIWorkspaceRoot + scanDocForTerms + buildDocSearchTerms ai_xai.go 267 xAI / OpenAI request/response types + normalizeAIMessages + latestUserMessage + callXAIChatCompletions + parseXAIError + extractOutputText ai_helpers.go 112 pure-function utilities (firstRegexMatch, compactStringMap, compactAnyMap, stringValue, stringSliceValue, uniqueStrings, clipString, fileExists) ai_runtime.go (rate limiter + metrics + audit log) is unchanged. This is a pure move: no logic changes, no new public API, no changes to HTTP routes. Each file carries only the imports it actually uses so goimports is clean on every file individually. Every exported symbol retained its original spelling so callers (routes.go, server.go, and the AI e2e tests) keep compiling without edits. Verification: go build ./... clean go vet ./... clean go test ./api/rest/... PASS staticcheck ./... clean on the SA* correctness family Advances completion criterion 6 (backend maintainability): 'no single Go file exceeds a few hundred lines; AI/LLM plumbing is separated from HTTP handlers; context-building is separated from upstream calls.'
2026-04-18 19:13:38 +00:00
package rest
import (
"bufio"
"os"
"path/filepath"
"strings"
)
func loadAIDocSnippets(query string) ([]AIDocSnippet, string, string) {
root := findAIWorkspaceRoot()
if root == "" {
return nil, "", "workspace docs root unavailable for ai doc retrieval"
}
relativePaths := []string{
"docs/11-references/ADDRESS_MATRIX_AND_STATUS.md",
"docs/11-references/LIQUIDITY_POOLS_MASTER_MAP.md",
"docs/11-references/DEPLOYED_TOKENS_BRIDGES_LPS_AND_ROUTING_STATUS.md",
"docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md",
"explorer-monorepo/docs/EXPLORER_API_ACCESS.md",
}
terms := buildDocSearchTerms(query)
if len(terms) == 0 {
terms = []string{"chain 138", "bridge", "liquidity"}
}
snippets := []AIDocSnippet{}
for _, rel := range relativePaths {
fullPath := filepath.Join(root, rel)
fileSnippets := scanDocForTerms(fullPath, rel, terms)
snippets = append(snippets, fileSnippets...)
if len(snippets) >= maxExplorerAIDocSnippets {
break
}
}
if len(snippets) == 0 {
return nil, root, "no matching workspace docs found for ai context"
}
if len(snippets) > maxExplorerAIDocSnippets {
snippets = snippets[:maxExplorerAIDocSnippets]
}
return snippets, root, ""
}
func findAIWorkspaceRoot() string {
candidates := []string{}
if envRoot := strings.TrimSpace(os.Getenv("EXPLORER_AI_WORKSPACE_ROOT")); envRoot != "" {
candidates = append(candidates, envRoot)
}
if cwd, err := os.Getwd(); err == nil {
candidates = append(candidates, cwd)
dir := cwd
for i := 0; i < 4; i++ {
dir = filepath.Dir(dir)
candidates = append(candidates, dir)
}
}
candidates = append(candidates, "/opt/explorer-monorepo", "/home/intlc/projects/proxmox")
for _, candidate := range candidates {
if candidate == "" {
continue
}
if fileExists(filepath.Join(candidate, "docs")) && (fileExists(filepath.Join(candidate, "explorer-monorepo")) || fileExists(filepath.Join(candidate, "smom-dbis-138")) || fileExists(filepath.Join(candidate, "config"))) {
return candidate
}
}
return ""
}
func scanDocForTerms(fullPath, relativePath string, terms []string) []AIDocSnippet {
file, err := os.Open(fullPath)
if err != nil {
return nil
}
defer file.Close()
normalizedTerms := make([]string, 0, len(terms))
for _, term := range terms {
term = strings.ToLower(strings.TrimSpace(term))
if len(term) >= 3 {
normalizedTerms = append(normalizedTerms, term)
}
}
scanner := bufio.NewScanner(file)
lineNumber := 0
snippets := []AIDocSnippet{}
for scanner.Scan() {
lineNumber++
line := scanner.Text()
lower := strings.ToLower(line)
for _, term := range normalizedTerms {
if strings.Contains(lower, term) {
snippets = append(snippets, AIDocSnippet{
Path: relativePath,
Line: lineNumber,
Snippet: clipString(strings.TrimSpace(line), 280),
})
break
}
}
if len(snippets) >= 2 {
break
}
}
return snippets
}
func buildDocSearchTerms(query string) []string {
words := strings.Fields(strings.ToLower(query))
stopWords := map[string]bool{
"what": true, "when": true, "where": true, "which": true, "with": true, "from": true,
"that": true, "this": true, "have": true, "about": true, "into": true, "show": true,
"live": true, "help": true, "explain": true, "tell": true,
}
terms := []string{}
for _, word := range words {
word = strings.Trim(word, ".,:;!?()[]{}\"'")
if len(word) < 4 || stopWords[word] {
continue
}
terms = append(terms, word)
}
for _, match := range addressPattern.FindAllString(query, -1) {
terms = append(terms, strings.ToLower(match))
}
for _, symbol := range []string{"cUSDT", "cUSDC", "cXAUC", "cEURT", "USDT", "USDC", "WETH", "WETH10", "Mainnet", "bridge", "liquidity", "pool"} {
if strings.Contains(strings.ToLower(query), strings.ToLower(symbol)) {
terms = append(terms, strings.ToLower(symbol))
}
}
return uniqueStrings(terms)
}