Skip to content

Commit 0d46831

Browse files
Add a new markdown parser for single prompts
* adds markdown parsing * adds new jsonrpc methods for errors
1 parent e238f17 commit 0d46831

File tree

8 files changed

+417
-84
lines changed

8 files changed

+417
-84
lines changed

README.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,68 @@ Try running the with the `--jsonrpc` to see a full example but the stdout stream
151151
{"jsonrpc":"2.0","method":"functions-done","params":null}Content-Length: 1703
152152
```
153153

154-
### Prompt file layout
154+
### Notification Methods
155+
156+
#### message
157+
158+
This is a message from the assitant role, or from a tool role.
159+
The params for the `message` method should be appended to the conversation. The `params` can be either
160+
`content` or `debug`.
161+
162+
```json
163+
{"params": {"content": "append this output to the current message"}}
164+
{"params": {"debug": "this is a debug message and should only be shown in debug mode"}}
165+
```
166+
167+
#### prompts
168+
169+
Generated user and system prompts are sent to the client so that they can be displayed. These
170+
are sent after extractors are expanded so that users can see the actual prompts sent to the AI model.
171+
172+
```json
173+
{"params": {"messages": [{"role": "system", "content": "system prompt message"}]}}
174+
```
175+
176+
#### functions
177+
178+
Functions are json encoded strings. When streaming, the content of the json params will change as
179+
the functions streams. This can be rendered in place to show the function definition completing
180+
as it streams.
181+
182+
```json
183+
{"params": "{}"}
184+
```
185+
186+
#### functions-done
187+
188+
This notification is sent when a function definition has stopped streaming, and is now being executed.
189+
The next notification after this will be a tool message.
190+
191+
```json
192+
{"params": ""}
193+
```
194+
195+
#### error
196+
197+
The `error` notification is not a message from the model, prompts, or tools. Instead, it represents a kind
198+
of _system_ error trying to run the conversation loop. It should always be shown to the user as it
199+
probably represents something like a networking error or a configuration problem.
200+
201+
```json
202+
{"params": {"content": "error message"}}
203+
```
204+
205+
### Request Methods
206+
207+
#### prompt
208+
209+
Send a user prompt into the converstation loop. The `prompt` method takes the following `params`.
210+
211+
```json
212+
{"params": {"content": "here is the user prompt"}}
213+
```
214+
215+
## Prompt file layout
155216

156217
Each prompt directory should contain a README.md describing the prompts and their purpose. Each prompt file
157218
is a markdown document that supports moustache templates for subsituting context extracted from the project.

runbook.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,28 @@ bb -m prompts run /Users/slim/docker/labs-make-runbook jimclark106 darwin prompt
4848
```
4949

5050
```sh
51-
bb -m prompts run /Users/slim/docker/labs-make-runbook jimclark106 darwin prompts/project_type --nostream \
51+
bb -m prompts run \
52+
--host-dir /Users/slim/docker/labs-make-runbook \
53+
--user jimclark106 \
54+
--platform darwin \
55+
--prompts-dir prompts/project_type \
56+
--nostream \
57+
--model "llama3.1" \
58+
--url http://localhost:11434/v1/chat/completions
59+
```
60+
61+
```sh
62+
bb -m prompts run \
63+
--host-dir /Users/slim/docker/labs-make-runbook \
64+
--user jimclark106 \
65+
--platform darwin \
66+
--prompts-dir prompts \
67+
--nostream \
5268
--model "llama3.1" \
5369
--url http://localhost:11434/v1/chat/completions
5470
```
5571

72+
5673
#### test prompts/dockerfiles (which uses prompts/project_type)
5774

5875
Now, verify that the prompts/dockerfiles prompts work with `gpt-4`.

src/docker.clj

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
[clojure.string :as string]
88
[creds])
99
(:import
10-
[java.util Base64]))
10+
[java.net UnixDomainSocketAddress]
11+
[java.nio ByteBuffer]
12+
[java.nio.channels SocketChannel]
13+
[java.util Arrays Base64]))
1114

1215
(defn encode [to-encode]
1316
(.encodeToString (Base64/getEncoder) (.getBytes to-encode)))
@@ -26,17 +29,17 @@
2629
(curl/post
2730
(format "http://localhost/images/create?fromImage=%s" image)
2831
(merge
29-
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
30-
:throw false}
31-
(when (or creds identity-token)
32-
{:headers {"X-Registry-Auth"
32+
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
33+
:throw false}
34+
(when (or creds identity-token)
35+
{:headers {"X-Registry-Auth"
3336
;; I don't think we'll be pulling images
3437
;; from registries that support identity tokens
35-
(-> (cond
36-
identity-token {:identitytoken identity-token}
37-
creds creds)
38-
(json/generate-string)
39-
(encode))}}))))
38+
(-> (cond
39+
identity-token {:identitytoken identity-token}
40+
creds creds)
41+
(json/generate-string)
42+
(encode))}}))))
4043

4144
(comment
4245
(let [pat (string/trim (slurp "/Users/slim/.secrets/dockerhub-pat-ai-tools-for-devs.txt"))]
@@ -69,11 +72,14 @@
6972
;; entrypoint is an array of strings
7073
;; env is a map
7174
;; Env is an array of name=value strings
72-
(defn create-container [{:keys [image entrypoint command host-dir env thread-id]}]
75+
;; Tty wraps the process in a pseudo terminal
76+
{:StdinOnce true
77+
:OpenStdin true}
78+
(defn create-container [{:keys [image entrypoint command host-dir env thread-id opts] :or {opts {:Tty true}}}]
7379
(let [payload (json/generate-string
7480
(merge
75-
{:Image image
76-
:Tty true}
81+
{:Image image}
82+
opts
7783
(when env {:env (->> env
7884
(map (fn [[k v]] (format "%s=%s" (name k) v)))
7985
(into []))})
@@ -132,13 +138,27 @@
132138
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
133139
:throw false}))
134140

141+
(defn attach-container-stdout-logs [{:keys [Id]}]
142+
;; this assumes no Tty so the output will be multiplexed back
143+
(curl/post
144+
(format "http://localhost/containers/%s/attach?stdout=true&logs=true" Id)
145+
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
146+
:as :bytes
147+
:throw false}))
148+
135149
;; should be 200 and then will have a StatusCode
136150
(defn wait-container [{:keys [Id]}]
137151
(curl/post
138152
(format "http://localhost/containers/%s/wait?condition=not-running" Id)
139153
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
140154
:throw false}))
141155

156+
(defn kill-container [{:keys [Id]}]
157+
(curl/post
158+
(format "http://localhost/containers/%s/kill" Id)
159+
{:raw-args ["--unix-socket" "/var/run/docker.sock"]
160+
:throw false}))
161+
142162
(defn ->json [response]
143163
(json/parse-string (:body response) keyword))
144164

@@ -196,7 +216,62 @@
196216

197217
(def extract-facts run-function)
198218

219+
(defn write-stdin [container-id content]
220+
(let [buf (ByteBuffer/allocate 1024)
221+
address (UnixDomainSocketAddress/of "/var/run/docker.sock")
222+
client (SocketChannel/open address)]
223+
224+
;;(while (not (.finishConnect client)))
225+
226+
(.configureBlocking client true)
227+
(.clear buf)
228+
(.put buf (.getBytes (String. (format "POST /containers/%s/attach?stdin=true&stream=true HTTP/1.1\n" container-id))))
229+
(.put buf (.getBytes (String. "Host: localhost\nConnection: Upgrade\nUpgrade: tcp\n\n")))
230+
(.put buf (.getBytes content))
231+
(.flip buf)
232+
233+
(while (.hasRemaining buf)
234+
(.write client buf))
235+
client))
236+
237+
(defn docker-stream-format->stdout [bytes]
238+
;; use xxd to look at the bytes
239+
#_(try
240+
(with-open [w (java.io.BufferedOutputStream.
241+
(java.io.FileOutputStream. "hey.txt"))]
242+
(.write w bytes))
243+
244+
(catch Throwable t
245+
(println t)))
246+
(String. (Arrays/copyOfRange bytes 8 (count bytes))))
247+
248+
(defn function-call-with-stdin [m]
249+
(let [x (merge
250+
m
251+
(create (assoc m
252+
:opts {:StdinOnce true
253+
:OpenStdin true
254+
:AttachStdin true})))]
255+
(start x)
256+
(assoc x :socket (write-stdin (:Id x) (:content m)))))
257+
258+
(defn finish-call
259+
"This is a blocking call that waits for the container to finish and then returns the output and exit code."
260+
[x]
261+
(.close (:socket x))
262+
(wait x)
263+
;; body is raw PTY output
264+
(let [s (docker-stream-format->stdout
265+
(:body
266+
(attach-container-stdout-logs x)) )
267+
info (inspect x)]
268+
(delete x)
269+
{:pty-output s
270+
:exit-code (-> info :State :ExitCode)
271+
:info info}))
272+
199273
(comment
274+
200275
(pprint
201276
(json/parse-string
202277
(extract-facts

src/jsonrpc.clj

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,93 @@
1-
(ns jsonrpc
1+
(ns jsonrpc
22
(:require
33
[cheshire.core :as json]
4-
[clojure.java.io :as io])
4+
[clojure.core.async :as async]
5+
[clojure.java.io :as io]
6+
[clojure.string :as string])
57
(:import
6-
[java.io OutputStream]))
8+
[java.io
9+
EOFException
10+
IOException
11+
InputStream
12+
OutputStream]))
713

814
(def ^:private write-lock (Object.))
915

16+
(defn ^:private read-n-bytes [^InputStream input content-length charset-s]
17+
(let [buffer (byte-array content-length)]
18+
(loop [total-read 0]
19+
(when (< total-read content-length)
20+
(let [new-read (.read input buffer total-read (- content-length total-read))]
21+
(when (< new-read 0)
22+
;; TODO: return nil instead?
23+
(throw (EOFException.)))
24+
(recur (+ total-read new-read)))))
25+
(String. ^bytes buffer ^String charset-s)))
26+
27+
(defn ^:private parse-header [line headers]
28+
(let [[h v] (string/split line #":\s*" 2)]
29+
(assoc headers h v)))
30+
31+
(defn ^:private parse-charset [content-type]
32+
(or (when content-type
33+
(when-let [[_ charset] (re-find #"(?i)charset=(.*)$" content-type)]
34+
(when (not= "utf8" charset)
35+
charset)))
36+
"utf-8"))
37+
38+
(defn ^:private read-message [input headers keyword-function]
39+
(try
40+
(let [content-length (Long/valueOf ^String (get headers "Content-Length"))
41+
charset-s (parse-charset (get headers "Content-Type"))
42+
content (read-n-bytes input content-length charset-s)
43+
m (json/parse-string content keyword-function)]
44+
;; even if the params should not be transformed to keywords,
45+
;; the top-level keywords still must be transformed
46+
(cond-> m
47+
(get m "id") (assoc :id (get m "id"))
48+
(get m "jsonrpc") (assoc :jsonrpc (get m "jsonrpc"))
49+
(get m "method") (assoc :method (get m "method"))
50+
(get m "params") (assoc :params (get m "params"))
51+
(get m "error") (assoc :error (get m "error"))
52+
(get m "result") (assoc :result (get m "result"))))
53+
(catch Exception _
54+
:parse-error)))
55+
56+
(defn ^:private read-header-line
57+
"Reads a line of input. Blocks if there are no messages on the input."
58+
[^InputStream input]
59+
(try
60+
(let [s (java.lang.StringBuilder.)]
61+
(loop []
62+
(let [b (.read input)] ;; blocks, presumably waiting for next message
63+
(case b
64+
-1 ::eof ;; end of stream
65+
#_lf 10 (str s) ;; finished reading line
66+
#_cr 13 (recur) ;; ignore carriage returns
67+
(do (.append s (char b)) ;; byte == char because header is in US-ASCII
68+
(recur))))))
69+
(catch IOException _e
70+
::eof)))
71+
72+
(defn input-stream->input-chan [input {:keys [close? keyword-function]
73+
:or {close? true, keyword-function keyword}}]
74+
(let [input (io/input-stream input)
75+
messages (async/chan 1)]
76+
(async/thread
77+
(loop [headers {}]
78+
(let [line (read-header-line input)]
79+
(cond
80+
;; input closed; also close channel
81+
(= line ::eof) (async/close! messages)
82+
;; a blank line after the headers indicates start of message
83+
(string/blank? line) (if (async/>!! messages (read-message input headers keyword-function))
84+
;; wait for next message
85+
(recur {})
86+
;; messages closed
87+
(when close? (.close input)))
88+
:else (recur (parse-header line headers))))))
89+
messages))
90+
1091
(defn ^:private write-message [^OutputStream output msg]
1192
(let [content (json/generate-string msg)
1293
content-bytes (.getBytes content "utf-8")]
@@ -28,22 +109,22 @@
28109
;; prompts({:messages [{:role "", :content ""}]})
29110
;; functions("") - meant to be updated in place
30111
;; functions-done("")
31-
(defn -notify [{:keys [debug]} method params]
32-
(case method
33-
:message (write-message (io/output-stream System/out) (notification method params))
34-
:prompts (write-message (io/output-stream System/out) (notification method params))
35-
:functions (write-message (io/output-stream System/out) (notification method params))
36-
:functions-done (write-message (io/output-stream System/out) (notification method params))))
112+
;; error({:content ""})
113+
(defn -notify [{:keys [_debug]} method params]
114+
(write-message (io/output-stream System/out) (notification method params)))
37115

38116
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
39117
(defn -println [{:keys [debug]} method params]
40118
(case method
41-
:message (cond
119+
:message (cond
42120
(:content params) (do (print (:content params)) (flush))
43121
(and debug (:debug params)) (do (println "### DEBUG\n") (println (:debug params))))
44122
:functions (do (print ".") (flush))
45123
:functions-done (println params)
46-
:prompts nil))
124+
:error (binding [*out* *err*]
125+
(println (:content params)))
126+
:prompts nil
127+
(binding [*out* *err*] (println (format "%s\n%s\n" method params)))))
47128

48129
(def ^:dynamic notify -notify)
49130

src/logging.clj

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
(ns logging
1+
(ns logging
22
(:require
33
[selmer.parser :as selmer]
44
[selmer.util :refer [without-escaping]]))
55

6+
(defn render [template data]
7+
(without-escaping
8+
(selmer/render template data)))
9+
610
(defn warn [template data]
711
(binding [*out* *err*]
812
(println "## WARNING\n")
9-
(println
10-
(without-escaping
11-
(selmer/render template data)))))
13+
(println (render template data))))

0 commit comments

Comments
 (0)