Skip to content

Commit

Permalink
Merge pull request #2 from SamuelVanie/development
Browse files Browse the repository at this point in the history
Add rag LLM functionality for chat like search with AI
  • Loading branch information
SamuelVanie authored Feb 7, 2024
2 parents 9243c20 + f4e2546 commit 0b835f1
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 42 deletions.
21 changes: 13 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Pretty neat alternative to eww in emacs.

## ❓ Why?

I wanted to use Youdotcom search engine from Emacs, so I wrote this package.
The You API is very simple and cheap to use, so I thought it would be an alternative for people who still want to use a search engine instead of chatbots.
I wanted to use Youdotcom engine directly in Emacs, so I wrote this package.
The You API is very simple, so I thought it would be an alternative for people who want to use a search engine boosted by AI directly in Emacs.
You will no more need to filter the results by yourself on the web and directly get the results inside of Emacs without leaving it.

You can also use it as a simple web browser, but this is not the spirit of an emacs user, right ?
Expand All @@ -43,35 +43,40 @@ You can also use it as a simple web browser, but this is not the spirit of an em

### MELPA

To be done...

```elisp
(use-package youdotcom
:bind ("C-c y" . youdotcom-enter))
```

## 🔑 Obtaining an API key

You need to obtain an API key from [You.com](https://api.you.com/).
Go to that website and get the Web Search API key, the Web LLM integration is not yet supported (I'm planning to add it in the future).
Go to that website and get the Web Search API key, and the Web LLM API key is you want to use the RAG LLM model.


## 💻 Usage

You will have to set the API key in your init file:

```elisp
(setq youdotcom-api-key "YOUR_API_KEY")
(setq youdotcom-search-api-key "YOUR_API_KEY")
(setq youdotcom-rag-api-key "YOUR_API_KEY")
```

Then you can use the following commands:

- `youdotcom-enter` : Will start the client and open the prompt for you to enter your query.
You can setup a keybinding for this command. (e.g. `(global-set-key (kbd "C-c y") 'youdotcom-enter)`)
You can setup a keybinding for this command. (e.g. `(global-set-key (kbd "C-c y") 'youdotcom-enter)`) or use the one provided in the use-package installation process.

In the prompt, you can use the following commands:

- `/help` : Will display the help message.
- `/clear` : Will clear the buffer.
- `/quit` : Stop the search engine's session and close the buffer.
- `/search <query>` : Will start the search with the query you entered. The results will be displayed in the buffer, each results will have a description and the URL associated with it.
- `/rag <query>` : Will start the search with the query you entered using the RAG LLM model. Using AI, the results will be done by the engine on the internet, analyzed and an answer will be given to you.

*NB: You can change the number of results displayed by changing the variable `youdotcom-number-of-results` (default is 1).*
*NB: You can change the number of results displayed by the search command by changing the variable `youdotcom-number-of-results` (default is 1).*

## 👊 Contributing

Expand Down
Binary file modified demo_2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
124 changes: 90 additions & 34 deletions youdotcom.el
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,22 @@
"A package to make quick searches on you.com."
:group 'applications)

(defcustom youdotcom-api-key ""
"Your secret API key for You.com."
(defcustom youdotcom-search-api-key ""
"Your secret API key for Search endpoint on api.you.com."
:type 'string
:group 'youdotcom)

(defcustom youdotcom-rag-api-key ""
"Your secret API key for rag endpoint on api.you.com."
:type 'string
:group 'youdotcom)

(defun youdotcom-get-api-key (type)
"Get the API key for the given TYPE."
(if (string= type "search")
youdotcom-search-api-key
youdotcom-rag-api-key))

(defvar youdotcom-buffer-name "*Youdotcom*"
"The name of the buffer for the Youdotcom session.")

Expand All @@ -35,21 +46,39 @@
:group 'youdotcom)


(defconst youdotcom-base-api-endpoint "https://api.ydc-index.io/search"
(defconst youdotcom-base-api-endpoint "https://api.ydc-index.io/"
"The base url of the you.com api for the search functionalities.")

(defun youdotcom-send-request (query callback)
"Send a request to the You.com's API with the given QUERY and CALLBACK."
(defun youdotcom-build-url (query type)
"Build a URL for the You.com's API with the given QUERY and TYPE."
(if (string= type "search")
(format "%ssearch?query=%s&num_web_results=%d"
youdotcom-base-api-endpoint
(url-hexify-string query)
youdotcom-number-of-results)
(if (string= type "rag")
(format "%srag?query=%s"
youdotcom-base-api-endpoint
(url-hexify-string query)))))

(defun youdotcom-verify-payload ()
"Verify that the payload is valid."
(unless (and (or (not (string-empty-p youdotcom-search-api-key))
(not (string-empty-p youdotcom-rag-api-key)))
(not (string-empty-p youdotcom-base-api-endpoint))
(> youdotcom-number-of-results 0))
(error "Invalid arguments or global variables")))

(defun youdotcom-send-request (query type callback)
"Send a request of TYPE to the API with the given QUERY and CALLBACK."
(youdotcom-verify-payload)
(let ((url-request-method "GET")
(url-request-extra-headers
`(("X-API-Key" . ,youdotcom-api-key)))
`(("X-API-Key" . ,(youdotcom-get-api-key type))
("Content-Type" . "application/json")))
(url-request-data nil))
(url-retrieve (format "%s?query=%s&num_web_results=%d"
youdotcom-base-api-endpoint
query
youdotcom-number-of-results)
(lambda ()
(funcall callback query)))))
(url-retrieve (youdotcom-build-url query type)
callback (list query type))))

(defun youdotcom-format-message (message)
"Format a MESSAGE as a string for display."
Expand All @@ -61,12 +90,23 @@
"Display the MESSAGES in the chat buffer."
(with-current-buffer (get-buffer-create youdotcom-buffer-name)
(goto-char (point-max))
(let ((current-point (point)))
(insert (youdotcom-format-message (pop messages)))
(add-face-text-property current-point (point-max) '(:foreground "red")))
(dolist (message messages)
(insert (youdotcom-format-message message)))
(insert "\n")
(goto-char (point-min))))

(defun youdotcom-parse-response (json)
"Parse the JSON response from You.com's API."
(defun youdotcom-parse-response (json type)
"Parse JSON response from the API for the given TYPE."
(if (string= type "search")
(youdotcom-parse-search-response json)
(if (string= type "rag")
(youdotcom-parse-rag-response json))))

(defun youdotcom-parse-search-response (json)
"Parse JSON response if called from the search endpoint."
(let* ((hits (alist-get 'hits json))
(response ""))
(dolist (hit hits)
Expand All @@ -81,6 +121,13 @@
"\n\n" (format "%s" url) "\n"))))
response))

(defun youdotcom-parse-rag-response (json)
"Parse JSON response if called from the rag endpoint."
(let* ((answer (alist-get 'answer json))
(response ""))
(setq response answer)
response))


(defun youdotcom-format-answer (query response)
"Format user's QUERY and the API's RESPONSE for easy display."
Expand All @@ -90,23 +137,24 @@
(("role" . "assistant")
("content" . ,response)))))

(defun youdotcom-handle-response (content)
"Extract the CONTENT from the API's response and change it to elisp list."
(defun youdotcom-handle-response (status content type)
"Extract CONTENT from the response and change it to elisp list.STATUS, ignored."
(ignore status)
(goto-char (point-min))
(re-search-forward "^$")
(let* ((json-object-type 'alist)
(json-array-type 'list)
(json-key-type 'symbol)
(json (json-read))
(response (youdotcom-parse-response json)))
(response (youdotcom-parse-response json type)))
;; REVIEW: This info extractions
;; is based on how the answers of the API are structured
;; it should be changed if the response changes
(youdotcom-format-answer content response)))

(defun youdotcom-send-message (content)
"Send a message with CONTENT and display the response."
(youdotcom-send-request content #'youdotcom-handle-response))
(defun youdotcom-send-message (content type)
"Send a message with CONTENT and display the response depending on TYPE."
(youdotcom-send-request content type #'youdotcom-handle-response))


(defvar youdotcom-session-started nil
Expand All @@ -131,21 +179,29 @@
(youdotcom-enter))

(defun youdotcom-start ()
"Enter a message or a command."
"Enter the command followed by the query."
(if youdotcom-session-started
(let ((input (read-string "> ")))
(cond ((string-equal input "/quit")
(kill-buffer youdotcom-buffer-name)
(setq youdotcom-session-started nil))
((string-equal input "/clear")
(erase-buffer))
((string-equal input "/help")
(message "Available commands: /quit, /clear, /help"))
(t
(switch-to-buffer (get-buffer youdotcom-buffer-name))
(youdotcom-send-message input))))
(message "Youdotcom session not started.")))

(let ((input (read-string "> ")))
(let ((command (car (split-string input)))
(query (mapconcat #'identity (cdr (split-string input)) " ")))
(cond ((string-equal command "/quit")
(kill-buffer youdotcom-buffer-name)
(setq youdotcom-session-started nil))
((string-equal command "/clear")
(erase-buffer))
((string-equal command "/help")
(message "Commands: /quit, /clear, /help, /search, /rag"))
((string-equal command "/search")
(if (string-empty-p query)
(message "Please provide a query")
(youdotcom-send-message query "search")))
((string-equal command "/rag")
(if (string-empty-p query)
(message "Please provide a query")
(youdotcom-send-message query "rag")))
(t
(message "Invalid command. type /help for available commands.")))))
(message "Youdotcom session not started.")))

(provide 'youdotcom)
;;; youdotcom.el ends here

0 comments on commit 0b835f1

Please sign in to comment.