|
74 | 74 | :error
|
75 | 75 | {:content
|
76 | 76 | (logging/render
|
77 |
| - "unable to run extractors \n```\n{{ container-definition }}\n```\n - {{ exception }}" |
| 77 | + "unable to run extractors \n```\n{{ container-definition }}\n```\n" |
78 | 78 | {:dir host-dir
|
79 |
| - :container-definition (str container-definition) |
80 |
| - :exception (str ex)})}) |
| 79 | + :container-definition (str container-definition)}) |
| 80 | + :exception (str ex)}) |
81 | 81 | m)))
|
82 | 82 |
|
83 | 83 | (defn- metadata-file [prompts-file]
|
|
95 | 95 | (map (fn [m] (merge (registry/get-extractor m) m))))]
|
96 | 96 | (if (seq extractors)
|
97 | 97 | extractors
|
98 |
| - [{:name "project-facts" |
99 |
| - :image "docker/lsp:latest" |
100 |
| - :entrypoint "/app/result/bin/docker-lsp" |
101 |
| - :command ["project-facts" |
102 |
| - "--vs-machine-id" "none" |
103 |
| - "--workspace" "/project"]}]))) |
| 98 | + []))) |
104 | 99 |
|
105 | 100 | (def hub-images
|
106 | 101 | #{"curl" "qrencode" "toilet" "figlet" "gh" "typos" "fzf" "jq" "fmpeg" "pylint"})
|
|
109 | 104 | "get either :functions or :tools collection
|
110 | 105 | returns collection of openai compatiable tool definitions augmented with container info"
|
111 | 106 | [f]
|
112 |
| - (->> |
113 |
| - (-> |
114 |
| - (markdown/parse-metadata (metadata-file f)) |
115 |
| - first |
116 |
| - (select-keys [:tools :functions]) |
117 |
| - seq |
118 |
| - first ;; will take the first either tools or functions randomly |
119 |
| - second ;; returns the tools or functions array |
120 |
| - ) |
121 |
| - (mapcat |
122 |
| - (fn [m] |
123 |
| - (if-let [tool (hub-images (:name m))] |
124 |
| - ;; these come from our own public hub images |
125 |
| - [{:type "function" |
126 |
| - :function |
127 |
| - {:name (format "%s-manual" tool) |
128 |
| - :description (format "Run the man page for %s" tool) |
129 |
| - :container |
130 |
| - {:image (format "vonwig/%s:latest" tool) |
131 |
| - :command |
132 |
| - ["{{raw|safe}}" "man"]}}} |
133 |
| - {:type "function" |
134 |
| - :function |
135 |
| - (merge |
136 |
| - {:description (format "Run a %s command." tool) |
137 |
| - :parameters |
138 |
| - {:type "object" |
139 |
| - :properties |
140 |
| - {:args |
141 |
| - {:type "string" |
142 |
| - :description (format "The arguments to pass to %s" tool)}}} |
143 |
| - :container |
144 |
| - {:image (format "vonwig/%s:latest" tool) |
145 |
| - :command ["{{raw|safe}}"]}} |
146 |
| - m)}] |
147 |
| - [{:type "function" :function (merge (registry/get-function m) (dissoc m :image))}]))))) |
| 107 | + (try |
| 108 | + (->> |
| 109 | + (-> |
| 110 | + (markdown/parse-metadata (metadata-file f)) |
| 111 | + first |
| 112 | + (select-keys [:tools :functions]) |
| 113 | + seq |
| 114 | + first ;; will take the first either tools or functions randomly |
| 115 | + second ;; returns the tools or functions array |
| 116 | + ) |
| 117 | + (mapcat |
| 118 | + (fn [m] |
| 119 | + (if-let [tool (hub-images (:name m))] |
| 120 | + ;; these come from our own public hub images |
| 121 | + [{:type "function" |
| 122 | + :function |
| 123 | + {:name (format "%s-manual" tool) |
| 124 | + :description (format "Run the man page for %s" tool) |
| 125 | + :container |
| 126 | + {:image (format "vonwig/%s:latest" tool) |
| 127 | + :command |
| 128 | + ["{{raw|safe}}" "man"]}}} |
| 129 | + {:type "function" |
| 130 | + :function |
| 131 | + (merge |
| 132 | + {:description (format "Run a %s command." tool) |
| 133 | + :parameters |
| 134 | + {:type "object" |
| 135 | + :properties |
| 136 | + {:args |
| 137 | + {:type "string" |
| 138 | + :description (format "The arguments to pass to %s" tool)}}} |
| 139 | + :container |
| 140 | + {:image (format "vonwig/%s:latest" tool) |
| 141 | + :command ["{{raw|safe}}"]}} |
| 142 | + m)}] |
| 143 | + [{:type "function" :function (merge (registry/get-function m) (dissoc m :image))}])))) |
| 144 | + (catch Throwable _ |
| 145 | + ;; TODO warn about empty yaml front matter? |
| 146 | + []))) |
148 | 147 |
|
149 | 148 | (defn collect-metadata
|
150 | 149 | "collect metadata from yaml front-matter in README.md
|
151 | 150 | skip functions and extractors"
|
152 | 151 | [f]
|
153 |
| - (dissoc |
154 |
| - (-> (markdown/parse-metadata (metadata-file f)) first) |
155 |
| - :extractors :functions)) |
| 152 | + (try |
| 153 | + (dissoc |
| 154 | + (-> (markdown/parse-metadata (metadata-file f)) first) |
| 155 | + :extractors :functions) |
| 156 | + (catch Throwable _ |
| 157 | + ;; TODO warn about empty yaml front matter? |
| 158 | + {}))) |
156 | 159 |
|
157 | 160 | (defn run-extractors
|
158 | 161 | "returns a map of extracted *math-context*
|
|
190 | 193 | m (merge (run-extractors opts) parameters)
|
191 | 194 | renderer (partial selma-render prompts (facts m user platform))
|
192 | 195 | prompts (if (fs/directory? prompts)
|
| 196 | + ;; directory based prompts |
193 | 197 | (->> (fs/list-dir prompts)
|
194 | 198 | (filter (name-matches prompt-file-pattern))
|
195 | 199 | (sort-by fs/file-name)
|
196 | 200 | (map (fn [f] {:role (let [[_ role] (re-find prompt-file-pattern (fs/file-name f))] role)
|
197 | 201 | :content (slurp (fs/file f))}))
|
198 | 202 | (into []))
|
199 |
| - (markdown-parser/parse-markdown (slurp prompts)))] |
| 203 | + ;; file based prompts |
| 204 | + (try |
| 205 | + (let [p (slurp prompts)] |
| 206 | + (markdown-parser/parse-markdown p)) |
| 207 | + (catch Throwable t |
| 208 | + (jsonrpc/notify :error {:content (format "failed to parse prompts from markdown %s" t)}) |
| 209 | + [])))] |
200 | 210 | (map renderer prompts)))
|
201 | 211 |
|
202 | 212 | (defn interpolate [m template]
|
|
215 | 225 | function-name - the name of the function that the LLM has selected
|
216 | 226 | json-arg-string - the JSON arg string that the LLM has generated
|
217 | 227 | resolve fail - callbacks"
|
218 |
| - [{:keys [functions user jwt timeout] :as opts} function-name json-arg-string {:keys [resolve fail]}] |
| 228 | + [{:keys [functions user jwt timeout level] :as opts :or {level 0}} function-name json-arg-string {:keys [resolve fail]}] |
219 | 229 | (if-let [definition (->
|
220 | 230 | (->> (filter #(= function-name (-> % :function :name)) functions)
|
221 | 231 | first)
|
|
258 | 268 |
|
259 | 269 | (= "prompt" (:type definition)) ;; asynchronous call to another agent - new conversation-loop
|
260 | 270 | (do
|
261 |
| - (jsonrpc/notify :message {:content (format "## (%s) sub-prompt" (:ref definition))}) |
| 271 | + (jsonrpc/notify :start {:level level |
| 272 | + :role "tool" |
| 273 | + :content (:ref definition)}) |
262 | 274 | (let [{:keys [messages _finish-reason]}
|
263 | 275 | (async/<!! (conversation-loop
|
264 | 276 | (assoc opts
|
| 277 | + :level (inc (or (:level opts) 0)) |
265 | 278 | :prompts (git/prompt-file (:ref definition))
|
266 | 279 | :parameters arg-context)))]
|
267 |
| - (jsonrpc/notify :message {:content (format "## (%s) end sub-prompt" (:ref definition))}) |
268 | 280 | (resolve (->> messages
|
269 | 281 | (filter #(= "assistant" (:role %)))
|
270 | 282 | (last)
|
|
275 | 287 | (fail (format "system failure %s" t)))))
|
276 | 288 | (fail "no function found")))
|
277 | 289 |
|
| 290 | +(defn- stop-looping [c s] |
| 291 | + (jsonrpc/notify :error {:content s}) |
| 292 | + (async/>!! c {:messages [{:role "assistant" :content s}] |
| 293 | + :finish-reason "error"})) |
| 294 | + |
278 | 295 | (defn- run-prompts
|
279 | 296 | "call openai compatible chat completion endpoint and handle tool requests
|
280 | 297 | params
|
281 | 298 | prompts is the conversation history
|
282 | 299 | args for extracting functions, host-dir, user, platform
|
283 | 300 | returns channel that will contain the final set of messages and a finish-reason"
|
284 |
| - [messages {:keys [prompts url model stream] :as opts}] |
| 301 | + [messages {:keys [prompts url model stream level] :as opts :or {level 0}}] |
285 | 302 | (let [m (collect-metadata prompts)
|
286 | 303 | functions (collect-functions prompts)
|
287 |
| - [c h] (openai/chunk-handler (partial |
288 |
| - function-handler |
289 |
| - (merge |
290 |
| - opts |
291 |
| - (select-keys m [:timeout]) |
292 |
| - {:functions functions})))] |
| 304 | + [c h] (openai/chunk-handler |
| 305 | + level |
| 306 | + (partial |
| 307 | + function-handler |
| 308 | + (merge |
| 309 | + opts |
| 310 | + (select-keys m [:timeout]) |
| 311 | + {:functions functions})))] |
293 | 312 | (try
|
294 |
| - (openai/openai |
295 |
| - (merge |
296 |
| - m |
297 |
| - {:messages messages} |
298 |
| - (when (seq functions) {:tools functions}) |
299 |
| - (when url {:url url}) |
300 |
| - (when model {:model model}) |
301 |
| - (when (and stream (nil? (:stream m))) {:stream stream})) h) |
| 313 | + (if (seq messages) |
| 314 | + (openai/openai |
| 315 | + (merge |
| 316 | + m |
| 317 | + {:messages messages |
| 318 | + :level level} |
| 319 | + (when (seq functions) {:tools functions}) |
| 320 | + (when url {:url url}) |
| 321 | + (when model {:model model}) |
| 322 | + (when (and stream (nil? (:stream m))) {:stream stream})) h) |
| 323 | + (stop-looping c "This is an empty set of prompts. Define prompts using h1 sections (eg `# prompt user`)" )) |
302 | 324 | (catch ConnectException _
|
303 |
| - ;; when the conversation-loop can not connect to an openai compatible endpoint |
304 |
| - (async/>!! c {:messages [{:role "assistant" :content "I cannot connect to an openai compatible endpoint."}] |
305 |
| - :finish-reason "error"})) |
| 325 | + (stop-looping c "I cannot connect to an openai compatible endpoint.")) |
306 | 326 | (catch Throwable t
|
307 |
| - (async/>!! c {:messages [{:role "assistant" :content (str t)}] |
308 |
| - :finish-reason "error"}))) |
| 327 | + (stop-looping c (str t)))) |
309 | 328 | c))
|
310 | 329 |
|
311 | 330 | (defn- conversation-loop
|
|
333 | 352 | (catch Throwable ex
|
334 | 353 | (let [c (async/promise-chan)]
|
335 | 354 | (jsonrpc/notify :error {:content
|
336 |
| - (format "not a valid prompt configuration: %s" (with-out-str (pprint opts))) |
| 355 | + (format "failure for prompt configuration:\n %s" (with-out-str (pprint (dissoc opts :pat :jwt)))) |
337 | 356 | :exception (str ex)})
|
338 | 357 | (async/>! c {:messages [] :done "error"})
|
339 | 358 | c))))
|
|
410 | 429 | {:content
|
411 | 430 | (json/generate-string
|
412 | 431 | (if (map? x)
|
413 |
| - (if (= "error" (:done x)) |
414 |
| - (update x :messages last) |
415 |
| - (select-keys x [:done])) |
| 432 | + (select-keys x [:done]) |
416 | 433 | x))})))
|
417 | 434 |
|
418 | 435 | (defn output-prompts [coll]
|
|
0 commit comments