branch: elpa/gptel
commit 975a5377fd93ea68e7a95398494710cd0925d6a7
Author: Michael Werner <michaelwer...@mailbox.org>
Commit: karthink <karthikchikmaga...@gmail.com>

    gptel-privategpt: Add support for PrivateGPT
    
    gptel-privategpt.el (gptel-privategpt,
    gptel--privategpt-parse-sources, gptel-make-privategpt,
    gptel--parse-response, gptel-curl--parse-stream): Add support for
    privateGPT based on the openai-API, including support for context
    and parsing of sources.
---
 gptel-privategpt.el | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 163 insertions(+)

diff --git a/gptel-privategpt.el b/gptel-privategpt.el
new file mode 100644
index 0000000000..e46c4f37e4
--- /dev/null
+++ b/gptel-privategpt.el
@@ -0,0 +1,163 @@
+;;; gptel-privategpt.el ---  Privategpt AI suppport for gptel  -*- 
lexical-binding: t; -*-
+
+;; Copyright (C) 2023  Karthik Chikmagalur
+
+;; Author: Karthik Chikmagalur <karthikchikmaga...@gmail.com>
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file adds support for Privategpt's Messages API to gptel
+
+;;; Code:
+(require 'cl-generic)
+(eval-when-compile
+  (require 'cl-lib))
+(require 'map)
+(require 'gptel)
+
+(defvar json-object-type)
+
+(declare-function prop-match-value "text-property-search")
+(declare-function text-property-search-backward "text-property-search")
+(declare-function json-read "json" ())
+
+
+
+;;; Privategpt (Messages API)
+(cl-defstruct (gptel-privategpt (:constructor gptel--make-privategpt)
+                               (:copier nil)
+                               (:include gptel-openai))
+  context sources)
+
+(defun gptel--privategpt-parse-sources (response)
+  (cl-loop with source-alist
+           for source across (map-nested-elt response '(:choices 0 :sources))
+           for name = (map-nested-elt source '(:document :doc_metadata 
:file_name))
+           for page = (map-nested-elt source '(:document :doc_metadata 
:page_label))
+           do (push page (alist-get name source-alist nil nil #'equal))
+           finally return
+           (cl-loop for (file-name . file-pages) in source-alist
+                    for pages = (delete-dups (delq nil file-pages))
+                    if pages
+                    collect (format "- %s (page %s)" file-name (mapconcat 
#'identity pages ", "))
+                    into source-items
+                    else collect (format "- %s" file-name) into source-items
+                    finally return (mapconcat #'identity (cons "\n\nSources:" 
source-items) "\n"))))
+
+(cl-defmethod gptel-curl--parse-stream ((_backend gptel-privategpt) info)
+  (let* ((content-strs))
+    (condition-case nil
+        (while (re-search-forward "^data:" nil t)
+          (save-match-data
+            (if (looking-at " *\\[DONE\\]")
+                (when-let ((sources-string (plist-get info :sources)))
+                  (push sources-string content-strs))
+              (let ((response (gptel--json-read)))
+               (unless (or (plist-get info :sources)
+                            (not (gptel-privategpt-sources (plist-get info 
:backend))))
+                  (plist-put info :sources (gptel--privategpt-parse-sources 
response)))
+               (let* ((delta (map-nested-elt response '(:choices 0 :delta)))
+                      (content (plist-get delta :content)))
+                 (push content content-strs))))))
+    (error
+     (goto-char (match-beginning 0))))
+  (apply #'concat (nreverse content-strs))))
+
+(cl-defmethod gptel--parse-response ((_backend gptel-privategpt) response info)
+  (let ((response-string (map-nested-elt response '(:choices 0 :message 
:content)))
+        (sources-string (and (gptel-privategpt-sources (plist-get info 
:backend))
+                             (gptel--privategpt-parse-sources response))))
+    (concat response-string sources-string)))
+
+(cl-defmethod gptel--request-data ((_backend gptel-privategpt) prompts)
+  "JSON encode PROMPTS for sending to ChatGPT."
+  (let ((prompts-plist
+         `(:model ,gptel-model
+          :messages [,@prompts]
+          :use_context ,(or (gptel-privategpt-context gptel-backend) 
:json-false)
+          :include_sources ,(or (gptel-privategpt-sources gptel-backend) 
:json-false)
+           :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 :max_tokens gptel-max-tokens))
+    prompts-plist))
+
+
+;;;###autoload
+(cl-defun gptel-make-privategpt
+    (name &key curl-args stream key
+          (header
+           (lambda () (when-let (key (gptel--get-api-key))
+                       `(("Authorization" . ,(concat "Bearer " key))))))
+          (host "localhost:8001")
+          (protocol "http")
+         (models '("private-gpt"))
+          (endpoint "/v1/chat/completions")
+          (context t) (sources t))
+  "Register an Privategpt API-compatible backend for gptel with NAME.
+
+Keyword arguments:
+
+CURL-ARGS (optional) is a list of additional Curl arguments.
+
+HOST (optional) is the API host, \"api.privategpt.com\" by default.
+
+MODELS is a list of available model names.
+
+STREAM is a boolean to toggle streaming responses, defaults to
+false.
+
+PROTOCOL (optional) specifies the protocol, https by default.
+
+ENDPOINT (optional) is the API endpoint for completions, defaults to
+\"/v1/messages\".
+
+HEADER (optional) is for additional headers to send with each
+request. It should be an alist or a function that retuns an
+alist, like:
+((\"Content-Type\" . \"application/json\"))
+
+KEY is a variable whose value is the API key, or function that
+returns the key.
+
+CONTEXT and SOURCES: if true (the default), use available context
+and provide sources used by the model to generate the response."
+  (declare (indent 1))
+  (let ((backend (gptel--make-privategpt
+                  :curl-args curl-args
+                  :name name
+                  :host host
+                  :header header
+                  :key key
+                  :models models
+                  :protocol protocol
+                  :endpoint endpoint
+                  :stream stream
+                  :url (if protocol
+                           (concat protocol "://" host endpoint)
+                         (concat host endpoint))
+                  :context context
+                  :sources sources)))
+    (prog1 backend
+      (setf (alist-get name gptel--known-backends
+                       nil nil #'equal)
+                  backend))))
+
+(provide 'gptel-privategpt)
+;;; gptel-backends.el ends here

Reply via email to