Assistants API Integration #539
Replies: 9 comments 38 replies
-
Support for the assistants API is not currently planned, as this is OpenAI only and I prefer to work on features that work across the board. Moving this to a discussion in case people have suggestions or thoughts. |
Beta Was this translation helpful? Give feedback.
-
I was playing around with setting this up. Here's my current minimal implementation of using the assistant API. My understanding is that the gptel-request API is built with single-step calls in mind. As pointed out above, the assistant API involves multiple steps. I am working around that by wrapping multiple calls to gptel-requests with different backends for each endpoint. Note that, I have created an assistant on the openai dashboard and using the id here. (defvar-local gptel-openai-assistant-thread-id nil)
(cl-defstruct (gptel-openai-assistant
(:constructor gptel-openai--make-assistant-step)
(:copier nil)
(:include gptel-openai))
step)
(cl-defmethod gptel--parse-response ((backend gptel-openai-assistant) response info)
(pcase (gptel-openai-assistant-step backend)
(:threads
(with-current-buffer (plist-get info :buffer)
(setq-local gptel-openai-assistant-thread-id (plist-get response :id))))
(:messages
"")
(:runs
;; TODO: peridically poll the run and check if it's completed. If so, get last message
;; https://platform.openai.com/docs/assistants/deep-dive#polling-for-updates
(error "not implemented yet!"))
(_ (error "Unknown step"))))
(cl-defmethod gptel--request-data ((backend gptel-openai-assistant) prompts)
(pcase (gptel-openai-assistant-step backend)
(:threads nil)
(:messages
(list :role "user"
:content `[(:type "text" :text ,(plist-get (car (last prompts)) :content))]))
(:runs
(let ((prompts-plist
;; Using cached assistant-id
`(:assistant_id ,(gethash 'openai-assistant-id configurations)
:stream ,(or (and gptel-stream gptel-use-curl
(gptel-backend-stream gptel-backend))
:json-false))))
(when gptel-temperature
(plist-put prompts-plist :temperature gptel-temperature))
(when gptel-max-tokens
(plist-put prompts-plist (if (memq gptel-model '(o1-preview o1-mini))
:max_completion_tokens :max_tokens)
gptel-max-tokens))
;; Merge request params with model and backend params.
(gptel--merge-plists
prompts-plist
(gptel-backend-request-params gptel-backend)
(gptel--model-request-params gptel-model))))
(_ (error "Unknown step"))))
(cl-defmethod gptel-curl--parse-stream ((backend gptel-openai-assistant) info)
(pcase (gptel-openai-assistant-step backend)
(:threads
(user-error "no streaming happening here!"))
(:messages
(user-error "no streaming happening here!"))
(:runs
(let* ((content-strs))
(condition-case nil
(while (re-search-forward "^data:" nil t)
(save-match-data
(unless (looking-at " *\\[DONE\\]")
;; TODO: Handle annotations
(when-let* ((response (gptel--json-read))
(content (map-nested-elt
response '(:delta :content 0 :text :value))))
(push content content-strs)))))
(error
(goto-char (match-beginning 0))))
(apply #'concat (nreverse content-strs))))))
(setq gptel-openai-assistant--threads-start-backend
(gptel-openai--make-assistant-step
:name "gptel-openai-assistant--threads-start"
:step :threads
:host "api.openai.com"
:key 'gptel-api-key
:stream nil
:header (lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))
("OpenAI-Beta" . "assistants=v2"))))
:protocol "https"
:endpoint "/v1/threads"
:models (gptel--process-models gptel--openai-models)
:url (concat "https://api.openai.com/v1/threads")))
(setq gptel-openai-assistant--add-message-backend
(gptel-openai--make-assistant-step
:name "gptel-openai-assistant--add-message"
:step :messages
:host "api.openai.com"
:key 'gptel-api-key
:stream nil
:header (lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))
("OpenAI-Beta" . "assistants=v2"))))
:protocol "https"
:endpoint "/v1/threads/thread_id/messages"
:models (gptel--process-models gptel--openai-models)
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/messages" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))))
(setq gptel-openai-assistant--runs-backend
(gptel-openai--make-assistant-step
:name "gptel-openai-assistant--runs"
:step :runs
:host "api.openai.com"
:key 'gptel-api-key
:header (lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))
("OpenAI-Beta" . "assistants=v2"))))
:protocol "https"
:endpoint "/v1/threads/thread_id/runs"
:models (gptel--process-models gptel--openai-models)
:stream t
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/runs" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to start run!")))))
(defun gptel-openai-assistant-start-thread (&optional callback)
"Start a assistant thread in the current buffer."
(let ((gptel-backend gptel-openai-assistant--threads-start-backend))
(funcall (if gptel-use-curl
#'gptel-curl-get-response #'gptel--url-get-response)
`(:data ,(gptel--request-data gptel-openai-assistant--threads-start-backend nil)
:buffer ,(current-buffer))
callback)))
(defun gptel-openai-assistant-add-messages (&optional callback)
"Add a message to the current thread in the buffer."
(let ((gptel-backend gptel-openai-assistant--add-message-backend))
(gptel-request nil :stream nil :callback callback)))
(defun gptel-openai-assistant-run ()
"Start a run on the thread in the current buffer."
;; TODO: should messages already in the thread be filtered out?
(let ((gptel-backend gptel-openai-assistant--runs-backend))
(gptel-request nil :stream t)))
(defun gptel-openai-assistant-send ()
"Add message to the current thread and run it. Create thread if one is not initialized."
(interactive)
(let (prompt position)
(cl-labels ((send-message-and-run (&optional _ _)
(gptel-openai-assistant-add-messages (lambda (_ _)
(gptel-openai-assistant-run)))))
(if gptel-openai-assistant-thread-id
(send-message-and-run)
(gptel-openai-assistant-start-thread #'send-message-and-run)))))
|
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
So my above solution only works if I had the fix I had discussed in #545 😅 Anyhow I ended up reimplementing everything with fsm in mind: Here's the current implementation I have;; This gets created when a new session/thread is started
(cl-defstruct (gptel-openai-assistant-session
(:constructor gptel-openai--make-assistant-session)
(:copier nil)
(:include gptel-openai))
(steps
'(:threads :messages :runs)
:documentation "The sequence of the steps.")
(step-idx -1 :documentation "Current step in steps sequence.")
cached-prompts
(step
:threads
:documentation "The current step of the session."))
(defvar gptel-openai-assistant--known-steps nil)
(cl-defstruct (gptel-openai-assistant-step
(:constructor gptel-openai-assistant--make-step))
(url
:documentation "String of function that returns the url as string")
(request-data-fn
:documentation "Takes backend and prompts as arg. See `gptel--request-data'")
(parse-response-fn
:documentation "Takes response and info as args. See `gptel--parse-response'")
(parse-stream-fn
:documentation "Takes info as args. See `gptel-curl--parse-stream'"))
(cl-defun gptel-openai-assistant--add-known-step (step &key url request-data-fn parse-response-fn parse-stream-fn)
(setf (alist-get step gptel-openai-assistant--known-steps)
(gptel-openai-assistant--make-step
:url url :request-data-fn request-data-fn :parse-response-fn parse-response-fn :parse-stream-fn parse-stream-fn)))
(gptel-openai-assistant--add-known-step
:threads
:url "https://api.openai.com/v1/threads"
:parse-response-fn (lambda (response info)
(with-current-buffer (plist-get info :buffer)
(setq-local gptel-openai-assistant-thread-id (plist-get response :id))
nil)))
(gptel-openai-assistant--add-known-step
:messages
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/messages" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
:request-data-fn (lambda (backend prompts)
(list :role "user"
:content
`[(:type "text"
:text ,(plist-get (car (last prompts)) :content))])))
(gptel-openai-assistant--add-known-step
:runs
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/runs" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
:request-data-fn (lambda (backend prompts)
(let ((_ (em gptel-stream gptel-use-curl (gptel-backend-stream backend) 333333333333))
(prompts-plist
`(:assistant_id ,(gethash 'openai-assistant-id configurations)
:stream ,(or (and gptel-stream gptel-use-curl
(gptel-backend-stream backend))
:json-false))))
(when gptel-temperature
(plist-put prompts-plist :temperature gptel-temperature))
(when gptel-max-tokens
(plist-put prompts-plist (if (memq gptel-model '(o1-preview o1-mini))
:max_completion_tokens :max_tokens)
gptel-max-tokens))
;; Merge request params with model and backend params.
(gptel--merge-plists
prompts-plist
(gptel-backend-request-params backend)
(gptel--model-request-params gptel-model))))
:parse-response-fn (lambda (response info)
;; TODO: peridically poll the run and check if it's completed. If so, get last message
;; https://platform.openai.com/docs/assistants/deep-dive#polling-for-updates
(error "not implemented yet!"))
:parse-stream-fn (lambda (info)
(let* ((content-strs))
(condition-case nil
(while (re-search-forward "^data:" nil t)
(save-match-data
(if (looking-at " *\\[DONE\\]")
;; Going to next step.....
(gptel-openai-assistant--update-session-step info)
(when-let* ((response (gptel--json-read))
(content (map-nested-elt
response '(:delta :content 0 :text :value)))
)
(push content content-strs)
(when-let (annotations (map-nested-elt
response '(:delta :content 0 :text :annotations)))
(cl-loop for annotation in annotations
for file-id = (map-nested-elt
annotation '(:file_citation :file_id))
if file-id do (push (format "(%s)" file-id) content-strs)))))))
(error
(goto-char (match-beginning 0))))
(apply #'concat (nreverse content-strs)))))
(defun gptel-openai-assistant--update-session-step (info &optional backend)
(cl-assert (or info backend))
(let* ((backend (or backend
(plist-get info :backend)))
(new-step
(when-let* ((step-idx (cl-incf (gptel-openai-assistant-session-step-idx backend)))
(_ (length> (gptel-openai-assistant-session-steps backend) step-idx)))
(nth step-idx (gptel-openai-assistant-session-steps backend))))
(target-buffer (or (and info
(plist-get info :buffer))
;; When being called first time, assuming it is
;; done from the current-buffer
(current-buffer)))
stream)
(setf
(gptel-openai-assistant-session-step backend) new-step
(gptel-backend-url backend) (with-current-buffer target-buffer
(when new-step
(if-let (step (alist-get new-step gptel-openai-assistant--known-steps))
(let ((url (em (gptel-openai-assistant-step-url step) new-step)))
(cl-typecase url
(string url)
(function (funcall url))
(t (error "Unknown value for url (%s) in step (%s)" url new-step))))
(error "Unknown step %s" new-step))))
;; This doesn't currently work unless I have set the process sentinel/fileter functions
;; stream (pcase new-step
;; (:threads nil)
;; (:messages nil)
;; (:runs t)))
)
(when new-step
(plist-put info :data (gptel--request-data backend (gptel-openai-assistant-session-cached-prompts backend))))
(when info
(plist-put info :wait new-step)
;; ;; KLUDGE: copied from `gptel-request'. Can this be managed upstream?
;; (plist-put info :stream (and stream
;; ;; TODO: model parameters?
;; gptel-stream gptel-use-curl
;; (gptel-backend-stream backend)))
;; KLUDGE: copied from `gptel-curl-get-response'. Can this be managed upstream?
;; (plist-put info :parser (cl--generic-method-function
;; (if stream
;; (cl-find-method
;; 'gptel-curl--parse-stream nil
;; (list (aref backend 0) t))
;; (cl-find-method
;; 'gptel--parse-response nil
;; (list (aref backend 0) t t)))))
)))
(cl-defmethod gptel--request-data ((backend gptel-openai-assistant-session) prompts)
(when (eq 0 (gptel-openai-assistant-session-step-idx backend))
(setf (gptel-openai-assistant-session-cached-prompts backend) prompts))
(let* ((step (gptel-openai-assistant-session-step backend))
(step-info (alist-get step gptel-openai-assistant--known-steps)))
(if step-info
(when-let (fn (gptel-openai-assistant-step-request-data-fn step-info))
(funcall fn backend prompts))
(error "Unknown step %s" step-info))))
(cl-defmethod gptel--parse-response ((_ gptel-openai-assistant-session) response info)
(prog1
(let* ((step (gptel-openai-assistant-session-step (plist-get info :backend)))
(step-info (alist-get step gptel-openai-assistant--known-steps)))
(if step-info
(when-let (fn (gptel-openai-assistant-step-parse-response-fn step-info))
(funcall fn response info))
(error "Unknown step %s" step)))
(gptel-openai-assistant--update-session-step info)))
(cl-defmethod gptel-curl--parse-stream ((_ gptel-openai-assistant-session) info)
(let* ((step (gptel-openai-assistant-session-step (plist-get info :backend)))
(step-info (alist-get step gptel-openai-assistant--known-steps)))
(if step-info
(when-let (fn (gptel-openai-assistant-step-parse-stream-fn step-info))
(funcall fn info))
(error "Unknown step %s" new-step))))
(defun gptel-openai-assistant-make-session (steps &optional stream)
(gptel-openai--make-assistant-session
:name "gptel-openai-assistant-session"
:step (car steps)
:steps steps
:host "api.openai.com"
:key 'gptel-api-key
:header (lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))
("OpenAI-Beta" . "assistants=v2"))))
:protocol "https"
:endpoint "/v1/threads/thread_id/runs"
:stream stream))
(defun gptel-openai-assistant-start-thread ()
"Start a assistant thread in the current buffer."
(interactive)
;; The info hasn't yet been created either.
;; FIXME: is this needed?
(setq-local gptel-backend (gptel-openai-assistant-make-session '(:threads)))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream nil))
(defun gptel-openai-assistant-add-message ()
"Start a assistant thread in the current buffer."
(interactive)
;; The info hasn't yet been created either.
;; FIXME: is this needed?
(setq-local gptel-backend (gptel-openai-assistant-make-session '(:messages)))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream nil))
(defun gptel-openai-assistant-start-run ()
"Start a assistant thread in the current buffer."
(interactive)
;; The info hasn't yet been created either.
;; FIXME: is this needed?
(setq-local gptel-backend (gptel-openai-assistant-make-session '(:runs) t))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream t))
(defun gptel-openai-assistant-send ()
"Add message to the current thread and run it. Create thread if one is not initialized."
(interactive)
(setq-local gptel-backend (gptel-openai-assistant-make-session '(:threads :messages)))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream t)) What have I done:
Earlier, I had used the callbacks to chain the method, which works fine as the fsm is started anew for each step (i.e., a new fsm for each of So either I should figure out how to mix streaming and non-streaming, or implement all steps for streaming/non-streaming. I am not entirely sure how to do the former. When doing the latter:
|
Beta Was this translation helpful? Give feedback.
-
Well, implementing the streaming based option turned out much simpler. Here's the full working code with some bug fixes on the above: EDIT: missed pre-amble (defun gptel--wait-again-p (info)
"Check if the fsm should WAIT."
(plist-get info :wait))
(defun gptel--delay-p (info)
"Test fsm transition to DELAY."
(plist-get info :delay))
(defun gptel-handle-on-wait-callback (fsm)
"Handle on-wait-callback."
(when-let ((callback (plist-get (gptel-fsm-info fsm) :on-wait-callback)))
(funcall callback)))
(defun gptel--handle-delay (fsm)
"Handle the delay state."
(let ((delay (plist-get (gptel-fsm-info fsm) :delay)))
(cl-assert (numberp delay))
(plist-put (gptel-fsm-info fsm) :delay nil)
(run-at-time delay nil (lambda () (gptel--fsm-transition fsm)))))
;; Adding TYPE -> WAIT & TYPE -> DELAY transitions
(setf (alist-get 'TYPE gptel-request--transitions) '((gptel--delay-p . DELAY)
(gptel--error-p . ERRS)
(gptel--tool-use-p . TOOL)
(gptel--wait-again-p . WAIT)
(t . DONE)))
;; MAYBE: this is useful to be connected to other states as well?
;; From DELAY go to what was expected
(setf (alist-get 'DELAY gptel-request--transitions) '((gptel--error-p . ERRS)
(gptel--tool-use-p . TOOL)
(gptel--wait-again-p . WAIT)
(t . DONE)))
(setf (alist-get 'DELAY gptel-request--handlers) '(gptel--handle-delay))
(setf (alist-get 'WAIT gptel-request--handlers) '(gptel-handle-on-wait-callback gptel--handle-wait))
(defvar-local gptel-openai-assistant-thread-id nil) (cl-defstruct (gptel-openai-assistant-session
(:constructor gptel-openai--make-assistant-session)
(:copier nil)
(:include gptel-openai))
(steps
'(:threads :messages :runs)
:documentation "The sequence of the steps.")
(step-idx -1 :documentation "Current step in steps sequence.")
cached-prompts
(step
:threads
:documentation "The current step of the session."))
(defvar gptel-openai-assistant--known-steps nil)
(cl-defstruct (gptel-openai-assistant-step
(:constructor gptel-openai-assistant--make-step))
(url
:documentation "String of function that returns the url as string")
(request-data-fn
:documentation "Takes backend and prompts as arg. See `gptel--request-data'")
(parse-response-fn
:documentation "Takes response and info as args. See `gptel--parse-response'")
(parse-stream-fn
:documentation "Takes info as args. See `gptel-curl--parse-stream'"))
(cl-defun gptel-openai-assistant--add-known-step (step &key url request-data-fn parse-response-fn parse-stream-fn)
(setf (alist-get step gptel-openai-assistant--known-steps)
(gptel-openai-assistant--make-step
:url url :request-data-fn request-data-fn :parse-response-fn parse-response-fn :parse-stream-fn parse-stream-fn)))
(gptel-openai-assistant--add-known-step
:threads
:url "https://api.openai.com/v1/threads"
:parse-response-fn (lambda (response info)
(with-current-buffer (plist-get info :buffer)
(setq-local gptel-openai-assistant-thread-id (plist-get response :id))
nil))
:parse-stream-fn (lambda (info)
(goto-char (point-min))
(re-search-forward "
?\n
?\n" nil t)
(condition-case err
(when-let* ((response (gptel--json-read))
(_ (equal "thread" (plist-get response :object))))
(gptel-openai-assistant--wait-or-update-step info)
(with-current-buffer (plist-get info :buffer)
(setq-local gptel-openai-assistant-thread-id (plist-get response :id)))
nil)
(json-parse-error
(goto-char (match-beginning 0)))
(error
(signal (car err) (cdr err))))))
(gptel-openai-assistant--add-known-step
:messages
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/messages" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
:request-data-fn (lambda (backend prompts)
(list :role "user"
:content
`[(:type "text"
:text ,(plist-get (car (last prompts)) :content))]))
:parse-stream-fn (lambda (info)
(goto-char (point-min))
(re-search-forward "
?\n
?\n" nil t)
(condition-case nil
(when-let* ((response (gptel--json-read))
(_ (equal "thread.message" (plist-get response :object))))
(gptel-openai-assistant--wait-or-update-step info)
nil)
(json-parse-error
(goto-char (match-beginning 0)))
(error
(signal (car err) (cdr err))))))
(gptel-openai-assistant--add-known-step
:runs
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/runs" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
:request-data-fn (lambda (backend prompts)
(let ((prompts-plist
`(:assistant_id ,(gethash 'openai-assistant-id configurations)
:stream ,(or (and gptel-stream gptel-use-curl
(gptel-backend-stream backend))
:json-false))))
(when gptel-temperature
(plist-put prompts-plist :temperature gptel-temperature))
(when gptel-max-tokens
(plist-put prompts-plist (if (memq gptel-model '(o1-preview o1-mini))
:max_completion_tokens :max_tokens)
gptel-max-tokens))
;; Merge request params with model and backend params.
(gptel--merge-plists
prompts-plist
(gptel-backend-request-params backend)
(gptel--model-request-params gptel-model))))
:parse-response-fn (lambda (response info)
;; TODO: peridically poll the run and check if it's completed. If so, get last message
;; https://platform.openai.com/docs/assistants/deep-dive#polling-for-updates
(error "not implemented yet!"))
:parse-stream-fn (lambda (info)
(let* ((content-strs))
(condition-case err
(while (re-search-forward "^data:" nil t)
(save-match-data
(if (looking-at " *\\[DONE\\]")
(prog1 nil
(gptel-openai-assistant--wait-or-update-step info))
(when-let* ((response (gptel--json-read))
(content (map-nested-elt
response '(:delta :content 0 :text :value)))
)
(push content content-strs)
(when-let (annotations (map-nested-elt
response '(:delta :content 0 :text :annotations)))
(cl-loop for annotation in annotations
for file-id = (map-nested-elt
annotation '(:file_citation :file_id))
if file-id do (push (format "(%s)" file-id) content-strs)))))))
((json-parse-error json-end-of-file)
(goto-char (match-beginning 0)))
(error
(signal (car err) (cdr err))))
(apply #'concat (nreverse content-strs)))))
(defun gptel-openai-assistant--wait-or-update-step (info)
(if (length>
(gptel-openai-assistant-session-steps (plist-get info :backend))
(1+ (gptel-openai-assistant-session-step-idx (plist-get info :backend))))
(progn
(plist-put info :wait t)
(plist-put info :on-wait-callback
(lambda ()
(gptel-openai-assistant--update-session-step info))))
(plist-put info :wait nil)))
(defun gptel-openai-assistant--update-session-step (info &optional backend)
(cl-assert (or info backend))
(let* ((backend (or backend
(plist-get info :backend)))
(new-step
(when-let* ((step-idx (cl-incf (gptel-openai-assistant-session-step-idx backend)))
(_ (length> (gptel-openai-assistant-session-steps backend) step-idx)))
(nth step-idx (gptel-openai-assistant-session-steps backend))))
(target-buffer (or (and info
(plist-get info :buffer))
;; When being called first time, assuming it is
;; done from the current-buffer
(current-buffer)))
stream)
(setf
(gptel-openai-assistant-session-step backend) new-step
(gptel-backend-url backend) (with-current-buffer target-buffer
(when new-step
(if-let (step (alist-get new-step gptel-openai-assistant--known-steps))
(let ((url (gptel-openai-assistant-step-url step)))
(cl-typecase url
(string url)
(function (funcall url))
(t (error "Unknown value for url (%s) in step (%s)" url new-step))))
(error "Unknown step %s" new-step))))
;; This doesn't currently work unless I have set the process sentinel/fileter functions
;; stream (pcase new-step
;; (:threads nil)
;; (:messages nil)
;; (:runs t)))
)
(when new-step
(plist-put info :data (gptel--request-data backend (gptel-openai-assistant-session-cached-prompts backend))))
(when info
(plist-put info :wait new-step)
(plist-put info :on-wait-callback nil)
;; ;; KLUDGE: copied from `gptel-request'. Can this be managed upstream?
;; (plist-put info :stream (and stream
;; ;; TODO: model parameters?
;; gptel-stream gptel-use-curl
;; (gptel-backend-stream backend)))
;; KLUDGE: copied from `gptel-curl-get-response'. Can this be managed upstream?
;; (plist-put info :parser (cl--generic-method-function
;; (if stream
;; (cl-find-method
;; 'gptel-curl--parse-stream nil
;; (list (aref backend 0) t))
;; (cl-find-method
;; 'gptel--parse-response nil
;; (list (aref backend 0) t t)))))
)))
(cl-defmethod gptel--request-data ((backend gptel-openai-assistant-session) prompts)
(when (eq 0 (gptel-openai-assistant-session-step-idx backend))
(setf (gptel-openai-assistant-session-cached-prompts backend) prompts))
(let* ((step (gptel-openai-assistant-session-step backend))
(step-info (alist-get step gptel-openai-assistant--known-steps)))
(if step-info
(when-let (fn (gptel-openai-assistant-step-request-data-fn step-info))
(funcall fn backend prompts))
(error "Unknown step %s" step-info))))
(cl-defmethod gptel--parse-response ((_ gptel-openai-assistant-session) response info)
(prog1
(let* ((step (gptel-openai-assistant-session-step (plist-get info :backend)))
(step-info (alist-get step gptel-openai-assistant--known-steps)))
(if step-info
(when-let (fn (gptel-openai-assistant-step-parse-response-fn step-info))
(funcall fn response info))
(error "Unknown step %s" step)))
(gptel-openai-assistant--update-session-step info)))
(cl-defmethod gptel-curl--parse-stream ((_ gptel-openai-assistant-session) info)
(let* ((step (gptel-openai-assistant-session-step (plist-get info :backend)))
(step-info (alist-get step gptel-openai-assistant--known-steps)))
(if step-info
(when-let (fn (gptel-openai-assistant-step-parse-stream-fn step-info))
(funcall fn info))
(error "Unknown step %s" new-step))))
(defun gptel-openai-assistant-make-session (steps &optional stream)
(gptel-openai--make-assistant-session
:name "gptel-openai-assistant-session"
:step (car steps)
:steps steps
:host "api.openai.com"
:key 'gptel-api-key
:header (lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))
("OpenAI-Beta" . "assistants=v2"))))
:protocol "https"
:endpoint "/v1/threads/thread_id/runs"
:stream stream))
(defun gptel-openai-assistant-start-thread ()
"Start a assistant thread in the current buffer."
(interactive)
;; The info hasn't yet been created either.
;; FIXME: is this needed?
(when (or (not gptel-openai-assistant-thread-id)
(y-or-n-p "There is already a thread in the buffer. Create a new thread? "))
(setq-local gptel-backend (gptel-openai-assistant-make-session '(:threads)))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream nil)))
(defun gptel-openai-assistant-add-message ()
"Start a assistant thread in the current buffer."
(interactive)
;; The info hasn't yet been created either.
;; FIXME: is this needed?
(setq-local gptel-backend (gptel-openai-assistant-make-session '(:messages)))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream nil))
(defun gptel-openai-assistant-start-run ()
"Start a assistant thread in the current buffer."
(interactive)
;; The info hasn't yet been created either.
;; FIXME: is this needed?
(setq-local gptel-backend (gptel-openai-assistant-make-session '(:runs) t))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream t))
(defun gptel-openai-assistant-send ()
"Add message to the current thread and run it. Create thread if one is not initialized."
(interactive)
(setq-local gptel-backend (gptel-openai-assistant-make-session
(if gptel-openai-assistant-thread-id
'(:messages :runs)
'(:threads :messages :runs))
t))
(gptel-openai-assistant--update-session-step nil gptel-backend)
(gptel-request nil :stream t)) |
Beta Was this translation helpful? Give feedback.
-
so, yet another implementation. What changed:
Some setup for by adding states to the fsm: (require 'gptel)
(defun gptel--wait-again-p (info)
"Check if the fsm should WAIT."
(plist-get info :wait))
(defun gptel--delay-p (info)
"Test fsm transition to DELAY."
(plist-get info :delay))
(defun gptel--await-p (info)
"Test fsm transition to AWAIT."
(plist-get info :await))
(defun gptel-handle-on-wait-again (fsm)
"Handle on-wait-callback."
(plist-put (gptel-fsm-info fsm) :wait nil))
;; Not used right now
(defun gptel-handle-on-wait-callback (fsm)
"Handle on-wait-callback."
(when-let ((callback (plist-get (gptel-fsm-info fsm) :on-wait-callback)))
(funcall callback)
(plist-put (gptel-fsm-info fsm) :on-wait-callback nil)))
(defun gptel--handle-delay (fsm)
"Handle the delay state."
(let ((delay (plist-get (gptel-fsm-info fsm) :delay)))
(cl-assert (numberp delay))
(plist-put (gptel-fsm-info fsm) :delay nil)
(run-at-time delay nil (lambda () (gptel--fsm-transition fsm)))))
;; Adding TYPE -> WAIT & TYPE -> DELAY transitions
(setf (alist-get 'TYPE gptel-request--transitions) '((gptel--await-p . AWAIT)
(gptel--delay-p . DELAY)
(gptel--error-p . ERRS)
(gptel--tool-use-p . TOOL)
(gptel--wait-again-p . WAIT)
(t . DONE)))
;; Not used right now - but will be useful when trying to poll using non streaming option
;; MAYBE: this is useful to be connected to other states as well?
;; From DELAY go to what was expected
(setf (alist-get 'DELAY gptel-request--transitions) '((gptel--await-p . AWAIT)
(gptel--error-p . ERRS)
(gptel--tool-use-p . TOOL)
(gptel--wait-again-p . WAIT)
(t . DONE)))
;; From AWAIT go to what was expected
(setf (alist-get 'AWAIT gptel-request--transitions)
'((gptel--delay-p . DELAY)
(gptel--error-p . ERRS)
(gptel--tool-use-p . TOOL)
(gptel--wait-again-p . WAIT)
(t . DONE)))
(push '(gptel--await-p . AWAIT) (alist-get 'INIT gptel-request--transitions))
(push '(gptel--await-p . AWAIT) (alist-get 'WAIT gptel-request--transitions))
(push '(gptel--await-p . AWAIT) (alist-get 'TOOL gptel-request--transitions))
(setf (alist-get 'DELAY gptel-request--handlers) '(gptel--handle-delay))
(setf (alist-get 'WAIT gptel-request--handlers) '(gptel-handle-on-wait-callback gptel-handle-on-wait-again gptel--handle-wait))
(cl-defgeneric gptel-backend--on-start-of-state (backend state info)
"When a the gptel fsm starts a new STATE, this will be called.
This method is called before the handlers of the new state are invoked.")
(cl-defmethod gptel-backend--on-start-of-state ((backend gptel-backend) state info))
(cl-defgeneric gptel-backend--on-end-of-state (backend state info)
"When a the gptel fsm transitions to a new state, this will be called with the old STATE.")
(cl-defmethod gptel-backend--on-end-of-state ((backend gptel-backend) state info))
(defun gptel--fsm-transition-handle-backend-on-start-on-end (oldfn fsm &optional new-state)
"Advice for `gptel--fsm-transition'.
Invokes the `gptel-backend--on-start-of-state' & `gptel-backend--on-end-of-state'."
(let* ((info (gptel-fsm-info fsm))
(backend (plist-get info :backend)))
(gptel-backend--on-end-of-state backend (gptel-fsm-state fsm) info)
;; the start is being called before the handlers of the next state..
(gptel-backend--on-start-of-state backend (gptel--fsm-next fsm) info)
(funcall oldfn fsm new-state)))
(advice-add #'gptel--fsm-transition :around #'gptel--fsm-transition-handle-backend-on-start-on-end) Implementation of openai-assistant: (defvar-local gptel-openai-assistant-thread-id nil)
;; TODO: Get this from api?
(defvar-local gptel-openai-assistant-assistant-id <assistant id copied from openai dashboard>)
(cl-defstruct (gptel-openai-assistant
(:constructor gptel-openai--make-assistant)
(:copier nil)
(:include gptel-openai))
messages-data)
(defvar url-http-end-of-headers)
(defun gptel--openai-assistant-url-retrive (method data url info callback)
"Get data from URL with DATA using METHOD (POST/GET).
INFO is info from gptel
CALLBACK is called with the response from calling url-retrive."
(let ((url-request-method "POST")
(url-request-extra-headers
(append `(("Content-Type" . "application/json")
("Authorization" . ,(concat "Bearer " (gptel--get-api-key)))
("OpenAI-Beta" . "assistants=v2"))))
(url-request-data data))
(url-retrieve (cl-typecase url
(string url)
(function (funcall url))
(t (error "Unknown value for url (%s)" url)))
(lambda (_)
(pcase-let ((`(,response ,http-status ,http-msg ,error)
(custom--url-parse-response))
(buf (current-buffer)))
(plist-put info :http-status http-status)
(plist-put info :status http-msg)
(when error
(plist-put info :error error))
(with-current-buffer (plist-get info :buffer)
(funcall callback response))
(kill-buffer buf)
))
nil t t)))
;; copied from `gptel--url-parse-response' beacuse we don't want the following:
;; (gptel--parse-response backend response proc-info)
(defun custom--url-parse-response ()
"Parse response from url-retrive."
(when gptel-log-level ;logging
(save-excursion
(goto-char url-http-end-of-headers)
(when (eq gptel-log-level 'debug)
(gptel--log (gptel--json-encode (buffer-substring-no-properties (point-min) (point)))
"response headers"))
(gptel--log (buffer-substring-no-properties (point) (point-max))
"response body")))
(if-let* ((http-msg (string-trim (buffer-substring (line-beginning-position)
(line-end-position))))
(http-status
(save-match-data
(and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg)
(match-string 1 http-msg))))
(response (progn (goto-char url-http-end-of-headers)
(condition-case nil
(gptel--json-read)
(error 'json-read-error)))))
(cond
;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194
((or (memq url-http-response-status '(200 100))
(string-match-p "\\(?:1\\|2\\)00 OK" http-msg))
(list response
http-status http-msg))
((plist-get response :error)
(list nil http-status http-msg (plist-get response :error)))
((eq response 'json-read-error)
(list nil http-status (concat "(" http-msg ") Malformed JSON in response.") "json-read-error"))
(t (list nil http-status (concat "(" http-msg ") Could not parse HTTP response.")
"Could not parse HTTP response.")))
(list nil (concat "(" http-msg ") Could not parse HTTP response.")
"Could not parse HTTP response.")))
(defun gptel-openai-assistant-start-thread (info &optional callback)
"Use the threads endpoint to start new thread.
Set the `gptel-openai-assistant-thread-id' of the buffer.
INFO is the info plist from gptel.
CALLBACK is invoked without any args after successfully creating a thread."
(gptel--openai-assistant-url-retrive
"POST" nil "https://api.openai.com/v1/threads"
info
(lambda (response)
(with-current-buffer
(plist-get info :buffer)
(setq-local gptel-openai-assistant-thread-id (plist-get response :id))
(when callback (funcall callback))))))
(defun gptel-openai-assistant-add-message (info callback)
"Use the messages endpoint to start new thread.
Needs the `gptel-openai-assistant-thread-id' of the buffer to be set.
INFO is the info plist from gptel.
CALLBACK is invoked without any args after successfully creating a thread."
(gptel--openai-assistant-url-retrive
"POST"
(encode-coding-string
(gptel--json-encode (gptel-openai-assistant-messages-data (plist-get info :backend)))
'utf-8)
(with-current-buffer
(plist-get info :buffer)
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/messages"
gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
info
(lambda (response)
(when callback (funcall callback)))))
(cl-defmethod gptel--request-data ((backend gptel-openai-assistant) prompts)
(setf (gptel-openai-assistant-messages-data backend)
(list :role "user"
:content
`[(:type "text"
:text ,(plist-get (car (last prompts)) :content))]))
(let ((prompts-plist
`(:assistant_id ,gptel-openai-assistant-assistant-id
:stream ,(or (and gptel-stream gptel-use-curl
(gptel-backend-stream backend))
:json-false))))
(when gptel-temperature
(plist-put prompts-plist :temperature gptel-temperature))
(when gptel-max-tokens
(plist-put prompts-plist (if (memq gptel-model '(o1-preview o1-mini))
:max_completion_tokens :max_tokens)
gptel-max-tokens))
;; Merge request params with model and backend params.
(gptel--merge-plists
prompts-plist
(gptel-backend-request-params backend)
(gptel--model-request-params gptel-model))))
(cl-defmethod gptel--parse-response ((_ gptel-openai-assistant) response info)
;; TODO: peridically poll the run and check if it's completed. If so, get last message
;; https://platform.openai.com/docs/assistants/deep-dive#polling-for-updates
(error "not implemented yet!"))
(cl-defmethod gptel-curl--parse-stream ((_ gptel-openai-assistant) info)
(let* ((content-strs))
(condition-case err
(while (re-search-forward "^data:" nil t)
(save-match-data
(unless (looking-at " *\\[DONE\\]")
(when-let* ((response (gptel--json-read))
(content (map-nested-elt
response '(:delta :content 0 :text :value)))
)
(if-let* ((annotations (map-nested-elt
response '(:delta :content 0 :text :annotations)))
(_ (length> annotations 0)))
(cl-loop for annotation across annotations
for file-id = (map-nested-elt
annotation '(:file_citation :file_id))
if file-id
do (push (format "[[file_citation:%s][%s]]" file-id content) content-strs))
(push content content-strs))))))
((json-parse-error json-end-of-file search-failed)
(goto-char (match-beginning 0)))
(error
(signal (car err) (cdr err))))
(apply #'concat (nreverse content-strs))))
(cl-defmethod gptel--handle-openai-assistant-await (fsm)
(let* ((info (gptel-fsm-info fsm))
(await-manual-state (plist-get info :await)))
(when (gptel-openai-assistant-p (plist-get info :backend))
;; This tells us await manual is happening from after the init
(pcase await-manual-state
(:send-message
(plist-put info :await nil)
(cl-labels ((send-message ()
(gptel-openai-assistant-add-message
info
(lambda ()
(plist-put info :wait t)
(gptel--fsm-transition fsm)))))
(if gptel-openai-assistant-thread-id
(send-message)
(gptel-openai-assistant-start-thread
info
(lambda ()
(if (plist-get info :error)
(gptel--fsm-transition fsm)
(send-message)))))))
(:await-runs-complete
(plist-put info :await nil)
;; TODO: The polling can happen here
(error "Not implemented yet"))))))
(cl-defmethod gptel-backend--on-end-of-state ((backend gptel-openai-assistant) state info)
(when (eq state 'INIT)
(plist-put info :await :send-message)))
;;;###autoload
(defun gptel-openai-make-assistant ()
"Create a openai-assistant backend."
(gptel-openai--make-assistant
:name "gptel-openai-assistant"
:host "api.openai.com"
:key 'gptel-api-key
:models (gptel--process-models gptel--openai-models)
:header (lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))
("OpenAI-Beta" . "assistants=v2"))))
:protocol "https"
:endpoint "/v1/threads/thread_id/runs"
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/runs" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
:stream t))
;;;###autoload
(defun gptel-openai-assistant-create-new-thread ()
"Create a new thread in the current buffer."
(interactive)
(gptel-openai-assistant-start-thread `(:buffer ,(buffer-name))))
(push 'gptel--handle-openai-assistant-await (alist-get 'AWAIT gptel-request--handlers))
(setf (alist-get "openai-assistant" gptel--known-backends
nil nil #'equal)
(gptel-openai-make-assistant)) |
Beta Was this translation helpful? Give feedback.
-
So, here's a full working version for both streaming and non-streaming versions. One issue that I ran into with implementing the non-streaming option was with gptel always sending POST requests. However, ideally, the GET method would be used to get the messages after the run has completed. As a workaround I am using the modify messages endpoint where I request an empty modification, which also returns the messages I am looking for. ;; Openai assistant api ******************************************************************
(defvar-local gptel-openai-assistant-thread-id nil)
;; TODO: Get this from api?
(defvar-local gptel-openai-assistant-assistant-id "<assistant id from dashboard>")
;; Helper functions **********************************************************************
(defvar url-http-end-of-headers)
(defun gptel-openai-assistant--url-retrive (method data url info callback)
"Get data from URL with DATA using METHOD (POST/GET).
INFO is info from gptel
CALLBACK is called with the response from calling url-retrive."
(let ((url-request-method method)
(url-request-extra-headers
(append `(("Content-Type" . "application/json")
("Authorization" . ,(concat "Bearer " (gptel--get-api-key)))
("OpenAI-Beta" . "assistants=v2"))))
(url-request-data data))
(when gptel-log-level ;logging
(when (eq gptel-log-level 'debug)
(gptel--log (gptel--json-encode
(mapcar (lambda (pair) (cons (intern (car pair)) (cdr pair)))
url-request-extra-headers))
"request headers"))
(when url-request-data
(gptel--log url-request-data "request body")))
(url-retrieve (cl-typecase url
(string url)
(function (funcall url))
(t (error "Unknown value for url (%s) in step" url)))
(lambda (_)
(pcase-let ((`(,response ,http-status ,http-msg ,error)
(gptel-openai-assistant--url-parse-response))
(buf (current-buffer)))
(plist-put info :http-status http-status)
(plist-put info :status http-msg)
(when error
(plist-put info :error error))
(with-current-buffer (plist-get info :buffer)
(funcall callback response))
(kill-buffer buf)
))
nil t t)))
;; copied from `gptel--url-parse-response' beacuse we don't want the following:
;; (gptel--parse-response backend response proc-info)
(defun gptel-openai-assistant--url-parse-response ()
"Parse response from url-retrive."
(when gptel-log-level ;logging
(save-excursion
(goto-char url-http-end-of-headers)
(when (eq gptel-log-level 'debug)
(gptel--log (gptel--json-encode (buffer-substring-no-properties (point-min) (point)))
"response headers"))
(gptel--log (buffer-substring-no-properties (point) (point-max))
"response body")))
(if-let* ((http-msg (string-trim (buffer-substring (line-beginning-position)
(line-end-position))))
(http-status
(save-match-data
(and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg)
(match-string 1 http-msg))))
(response (progn (goto-char url-http-end-of-headers)
(condition-case nil
(gptel--json-read)
(error 'json-read-error)))))
(cond
;; FIXME Handle the case where HTTP 100 is followed by HTTP (not 200) BUG #194
((or (memq url-http-response-status '(200 100))
(string-match-p "\\(?:1\\|2\\)00 OK" http-msg))
(list response
http-status http-msg))
((plist-get response :error)
(list nil http-status http-msg (plist-get response :error)))
((eq response 'json-read-error)
(list nil http-status (concat "(" http-msg ") Malformed JSON in response.") "json-read-error"))
(t (list nil http-status (concat "(" http-msg ") Could not parse HTTP response.")
"Could not parse HTTP response.")))
(list nil (concat "(" http-msg ") Could not parse HTTP response.")
"Could not parse HTTP response.")))
;; Core function to work with gptel ******************************************************
(cl-defstruct (gptel-openai-assistant
(:constructor gptel-openai--make-assistant)
(:copier nil)
(:include gptel-openai))
messages-data)
(defun gptel-openai-assistant-start-thread (info &optional callback)
"Use the threads endpoint to start new thread.
Set the `gptel-openai-assistant-thread-id' of the buffer.
INFO is the info plist from gptel.
CALLBACK is invoked without any args after successfully creating a thread."
(gptel-openai-assistant--url-retrive
"POST" nil "https://api.openai.com/v1/threads"
info
(lambda (response)
(with-current-buffer
(plist-get info :buffer)
(setq-local gptel-openai-assistant-thread-id (plist-get response :id))
(when callback (funcall callback))))))
(defun gptel-openai-assistant-add-message (info callback)
"Use the messages endpoint to start new thread.
Needs the `gptel-openai-assistant-thread-id' of the buffer to be set.
INFO is the info plist from gptel.
CALLBACK is invoked without any args after successfully creating a thread."
(gptel-openai-assistant--url-retrive
"POST"
(encode-coding-string
(gptel--json-encode (gptel-openai-assistant-messages-data (plist-get info :backend)))
'utf-8)
(with-current-buffer
(plist-get info :buffer)
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/messages"
gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
info
(lambda (response)
(when callback (funcall callback)))))
(cl-defmethod gptel--request-data ((backend gptel-openai-assistant) prompts)
(setf (gptel-openai-assistant-messages-data backend)
(list :role "user"
:content
`[(:type "text"
:text ,(plist-get (car (last prompts)) :content))]))
(if (and gptel-stream gptel-use-curl
(gptel-backend-stream backend))
(let ((prompts-plist
`(:assistant_id ,gptel-openai-assistant-assistant-id
:stream ,(or (and gptel-stream gptel-use-curl
(gptel-backend-stream backend))
:json-false))))
(when gptel-temperature
(plist-put prompts-plist :temperature gptel-temperature))
(when gptel-max-tokens
(plist-put prompts-plist (if (memq gptel-model '(o1-preview o1-mini))
:max_completion_tokens :max_tokens)
gptel-max-tokens))
;; Merge request params with model and backend params.
(gptel--merge-plists
prompts-plist
(gptel-backend-request-params backend)
(gptel--model-request-params gptel-model)))
`(:metadata nil)))
(cl-defmethod gptel--parse-response ((_ gptel-openai-assistant) response info)
(string-join
(mapcar
(lambda (content)
(let ((str (map-nested-elt content '(:text :value))))
(mapcar
(lambda (annotation)
(setf str
(string-replace
(plist-get annotation :text)
(format "[file_citation:%s]" (map-nested-elt annotation '(:file_citation :file_id)))
str)))
(map-nested-elt content '(:text :annotations)))
str))
(plist-get response :content))
" "))
(cl-defmethod gptel-curl--parse-stream ((_ gptel-openai-assistant) info)
(let* ((content-strs))
(condition-case err
(while (re-search-forward "^data:" nil t)
(save-match-data
(unless (looking-at " *\\[DONE\\]")
(when-let* ((response (gptel--json-read))
(content (map-nested-elt
response '(:delta :content 0 :text :value)))
)
(if-let* ((annotations (map-nested-elt
response '(:delta :content 0 :text :annotations)))
(_ (length> annotations 0)))
(cl-loop for annotation across annotations
for file-id = (map-nested-elt
annotation '(:file_citation :file_id))
if file-id
do (push (format "[file_citation:%s]" file-id content) content-strs))
(push content content-strs))))))
((json-parse-error json-end-of-file search-failed)
(goto-char (match-beginning 0)))
(error
(signal (car err) (cdr err))))
(apply #'concat (nreverse content-strs))))
(defun gptel-openai-assistant--handle-await (fsm)
(let* ((info (gptel-fsm-info fsm))
(await-manual-state
(or (plist-get info :openai-assistant-await)
(let ((history (plist-get info :history)))
(when (and
(eq (car history) 'INIT)
(null (cdr history)))
:send-message)))))
(plist-put info :openai-assistant-await nil)
(when (gptel-openai-assistant-p (plist-get info :backend))
(with-current-buffer (plist-get (gptel-fsm-info fsm) :buffer)
(gptel--update-status " Waiting..." 'warning))
;; This tells us await manual is happening from after the init
(pcase await-manual-state
(:send-message
(cl-labels ((send-message ()
(gptel-openai-assistant-add-message
info
(lambda ()
(if (plist-get info :stream)
(plist-put info :openai-assistant-wait t)
(plist-put info :openai-assistant-await :openai-assistant-runs))
(gptel--fsm-transition fsm)))))
(if gptel-openai-assistant-thread-id
(send-message)
(gptel-openai-assistant-start-thread
info
(lambda ()
(if (plist-get info :error)
(gptel--fsm-transition fsm)
(send-message)))))))
(:openai-assistant-runs
(gptel-openai-assistant--url-retrive
"POST"
(encode-coding-string
(gptel--json-encode `(:assistant_id ,gptel-openai-assistant-assistant-id))
'utf-8)
(with-current-buffer
(plist-get info :buffer)
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/runs"
gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
info
(lambda (response)
(unless (plist-get info :error)
(plist-put info :openai-assistant-run-id (plist-get response :id))
(plist-put info :openai-assistant-await :openai-assistant-runs-completed-p)
(plist-put info :openai-assistant-delay 0.5))
(gptel--fsm-transition fsm))))
(:openai-assistant-runs-completed-p
(gptel-openai-assistant--url-retrive
"GET"
nil
(with-current-buffer
(plist-get info :buffer)
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/runs/%s"
gptel-openai-assistant-thread-id
(plist-get info :openai-assistant-run-id))
(user-error "No thread in current buffer to add messages!")))
info
(lambda (response)
(unless (plist-get info :error)
(pcase (plist-get response :status)
((or "in_progress" "queued")
(plist-put info :openai-assistant-await :openai-assistant-runs-completed-p)
(plist-put info :openai-assistant-delay 0.5))
("completed"
(plist-put info :openai-assistant-await :openai-assistant-list-messages))
(status (error "Unhandle state for run %s" status))))
(gptel--fsm-transition fsm))))
(:openai-assistant-list-messages
(gptel-openai-assistant--url-retrive
"GET"
nil
(with-current-buffer
(plist-get info :buffer)
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/messages?run_id=%s"
gptel-openai-assistant-thread-id
(plist-get info :openai-assistant-run-id))
(user-error "No thread in current buffer to add messages!")))
info
(lambda (response)
(unless (plist-get info :error)
(plist-put info :openai-assistant-message-id (map-nested-elt response '(:data 0 :id)))
(plist-put info :openai-assistant-wait t))
(gptel--fsm-transition fsm))))
(_ (error "Unknown await state %s" await-manual-state))))))
;;;###autoload
(defun gptel-make-openai-assistant ()
"Create a openai-assistant backend."
(gptel-openai--make-assistant
:name "gptel-openai-assistant"
:host "api.openai.com"
:key 'gptel-api-key
:models (gptel--process-models gptel--openai-models)
:header (lambda () (when-let (key (gptel--get-api-key))
`(("Authorization" . ,(concat "Bearer " key))
("OpenAI-Beta" . "assistants=v2"))))
:protocol "https"
:endpoint "/v1/threads/thread_id/runs"
:url (lambda ()
(if gptel-openai-assistant-thread-id
(format "https://api.openai.com/v1/threads/%s/runs" gptel-openai-assistant-thread-id)
(user-error "No thread in current buffer to add messages!")))
:stream t))
;;;###autoload
(defun gptel-openai-assistant-create-new-thread ()
"Create a new thread in the current buffer."
(interactive)
(gptel-openai-assistant-start-thread `(:buffer ,(buffer-name))))
;; Modify gptel vars *********************************************************************
(setf (alist-get "openai-assistant" gptel--known-backends
nil nil #'equal)
(gptel-make-openai-assistant))
(defun gptel-openai-assistant--backend-is-oaia-p (info)
"Check if backend is openai-assistant."
(gptel-openai-assistant-p (plist-get info :backend)))
(defun gptel-openai-assistant--init-to-await (info)
"If in first INIT, move to AWAIT"
(and (gptel-openai-assistant--backend-is-oaia-p info)
;; If :history is not set or empty, means state is at INIT
(null (plist-get info :history))))
(defun gptel-openai-assistant--wait-again-p (info)
"Check if the fsm should WAIT."
(and (gptel-openai-assistant--backend-is-oaia-p info)
(plist-get info :openai-assistant-wait)))
(defun gptel-openai-assistant--delay-p (info)
"Test fsm transition to DELAY."
(and (gptel-openai-assistant--backend-is-oaia-p info)
(plist-get info :openai-assistant-delay)))
(defun gptel-openai-assistant--await-p (info)
"Test fsm transition to AWAIT."
(and (gptel-openai-assistant--backend-is-oaia-p info)
(plist-get info :openai-assistant-await)))
(defun gptel-openai-assistant--handle-wait (fsm)
"Handle wait-again."
(let ((info (gptel-fsm-info fsm)))
(when (gptel-openai-assistant--backend-is-oaia-p info)
(unless gptel-openai-assistant-thread-id
(user-error "No thread in current buffer to add messages!"))
(plist-put info :openai-assistant-wait nil)
(setf (gptel-backend-url (plist-get info :backend))
(if (plist-get info :stream)
(format "https://api.openai.com/v1/threads/%s/runs" gptel-openai-assistant-thread-id)
;; FIXME: gptel-always sends POST. Ideally, this should be the GET endpoint
;; So using the endpoint for updating messages to get messages related to a run.
(format "https://api.openai.com/v1/threads/%s/messages/%s"
gptel-openai-assistant-thread-id
(plist-get info :openai-assistant-message-id)))))))
(defun gptel-openai-assistant--handle-delay (fsm)
"Handle the delay state."
(let ((info (gptel-fsm-info fsm)))
(and (gptel-openai-assistant--backend-is-oaia-p info)
(let ((delay (plist-get info :openai-assistant-delay)))
(cl-assert (numberp delay))
(plist-put info :openai-assistant-delay nil)
(run-at-time delay nil (lambda () (gptel--fsm-transition fsm)))))))
;; INIT should transition to AWAIT
(push '(gptel-openai-assistant--init-to-await . AWAIT) (alist-get 'INIT gptel-request--transitions))
;; Adding TYPE -> WAIT/DELAY/AWAIT
;; DELAY & AWAIT has to happen at the very beginning
;; WAIT should be tested right before DONE
(setf (alist-get 'TYPE gptel-request--transitions)
(append
'((gptel-openai-assistant--delay-p . DELAY)
(gptel-openai-assistant--await-p . AWAIT))
;; *sigh*
(cl-loop for transition in (alist-get 'TYPE gptel-request--transitions)
;; Insert the wait again, right before DONE
if (eq (cdr transition) 'DONE)
collect '(gptel-openai-assistant--wait-again-p . WAIT) into retval
collect transition into retval
finally return retval)))
;; From DELAY go to what was expected
;; Same as TYPE but without the DELAY itself!
(setf (alist-get 'DELAY gptel-request--transitions) (cl-loop for transition in (alist-get 'TYPE gptel-request--transitions)
unless (eq (cdr transition) 'DELAY)
collect transition))
;; From AWAIT go to what was expected. Same as TYPE
(setf (alist-get 'AWAIT gptel-request--transitions) (alist-get 'TYPE gptel-request--transitions))
;; AWAIT should be tested for first in WAIT and TOOL
(push '(gptel-openai-assistant--await-p . AWAIT) (alist-get 'WAIT gptel-request--transitions))
(push '(gptel-openai-assistant--await-p . AWAIT) (alist-get 'TOOL gptel-request--transitions))
;; Adding DELAY handler
(push '(DELAY gptel-openai-assistant--handle-delay) gptel-request--handlers)
;; Handle the wait-again in WAIT
(push 'gptel-openai-assistant--handle-wait (alist-get 'WAIT gptel-request--handlers))
;; Handle AWAIT
(push '(AWAIT gptel-openai-assistant--handle-await) gptel-request--handlers) |
Beta Was this translation helpful? Give feedback.
-
You can use |
Beta Was this translation helpful? Give feedback.
-
@ahmed-shariff Where are we on the Assistants API integration? Is there anything I need to do? |
Beta Was this translation helpful? Give feedback.
-
It looks like nobody has proposed any new issues here. Maybe it's the
challenge to bring all those new things into this great emacs ai
package. I'll just bring some basic the things into users attention. I
hope as we further learn the basics of how the Assistant API works, We
can extend our package with even more power.
The documentaion is Here OpenAI Platform
Step 1: Create an Assistant
I don't know if we can set up the assistent setting from the
web interface instead of the Python script. Because what I
found is that on the web interface, we have more easily
control over the things. Ideally, I'm thinking about once we
create a new assistant, we have a unique ID for that system,
and then we can use that ID to communicate the things that we
are interested in.
Step 2: Create a Thread
Step 3: Add a Message to a Thread
Step two and three looks to me is the same thing, which we
kind of start a new section to talk to this assistant. But,
yeah, different from our previous function in this package,
now we have ability to upload files, and in the near future we
can upload images in the coming months.
Step 4: Run the Assistant
Step 5: Display the Assistant's Response
Steps four and five is essentially we are sending our input to
API and we expect data or output from the API and then we
decode the content and properly process within Emacs, either
we do region replacement or we do some magic Org-mode
manipulations.
For me, I need to figure out a minimum elisp function to properly set
up the request information and then gather feedback from the current
API and from there I can probably tweak about the new API.
Beta Was this translation helpful? Give feedback.
All reactions