gptel-privategpt.el (6830B)
1 ;;; gptel-privategpt.el --- Privategpt AI suppport for gptel -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2023 Karthik Chikmagalur 4 5 ;; Author: Karthik Chikmagalur <karthikchikmagalur@gmail.com> 6 7 ;; This program is free software; you can redistribute it and/or modify 8 ;; it under the terms of the GNU General Public License as published by 9 ;; the Free Software Foundation, either version 3 of the License, or 10 ;; (at your option) any later version. 11 12 ;; This program is distributed in the hope that it will be useful, 13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 ;; GNU General Public License for more details. 16 17 ;; You should have received a copy of the GNU General Public License 18 ;; along with this program. If not, see <https://www.gnu.org/licenses/>. 19 20 ;;; Commentary: 21 22 ;; This file adds support for Privategpt's Messages API to gptel 23 24 ;;; Code: 25 (require 'cl-generic) 26 (eval-when-compile 27 (require 'cl-lib)) 28 (require 'map) 29 (require 'gptel) 30 31 (defvar json-object-type) 32 33 (declare-function prop-match-value "text-property-search") 34 (declare-function text-property-search-backward "text-property-search") 35 (declare-function json-read "json" ()) 36 37 38 39 ;;; Privategpt (Messages API) 40 (cl-defstruct (gptel-privategpt (:constructor gptel--make-privategpt) 41 (:copier nil) 42 (:include gptel-openai)) 43 context sources) 44 45 (defun gptel--privategpt-parse-sources (response) 46 (cl-loop with source-alist 47 for source across (map-nested-elt response '(:choices 0 :sources)) 48 for name = (map-nested-elt source '(:document :doc_metadata :file_name)) 49 for page = (map-nested-elt source '(:document :doc_metadata :page_label)) 50 do (push page (alist-get name source-alist nil nil #'equal)) 51 finally return 52 (cl-loop for (file-name . file-pages) in source-alist 53 for pages = (delete-dups (delq nil file-pages)) 54 if pages 55 collect (format "- %s (page %s)" file-name (mapconcat #'identity pages ", ")) 56 into source-items 57 else collect (format "- %s" file-name) into source-items 58 finally return (mapconcat #'identity (cons "\n\nSources:" source-items) "\n")))) 59 60 (cl-defmethod gptel-curl--parse-stream ((_backend gptel-privategpt) info) 61 (let* ((content-strs)) 62 (condition-case nil 63 (while (re-search-forward "^data:" nil t) 64 (save-match-data 65 (if (looking-at " *\\[DONE\\]") 66 (when-let ((sources-string (plist-get info :sources))) 67 (push sources-string content-strs)) 68 (let ((response (gptel--json-read))) 69 (unless (or (plist-get info :sources) 70 (not (gptel-privategpt-sources (plist-get info :backend)))) 71 (plist-put info :sources (gptel--privategpt-parse-sources response))) 72 (let* ((delta (map-nested-elt response '(:choices 0 :delta))) 73 (content (plist-get delta :content))) 74 (push content content-strs)))))) 75 (error 76 (goto-char (match-beginning 0)))) 77 (apply #'concat (nreverse content-strs)))) 78 79 (cl-defmethod gptel--parse-response ((_backend gptel-privategpt) response info) 80 (let ((response-string (map-nested-elt response '(:choices 0 :message :content))) 81 (sources-string (and (gptel-privategpt-sources (plist-get info :backend)) 82 (gptel--privategpt-parse-sources response)))) 83 (concat response-string sources-string))) 84 85 (cl-defmethod gptel--request-data ((_backend gptel-privategpt) prompts) 86 "JSON encode PROMPTS for sending to ChatGPT." 87 (let ((prompts-plist 88 `(:model ,(gptel--model-name gptel-model) 89 :messages [,@prompts] 90 :use_context ,(or (gptel-privategpt-context gptel-backend) :json-false) 91 :include_sources ,(or (gptel-privategpt-sources gptel-backend) :json-false) 92 :stream ,(or (and gptel-stream gptel-use-curl 93 (gptel-backend-stream gptel-backend)) 94 :json-false)))) 95 (when gptel-temperature 96 (plist-put prompts-plist :temperature gptel-temperature)) 97 (when gptel-max-tokens 98 (plist-put prompts-plist :max_tokens gptel-max-tokens)) 99 ;; Merge request params with model and backend params. 100 (gptel--merge-plists 101 prompts-plist 102 (gptel-backend-request-params gptel-backend) 103 (gptel--model-request-params gptel-model)))) 104 105 106 ;;;###autoload 107 (cl-defun gptel-make-privategpt 108 (name &key curl-args stream key request-params 109 (header 110 (lambda () (when-let (key (gptel--get-api-key)) 111 `(("Authorization" . ,(concat "Bearer " key)))))) 112 (host "localhost:8001") 113 (protocol "http") 114 (models '(private-gpt)) 115 (endpoint "/v1/chat/completions") 116 (context t) (sources t)) 117 "Register an Privategpt API-compatible backend for gptel with NAME. 118 119 Keyword arguments: 120 121 CURL-ARGS (optional) is a list of additional Curl arguments. 122 123 HOST (optional) is the API host, \"api.privategpt.com\" by default. 124 125 MODELS is a list of available model names. 126 127 STREAM is a boolean to toggle streaming responses, defaults to 128 false. 129 130 PROTOCOL (optional) specifies the protocol, https by default. 131 132 ENDPOINT (optional) is the API endpoint for completions, defaults to 133 \"/v1/messages\". 134 135 HEADER (optional) is for additional headers to send with each 136 request. It should be an alist or a function that retuns an 137 alist, like: 138 ((\"Content-Type\" . \"application/json\")) 139 140 KEY is a variable whose value is the API key, or function that 141 returns the key. 142 143 CONTEXT and SOURCES: if true (the default), use available context 144 and provide sources used by the model to generate the response. 145 146 REQUEST-PARAMS (optional) is a plist of additional HTTP request 147 parameters (as plist keys) and values supported by the API. Use 148 these to set parameters that gptel does not provide user options 149 for." 150 (declare (indent 1)) 151 (let ((backend (gptel--make-privategpt 152 :curl-args curl-args 153 :name name 154 :host host 155 :header header 156 :key key 157 :models models 158 :protocol protocol 159 :endpoint endpoint 160 :stream stream 161 :request-params request-params 162 :url (if protocol 163 (concat protocol "://" host endpoint) 164 (concat host endpoint)) 165 :context context 166 :sources sources))) 167 (prog1 backend 168 (setf (alist-get name gptel--known-backends 169 nil nil #'equal) 170 backend)))) 171 172 (provide 'gptel-privategpt) 173 ;;; gptel-backends.el ends here