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_deps
|
||||
test_eval
|
||||
test_macros
|
||||
test_ai_assist
|
||||
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
|
||||
|
||||
build:
|
||||
nim c -o:cljnim src/cljnim.nim
|
||||
nim c -d:ssl -o:cljnim src/cljnim.nim
|
||||
|
||||
test:
|
||||
nim c -r tests/test_reader.nim
|
||||
@@ -11,7 +11,9 @@ test:
|
||||
nim c -r tests/test_pvec.nim
|
||||
nim c -r tests/test_pmap.nim
|
||||
nim c -r tests/test_deps.nim
|
||||
nim c -r tests/test_macros.nim
|
||||
nim c -r tests/test_eval.nim
|
||||
nim c --path:src -r tests/test_ai_assist.nim
|
||||
|
||||
test-reader:
|
||||
nim c -r tests/test_reader.nim
|
||||
@@ -19,6 +21,13 @@ test-reader:
|
||||
test-emitter:
|
||||
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
|
||||
./cljnim run examples/hello.clj
|
||||
|
||||
@@ -36,3 +45,8 @@ clean:
|
||||
find . -name "test_reader" -delete
|
||||
find . -name "test_emitter" -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.
|
||||
|
||||
## 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
|
||||
|
||||
```
|
||||
@@ -131,14 +183,25 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for details.
|
||||
│ ├── reader.nim # Clojure reader / EDN parser
|
||||
│ ├── emitter.nim # Nim code generator
|
||||
│ ├── repl.nim # Human & AI REPL
|
||||
│ ├── eval.nim # Tree-walking interpreter
|
||||
│ ├── deps.nim # Dependency resolution
|
||||
│ ├── core.nim # Core runtime functions
|
||||
│ ├── types.nim # Clojure value types
|
||||
│ ├── macros.nim # Macro expansion engine
|
||||
│ └── runtime.nim # Runtime implementation
|
||||
├── 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/
|
||||
│ ├── hello.clj
|
||||
│ └── math.clj
|
||||
│ ├── math.clj
|
||||
│ ├── core.clj
|
||||
│ ├── macros.clj
|
||||
│ ├── ffi.clj
|
||||
│ ├── interop.clj
|
||||
│ └── ai_tools.clj
|
||||
├── docs/ # Documentation (EN + BG)
|
||||
├── tests/
|
||||
├── Makefile
|
||||
@@ -148,19 +211,18 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for details.
|
||||
## Supported Features
|
||||
|
||||
### 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`
|
||||
- Arithmetic: `+`, `-`, `*`, `/`, `=`, `<`, `>`
|
||||
- Functions: `println`
|
||||
- Functions: `println`, `map`, `filter`, `reduce`
|
||||
- Human REPL and JSON REPL
|
||||
- AOT compilation to native binaries
|
||||
|
||||
### In Progress 🔄
|
||||
- Persistent data structures (Vector, Map, Set)
|
||||
- `defmacro` system
|
||||
- Persistent data structures (Vector, Map, Set) via HAMT
|
||||
- Macro system (`defmacro`, `->`, `->>`, `and`, `or`, `when`, `cond`)
|
||||
- Nim/C interop
|
||||
- File operations from REPL
|
||||
- Git operations from REPL
|
||||
- File and 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.
|
||||
|
||||
|
||||
@@ -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
|
||||
+56
-8
@@ -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,9 +233,13 @@ 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)
|
||||
@@ -233,6 +255,14 @@ proc main() =
|
||||
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:
|
||||
stderr.writeLine("Error: Missing input file")
|
||||
@@ -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
@@ -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 = ""
|
||||
|
||||
@@ -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