From f581baacca7d7a5f619e417baa134093496a73e0 Mon Sep 17 00:00:00 2001 From: Jim Clark Date: Wed, 3 Jul 2024 00:14:59 -0700 Subject: [PATCH] Add conversation loop --- prompts/dockerfiles/020_system_prompt.md | 2 - prompts/dockerfiles/npm-best-practices.md | 7 ++- prompts/src/openai.clj | 67 +++++++++++++---------- prompts/src/prompts.clj | 59 ++++++++++++-------- 4 files changed, 81 insertions(+), 54 deletions(-) diff --git a/prompts/dockerfiles/020_system_prompt.md b/prompts/dockerfiles/020_system_prompt.md index fc7563a..baff000 100644 --- a/prompts/dockerfiles/020_system_prompt.md +++ b/prompts/dockerfiles/020_system_prompt.md @@ -1,5 +1,3 @@ -{{#npm}} {{>npm-best-practices}} -{{/npm}} diff --git a/prompts/dockerfiles/npm-best-practices.md b/prompts/dockerfiles/npm-best-practices.md index afea37b..a1165d3 100644 --- a/prompts/dockerfiles/npm-best-practices.md +++ b/prompts/dockerfiles/npm-best-practices.md @@ -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. @@ -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. diff --git a/prompts/src/openai.clj b/prompts/src/openai.clj index 2aa4aca..73c5576 100644 --- a/prompts/src/openai.clj +++ b/prompts/src/openai.clj @@ -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 @@ -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 @@ -105,14 +104,21 @@ [] (let [e (async/> (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)) @@ -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 diff --git a/prompts/src/prompts.clj b/prompts/src/prompts.clj index c5101e8..bcd5a7b 100644 --- a/prompts/src/prompts.clj +++ b/prompts/src/prompts.clj @@ -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])) @@ -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) @@ -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/