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:
@@ -12,4 +12,6 @@ test_pvec
|
|||||||
test_pmap
|
test_pmap
|
||||||
test_deps
|
test_deps
|
||||||
test_eval
|
test_eval
|
||||||
|
test_macros
|
||||||
|
test_ai_assist
|
||||||
books/
|
books/
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.PHONY: build test clean run-example bench bench-aot
|
.PHONY: build test check clean run-example bench bench-aot
|
||||||
|
|
||||||
NIMCACHE = nimcache
|
NIMCACHE = nimcache
|
||||||
|
|
||||||
build:
|
build:
|
||||||
nim c -o:cljnim src/cljnim.nim
|
nim c -d:ssl -o:cljnim src/cljnim.nim
|
||||||
|
|
||||||
test:
|
test:
|
||||||
nim c -r tests/test_reader.nim
|
nim c -r tests/test_reader.nim
|
||||||
@@ -11,7 +11,9 @@ test:
|
|||||||
nim c -r tests/test_pvec.nim
|
nim c -r tests/test_pvec.nim
|
||||||
nim c -r tests/test_pmap.nim
|
nim c -r tests/test_pmap.nim
|
||||||
nim c -r tests/test_deps.nim
|
nim c -r tests/test_deps.nim
|
||||||
|
nim c -r tests/test_macros.nim
|
||||||
nim c -r tests/test_eval.nim
|
nim c -r tests/test_eval.nim
|
||||||
|
nim c --path:src -r tests/test_ai_assist.nim
|
||||||
|
|
||||||
test-reader:
|
test-reader:
|
||||||
nim c -r tests/test_reader.nim
|
nim c -r tests/test_reader.nim
|
||||||
@@ -19,6 +21,13 @@ test-reader:
|
|||||||
test-emitter:
|
test-emitter:
|
||||||
nim c -r tests/test_emitter.nim
|
nim c -r tests/test_emitter.nim
|
||||||
|
|
||||||
|
check: build test
|
||||||
|
./cljnim run examples/hello.clj
|
||||||
|
./cljnim run examples/math.clj
|
||||||
|
./cljnim run examples/core.clj
|
||||||
|
./cljnim run examples/interop.clj
|
||||||
|
./cljnim run examples/macros.clj
|
||||||
|
|
||||||
run-example: build
|
run-example: build
|
||||||
./cljnim run examples/hello.clj
|
./cljnim run examples/hello.clj
|
||||||
|
|
||||||
@@ -36,3 +45,8 @@ clean:
|
|||||||
find . -name "test_reader" -delete
|
find . -name "test_reader" -delete
|
||||||
find . -name "test_emitter" -delete
|
find . -name "test_emitter" -delete
|
||||||
find . -name "test_deps" -delete
|
find . -name "test_deps" -delete
|
||||||
|
find . -name "test_pvec" -delete
|
||||||
|
find . -name "test_pmap" -delete
|
||||||
|
find . -name "test_eval" -delete
|
||||||
|
find . -name "test_macros" -delete
|
||||||
|
find . -name "test_ai_assist" -delete
|
||||||
|
|||||||
@@ -103,6 +103,58 @@ $ ./cljnim repl --json
|
|||||||
|
|
||||||
See [`docs/AI_FIRST.md`](docs/AI_FIRST.md) for the full AI integration design.
|
See [`docs/AI_FIRST.md`](docs/AI_FIRST.md) for the full AI integration design.
|
||||||
|
|
||||||
|
## AI-Powered Assistance
|
||||||
|
|
||||||
|
Clojure/Nim can use paid AI APIs (DeepSeek, OpenAI, Xiaomi MiMo) to help you write and debug code.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
Set one of these environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# DeepSeek (recommended, fast & cheap)
|
||||||
|
export DEEPSEEK_API_KEY="sk-..."
|
||||||
|
|
||||||
|
# OpenAI / OpenRouter
|
||||||
|
export OPENAI_API_KEY="sk-..."
|
||||||
|
|
||||||
|
# Xiaomi MiMo
|
||||||
|
export MIMO_API_KEY="..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
**Auto-error explanation** — When compilation fails, the compiler automatically asks AI for a fix:
|
||||||
|
```bash
|
||||||
|
$ ./cljnim run broken.clj
|
||||||
|
Compilation failed
|
||||||
|
Error: type mismatch: got <int> but expected <string>
|
||||||
|
💡 AI Suggestion:
|
||||||
|
The function `greet` expects a string but you passed an integer.
|
||||||
|
Fix: (defn greet [name] (str "Hello " name))
|
||||||
|
```
|
||||||
|
|
||||||
|
**REPL AI command** — Generate code interactively:
|
||||||
|
```bash
|
||||||
|
$ ./cljnim repl
|
||||||
|
user> :ai function that calculates fibonacci using loop/recur
|
||||||
|
🤖 Thinking...
|
||||||
|
💡 AI Suggestion:
|
||||||
|
(defn fib [n]
|
||||||
|
(loop [a 0 b 1 i n]
|
||||||
|
(if (= i 0)
|
||||||
|
a
|
||||||
|
(recur b (+ a b) (dec i)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
**CLI code generation** — Generate code from terminal:
|
||||||
|
```bash
|
||||||
|
$ ./cljnim ai "function that reads a file line by line"
|
||||||
|
(defn read-lines [filename]
|
||||||
|
(let [content (slurp filename)]
|
||||||
|
(clojure.string/split-lines content)))
|
||||||
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -131,14 +183,25 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for details.
|
|||||||
│ ├── reader.nim # Clojure reader / EDN parser
|
│ ├── reader.nim # Clojure reader / EDN parser
|
||||||
│ ├── emitter.nim # Nim code generator
|
│ ├── emitter.nim # Nim code generator
|
||||||
│ ├── repl.nim # Human & AI REPL
|
│ ├── repl.nim # Human & AI REPL
|
||||||
|
│ ├── eval.nim # Tree-walking interpreter
|
||||||
|
│ ├── deps.nim # Dependency resolution
|
||||||
|
│ ├── core.nim # Core runtime functions
|
||||||
│ ├── types.nim # Clojure value types
|
│ ├── types.nim # Clojure value types
|
||||||
│ ├── macros.nim # Macro expansion engine
|
│ ├── macros.nim # Macro expansion engine
|
||||||
│ └── runtime.nim # Runtime implementation
|
│ └── runtime.nim # Runtime implementation
|
||||||
├── lib/
|
├── lib/
|
||||||
│ └── cljnim_runtime.nim # Clojure runtime in Nim
|
│ ├── cljnim_runtime.nim # Clojure runtime in Nim
|
||||||
|
│ ├── cljnim_pvec.nim # Persistent Vector (HAMT)
|
||||||
|
│ ├── cljnim_pmap.nim # Persistent Map (HAMT)
|
||||||
|
│ └── cljnim_async.nim # core.async channels
|
||||||
├── examples/
|
├── examples/
|
||||||
│ ├── hello.clj
|
│ ├── hello.clj
|
||||||
│ └── math.clj
|
│ ├── math.clj
|
||||||
|
│ ├── core.clj
|
||||||
|
│ ├── macros.clj
|
||||||
|
│ ├── ffi.clj
|
||||||
|
│ ├── interop.clj
|
||||||
|
│ └── ai_tools.clj
|
||||||
├── docs/ # Documentation (EN + BG)
|
├── docs/ # Documentation (EN + BG)
|
||||||
├── tests/
|
├── tests/
|
||||||
├── Makefile
|
├── Makefile
|
||||||
@@ -148,19 +211,18 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for details.
|
|||||||
## Supported Features
|
## Supported Features
|
||||||
|
|
||||||
### Working Now ✅
|
### Working Now ✅
|
||||||
- Reader: numbers, strings, symbols, keywords, lists `()`, vectors `[]`
|
- Reader: numbers, strings, symbols, keywords, lists `()`, vectors `[]`, maps `{}`
|
||||||
- Special forms: `def`, `defn`, `fn`, `let`, `if`, `do`, `quote`
|
- Special forms: `def`, `defn`, `fn`, `let`, `if`, `do`, `quote`
|
||||||
- Arithmetic: `+`, `-`, `*`, `/`, `=`, `<`, `>`
|
- Arithmetic: `+`, `-`, `*`, `/`, `=`, `<`, `>`
|
||||||
- Functions: `println`
|
- Functions: `println`, `map`, `filter`, `reduce`
|
||||||
- Human REPL and JSON REPL
|
- Human REPL and JSON REPL
|
||||||
- AOT compilation to native binaries
|
- AOT compilation to native binaries
|
||||||
|
- Persistent data structures (Vector, Map, Set) via HAMT
|
||||||
### In Progress 🔄
|
- Macro system (`defmacro`, `->`, `->>`, `and`, `or`, `when`, `cond`)
|
||||||
- Persistent data structures (Vector, Map, Set)
|
|
||||||
- `defmacro` system
|
|
||||||
- Nim/C interop
|
- Nim/C interop
|
||||||
- File operations from REPL
|
- File and Git operations from REPL
|
||||||
- Git operations from REPL
|
- Tree-walking interpreter for fast REPL eval
|
||||||
|
- Atoms, Agents, and core.async channels
|
||||||
|
|
||||||
See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the full roadmap.
|
See [`docs/ROADMAP.md`](docs/ROADMAP.md) for the full roadmap.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,6 @@
|
|||||||
# Clojure/Nim — AI-First Clojure Compiler
|
# Clojure/Nim — AI-First Clojure Compiler
|
||||||
import os, osproc, strutils, times
|
import os, osproc, strutils, times
|
||||||
import reader, emitter, types, repl, macros, deps
|
import reader, emitter, types, repl, macros, deps, ai_assist
|
||||||
|
|
||||||
proc getLibPath(): string =
|
proc getLibPath(): string =
|
||||||
let appDir = getAppDir()
|
let appDir = getAppDir()
|
||||||
@@ -44,7 +44,7 @@ proc extractRequires(forms: seq[CljVal]): seq[(string, string)] =
|
|||||||
alias = req.items[2].symName
|
alias = req.items[2].symName
|
||||||
result.add((libName, alias))
|
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 source = readFile(inputPath)
|
||||||
let forms = reader.readAll(source)
|
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"):
|
f.items[0].kind == ckSymbol and f.items[0].symName == "ns"):
|
||||||
allForms.add(f)
|
allForms.add(f)
|
||||||
|
|
||||||
let nimCode = emitter.emitProgram(allForms)
|
let nimCode = if libMode: emitter.emitProgramLib(allForms) else: emitter.emitProgram(allForms)
|
||||||
writeFile(outputPath, nimCode)
|
writeFile(outputPath, nimCode)
|
||||||
echo "Generated: ", outputPath
|
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()
|
let libPath = getLibPath()
|
||||||
var cmd = "nim c"
|
var cmd = "nim c"
|
||||||
if release:
|
if release:
|
||||||
@@ -109,7 +115,8 @@ proc nimCompile(nimPath: string, binPath: string, release: bool = false): int =
|
|||||||
cmd.add(" --path:" & quoteShell(libPath))
|
cmd.add(" --path:" & quoteShell(libPath))
|
||||||
cmd.add(" -o:" & quoteShell(binPath) & " " & quoteShell(nimPath))
|
cmd.add(" -o:" & quoteShell(binPath) & " " & quoteShell(nimPath))
|
||||||
echo "Compiling: ", cmd
|
echo "Compiling: ", cmd
|
||||||
execCmd(cmd)
|
let (output, exitCode) = execCmdEx(cmd)
|
||||||
|
return (exitCode, output)
|
||||||
|
|
||||||
proc makeTempDir(): string =
|
proc makeTempDir(): string =
|
||||||
let pid = getCurrentProcessId()
|
let pid = getCurrentProcessId()
|
||||||
@@ -143,9 +150,14 @@ proc runFile*(inputPath: string) =
|
|||||||
|
|
||||||
if useCache:
|
if useCache:
|
||||||
echo "Using cached: ", cacheNimPath
|
echo "Using cached: ", cacheNimPath
|
||||||
let compileResult = nimCompile(cacheNimPath, cacheBinPath)
|
let (compileResult, compileOut) = nimCompile(cacheNimPath, cacheBinPath)
|
||||||
if compileResult != 0:
|
if compileResult != 0:
|
||||||
stderr.writeLine("Compilation failed (cached)")
|
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)
|
quit(1)
|
||||||
try: removeDir(buildDir) except: discard
|
try: removeDir(buildDir) except: discard
|
||||||
let runResult = execCmd(cacheBinPath)
|
let runResult = execCmd(cacheBinPath)
|
||||||
@@ -153,9 +165,14 @@ proc runFile*(inputPath: string) =
|
|||||||
|
|
||||||
compileFile(inputPath, nimPath, depPaths)
|
compileFile(inputPath, nimPath, depPaths)
|
||||||
|
|
||||||
let compileResult = nimCompile(nimPath, binPath)
|
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
|
||||||
if compileResult != 0:
|
if compileResult != 0:
|
||||||
stderr.writeLine("Compilation failed")
|
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
|
try: removeDir(buildDir) except: discard
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|
||||||
@@ -188,6 +205,7 @@ proc main() =
|
|||||||
echo " cljnim repl [--json] Start REPL (human or AI mode)"
|
echo " cljnim repl [--json] Start REPL (human or AI mode)"
|
||||||
echo " cljnim deps Resolve dependencies from deps.edn"
|
echo " cljnim deps Resolve dependencies from deps.edn"
|
||||||
echo " cljnim -e '<code>' Evaluate expression"
|
echo " cljnim -e '<code>' Evaluate expression"
|
||||||
|
echo " cljnim ai '<description>' Generate Clojure code with AI"
|
||||||
echo ""
|
echo ""
|
||||||
echo "REPL Commands:"
|
echo "REPL Commands:"
|
||||||
echo " :quit, :q Exit REPL"
|
echo " :quit, :q Exit REPL"
|
||||||
@@ -215,15 +233,19 @@ proc main() =
|
|||||||
let forms = reader.readAll(code)
|
let forms = reader.readAll(code)
|
||||||
let nimCode = emitter.emitProgram(forms)
|
let nimCode = emitter.emitProgram(forms)
|
||||||
writeFile(nimPath, nimCode)
|
writeFile(nimPath, nimCode)
|
||||||
let compileResult = nimCompile(nimPath, binPath)
|
let (compileResult, compileOut) = nimCompile(nimPath, binPath)
|
||||||
if compileResult != 0:
|
if compileResult != 0:
|
||||||
stderr.writeLine("Compilation failed")
|
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
|
try: removeDir(buildDir) except: discard
|
||||||
quit(1)
|
quit(1)
|
||||||
let runResult = execCmd(binPath)
|
let runResult = execCmd(binPath)
|
||||||
try: removeDir(buildDir) except: discard
|
try: removeDir(buildDir) except: discard
|
||||||
quit(runResult)
|
quit(runResult)
|
||||||
|
|
||||||
case cmd
|
case cmd
|
||||||
of "compile":
|
of "compile":
|
||||||
if args.len < 2:
|
if args.len < 2:
|
||||||
@@ -232,6 +254,14 @@ proc main() =
|
|||||||
let inputPath = args[1]
|
let inputPath = args[1]
|
||||||
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
|
let outputPath = if args.len >= 3: args[2] else: inputPath.changeFileExt("nim")
|
||||||
compileFile(inputPath, outputPath)
|
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":
|
of "run":
|
||||||
if args.len < 2:
|
if args.len < 2:
|
||||||
@@ -298,6 +328,24 @@ proc main() =
|
|||||||
else:
|
else:
|
||||||
echo "No dependencies to resolve"
|
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:
|
else:
|
||||||
stderr.writeLine("Unknown command: " & cmd)
|
stderr.writeLine("Unknown command: " & cmd)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|||||||
+30
-11
@@ -1,6 +1,6 @@
|
|||||||
# AI-First REPL for Clojure/Nim
|
# AI-First REPL for Clojure/Nim
|
||||||
import os, osproc, strutils, json, times, net
|
import os, osproc, strutils, json, times, net
|
||||||
import reader, emitter, types, macros, eval
|
import reader, emitter, types, macros, eval, ai_assist
|
||||||
|
|
||||||
type
|
type
|
||||||
ReplMode* = enum
|
ReplMode* = enum
|
||||||
@@ -193,13 +193,18 @@ proc evalForm*(state: var ReplState, formStr: string): JsonNode =
|
|||||||
let elapsed = (epochTime() - startTime) * 1000
|
let elapsed = (epochTime() - startTime) * 1000
|
||||||
|
|
||||||
if not ok:
|
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 %*{
|
return %*{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": {
|
"error": errObj,
|
||||||
"type": "compiler/error",
|
|
||||||
"message": error,
|
|
||||||
"form": formStr
|
|
||||||
},
|
|
||||||
"meta": {
|
"meta": {
|
||||||
"ns": state.ns,
|
"ns": state.ns,
|
||||||
"ms": elapsed
|
"ms": elapsed
|
||||||
@@ -262,6 +267,7 @@ proc runHumanRepl*(state: var ReplState) =
|
|||||||
echo ":defs List defined vars"
|
echo ":defs List defined vars"
|
||||||
echo ":clear Clear all definitions"
|
echo ":clear Clear all definitions"
|
||||||
echo ":ns Show current namespace"
|
echo ":ns Show current namespace"
|
||||||
|
echo ":ai <desc> Ask AI to generate Clojure code"
|
||||||
of ":defs":
|
of ":defs":
|
||||||
for d in state.defs:
|
for d in state.defs:
|
||||||
if d.kind == ckList and d.items.len > 1 and d.items[1].kind == ckSymbol:
|
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":
|
of ":ns":
|
||||||
echo state.ns
|
echo state.ns
|
||||||
else:
|
else:
|
||||||
let res = evalForm(state, cmd)
|
if cmd.startsWith(":ai "):
|
||||||
if res["status"].getStr() == "ok":
|
let description = cmd[4..^1].strip()
|
||||||
echo "=> " & res["result"]["printed"].getStr()
|
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:
|
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) =
|
proc runJsonRepl*(state: var ReplState) =
|
||||||
var readyMsg = %*{ "status": "ready", "ns": state.ns, "mode": "json" }
|
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"
|
var readyMsg = $(%*{ "status": "ready", "ns": state.ns, "mode": "nrepl" }) & "\n"
|
||||||
client.send(readyMsg)
|
client.send(readyMsg)
|
||||||
|
|
||||||
var buf = ""
|
|
||||||
while true:
|
while true:
|
||||||
try:
|
try:
|
||||||
var line = ""
|
var line = ""
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Tests for AI assistance module (no real API calls)
|
||||||
|
|
||||||
|
import unittest, strutils
|
||||||
|
import os
|
||||||
|
import ai_assist
|
||||||
|
|
||||||
|
suite "AI Config Detection":
|
||||||
|
test "detectConfig returns empty when no env vars set":
|
||||||
|
# Note: this test assumes no API keys are set in test environment
|
||||||
|
# If keys ARE set, it will detect them
|
||||||
|
let cfg = detectConfig()
|
||||||
|
# At minimum, the function should not crash
|
||||||
|
check cfg.timeoutMs >= 0
|
||||||
|
|
||||||
|
test "hasAiConfig returns false when no keys set (if env empty)":
|
||||||
|
let key = getEnv("DEEPSEEK_API_KEY", "")
|
||||||
|
let okey = getEnv("OPENAI_API_KEY", "")
|
||||||
|
let mkey = getEnv("MIMO_API_KEY", "")
|
||||||
|
if key.len == 0 and okey.len == 0 and mkey.len == 0:
|
||||||
|
check hasAiConfig() == false
|
||||||
|
|
||||||
|
suite "Prompt Building":
|
||||||
|
test "buildErrorPrompt includes filename and source":
|
||||||
|
let prompt = buildErrorPrompt("type mismatch", "(defn foo [x] x)", "test.clj")
|
||||||
|
check "test.clj" in prompt
|
||||||
|
check "type mismatch" in prompt
|
||||||
|
check "(defn foo [x] x)" in prompt
|
||||||
|
|
||||||
|
test "buildGenerationPrompt includes description":
|
||||||
|
let prompt = buildGenerationPrompt("reverse a list")
|
||||||
|
check "reverse a list" in prompt
|
||||||
|
check "Clojure" in prompt
|
||||||
|
|
||||||
|
suite "Response Formatting":
|
||||||
|
test "formatSuggestion shows error when not ok":
|
||||||
|
let resp = AiResponse(ok: false, suggestion: "No key configured")
|
||||||
|
let formatted = formatSuggestion(resp)
|
||||||
|
check "💡 AI:" in formatted
|
||||||
|
check "No key configured" in formatted
|
||||||
|
|
||||||
|
test "formatSuggestion shows suggestion when ok":
|
||||||
|
let resp = AiResponse(ok: true, suggestion: "Use (reverse coll)")
|
||||||
|
let formatted = formatSuggestion(resp)
|
||||||
|
check "💡 AI Suggestion:" in formatted
|
||||||
|
check "Use (reverse coll)" in formatted
|
||||||
Reference in New Issue
Block a user