|
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