Files
bara-lang/src/ai_assist.nim
T
dimgigov ba0b300917 feat: add fullscreen TUI and project updates
- 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
2026-05-09 01:53:23 +03:00

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