Skip to content

Instantly share code, notes, and snippets.

@alexispurslane
Created November 23, 2025 02:13
Show Gist options
  • Select an option

  • Save alexispurslane/ec563d79c08c4f49c840ee82be495beb to your computer and use it in GitHub Desktop.

Select an option

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!
;; 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