From 9c3a74d7a54015830b375766b69396ad511775a4 Mon Sep 17 00:00:00 2001 From: Jim Clark Date: Mon, 26 Aug 2024 17:12:12 -0700 Subject: [PATCH] Support single-file prompts * adding --prompts-file arg to point just at a README.md with prompts sections --- prompts/qrencode/100_user_prompts.md | 2 - prompts/qrencode/README.md | 8 ++ runbook.md | 39 ++++++++ src/git.clj | 4 +- src/markdown.clj | 9 +- src/prompts.clj | 139 +++++++++++++++------------ test/cli.clj | 50 ++++++---- test/openai_t.clj | 2 +- test/prompts_t.clj | 18 ++-- 9 files changed, 173 insertions(+), 98 deletions(-) delete mode 100644 prompts/qrencode/100_user_prompts.md diff --git a/prompts/qrencode/100_user_prompts.md b/prompts/qrencode/100_user_prompts.md deleted file mode 100644 index 950be7b..0000000 --- a/prompts/qrencode/100_user_prompts.md +++ /dev/null @@ -1,2 +0,0 @@ -Generate a QR code for the url -https://github.com/docker/labs-ai-tools-for-devs. diff --git a/prompts/qrencode/README.md b/prompts/qrencode/README.md index c1710fc..c14daaa 100644 --- a/prompts/qrencode/README.md +++ b/prompts/qrencode/README.md @@ -16,3 +16,11 @@ functions: image: vonwig/qrencode:latest --- +# Prompt user + +Generate a QR code for the +url https://github.com/docker/labs-ai-tools-for-devs and write it to file `qrcode.png`. + +# Result + +This function generates a QR code for a URL. The QR code is saved as a PNG file. diff --git a/runbook.md b/runbook.md index 8755aff..5d00070 100644 --- a/runbook.md +++ b/runbook.md @@ -6,6 +6,12 @@ bb -m prompts --help ``` +### run without --host-dir + +```sh +bb -m prompts +``` + ### Plain prompt Generation ```sh @@ -58,6 +64,8 @@ bb -m prompts run \ --url http://localhost:11434/v1/chat/completions ``` +TODO - this should fail better because the prompts-dir is not valid. + ```sh bb -m prompts run \ --host-dir /Users/slim/docker/labs-make-runbook \ @@ -133,6 +141,37 @@ bb -m prompts run \ --model "llama3-groq-tool-use:latest" ``` +#### Test single file prompts + +```sh +rm ~/docker/labs-make-runbook/qrcode.png +``` + +```sh +bb -m prompts run \ + --host-dir /Users/slim/docker/labs-make-runbook \ + --user jimclark106 \ + --platform darwin \ + --prompts-file prompts/qrencode/README.md +``` + +```sh +open ~/docker/labs-make-runbook/qrcode.png +``` + +```sh +bb -m prompts run \ + --host-dir /Users/slim/docker/labs-make-runbook \ + --user jimclark106 \ + --platform darwin \ + --prompts-file prompts/qrencode/README.md \ + --url http://localhost:11434/v1/chat/completions \ + --model "llama3.1" \ + --nostream \ + --debug +``` + + #### Using Containerized runner ```sh diff --git a/src/git.clj b/src/git.clj index 8e5472a..ce2fe0e 100644 --- a/src/git.clj +++ b/src/git.clj @@ -52,7 +52,7 @@ (fs/create-dirs default-dir) default-dir)))) -(defn prompt-dir +(defn prompt-file "returns the path or nil if the github ref does not resolve throws if the path in the repo does not exist or if the clone fails" [ref] @@ -85,7 +85,7 @@ (fs/create-dir prompts-cache) (def x "github:docker/labs-make-runbook?ref=main&path=prompts/docker") (def git-ref (parse-github-ref x)) - (prompt-dir "github:docker/labs-make-runbook?ref=main&path=prompts/docker") + (prompt-file "github:docker/labs-make-runbook?ref=main&path=prompts/docker") (parse-github-ref nil) (parse-github-ref "") (parse-github-ref "github:docker/labs-make-runbook") diff --git a/src/markdown.clj b/src/markdown.clj index 5d9f723..d107849 100644 --- a/src/markdown.clj +++ b/src/markdown.clj @@ -54,8 +54,7 @@ (filter heading-1-section?) (map (comp zip/node zip/up zip/up)) (filter (partial prompt-section? content)) - (map (partial node-content content)) - (pprint))) + (map (partial node-content content)))) (defn parse-markdown [content] (let [x (docker/function-call-with-stdin @@ -66,11 +65,13 @@ (docker/finish-call x)))] (->> s (edn/read-string) - (extract-prompts content)))) + (extract-prompts content) + (into [])))) (comment + (string/split content #"\n") - (def content (slurp "test.md")) + (def content (slurp "prompts/qrencode/README.md" )) (pprint (parse-markdown content)) (def t diff --git a/src/prompts.clj b/src/prompts.clj index 02b9837..f936701 100644 --- a/src/prompts.clj +++ b/src/prompts.clj @@ -15,6 +15,7 @@ [jsonrpc] [logging :refer [warn]] [markdown.core :as markdown] + [markdown :as markdown-parser] [medley.core :as medley] [openai] [pogonos.core :as stache] @@ -43,30 +44,19 @@ (defn- name-matches [re] (fn [p] (re-matches re (fs/file-name p)))) -(defn- selma-render [prompts-dir m f] - [{:content (stache/render-string - (slurp f) - m - {:partials (partials/file-partials [prompts-dir] ".md")})} f]) - -(def prompt-file-pattern #".*_(.*)_.*.md") - -(defn- merge-role [[m f]] - (merge m {:role (let [[_ role] (re-find prompt-file-pattern (fs/file-name f))] role)})) - (defn fact-reducer "reduces into m using a container function params - dir - the host dir that the container will mount read-only at /project + host-dir - the host dir that the container will mount read-only at /project m - the map to merge into container-definition - the definition for the function" - [dir m container-definition] + [host-dir m container-definition] (try (medley/deep-merge m (let [{:keys [pty-output exit-code]} (docker/extract-facts (-> container-definition - (assoc :host-dir dir)))] + (assoc :host-dir host-dir)))] (when (= 0 exit-code) (let [context (case (:output-handler container-definition) @@ -82,14 +72,17 @@ {:content (logging/render "unable to run extractors \n```\n{{ container-definition }}\n```\n - {{ exception }}" - {:dir dir + {:dir host-dir :container-definition (str container-definition) :exception (str ex)})}) m))) -(defn collect-extractors [dir] +(defn- metadata-file [prompts-file] + (if (fs/directory? prompts-file) (io/file prompts-file "README.md") prompts-file)) + +(defn collect-extractors [f] (let [extractors (->> - (-> (markdown/parse-metadata (io/file dir "README.md")) first :extractors) + (-> (markdown/parse-metadata (metadata-file f)) first :extractors) (map (fn [m] (merge (registry/get-extractor m) m))))] (if (seq extractors) extractors @@ -100,17 +93,17 @@ "--vs-machine-id" "none" "--workspace" "/project"]}]))) -(defn collect-functions [dir] +(defn collect-functions [f] (->> - (-> (markdown/parse-metadata (io/file dir "README.md")) first :functions) + (-> (markdown/parse-metadata (metadata-file f)) first :functions) (map (fn [m] {:type "function" :function (merge (registry/get-function m) m)})))) (defn collect-metadata "collect metadata from yaml front-matter in README.md skip functions and extractors" - [dir] + [f] (dissoc - (-> (markdown/parse-metadata (io/file dir "README.md")) first) + (-> (markdown/parse-metadata (metadata-file f)) first) :extractors :functions)) (defn run-extractors @@ -119,27 +112,44 @@ project-root - the host project root dir identity-token - a valid Docker login auth token dir - a prompts directory with a valid README.md" - [{:keys [host-dir prompts-dir user pat]}] + [{:keys [host-dir prompts user pat]}] (reduce (partial fact-reducer host-dir) {} - (->> (collect-extractors prompts-dir) + (->> (collect-extractors prompts) (map (fn [m] (merge m (when user {:user user}) (when pat {:pat pat}))))))) +(defn- selma-render [prompts-file m message] + (update message + :content + (fn [content] + (stache/render-string + content + m + {:partials (partials/file-partials + [(if (fs/directory? prompts-file) prompts-file (fs/parent prompts-file))] + ".md")})))) + +(def prompt-file-pattern #".*_(.*)_.*.md") + (defn get-prompts "run extractors and then render prompt templates returns ordered collection of chat messages" - [{:keys [prompts-dir user platform] :as opts}] + [{:keys [prompts user platform] :as opts}] (let [;; TODO the docker default no longer makes sense here m (run-extractors opts) - renderer (partial selma-render prompts-dir (facts m user platform)) - prompts (->> (fs/list-dir prompts-dir) - (filter (name-matches prompt-file-pattern)) - (sort-by fs/file-name) - (into []))] - (map (comp merge-role renderer fs/file) prompts))) + renderer (partial selma-render prompts (facts m user platform)) + prompts (if (fs/directory? prompts) + (->> (fs/list-dir prompts) + (filter (name-matches prompt-file-pattern)) + (sort-by fs/file-name) + (map (fn [f] {:role (let [[_ role] (re-find prompt-file-pattern (fs/file-name f))] role) + :content (slurp (fs/file f))})) + (into [])) + (markdown-parser/parse-markdown (slurp prompts)))] + (map renderer prompts))) (declare conversation-loop) @@ -181,7 +191,7 @@ (jsonrpc/notify :message {:content (format "## (%s) sub-prompt" (:ref definition))}) (let [{:keys [messages _finish-reason]} (async/> messages (filter #(= "assistant" (:role %))) @@ -199,9 +209,9 @@ prompts is the conversation history args for extracting functions, host-dir, user, platform returns channel that will contain the final set of messages and a finish-reason" - [prompts {:keys [prompts-dir url model stream] :as opts}] - (let [m (collect-metadata prompts-dir) - functions (collect-functions prompts-dir) + [messages {:keys [prompts url model stream] :as opts}] + (let [m (collect-metadata prompts) + functions (collect-functions prompts) [c h] (openai/chunk-handler (partial function-handler (merge @@ -211,7 +221,7 @@ (openai/openai (merge m - {:messages prompts} + {:messages messages} (when (seq functions) {:tools functions}) (when url {:url url}) (when model {:model model}) @@ -235,18 +245,18 @@ (async/go-loop [thread []] ;; get-prompts can only use extractors - we can't refine ;; them based on output from function calls that the LLM plans - (let [prompts (if (not (seq thread)) - new-prompts - thread) - {:keys [messages finish-reason] :as m} - (async/ + (tools.cli/parse-opts + ["--platform" "Darwin" + "--prompts-dir" "/Users/slime" + "--thread-id" "anything"] prompts/cli-opts) + :errors + count))) + (t/is + (-> + (tools.cli/parse-opts + ["--platform" "Darwin" + "--prompts" "github:docker/labs-make-runbook" + "--thread-id" "anything"] prompts/cli-opts) + :options + :prompts + (.exists)))) diff --git a/test/openai_t.clj b/test/openai_t.clj index 34b9ff1..1fc8757 100644 --- a/test/openai_t.clj +++ b/test/openai_t.clj @@ -7,7 +7,7 @@ (:import [java.net ConnectException])) -(alter-var-root #'jsonrpc/notify (constantly jsonrpc/-println)) +(alter-var-root #'jsonrpc/notify (partial (constantly jsonrpc/-println) {:debug true})) (t/deftest update-tool-calls (t/is diff --git a/test/prompts_t.clj b/test/prompts_t.clj index 5124f56..2b72273 100644 --- a/test/prompts_t.clj +++ b/test/prompts_t.clj @@ -8,11 +8,10 @@ (t/is (.startsWith (-> - (#'prompts/selma-render - "prompts/dockerfiles" - {} - "prompts/dockerfiles/020_system_prompt.md") - first + (#'prompts/selma-render + (fs/file "prompts/dockerfiles") + {} + {:role "system" :content (slurp "prompts/dockerfiles/020_system_prompt.md")}) :content) "\nWrite Dockerfiles"))) @@ -48,10 +47,9 @@ (comment (prompts/run-extractors - {:host-dir "/Users/slim/docker/labs-ai-tools-for-devs/" - :user "jimclark106" - :prompts-dir (fs/file "prompts/docker")}) + {:host-dir "/Users/slim/docker/labs-ai-tools-for-devs/" + :user "jimclark106" + :prompts (fs/file "prompts/docker")}) (prompts/collect-functions (fs/file "prompts/dockerfiles")) - (prompts/collect-extractors (fs/file "prompts/docker")) - ) + (prompts/collect-extractors (fs/file "prompts/docker")))