Skip to content

Commit dda0fea

Browse files
author
Bruce Hauman
committed
Add prompt-cli for AI-powered command-line interaction
Introduces a new CLI that enables users to interact with AI agents that have access to all Clojure MCP tools via nREPL. Key features: - Command-line interface for sending prompts to AI agents - Automatic nREPL connection and environment detection - Support for custom agent configurations and models - Access to all MCP tools (file operations, REPL eval, code search) - Project context loading (code index and summaries) - Configurable via .clojure-mcp/config.edn Components added: - prompt_cli.clj: Main CLI implementation with arg parsing - chat_listener.clj: Streaming chat listener for agent responses - message_conv.clj: Message conversion utilities - tool_format.clj: Custom formatting for tool requests/results - Comprehensive documentation in doc/prompt-cli.md Usage: clojure -M:prompt-cli -p "Your prompt here"
1 parent c890891 commit dda0fea

File tree

14 files changed

+838
-27
lines changed

14 files changed

+838
-27
lines changed

deps.edn

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
nrepl/nrepl {:mvn/version "1.3.1"}
55
org.clojure/tools.logging {:mvn/version "1.3.0"}
66
org.clojure/tools.cli {:mvn/version "1.1.230"}
7+
org.clj-commons/pretty {:mvn/version "3.6.7"}
78

89
;; for prompt templating
910
pogonos/pogonos {:mvn/version "0.2.1"}
@@ -69,6 +70,10 @@
6970
:exec-args {:port 7888 :shadow-build "app" :shadow-port 7889}}
7071

7172
;; below are dev set ups that need a logback.xml file
73+
:prompt-cli
74+
{:extra-deps {org.slf4j/slf4j-nop {:mvn/version "2.0.16"}}
75+
:main-opts ["-m" "clojure-mcp.prompt-cli"]}
76+
7277
:dev-mcp
7378
{:extra-deps {ch.qos.logback/logback-classic {:mvn/version "1.4.14"}}
7479
:extra-paths ["dev" "test"]

doc/prompt-cli.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Clojure MCP Prompt CLI
2+
3+
A command-line interface for interacting with an AI agent that has access to all Clojure MCP tools.
4+
5+
## Prerequisites
6+
7+
- A running nREPL server (default port 7888, configurable)
8+
- Configured API keys for the chosen model (Anthropic, OpenAI, etc.)
9+
10+
## Usage
11+
12+
Start your nREPL server:
13+
```bash
14+
clojure -M:nrepl
15+
```
16+
17+
In another terminal, run the CLI:
18+
```bash
19+
clojure -M:prompt-cli -p "Your prompt here"
20+
```
21+
22+
## Options
23+
24+
- `-p, --prompt PROMPT` - The prompt to send to the agent (required)
25+
- `-m, --model MODEL` - Override the default model (e.g., `:openai/gpt-4`, `:anthropic/claude-3-5-sonnet`)
26+
- `-c, --config CONFIG` - Path to a custom agent configuration file (optional)
27+
- `-d, --dir DIRECTORY` - Working directory (defaults to REPL's working directory)
28+
- `-P, --port PORT` - nREPL server port (default: 7888)
29+
- `-h, --help` - Show help message
30+
31+
## Examples
32+
33+
Basic usage with default model:
34+
```bash
35+
clojure -M:prompt-cli -p "What namespaces are available?"
36+
```
37+
38+
Use a specific model:
39+
```bash
40+
clojure -M:prompt-cli -p "Evaluate (+ 1 2)" -m :openai/gpt-4
41+
```
42+
43+
Create code:
44+
```bash
45+
clojure -M:prompt-cli -p "Create a fibonacci function"
46+
```
47+
48+
Use a custom agent configuration:
49+
```bash
50+
clojure -M:prompt-cli -p "Analyze this project" -c my-custom-agent.edn
51+
```
52+
53+
Connect to a different nREPL port:
54+
```bash
55+
clojure -M:prompt-cli -p "Run tests" -P 8888
56+
```
57+
58+
Specify a working directory:
59+
```bash
60+
clojure -M:prompt-cli -p "List files" -d /path/to/project
61+
```
62+
63+
## Configuration
64+
65+
The CLI properly initializes the nREPL connection with:
66+
- Automatic detection of the working directory from the REPL
67+
- Loading of `.clojure-mcp/config.edn` from the working directory
68+
- Environment detection and initialization (Clojure, ClojureScript, etc.)
69+
- Loading of REPL helper functions
70+
71+
## Default Agent Configuration
72+
73+
By default, the CLI uses the `parent-agent-config` which includes:
74+
- The Clojure REPL system prompt
75+
- Access to all available tools
76+
- Project context (code index and summary)
77+
- Stateless memory (each invocation is independent)
78+
79+
## Custom Agent Configuration
80+
81+
You can create a custom agent configuration file in EDN format:
82+
83+
```clojure
84+
{:id :my-agent
85+
:name "my_agent"
86+
:description "My custom agent"
87+
:system-message "Your system prompt here..."
88+
:context true ; Include project context
89+
:enable-tools [:read_file :clojure_eval :grep] ; Specific tools or [:all]
90+
:memory-size 100 ; Or false for stateless
91+
:model :anthropic/claude-3-5-sonnet-20241022}
92+
```
93+
94+
## Environment Variables
95+
96+
Set `DEBUG=1` to see stack traces on errors:
97+
```bash
98+
DEBUG=1 clojure -M:prompt-cli -p "Your prompt"
99+
```
100+
101+
## Model Configuration
102+
103+
Models can be configured in `.clojure-mcp/config.edn`:
104+
```clojure
105+
{:models {:openai/my-gpt4 {:model-name "gpt-4"
106+
:temperature 0.3
107+
:api-key [:env "OPENAI_API_KEY"]}}}
108+
```
109+
110+
Then use with:
111+
```bash
112+
clojure -M:prompt-cli -p "Your prompt" -m :openai/my-gpt4
113+
```
114+
115+
## Tool Configuration
116+
117+
The agent has access to all tools by default, which are filtered based on the project's `.clojure-mcp/config.edn` settings:
118+
- `enable-tools` and `disable-tools` settings are respected
119+
- Tool-specific configurations from `tools-config` are applied
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
;; Example custom agent configuration for prompt-cli
2+
;; Save this as custom-agent.edn and use with:
3+
;; clojure -M:prompt-cli -p "Your prompt" -c custom-agent.edn
4+
5+
{:id :custom-agent
6+
:name "custom_agent"
7+
:description "Custom agent with limited tools for focused tasks"
8+
9+
;; System message can be a string or a path to a resource file
10+
:system-message "You are a helpful Clojure assistant focused on code analysis.
11+
Focus on understanding and explaining code rather than making changes.
12+
Be concise and clear in your explanations."
13+
14+
;; Context controls whether to include project summary and code index
15+
:context false ; Set to true to include project context
16+
17+
;; Specify which tools the agent can use
18+
;; Use [:all] for all tools, or list specific tool IDs
19+
:enable-tools [:read_file
20+
:grep
21+
:glob_files
22+
:clojure_inspect_project
23+
:think]
24+
25+
;; Memory configuration
26+
;; false = stateless (each invocation is independent)
27+
;; number = conversation window size
28+
:memory-size false
29+
30+
;; Optional: specify a model (can be overridden with -m flag)
31+
;; :model :anthropic/claude-3-5-sonnet-20241022
32+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
(ns clojure-mcp.agent.langchain.chat-listener
2+
"Creates ChatModelListener instances from Clojure functions that work with EDN data"
3+
(:require
4+
[clojure-mcp.agent.langchain.message-conv :as msg-conv]
5+
[clojure.tools.logging :as log])
6+
(:import
7+
[dev.langchain4j.model.chat.listener
8+
ChatModelListener
9+
ChatModelRequestContext
10+
ChatModelResponseContext
11+
ChatModelErrorContext]
12+
[dev.langchain4j.model.chat.request
13+
ChatRequestParameters]
14+
[dev.langchain4j.model.chat.response
15+
ChatResponseMetadata]
16+
[dev.langchain4j.model.output
17+
TokenUsage]))
18+
19+
(defn- chat-request-parameters->edn
20+
"Convert ChatRequestParameters to EDN"
21+
[^ChatRequestParameters params]
22+
(when params
23+
{:model-name (.modelName params)
24+
:temperature (.temperature params)
25+
:top-p (.topP params)
26+
:top-k (.topK params)
27+
:frequency-penalty (.frequencyPenalty params)
28+
:presence-penalty (.presencePenalty params)
29+
:max-output-tokens (.maxOutputTokens params)
30+
:stop-sequences (some-> (.stopSequences params) vec)
31+
:tool-specifications (some-> (.toolSpecifications params) vec)
32+
:tool-choice (.toolChoice params)
33+
:response-format (.responseFormat params)}))
34+
35+
(defn- token-usage->edn
36+
"Convert TokenUsage to EDN"
37+
[^TokenUsage usage]
38+
(when usage
39+
{:input-token-count (.inputTokenCount usage)
40+
:output-token-count (.outputTokenCount usage)
41+
:total-token-count (.totalTokenCount usage)}))
42+
43+
(defn- chat-response-metadata->edn
44+
"Convert ChatResponseMetadata to EDN"
45+
[^ChatResponseMetadata metadata]
46+
(when metadata
47+
{:id (.id metadata)
48+
:model-name (.modelName metadata)
49+
:finish-reason (some-> (.finishReason metadata) str)
50+
:token-usage (token-usage->edn (.tokenUsage metadata))}))
51+
52+
(defn- chat-request-context->edn
53+
"Convert ChatModelRequestContext to EDN"
54+
[^ChatModelRequestContext ctx]
55+
(let [chat-request (.chatRequest ctx)]
56+
{:messages (-> (.messages chat-request)
57+
msg-conv/messages->edn
58+
msg-conv/parse-messages-tool-arguments)
59+
:parameters (chat-request-parameters->edn (.parameters chat-request))
60+
:model-provider (str (.modelProvider ctx))
61+
:attributes (into {} (.attributes ctx))
62+
:ctx ctx}))
63+
64+
(defn- chat-response-context->edn
65+
"Convert ChatModelResponseContext to EDN"
66+
[^ChatModelResponseContext ctx]
67+
(let [chat-response (.chatResponse ctx)
68+
chat-request (.chatRequest ctx)]
69+
{:ai-message (-> (.aiMessage chat-response)
70+
msg-conv/message->edn
71+
msg-conv/parse-tool-arguments)
72+
:metadata (chat-response-metadata->edn (.metadata chat-response))
73+
:request {:messages (msg-conv/messages->edn (.messages chat-request))
74+
:parameters (chat-request-parameters->edn (.parameters chat-request))}
75+
:model-provider (str (.modelProvider ctx))
76+
:attributes (into {} (.attributes ctx))
77+
:ctx ctx}))
78+
79+
(defn- chat-error-context->edn
80+
"Convert ChatModelErrorContext to EDN"
81+
[^ChatModelErrorContext ctx]
82+
(let [chat-request (.chatRequest ctx)]
83+
{:error {:message (.getMessage (.error ctx))
84+
:class (-> (.error ctx) class .getName)
85+
:stack-trace (mapv str (.getStackTrace (.error ctx)))}
86+
:request (when chat-request
87+
{:messages (msg-conv/messages->edn (.messages chat-request))
88+
:parameters (chat-request-parameters->edn (.parameters chat-request))})
89+
:model-provider (str (.modelProvider ctx))
90+
:attributes (into {} (.attributes ctx))}))
91+
92+
(defn create-listener
93+
"Create a ChatModelListener from Clojure functions.
94+
95+
Args:
96+
- handlers: Map with optional keys:
97+
:on-request - (fn [request-edn] ...) called before sending request
98+
:on-response - (fn [response-edn] ...) called after receiving response
99+
:on-error - (fn [error-edn] ...) called on errors
100+
101+
Each handler receives EDN data converted from the Java objects.
102+
103+
Example:
104+
(create-listener
105+
{:on-request (fn [req] (log/info \"Request:\" req))
106+
:on-response (fn [resp] (log/info \"Response:\" resp))
107+
:on-error (fn [err] (log/error \"Error:\" err))})"
108+
[{:keys [on-request on-response on-error]}]
109+
(reify ChatModelListener
110+
(onRequest [_ request-context]
111+
(when on-request
112+
(try
113+
(on-request (chat-request-context->edn request-context))
114+
(catch Exception e
115+
#_(log/error e "Error in on-request handler")))))
116+
117+
(onResponse [_ response-context]
118+
(when on-response
119+
(try
120+
(on-response (chat-response-context->edn response-context))
121+
(catch Exception e
122+
#_(log/error e "Error in on-response handler")))))
123+
124+
(onError [_ error-context]
125+
(when on-error
126+
(try
127+
(on-error (chat-error-context->edn error-context))
128+
(catch Exception e
129+
#_(log/error e "Error in on-error handler")))))))
130+
131+
(defn logging-listener
132+
"Create a listener that logs all events at specified levels.
133+
134+
Args (optional):
135+
- log-level: Map with :request, :response, :error levels
136+
(defaults to :info for all)
137+
138+
Example:
139+
(logging-listener {:request :debug :response :info :error :error})"
140+
([]
141+
(logging-listener {}))
142+
([{:keys [request response error]
143+
:or {request :info response :info error :error}}]
144+
(create-listener
145+
{:on-request (fn [req]
146+
(log/log request "Chat request:" req))
147+
:on-response (fn [resp]
148+
(log/log response "Chat response:" resp))
149+
:on-error (fn [err]
150+
(log/log error "Chat error:" err))})))
151+
152+
(defn token-tracking-listener
153+
"Create a listener that tracks token usage.
154+
155+
Args:
156+
- usage-atom: Atom to accumulate token usage stats
157+
158+
The atom will be updated with:
159+
{:total-input-tokens n
160+
:total-output-tokens n
161+
:total-tokens n
162+
:request-count n}"
163+
[usage-atom]
164+
(create-listener
165+
{:on-response
166+
(fn [resp]
167+
(when-let [usage (get-in resp [:metadata :token-usage])]
168+
(swap! usage-atom
169+
(fn [stats]
170+
(-> stats
171+
(update :total-input-tokens (fnil + 0) (:input-token-count usage))
172+
(update :total-output-tokens (fnil + 0) (:output-token-count usage))
173+
(update :total-tokens (fnil + 0) (:total-token-count usage))
174+
(update :request-count (fnil inc 0)))))))}))

0 commit comments

Comments
 (0)