Skip to content

Instantly share code, notes, and snippets.

@pdenno
Created March 27, 2024 00:56
Show Gist options
  • Save pdenno/c77cf10b5dc7d62c0e3eb157c189dda1 to your computer and use it in GitHub Desktop.
Save pdenno/c77cf10b5dc7d62c0e3eb157c189dda1 to your computer and use it in GitHub Desktop.
assistant stuff from llm.clj
;;;------------------------------------- assistants ----------------------------------------------------
(s/def ::name string?)
(s/def ::instructions string?)
(s/def ::assistant-args (s/keys :req-un [::name ::instructions]))
(defn make-assistant
"Create an assistant with the given parameters. Provide a map like with following keys:
:name - a string, no default.
:instructions - a string, the systems instructions; defaults to 'You are a helpful assistant.',
:model - a string; defaults to 'gpt-4-1106-preview',
:tools - a vector containing maps; defaults to [{:type 'code_interpreter'}]."
[& {:keys [name model-class instructions tools metadata]
:or {model-class :gpt-4
metadata {}
tools [{:type "code_interpreter"}]} :as obj}]
(s/valid? ::assistant-args obj)
(let [key (get-api-key :llm)]
(openai/create-assistant {:name name
:model (pick-llm model-class)
:metadata metadata
:instructions instructions
:tools tools} ; Will be good for csv and xslx, at least.
{:api-key key})))
(defn make-thread
[& {:keys [assistant-id metadata] :or {metadata {}}}]
(let [key (get-api-key :llm)]
(openai/create-thread {:assistant_id assistant-id
:metadata metadata}
{:api-key key})))
(defn get-thread
"Get the thread object of the argument PID."
[pid]
(let [eid (db/project-exists? pid)]
(-> (resolve-db-id {:db/id eid}
(connect-atm pid)
:keep-set #{:project/surrogate :surrogate/thread-str})
:project/surrogate
:surrogate/thread-str
edn/read-string)))
(defn get-assistant
"Get the thread object of the argument PID."
[pid]
(let [eid (db/project-exists? pid)]
(-> (resolve-db-id {:db/id eid}
(connect-atm pid)
:keep-set #{:project/surrogate :surrogate/assistant-obj-str})
:project/surrogate
:surrogate/assistant-obj-str
edn/read-string)))
;;; This is entirely because the OpenAI objects have stuff I find distracting!
(def keep-prop? #{:role :content :created_at})
(defn message-salient
"Return interesting parts of messages from the structure returned from openai/list-messages."
[msgs]
(let [_has-more? (:has_more msgs)] ; ToDo: later.
(->> msgs
:data
(mapv (fn [msg] (reduce-kv (fn [m k v] (if (keep-prop? k) (assoc m k v) m)) {} msg)))
(sort-by :created)
vec)))
(defn query-on-thread
"Create a message for ROLE on the project's (PID) thread and run it, returning the result text.
pid - The project's ID (keyword),
role - #{'user' 'assistant'},
msg-text - a string."
[pid role msg-text & {:keys [timeout-secs] :or {timeout-secs 40}}]
(assert (keyword? pid))
(assert (#{"user" "assistant"} role))
(assert (string? msg-text))
(let [key (get-api-key :llm)
aid (-> pid get-assistant :id)
tid (-> pid get-thread :id)
_msg (openai/create-message ; Apparently the thread_id links the run to msg.
{:thread_id tid
:role role
:content msg-text}
{:api-key key})
;; https://platform.openai.com/docs/assistants/overview?context=without-streaming
;; Once all the user Messages have been added to the Thread, you can Run the Thread with any Assistant.
run (openai/create-run
{:thread_id tid
:assistant_id aid}
{:api-key key})]
(loop [secs 0]
(let [r (openai/retrieve-run {:thread_id tid :run-id (:id run)} {:api-key key})]
(Thread/sleep 1000)
(cond (> secs timeout-secs) (throw (ex-info "query-on-thread: Timeout:" {:msg-text msg-text})),
(= "completed" (:status r)) (let [[m1 m2] (-> (openai/list-messages {:thread_id tid :limit 2} {:api-key key})
message-salient)]
(if (= msg-text (-> m2 :content first :text :value))
(-> m1 :content first :text :value)
(throw (ex-info "query-on-thread: Response not synced to query:" {:m1 m1 :m2 m2})))),
(#{"expired" "failed"} (:status r)) (throw (ex-info "query-on-thread failed:" {:status (:status r)}))
:else (recur (inc secs)))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment