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:
@@ -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
|
||||
Reference in New Issue
Block a user