Skip to content

Commit

Permalink
Add conversation loop
Browse files Browse the repository at this point in the history
  • Loading branch information
slimslenderslacks committed Jul 3, 2024
1 parent 8dca8de commit f581baa
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 54 deletions.
2 changes: 0 additions & 2 deletions prompts/dockerfiles/020_system_prompt.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@

{{#npm}}
{{>npm-best-practices}}
{{/npm}}

7 changes: 6 additions & 1 deletion prompts/dockerfiles/npm-best-practices.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Write Node.js Dockerfiles using three stages. Do these three steps sequentially.
Write Dockerfiles for NPM projects using three stages. Do these three steps sequentially.
* the first node depemdencies stage should be called "deps"
and it should fetch the runtime dependencies using npm ci
`with the --omit=dev` flag.
Expand All @@ -11,3 +11,8 @@ Write Node.js Dockerfiles using three stages. Do these three steps sequentially
1. it copies the node_modules directory from the deps stage.
2. it copies the dist directory from the build stage.
3. it then runs npm start

If you need to use a RUN statement containing `npm ci` always
add the argument `--mount=type=cache,target=/root/.npm` to the RUN instruction.
The `--mount` argument should be placed between the word RUN and the npm command.
This will cache the npm packages in the docker build cache and speed up the build process.
67 changes: 38 additions & 29 deletions prompts/src/openai.clj
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,27 @@
(let [c (async/chan)]
(function-handler
function-name
(json/parse-string arguments true)
arguments
{:resolve
(fn [output]
(jsonrpc/notify :message {:content (format "## ROLE tool\n%s\n" output)})
(async/go (async/>! c {:content output :role "tool" :tool_call_id tool-call-id}))
;; add message with output to the conversation and call complete again
;; add the assistant message that requested the tool be called
;; {:tool_calls [] :role "assistant" :name "optional"}
;; also ad the tool message with the response from the tool
;; {:content "" :role "tool" :tool_call_id ""}

;; this is also where we need to trampoline because we are potentially in a loop here
;; in some ways we should probably just create channels and call these threads anyway
;; I don't know how much we need a formal assistant api to make progress
)
(jsonrpc/notify :message {:content (format "## ROLE tool (%s)\n%s\n" function-name output)})
(async/go
(async/>! c {:content output :role "tool" :tool_call_id tool-call-id})
(async/close! c)))
:fail (fn [output]
(jsonrpc/notify :message {:content (format "## ROLE tool\n function call %s failed %s" function-name output)})
(async/go (async/>! c {:content output :role "tool" :tool_call_id tool-call-id})))})
(async/go
(async/>! c {:content output :role "tool" :tool_call_id tool-call-id})
(async/close! c)))})
c))

(defn make-tool-calls [function-handler tool-calls]
(doseq [{{:keys [arguments name]} :function tool-call-id :id} tool-calls]
(jsonrpc/notify :message {:content (format "\n... calling %s\n" name)})
(call-function function-handler name arguments tool-call-id)))
(defn make-tool-calls
" returns channel with all messages from completed executions of tools"
[function-handler tool-calls]
(->>
(for [{{:keys [arguments name]} :function tool-call-id :id} tool-calls]
(call-function function-handler name arguments tool-call-id) )
(async/merge)))

(defn function-merge [m {:keys [name arguments]}]
(cond-> m
Expand All @@ -76,9 +73,11 @@
(defn update-tool-calls [m tool-calls]
(reduce
(fn [m {:keys [index id function]}]
(update-in m [:tool-calls (or index id) :function]
(fnil function-merge {}) (merge function
(when id {:id id}))))
(-> m
(update-in [:tool-calls (or index id) :function]
(fnil function-merge {}) function)
(update-in [:tool-calls (or index id)]
(fnil merge {}) (when id {:id id}))))
m tool-calls))

(comment
Expand All @@ -105,14 +104,21 @@
[]
(let [e (async/<! c)]
(cond
(:done e) (let [{calls :tool-calls finish-reason :finish-reason} @response]
(:done e) (let [{calls :tool-calls finish-reason :finish-reason} @response
messages [{:role "assistant" :tool_calls (->> (vals calls)
(map #(assoc % :type "function")))}]]
(jsonrpc/notify :functions-done (vals calls))
(jsonrpc/notify :message {:content "\n---\n\n"})
;; make-tool-calls returns nil
(make-tool-calls
(:tool-handler e)
(vals calls))
{:finish-reason finish-reason})
;; make-tool-calls returns a channel with results of tool call messages
;; so we can continue the conversation
{:finish-reason finish-reason
:messages
(async/<!
(->>
(make-tool-calls
(:tool-handler e)
(vals calls))
(async/reduce conj messages)))})
:else (let [{:keys [tool_calls finish-reason]} e]
(swap! response update-tool-calls tool_calls)
(when finish-reason (swap! response assoc :finish-reason finish-reason))
Expand All @@ -124,12 +130,15 @@
{:done true}
(json/parse-string s true)))

(defn chunk-handler [function-handler]
(defn chunk-handler
"sets up a response handler loop for use with an OpenAI API call
returns [channel handler] - channel will emit the updated chat messages after dispatching any functions"
[function-handler]
(let [c (async/chan 1)]
[(response-loop c)
(fn [chunk]
;; TODO this only supports when there's a single choice
(let [{[{:keys [delta message finish_reason _role] :as choice}] :choices
(let [{[{:keys [delta message finish_reason _role]}] :choices
done? :done _completion-id :id}
;; only streaming events will be SSE data fields
(some-> chunk
Expand Down
59 changes: 37 additions & 22 deletions prompts/src/prompts.clj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
[medley.core :as medley]
[openai]
[jsonrpc]
[clojure.pprint :refer [pprint]]
[pogonos.core :as stache]
[pogonos.partials :as partials]
[selmer.parser :as selmer]))
Expand Down Expand Up @@ -160,7 +161,7 @@
(into []))]
(map (comp merge-role renderer fs/file) prompts)))

(defn function-handler [{:keys [functions] :as opts} function-name arg-map {:keys [resolve fail]}]
(defn function-handler [{:keys [functions] :as opts} function-name json-arg-string {:keys [resolve fail]}]
(if-let [definition (->
(->> (filter #(= function-name (-> % :function :name)) functions)
first)
Expand All @@ -171,35 +172,47 @@
(let [function-call (merge
(:container definition)
(dissoc opts :functions)
{:command [(json/generate-string arg-map)]})
{:command [json-arg-string]})
{:keys [pty-output exit-code]} (docker/run-function function-call)]
(if (= 0 exit-code)
(resolve pty-output)
(fail (format "call exited with non-zero code (%d): %s" exit-code pty-output))))
(= "prompt" (:type definition)) ;; asynchronous call to another agent
(do
(resolve "some json output")))
(resolve "This is an NPM project.")))
(catch Throwable t
(fail (format "system failure %s" t))))
(fail "no function found")))

(defn- run-prompts
[& args]
(defn- run-prompts
[prompts & args]
(let [prompt-dir (get-dir (last args))
m (collect-metadata prompt-dir)
functions (collect-functions prompt-dir)
[c h] (openai/chunk-handler (partial
function-handler
{:functions functions
:host-dir (first (rest args))}))]
function-handler
{:functions functions
:host-dir (first (rest args))}))]

(openai/openai
(merge
{:messages (apply get-prompts (rest args))}
(when (seq functions) {:tools functions})
m)
h)
{:event (async/<!! c)}))
(merge
{:messages prompts}
(when (seq functions) {:tools functions})
m) h)
c))

(defn- conversation-loop
[& args]
(async/go-loop
[prompts (apply get-prompts (rest args))]
(let [{:keys [messages finish-reason] :as m} (async/<!! (apply run-prompts prompts args))]
(if (= "tool_calls" finish-reason)
(do
(jsonrpc/notify :message {:content (with-out-str (pprint m))})
(recur (concat prompts messages)))
(do
(jsonrpc/notify :message {:content (with-out-str (pprint m))})
{:done finish-reason})))))

(defn- -run-command
[& args]
Expand Down Expand Up @@ -228,7 +241,7 @@
(update-in m [:prompts] (fn [coll] (remove (fn [{:keys [type]}] (= type (second args))) coll)))))

(= "run" (first args))
(apply run-prompts args)
(async/<!! (apply conversation-loop args))

:else
(apply get-prompts args)))
Expand All @@ -245,13 +258,15 @@
(let [{:keys [arguments options]} (cli/parse-opts args cli-opts)]
;; positional args are
;; host-project-root user platform prompt-dir-or-github-ref & {opts}
(binding [jsonrpc/notify (if (:jsonrpc options)
jsonrpc/-notify
jsonrpc/-println)]
(apply prompts (concat
arguments
(when (:identity-token options)
[:identity-token (:identity-token options)])))))
(alter-var-root
#'jsonrpc/notify
(fn [_] (if (:jsonrpc options)
jsonrpc/-notify
jsonrpc/-println)))
(apply prompts (concat
arguments
(when (:identity-token options)
[:identity-token (:identity-token options)]))))
(catch Throwable t
(warn "Error: {{ exception }}" {:exception t})
(System/exit 1))))
Expand Down

0 comments on commit f581baa

Please sign in to comment.