Created
November 23, 2025 02:13
-
-
Save alexispurslane/ec563d79c08c4f49c840ee82be495beb to your computer and use it in GitHub Desktop.
Perplexica integration for GPTel. Worked as of whenever I made it last, don't think GPTel has changed anything. supports citations as org-mode footnotes!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;; gptel-perplexica.el --- implements a Perplexica backend for GPTel -*- lexical-binding: t -*- | |
| ;;; Perplexica | |
| (require 'gptel) | |
| (require 'gptel-transient) | |
| (require 'gptel-openai) | |
| (cl-defstruct (gptel-perplexica (:constructor gptel--make-perplexica) | |
| (:copier nil) | |
| (:include gptel-openai)) | |
| provider) | |
| (defsubst gptel--perplexica-parse-citations (citations) | |
| (let ((counter 0)) | |
| (concat (if (length> citations 0) | |
| "\n\n## Citations:\n" | |
| "") | |
| (mapconcat (lambda (citation) | |
| (setq counter (1+ counter)) | |
| (format " | |
| [fn:%d] [[%s][%s]] | |
| :PAGECONTENT: | |
| %s | |
| :END:" | |
| counter | |
| (map-nested-elt citation '(:metadata :url)) | |
| (map-nested-elt citation '(:metadata :title)) | |
| (map-nested-elt citation '(:pageContent)))) | |
| citations "\n")))) | |
| (cl-defmethod gptel--parse-response ((_backend gptel-perplexica) response _info) | |
| "Parse Perplexica response RESPONSE." | |
| (let ((response-string (map-elt response ':message)) | |
| (citations-string (let ((citations (map-elt response :sources))) | |
| (when (and citations (length> citations 0)) | |
| (gptel--perplexica-parse-citations citations))))) | |
| (concat response-string citations-string))) | |
| (add-hook 'gptel-post-response-functions (defun gptel--perplexica-finish-up (beg end) | |
| (org-fold--hide-drawers beg end) | |
| (org-table-map-tables 'org-table-align))) | |
| (cl-defmethod gptel-curl--parse-stream ((_backend gptel-perplexica) info) | |
| "Parse a Perplexica API data stream with INFO. | |
| If available, collect citations at the end and include them with | |
| the response." | |
| (let ((resp (cl-call-next-method)) | |
| (content-strs '())) | |
| (condition-case nil | |
| (while (search-forward "{" nil t) | |
| (let* ((start (1- (point))) | |
| (_ (end-of-line)) | |
| (end (1+ (point))) | |
| (json (json-parse-string (buffer-substring-no-properties start end) | |
| :object-type 'plist))) | |
| (pcase (plist-get json :type) | |
| ('"sources" (plist-put info :sources (plist-get json :data))) | |
| ('"response" (push (plist-get json :data) content-strs)) | |
| ('"done" (push (gptel--perplexica-parse-citations (plist-get info :sources)) content-strs))))) | |
| (error (goto-char 0))) | |
| (apply #'concat (nreverse content-strs)))) | |
| ;;;###autoload | |
| (cl-defun gptel-make-perplexica | |
| (name &key curl-args stream key | |
| (header (lambda () '())) | |
| (host "localhost:3000") | |
| (protocol "http") | |
| (models '()) | |
| (endpoint "/api/search") | |
| (provider "openai") | |
| request-params) | |
| "Register a Perplexica backend for gptel with NAME. | |
| Keyword arguments: | |
| CURL-ARGS (optional) is a list of additional Curl arguments. | |
| HOST (optional) is the API host, \"api.perplexica.ai\" by default. | |
| MODELS is a list of available model names. | |
| STREAM is a boolean to toggle streaming responses. | |
| PROTOCOL (optional) specifies the protocol, https by default. | |
| ENDPOINT (optional) is the API endpoint for completions. | |
| HEADER (optional) is for additional headers to send with each | |
| request. It should be an alist or a function that returns an | |
| alist. | |
| KEY is a variable whose value is the API key, or function that | |
| returns the key. | |
| REQUEST-PARAMS (optional) is a plist of additional HTTP request | |
| parameters. | |
| PROVIDER (optional) tells Perplexica which provider to use." | |
| (declare (indent 1)) | |
| (let ((backend (gptel--make-perplexica | |
| :curl-args curl-args | |
| :name name | |
| :host host | |
| :header header | |
| :key key | |
| :models models | |
| :protocol protocol | |
| :endpoint endpoint | |
| :stream stream | |
| :provider provider | |
| :request-params request-params | |
| :url (if protocol | |
| (concat protocol "://" host endpoint) | |
| (concat host endpoint))))) | |
| (prog1 backend | |
| (setf (alist-get name gptel--known-backends | |
| nil nil #'equal) | |
| backend)))) | |
| (cl-defmethod gptel--request-data ((backend gptel-perplexica) prompts) | |
| "JSON encode PROMPTS for sending to Perplexica." | |
| (let ((prompts-plist | |
| `( :chatModel ( :provider ,(gptel-perplexica-provider backend) | |
| :name ,(symbol-name gptel-model)) | |
| :optimizationMode ,(caddr (cl-find gptel-perplexica--current-quality-level | |
| gptel-perplexica--quality-levels | |
| :test #'string-equal | |
| :key #'car)) | |
| :history [,@(mapcar (lambda (prompt) (vector (plist-get prompt :role) (plist-get prompt :content))) | |
| prompts)] | |
| :focusMode ,(caddr (cl-find gptel-perplexica--current-lens | |
| gptel-perplexica--lenses | |
| :test #'string-equal | |
| :key #'car)) | |
| :query ,(when (length= prompts 1) (plist-get (car prompts) :content)) | |
| :stream ,(or gptel-stream :json-false)))) | |
| (when (not (gptel--model-capable-p 'nosystem)) | |
| (plist-put prompts-plist :systemInstructions (concat gptel--system-message "Adhere to these instructions in addition to the system prompt: | |
| 1. Use tables for all comparisons between two or more things. | |
| 2. **IMPORTANT**: Instead of outputting citations in [number] format, output them in [fn:number] format. Examples: | |
| [1] should be formatted as [fn:1] | |
| [2] should be formatted as [fn:2] | |
| etc"))) | |
| (message "%s" (json-encode (gptel--merge-plists | |
| prompts-plist | |
| (gptel-backend-request-params gptel-backend) | |
| (gptel--model-request-params gptel-model)))) | |
| (gptel--merge-plists | |
| prompts-plist | |
| (gptel-backend-request-params gptel-backend) | |
| (gptel--model-request-params gptel-model)))) | |
| (defvar gptel-perplexica--lenses | |
| '(("All" "Search across all of the internet" "webSearch") | |
| ("Academic" "Search in published academic papers" "academicSearch") | |
| ("Local Research" "Search and interact with local files with citations" "localResearch") | |
| ("Chat" "Have a creative conversation" "chat") | |
| ("Wolfram Alpha" "Chat using a computational knowledge engine" "wolframAlphaSearch") | |
| ("Reddit" "Search for discussions and opinions" "redditSearch") | |
| ("Youtube" "Search and watch videos" "youtubeSearch")) | |
| "A list of the lenses that Perplexica (boarder2's fork) provides, in (LENS-NAME LENS-DESCRIPTION LENS-ID) form.") | |
| (defvar gptel-perplexica--quality-levels | |
| '(("Speed" "Prioritize speed and get the quickest possible answer. Only uses SearXNG summaries." "speed") | |
| ("Balanced" "Find the right balance between speed and accuracy. Uses web scraping to get partial content from the actual web pages." "balanced") | |
| ("Quality" "Get the most thorough and accurate answer. Uses a headless web browser to retrieve and summarize full web content." "quality")) | |
| "A list of the quality levels that Perplexica (boarder2's fork) provides, in (QUALITY-NAME QUALITY-DESCRIPTION QUALITY-ID) form.") | |
| (defvar gptel-perplexica--current-lens "All") | |
| (defvar gptel-perplexica--current-quality-level "Speed") | |
| (defun create-completing-read-table-with-metadata (table) | |
| (lambda (prompt _ _history) | |
| (completing-read | |
| prompt | |
| (lambda (str pred flag) | |
| (pcase flag | |
| ('metadata | |
| `(metadata | |
| (annotation-function . ,(lambda (c) | |
| (format "\t%s" | |
| (cadr (cl-find c table :test #'string-equal :key #'car))))))) | |
| ('t | |
| (if (string-blank-p str) | |
| (all-completions str table) | |
| (all-completions | |
| str | |
| (lambda (s _ _) | |
| (mapcar | |
| (cl-remove-if-not | |
| (lambda (x) | |
| (unless (string-blank-p str) | |
| (or (s-contains-p str (car x) :ignore-case) | |
| (s-contains-p str (cadr x) :ignore-case))))) | |
| table)))))))))) | |
| (with-eval-after-load "gptel-transient" | |
| (transient-define-infix gptel-perplexica--infix-lens () | |
| "" | |
| :description "Focus mode" | |
| :class 'gptel-lisp-variable | |
| :variable 'gptel-perplexica--current-lens | |
| :set-value #'gptel--set-with-scope | |
| :key "-l" | |
| :prompt "Focus mode: " | |
| :reader (create-completing-read-table-with-metadata gptel-perplexica--lenses)) | |
| (transient-define-infix gptel-perplexica--infix-quality () | |
| "" | |
| :description "Optimization mode" | |
| :class 'gptel-lisp-variable | |
| :variable 'gptel-perplexica--current-quality-level | |
| :set-value #'gptel--set-with-scope | |
| :key "-o" | |
| :prompt "Optimization mode: " | |
| :reader (create-completing-read-table-with-metadata gptel-perplexica--quality-levels)) | |
| (transient-append-suffix 'gptel-menu (list 1) | |
| ["Perplexica" | |
| (gptel-perplexica--infix-lens) | |
| (gptel-perplexica--infix-quality)])) | |
| (with-eval-after-load "gptel" | |
| (gptel-make-tool | |
| :name "perplexica_search" | |
| :description "Use Perplexica (an open source Perplexity alternative) to construct a detailed, cited report on a topic." | |
| :async t | |
| :function (lambda (callback search lens quality) | |
| (let ((gptel-backend (alist-get "Perplexica" gptel--known-backends nil nil #'equal)) | |
| (gptel-perplexica--current-lens lens) | |
| (gptel-perplexica--quality-levels quality)) | |
| (gptel-request search | |
| :callback (lambda (response info) (funcall callback response))))) | |
| :args (list | |
| '( :name "search" | |
| :type string | |
| :description "The topic or question to search about") | |
| '( :name "focus_mode" | |
| :type string | |
| :description "Determines what to focus the search results used as sources on. Can be one of: 'All', 'Academic', 'Youtube', 'Reddit', or 'Wolfram Alpha'") | |
| '( :name "optimization_mode" | |
| :type string | |
| :description "How thorough to be in gathering sources. Trades off against speed. CAn be one of: 'Speed', 'Balanced', or 'Quality'. Almost always choose Speed, unless you're running a search for a second time; then choose Balanced or even sometimes Quality.")) | |
| :include t | |
| :confirm nil | |
| :category "internet")) | |
| (provide 'gptel-perplexica) | |
| ;;; gptel-perplexica.el ends here |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment