feat: AI-assisted compilation and code generation

- New src/ai_assist.nim module with DeepSeek/OpenAI/MiMo API support
- AI explains compiler errors automatically when compilation fails
- New CLI command: cljnim ai '<description>' for code generation
- REPL :ai command for interactive AI assistance
- API keys read from environment vars (DEEPSEEK_API_KEY, OPENAI_API_KEY, MIMO_API_KEY)
- Tests for prompt building and response formatting
- Updated README with AI integration docs
This commit is contained in:
2026-05-08 22:21:27 +03:00
parent 6bed638300
commit 6249d8a251
7 changed files with 382 additions and 32 deletions
+160
View File
@@ -0,0 +1,160 @@
# 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]
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
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 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 config = detectConfig()
let prompt = buildErrorPrompt(errorMsg, sourceCode, fileName)
return callAiApi(config, prompt)
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 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
+57 -9
View File
@@ -1,6 +1,6 @@
# Clojure/Nim — AI-First Clojure Compiler
import os, osproc, strutils, times
import reader, emitter, types, repl, macros, deps
import reader, emitter, types, repl, macros, deps, ai_assist
proc getLibPath(): string =
let appDir = getAppDir()
@@ -44,7 +44,7 @@ proc extractRequires(forms: seq[CljVal]): seq[(string, string)] =
alias = req.items[2].symName
result.add((libName, alias))
proc compileFile*(inputPath: string, outputPath: string, extraPaths: seq[string] = @[]) =
proc compileFileInternal(inputPath: string, outputPath: string, extraPaths: seq[string] = @[], libMode: bool = false) =
let source = readFile(inputPath)
let forms = reader.readAll(source)
@@ -97,11 +97,17 @@ proc compileFile*(inputPath: string, outputPath: string, extraPaths: seq[string]
f.items[0].kind == ckSymbol and f.items[0].symName == "ns"):
allForms.add(f)
let nimCode = emitter.emitProgram(allForms)
let nimCode = if libMode: emitter.emitProgramLib(allForms) else: emitter.emitProgram(allForms)
writeFile(outputPath, nimCode)
echo "Generated: ", outputPath
proc nimCompile(nimPath: string, binPath: string, release: bool = false): int =
proc compileFile*(inputPath: string, outputPath: string, extraPaths: seq[string] = @[]) =
compileFileInternal(inputPath, outputPath, extraPaths, false)
proc compileFileLib*(inputPath: string, outputPath: string, extraPaths: seq[string] = @[]) =
compileFileInternal(inputPath, outputPath, extraPaths, true)
proc nimCompile(nimPath: string, binPath: string, release: bool = false): tuple[exitCode: int, output: string] =
let libPath = getLibPath()
var cmd = "nim c"
if release:
@@ -109,7 +115,8 @@ proc nimCompile(nimPath: string, binPath: string, release: bool = false): int =
cmd.add(" --path:" & quoteShell(libPath))
cmd.add(" -o:" & quoteShell(binPath) & " " & quoteShell(nimPath))
echo "Compiling: ", cmd
execCmd(cmd)
let (output, exitCode) = execCmdEx(cmd)
return (exitCode, output)
proc makeTempDir(): string =
let pid = getCurrentProcessId()
@@ -143,9 +150,14 @@ proc runFile*(inputPath: string) =
if useCache:
echo "Using cached: ", cacheNimPath
let compileResult = nimCompile(cacheNimPath, cacheBinPath)
let (compileResult, compileOut) = nimCompile(cacheNimPath, cacheBinPath)
if compileResult != 0:
stderr.writeLine("Compilation failed (cached)")
stderr.writeLine(compileOut)
if ai_assist.hasAiConfig():
let source = readFile(inputPath)
let aiRes = ai_assist.explainError(compileOut, source, inputPath)
stderr.writeLine(ai_assist.formatSuggestion(aiRes))
quit(1)
try: removeDir(buildDir) except: discard
let runResult = execCmd(cacheBinPath)
@@ -153,9 +165,14 @@ proc runFile*(inputPath: string) =
compileFile(inputPath, nimPath, depPaths)
let compileResult = nimCompile(nimPath, binPath)
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
if compileResult != 0:
stderr.writeLine("Compilation failed")
stderr.writeLine(compileOut)
if ai_assist.hasAiConfig():
let source = readFile(inputPath)
let aiRes = ai_assist.explainError(compileOut, source, inputPath)
stderr.writeLine(ai_assist.formatSuggestion(aiRes))
try: removeDir(buildDir) except: discard
quit(1)
@@ -188,6 +205,7 @@ proc main() =
echo " cljnim repl [--json] Start REPL (human or AI mode)"
echo " cljnim deps Resolve dependencies from deps.edn"
echo " cljnim -e '<code>' Evaluate expression"
echo " cljnim ai '<description>' Generate Clojure code with AI"
echo ""
echo "REPL Commands:"
echo " :quit, :q Exit REPL"
@@ -215,15 +233,19 @@ proc main() =
let forms = reader.readAll(code)
let nimCode = emitter.emitProgram(forms)
writeFile(nimPath, nimCode)
let compileResult = nimCompile(nimPath, binPath)
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
if compileResult != 0:
stderr.writeLine("Compilation failed")
stderr.writeLine(compileOut)
if ai_assist.hasAiConfig():
let aiRes = ai_assist.explainError(compileOut, code, "-e expression")
stderr.writeLine(ai_assist.formatSuggestion(aiRes))
try: removeDir(buildDir) except: discard
quit(1)
let runResult = execCmd(binPath)
try: removeDir(buildDir) except: discard
quit(runResult)
case cmd
of "compile":
if args.len < 2:
@@ -232,6 +254,14 @@ proc main() =
let inputPath = args[1]
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
compileFile(inputPath, outputPath)
of "compile-lib":
if args.len < 2:
stderr.writeLine("Error: Missing input file")
quit(1)
let inputPath = args[1]
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
compileFileLib(inputPath, outputPath)
of "run":
if args.len < 2:
@@ -298,6 +328,24 @@ proc main() =
else:
echo "No dependencies to resolve"
of "ai":
if not ai_assist.hasAiConfig():
stderr.writeLine("Error: No AI API key configured.")
stderr.writeLine("Set DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY environment variable.")
quit(1)
if args.len < 2:
stderr.writeLine("Error: Missing description for AI generation")
stderr.writeLine("Usage: cljnim ai 'function that reverses a list'")
quit(1)
let description = args[1]
echo "🤖 Asking AI to generate code..."
let aiRes = ai_assist.generateCode(description)
if aiRes.ok:
echo aiRes.suggestion
else:
stderr.writeLine(aiRes.suggestion)
quit(1)
else:
stderr.writeLine("Unknown command: " & cmd)
quit(1)
+30 -11
View File
@@ -1,6 +1,6 @@
# AI-First REPL for Clojure/Nim
import os, osproc, strutils, json, times, net
import reader, emitter, types, macros, eval
import reader, emitter, types, macros, eval, ai_assist
type
ReplMode* = enum
@@ -193,13 +193,18 @@ proc evalForm*(state: var ReplState, formStr: string): JsonNode =
let elapsed = (epochTime() - startTime) * 1000
if not ok:
var errObj = %*{
"type": "compiler/error",
"message": error,
"form": formStr
}
if ai_assist.hasAiConfig():
let aiRes = ai_assist.explainError(error, formStr, "REPL input")
if aiRes.ok:
errObj["ai_suggestion"] = %aiRes.suggestion
return %*{
"status": "error",
"error": {
"type": "compiler/error",
"message": error,
"form": formStr
},
"error": errObj,
"meta": {
"ns": state.ns,
"ms": elapsed
@@ -262,6 +267,7 @@ proc runHumanRepl*(state: var ReplState) =
echo ":defs List defined vars"
echo ":clear Clear all definitions"
echo ":ns Show current namespace"
echo ":ai <desc> Ask AI to generate Clojure code"
of ":defs":
for d in state.defs:
if d.kind == ckList and d.items.len > 1 and d.items[1].kind == ckSymbol:
@@ -272,11 +278,25 @@ proc runHumanRepl*(state: var ReplState) =
of ":ns":
echo state.ns
else:
let res = evalForm(state, cmd)
if res["status"].getStr() == "ok":
echo "=> " & res["result"]["printed"].getStr()
if cmd.startsWith(":ai "):
let description = cmd[4..^1].strip()
if description.len == 0:
echo "Usage: :ai <description>"
elif not ai_assist.hasAiConfig():
echo "No AI API key configured. Set DEEPSEEK_API_KEY, OPENAI_API_KEY, or MIMO_API_KEY."
else:
echo "🤖 Thinking..."
let aiRes = ai_assist.generateCode(description)
echo ai_assist.formatSuggestion(aiRes)
else:
echo "Error: " & res["error"]["message"].getStr()
let res = evalForm(state, cmd)
if res["status"].getStr() == "ok":
echo "=> " & res["result"]["printed"].getStr()
else:
echo "Error: " & res["error"]["message"].getStr()
if res["error"].hasKey("ai_suggestion"):
echo ""
echo ai_assist.formatSuggestion(AiResponse(ok: true, suggestion: res["error"]["ai_suggestion"].getStr("")))
proc runJsonRepl*(state: var ReplState) =
var readyMsg = %*{ "status": "ready", "ns": state.ns, "mode": "json" }
@@ -385,7 +405,6 @@ proc handleClient(state: var ReplState, client: Socket) =
var readyMsg = $(%*{ "status": "ready", "ns": state.ns, "mode": "nrepl" }) & "\n"
client.send(readyMsg)
var buf = ""
while true:
try:
var line = ""