ba0b300917
- New TUI screens: Main Menu, Compile, Run, REPL, AI Generator, AI Settings, Help - AI configuration persisted in ~/.config/cljnim/config.json - Added illwill dependency for terminal UI - Updated experiments, examples, docs, and core modules
245 lines
7.5 KiB
Nim
245 lines
7.5 KiB
Nim
# AI Assistance for Clojure/Nim Compiler
|
|
# Supports DeepSeek API and OpenAI-compatible APIs (Xiaomi MiMo, etc.)
|
|
# API keys are read from environment variables — never hardcoded.
|
|
|
|
import std/[httpclient, json, os, strutils, uri, tables, times]
|
|
|
|
type
|
|
AiProvider* = enum
|
|
aiDeepSeek
|
|
aiOpenAiCompatible
|
|
|
|
AiConfig* = object
|
|
provider*: AiProvider
|
|
apiKey*: string
|
|
baseUrl*: string
|
|
model*: string
|
|
timeoutMs*: int
|
|
|
|
AiResponse* = object
|
|
ok*: bool
|
|
suggestion*: string
|
|
rawJson*: string
|
|
|
|
proc detectConfig*(): AiConfig =
|
|
## Auto-detect AI configuration from environment variables
|
|
result.timeoutMs = 15000
|
|
|
|
# DeepSeek
|
|
let deepseekKey = getEnv("DEEPSEEK_API_KEY", "")
|
|
if deepseekKey.len > 0:
|
|
return AiConfig(
|
|
provider: aiDeepSeek,
|
|
apiKey: deepseekKey,
|
|
baseUrl: "https://api.deepseek.com",
|
|
model: getEnv("DEEPSEEK_MODEL", "deepseek-chat"),
|
|
timeoutMs: parseInt(getEnv("AI_TIMEOUT_MS", "15000"))
|
|
)
|
|
|
|
# OpenAI-compatible (Xiaomi MiMo, OpenRouter, etc.)
|
|
let openaiKey = getEnv("OPENAI_API_KEY", "")
|
|
if openaiKey.len > 0:
|
|
return AiConfig(
|
|
provider: aiOpenAiCompatible,
|
|
apiKey: openaiKey,
|
|
baseUrl: getEnv("OPENAI_BASE_URL", "https://api.openai.com"),
|
|
model: getEnv("OPENAI_MODEL", "gpt-4o-mini"),
|
|
timeoutMs: parseInt(getEnv("AI_TIMEOUT_MS", "15000"))
|
|
)
|
|
|
|
# Xiaomi MiMo (OpenAI-compatible)
|
|
let mimoKey = getEnv("MIMO_API_KEY", "")
|
|
if mimoKey.len > 0:
|
|
return AiConfig(
|
|
provider: aiOpenAiCompatible,
|
|
apiKey: mimoKey,
|
|
baseUrl: getEnv("MIMO_BASE_URL", "https://api.mi-mo.ai"),
|
|
model: getEnv("MIMO_MODEL", "mimo-chat"),
|
|
timeoutMs: parseInt(getEnv("AI_TIMEOUT_MS", "15000"))
|
|
)
|
|
|
|
# No API key found
|
|
return AiConfig(provider: aiDeepSeek, apiKey: "", baseUrl: "", model: "", timeoutMs: 0)
|
|
|
|
proc hasAiConfig*(): bool =
|
|
detectConfig().apiKey.len > 0
|
|
|
|
type
|
|
ErrorCacheEntry = object
|
|
suggestion: string
|
|
timestamp: float64
|
|
|
|
var errorCache = initTable[string, ErrorCacheEntry]()
|
|
var errorCacheMaxSize = 50
|
|
|
|
proc errorCacheKey(errorMsg, fileName: string): string =
|
|
result = fileName & "::" & errorMsg
|
|
if result.len > 200:
|
|
result = result[0..199]
|
|
|
|
proc cacheError*(errorMsg, fileName: string, suggestion: string) =
|
|
if errorCache.len >= errorCacheMaxSize:
|
|
errorCache.clear()
|
|
errorCache[errorCacheKey(errorMsg, fileName)] = ErrorCacheEntry(
|
|
suggestion: suggestion,
|
|
timestamp: epochTime()
|
|
)
|
|
|
|
proc getCachedError*(errorMsg, fileName: string): string =
|
|
let key = errorCacheKey(errorMsg, fileName)
|
|
if key in errorCache:
|
|
let entry = errorCache[key]
|
|
if epochTime() - entry.timestamp < 3600.0:
|
|
return entry.suggestion
|
|
return ""
|
|
|
|
proc buildErrorPrompt*(errorMsg, sourceCode, fileName: string): string =
|
|
## Build a prompt for the AI to analyze a compiler error
|
|
result = """You are an expert Clojure/Nim compiler assistant. The user got a compilation error.
|
|
|
|
**File:** """ & fileName & """
|
|
|
|
**Source code:**
|
|
```clojure
|
|
""" & sourceCode & """
|
|
```
|
|
|
|
**Compiler error:**
|
|
```
|
|
""" & errorMsg & """
|
|
```
|
|
|
|
Please explain the error in simple terms and suggest a fix. Keep your response under 200 words.
|
|
If the error is in Clojure code, show the corrected Clojure snippet.
|
|
Respond in the same language as the user's source code comments (Bulgarian or English).
|
|
"""
|
|
|
|
proc buildGenerationPrompt*(description: string): string =
|
|
## Build a prompt for AI code generation
|
|
result = """You are an expert Clojure programmer. Generate a Clojure function based on this description:
|
|
|
|
""" & description & """
|
|
|
|
Requirements:
|
|
- Use idiomatic Clojure
|
|
- Include docstring
|
|
- Use loop/recur instead of recursion if possible
|
|
- Return ONLY the Clojure code, no explanations
|
|
"""
|
|
|
|
proc buildOptimizationPrompt*(code: string): string =
|
|
## Build a prompt for AI optimization suggestions
|
|
result = """You are an expert Clojure performance engineer. Analyze this Clojure code and suggest optimizations:
|
|
|
|
```clojure
|
|
""" & code & """
|
|
```
|
|
|
|
Consider:
|
|
- SIMD/vectorization opportunities
|
|
- loop/recur vs recursion
|
|
- Persistent data structure usage
|
|
- Transients for batch operations
|
|
- Parallelization opportunities (pmap, reducers)
|
|
|
|
Keep response under 200 words. Return ONLY Clojure code suggestions, no explanations.
|
|
"""
|
|
|
|
proc buildDebugPrompt*(code: string, evalResult: string): string =
|
|
## Build a prompt for AI debugging analysis
|
|
result = """You are an expert Clojure debugger. Analyze this Clojure expression and its result:
|
|
|
|
**Expression:**
|
|
```clojure
|
|
""" & code & """
|
|
```
|
|
|
|
**Result:**
|
|
```
|
|
""" & evalResult & """
|
|
```
|
|
|
|
Explain what happened step by step. If there's a bug or unexpected behavior, explain why.
|
|
Keep response under 200 words.
|
|
Respond in the same language as the user's source code comments (Bulgarian or English).
|
|
"""
|
|
|
|
proc callAiApi*(config: AiConfig, prompt: string): AiResponse =
|
|
## Call the AI API and return the response
|
|
if config.apiKey.len == 0:
|
|
return AiResponse(ok: false, suggestion: "No AI API key configured. Set DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY environment variable.")
|
|
|
|
let client = newHttpClient(timeout = config.timeoutMs)
|
|
defer: client.close()
|
|
|
|
let url = config.baseUrl & "/v1/chat/completions"
|
|
let body = %*{
|
|
"model": config.model,
|
|
"messages": [
|
|
{"role": "user", "content": prompt}
|
|
],
|
|
"temperature": 0.3,
|
|
"max_tokens": 800
|
|
}
|
|
|
|
client.headers["Authorization"] = "Bearer " & config.apiKey
|
|
client.headers["Content-Type"] = "application/json"
|
|
|
|
try:
|
|
let resp = client.post(url, body = $body)
|
|
let respBody = resp.body
|
|
result.rawJson = respBody
|
|
|
|
if resp.code.int != 200:
|
|
return AiResponse(ok: false, suggestion: "AI API error (HTTP " & $resp.code.int & "): " & respBody)
|
|
|
|
let jsonResp = parseJson(respBody)
|
|
if jsonResp.hasKey("choices") and jsonResp["choices"].len > 0:
|
|
let content = jsonResp["choices"][0]["message"]["content"].getStr("")
|
|
return AiResponse(ok: true, suggestion: content, rawJson: respBody)
|
|
else:
|
|
return AiResponse(ok: false, suggestion: "Unexpected AI API response format", rawJson: respBody)
|
|
|
|
except CatchableError as e:
|
|
return AiResponse(ok: false, suggestion: "AI request failed: " & e.msg)
|
|
|
|
proc explainError*(errorMsg, sourceCode, fileName: string): AiResponse =
|
|
## High-level helper: explain a compiler error using AI
|
|
let cached = getCachedError(errorMsg, fileName)
|
|
if cached.len > 0:
|
|
return AiResponse(ok: true, suggestion: cached)
|
|
let config = detectConfig()
|
|
let prompt = buildErrorPrompt(errorMsg, sourceCode, fileName)
|
|
let res = callAiApi(config, prompt)
|
|
if res.ok:
|
|
cacheError(errorMsg, fileName, res.suggestion)
|
|
return res
|
|
|
|
proc generateCode*(description: string): AiResponse =
|
|
## High-level helper: generate Clojure code from description
|
|
let config = detectConfig()
|
|
let prompt = buildGenerationPrompt(description)
|
|
return callAiApi(config, prompt)
|
|
|
|
proc optimizeCode*(code: string): AiResponse =
|
|
## High-level helper: suggest optimizations for Clojure code
|
|
let config = detectConfig()
|
|
let prompt = buildOptimizationPrompt(code)
|
|
return callAiApi(config, prompt)
|
|
|
|
proc debugCode*(code: string, evalResult: string): AiResponse =
|
|
## High-level helper: debug a Clojure expression and its result
|
|
let config = detectConfig()
|
|
let prompt = buildDebugPrompt(code, evalResult)
|
|
return callAiApi(config, prompt)
|
|
|
|
proc formatSuggestion*(response: AiResponse): string =
|
|
## Format AI response for terminal display
|
|
if not response.ok:
|
|
return "💡 AI: " & response.suggestion
|
|
|
|
var res = "💡 AI Suggestion:\n"
|
|
for line in response.suggestion.splitLines():
|
|
res.add(" " & line & "\n")
|
|
return res
|