Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c8caf17
Use robby-api-url when getting models
stevemolitor Mar 31, 2024
d388e9e
Log raw curl response
stevemolitor Mar 31, 2024
908e27e
Fix bug setting robby -openai-api-key
stevemolitor Mar 31, 2024
86074a5
lazy load robby models
stevemolitor Apr 1, 2024
2e09746
Refactor API request functions and add chat URL function
stevemolitor Apr 1, 2024
097710f
fix models URL
stevemolitor Apr 1, 2024
76545a2
Add to robby-api-url docstring
stevemolitor Apr 1, 2024
b17441c
Make robby log buffer read-only
stevemolitor Apr 2, 2024
c0f7962
support multiple providers
stevemolitor Apr 2, 2024
3e07599
Make sure to stop the spinner when killing robby process
stevemolitor Apr 2, 2024
a1eaa26
Fix bug losing history with streaming responses
stevemolitor Apr 2, 2024
18f49b1
Added new test for robby--request-input
stevemolitor Apr 2, 2024
feb7f00
Fix togetherai default model
stevemolitor Apr 3, 2024
8aa8612
Pass default model for provider if no model specified in API options
stevemolitor Apr 3, 2024
49be612
Add clean-compiled as dependency to compile in Makefile
stevemolitor Apr 3, 2024
ce54270
Refactor providers logic to remove circular deps and simplify
stevemolitor Apr 3, 2024
0d89652
Add OpenAI and TogetherAI providers, dynamically load custom var
stevemolitor Apr 19, 2024
945da9e
fix robby--provider-type customization helper, clean up cruft
stevemolitor Apr 21, 2024
1730441
replace condition-case with ignore-errors in one instance
stevemolitor Apr 21, 2024
830efe0
minor cleanups
stevemolitor Apr 21, 2024
894929d
remove duplicate robby-run-command definition
stevemolitor Apr 21, 2024
4aa4977
fix compilation error re robby--provider-settings not defined
stevemolitor Apr 21, 2024
8f1fe1e
check HTTP status on API requests, better error handling
stevemolitor Apr 22, 2024
f6fc59f
fix missing robby--provider-settings in tests
stevemolitor Apr 22, 2024
a0ff2e0
Add API error handling and tests
stevemolitor Apr 26, 2024
abf7713
Add Mistral AI Provider to Robby
stevemolitor Apr 26, 2024
b2e1750
Remove unused robby--models-url function
stevemolitor Apr 26, 2024
c5c8fb3
Fix bug fetching models
stevemolitor May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ test: install
-l ./test/robby-request-test.el \
-l ./test/robby-test-env.el \
-l ./test/robby-utils-test.el \
-l ./test/robby-validation-test.el \
-eval '(ert-run-tests-batch-and-exit "$(MATCH)")'

EL_FILES := $(wildcard *.el)
Expand All @@ -41,7 +42,7 @@ EL_FILES := $(wildcard *.el)
checkdoc:
for FILE in ${EL_FILES}; do $(EMACS) --batch -L . -l ./test/robby-test-env.el -eval "(checkdoc-file \"$$FILE\")" ; done

compile: install
compile: install clean-compiled
$(EMACS) --batch -L . -l ./test/robby-test-env.el -f batch-byte-compile robby-*.el

lint: install
Expand Down
16 changes: 16 additions & 0 deletions fixtures/streaming-response-complete-status-200.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
data:
{"id":"chatcmpl-a","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data:
{"id":"chatcmpl-a","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data:
{"id":"chatcmpl-7bx5i7z5CscbA6mQgHGJ2qlXiv15s","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" there!"},"finish_reason":null}]}

data:
{"id":"chatcmpl-7bx5i7z5CscbA6mQgHGJ2qlXiv15s","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data:
[DONE]

HTTP STATUS: 200
17 changes: 17 additions & 0 deletions fixtures/streaming-response-complete-status-400.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
data:
{"id":"chatcmpl-a","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data:
{"id":"chatcmpl-a","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data:
{"id":"chatcmpl-7bx5i7z5CscbA6mQgHGJ2qlXiv15s","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":" there!"},"finish_reason":null}]}

data:
{"id":"chatcmpl-7bx5i7z5CscbA6mQgHGJ2qlXiv15s","object":"chat.completion.chunk","created":1689279638,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data:
[DONE]

HTTP STATUS: 400

12 changes: 12 additions & 0 deletions fixtures/streaming-response-openai-model-not-found.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"error": {
"message": "The model `adfasdf` does not exist or you do not have access to it.",
"type": "invalid_request_error",
"param": null,
"code": "model_not_found"
}
}

HTTP STATUS: 404


1 change: 1 addition & 0 deletions fixtures/streaming-response-togetherai-model-not-found.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"error":{"message":"Unable to access model adfasdf. Please visit https://api.together.xyz to see the list of supported models or contact the owner to request access.","type":"invalid_request_error","param":null,"code":"model_not_found"}} HTTP STATUS: 404
17 changes: 7 additions & 10 deletions robby-api-key.el
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
;;; Code:
(require 'auth-source)

;; declared in robby-customization.el
(defvar robby-openai-api-key)

;; declared in robby-providers.el
(declare-function robby--provider-host ())

(defun robby--get-api-key-from-auth-source ()
"Get api key from auth source."
(if-let ((secret (plist-get (car (auth-source-search
:host "api.openai.com"
:host (robby--provider-host)
:user "apikey"
:require '(:secret)))
:secret)))
Expand All @@ -19,15 +25,6 @@
secret)
(user-error "No `robby-api-key' found in auth source")))

(defcustom robby-openai-api-key #'robby--get-api-key-from-auth-source
"OpenAI API key.

A string, or a function that returns the API key."
:group 'robby
:type '(choice
(string :tag "OpenAI API key")
(function :tag "Function that returns the OpenAI API key")))

(defun robby--get-api-key ()
"Get api key from `robby-api-key'."
(cond
Expand Down
47 changes: 37 additions & 10 deletions robby-customization.el
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

;;; Code:

(require 'map)
(require 'spinner)

(require 'robby-api-key)
(require 'robby-validation)
(require 'robby-utils)
(require 'robby-validation)

;;; Variable declarations
(defvar robby--provider-settings) ; defined in robby-provider.el

;;; function to validate custom api options
(defun robby--validate-custom-api-option (name)
Expand All @@ -30,6 +34,15 @@ Return an error message if the value is invalid, or nil if it is valid."
:group 'tools
:tag "robby")

(defcustom robby-openai-api-key #'robby--get-api-key-from-auth-source
"OpenAI API key.

A string, or a function that returns the API key."
:group 'robby
:type '(choice
(string :tag "OpenAI API key")
(function :tag "Function that returns the OpenAI API key")))

(defcustom robby-logging nil
"Log to *robby-log* buffer if t."
:type 'boolean
Expand Down Expand Up @@ -83,24 +96,38 @@ It should include a `%s' placeholder for the spinner."
:type 'string
:group 'robby)

(defcustom robby-chat-system-message "You are an AI tool embedded within Emacs. Assist users with their tasks and provide information as needed. Do not engage in harmful or malicious behavior. Please provide helpful information. Answer concisely."
(defcustom robby-chat-system-message
"You are an AI tool embedded within Emacs. Assist users with their tasks and provide information as needed. Do not engage in harmful or malicious behavior. Please provide helpful information. Answer concisely."
"System message to use with OpenAI Chat API."
:type 'string
:group 'robby)

(defcustom robby-api-url "https://api.openai.com/v1/chat/completions"
"URL to use for OpenAI API requests."
:type 'string
:group 'robby)

;;; chat api options
(defgroup robby-chat-api nil
"Options to pass to the chat API."
:group 'robby)

(defcustom robby-chat-model "gpt-3.5-turbo"
"The model to use with the completions API."
:type 'string
(defun robby--provider-type ()
"Get the `robby-provider' custom type.

Includes a choice for each provider added via
`robby-add-provider'."
`(choice
,@(seq-map
(lambda (provider-settings)
`(const :tag ,(plist-get (cdr provider-settings) :name) ,(car provider-settings)))
robby--provider-settings)))

(defcustom robby-provider 'openai
"The AI provider to use."
:type (robby--provider-type)
:group 'robby)

(defcustom robby-chat-model nil
"The model to use with the chat completions API.

Nil means use the default model for the provider."
:type '(choice string (const nil))
:group 'robby-chat-api)

(defcustom robby-chat-max-tokens 2000
Expand Down
1 change: 0 additions & 1 deletion robby-define-command.el
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

(require 'robby-run-command)


(cl-defmacro robby-define-command (name
docstring
&key
Expand Down
6 changes: 3 additions & 3 deletions robby-logging.el
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"Insert MSG in `robby--log' buffer."
(if robby-logging
(with-current-buffer (get-buffer-create robby--log-buffer)
(insert msg))))

(provide 'robby-logging)
(setq buffer-read-only nil)
(insert msg)
(setq buffer-read-only t))))

(provide 'robby-logging)

Expand Down
26 changes: 26 additions & 0 deletions robby-mistralai-provider.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
;;; robby-mistralai-provider.el --- robby mistralai provider -*- lexical-binding:t -*-

;;; Code:

(require 'robby-provider)

(require 'cl-generic)

(robby-add-provider
:symbol 'mistralai
:name "Mistral AI"
:host "api.mistral.ai"
:default-model "mistral-small"
:models-path "/v1/models")

;; example:
;; ((object . "error") (message . "Invalid model: gpt-4-turbo-preview") (type . "invalid_model") (param) (code . "1500"))
(cl-defmethod robby-provider-parse-error :around (data &context (robby-provider (eql 'mistralai)))
"Parse mistralai error from response DATA."
(let ((object (alist-get 'object data)))
(when (and (stringp "error") (string= object "error"))
(alist-get 'message data))))

(provide 'robby-mistralai-provider)

;;; robby-mistralai-provider.el ends here
20 changes: 10 additions & 10 deletions robby-models.el
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,36 @@
(require 'robby-api-key)
(require 'robby-request)
(require 'robby-customization)
(require 'robby-provider)

(defvar robby-models nil)
(defvar robby--models nil)

(defun robby--get-models ()
"Get the list of available models from OpenAI.

Make request to OpenAI API to get the list of available models."
(if robby-models
robby-models
Make request to OpenAI API to get the list of available models."
(if robby--models
robby--models
(let* ((inhibit-message t)
(message-log-max nil)
(url "https://api.openai.com/v1/models")
(url (concat "https://" (robby--provider-host) (robby--provider-models-path)))
(url-request-method "GET")
(url-request-extra-headers
`(("Content-Type" . "application/json")
("Authorization" . ,(concat "Bearer " (robby--get-api-key)))))
(inhibit-message t)
(message-log-max nil))
(with-current-buffer (url-retrieve-synchronously url)
(robby--log (format "Models response: %s\n" (buffer-string)))
(goto-char (point-min))
(re-search-forward "^{")
(re-search-forward "^[[{]")
(backward-char 1)
(let* ((json-object-type 'alist)
(resp (json-read))
(err (robby--request-parse-error-data resp)))
(err (robby-provider-parse-error resp)))
(if err
(error "Error fetching models: %S" err)
(let* ((all-models (seq-map (lambda (obj) (cdr (assoc 'id obj))) (cdr (assoc 'data resp))))
(gpt-models (seq-filter (lambda (name) (string-prefix-p "gpt" name)) all-models)))
(setq robby-models gpt-models))))))))
(setq robby--models (robby-provider-parse-models resp))))))))

(provide 'robby-models)

Expand Down
25 changes: 25 additions & 0 deletions robby-openai-provider.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
;;; robby-openai-provider.el --- robby openai provider -*- lexical-binding:t -*-

;;; Code:

(require 'robby-provider)

(require 'cl-generic)

(robby-add-provider
:symbol 'openai
:name "OpenAI"
:host "api.openai.com"
:default-model "gpt-3.5-turbo"
:models-path "/v1/models")

(cl-defmethod robby-provider-parse-models :around (data &context (robby-provider (eql 'openai)))
(let ((models (cl-call-next-method data)))
(seq-filter
(lambda (name)
(string-prefix-p "gpt" name))
models)))

(provide 'robby-openai-provider)

;;; robby-openai-provider.el ends here
2 changes: 1 addition & 1 deletion robby-process.el
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Emacs Lisp, do not print messages if SILENTP is t.
Note that you cannot currently kill the last robby process if you
are using `url-retreive'; you must be using `curl'"
(interactive)
(robby--spinner-stop)
(if (robby--process-running-p)
(progn
(robby--spinner-stop)
(kill-process robby--last-process)
(when (not silentp)
(message "robby process killed")))
Expand Down
59 changes: 59 additions & 0 deletions robby-provider.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
;;; robby-provider.el --- Support Multiple AI Services -*- lexical-binding:t -*-

;;; Commentary:

;;; Code:
(require 'cl-generic)

;; declared in robby-custom.el
(defvar robby-provider)

(defvar robby--provider-settings nil
"Global alist of provider settings.")

(defun robby--get-provider-settings ()
"Return the settings of the current provider."
(alist-get robby-provider robby--provider-settings))

(defun robby--provider-name ()
"Return the name of the current provider."
(plist-get (robby--get-provider-settings) :name))

(defun robby--provider-host ()
"Return the host of the current provider."
(plist-get (robby--get-provider-settings) :host))

(defun robby--provider-default-model ()
"Return the default model to use with the the current provider."
(plist-get (robby--get-provider-settings) :default-model))

(defun robby--provider-api-base-path ()
"Return the API base path of the current provider."
(plist-get (robby--get-provider-settings) :api-base-path))

(defun robby--provider-models-path ()
"Return the models path of the current provider."
(plist-get (robby--get-provider-settings) :models-path))

(cl-defun robby-add-provider (&key symbol name host default-model api-base-path models-path)
"Register a new robby provider."
(let* ((settings `(:name ,name
:host ,host
:default-model ,default-model
:api-base-path ,(or api-base-path "/v1/chat/completions")
:models-path ,(or models-path "/v1/models"))))
(add-to-list 'robby--provider-settings (cons symbol settings))))

(cl-defmethod robby-provider-parse-error (data)
"Get error from response DATA.

DATA is an alist of the JSON parsed response from the provider."
(cdr (assoc 'message (assoc 'error data))))

(cl-defmethod robby-provider-parse-models (data)
"Get models from response DATA."
(seq-map (lambda (obj) (cdr (assoc 'id obj))) (cdr (assoc 'data data))))

(provide 'robby-provider)

;;; robby-provider.el ends here
Loading