From e4d67694e15b7d2578e978a46052a2870babfdb5 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 21 Mar 2023 21:01:14 -0600 Subject: [PATCH 1/7] Add search to method, variable names meant for khoj search in khoj.el In preparation to introduce Khoj chat in Emacs --- src/interface/emacs/khoj.el | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 1d405879..29026787 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -97,8 +97,8 @@ (defconst khoj--query-prompt "🦅Khoj: " "Query prompt shown in the minibuffer.") -(defconst khoj--buffer-name "*🦅Khoj*" - "Name of buffer to show results from Khoj.") +(defconst khoj--search-buffer-name "*🦅Khoj Search*" + "Name of buffer to show search results from Khoj.") (defvar khoj--content-type "org" "The type of content to perform search on.") @@ -283,14 +283,14 @@ Use `which-key` if available, else display simple message in echo area" (json-parse-buffer :object-type 'alist) (mapcar 'intern))))) -(defun khoj--construct-api-query (query content-type &optional rerank) - "Construct API Query from QUERY, CONTENT-TYPE and (optional) RERANK params." +(defun khoj--construct-search-api-query (query content-type &optional rerank) + "Construct Search API Query from QUERY, CONTENT-TYPE and (optional) RERANK params." (let ((rerank (or rerank "false")) (encoded-query (url-hexify-string query))) (format "%s/api/search?q=%s&t=%s&r=%s&n=%s" khoj-server-url encoded-query content-type rerank khoj-results-count))) -(defun khoj--query-api-and-render-results (query-url content-type query buffer-name) - "Query Khoj QUERY-URL. Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." +(defun khoj--query-search-api-and-render-results (query-url content-type query buffer-name) + "Query Khoj Search with QUERY-URL. Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." ;; get json response from api (with-current-buffer buffer-name (let ((inhibit-read-only t) @@ -328,9 +328,9 @@ Use `which-key` if available, else display simple message in echo area" (defun khoj--incremental-search (&optional rerank) "Perform Incremental Search on Khoj. Allow optional RERANK of results." (let* ((rerank-str (cond (rerank "true") (t "false"))) - (khoj-buffer-name (get-buffer-create khoj--buffer-name)) + (khoj-buffer-name (get-buffer-create khoj--search-buffer-name)) (query (minibuffer-contents-no-properties)) - (query-url (khoj--construct-api-query query khoj--content-type rerank-str))) + (query-url (khoj--construct-search-api-query query khoj--content-type rerank-str))) ;; Query khoj API only when user in khoj minibuffer and non-empty query ;; Prevents querying if ;; 1. user hasn't started typing query @@ -349,7 +349,7 @@ Use `which-key` if available, else display simple message in echo area" (when rerank (setq khoj--rerank t) (message "Khoj: Rerank Results")) - (khoj--query-api-and-render-results + (khoj--query-search-api-and-render-results query-url khoj--content-type query @@ -377,7 +377,7 @@ Use `which-key` if available, else display simple message in echo area" (defun khoj-incremental () "Natural, Incremental Search for your personal notes, transactions and music." (interactive) - (let* ((khoj-buffer-name (get-buffer-create khoj--buffer-name))) + (let* ((khoj-buffer-name (get-buffer-create khoj--search-buffer-name))) ;; switch to khoj results buffer (switch-to-buffer khoj-buffer-name) ;; open and setup minibuffer for incremental search @@ -442,14 +442,14 @@ Paragraph only starts at first text after blank line." ;; get paragraph, if in text mode (t (khoj--get-current-paragraph-text)))) - (query-url (khoj--construct-api-query query content-type rerank)) + (query-url (khoj--construct-search-api-query query content-type rerank)) ;; extract heading to show in result buffer from query (query-title (format "Similar to: %s" (replace-regexp-in-string "^[#\\*]* " "" (car (split-string query "\n"))))) - (buffer-name (get-buffer-create khoj--buffer-name))) + (buffer-name (get-buffer-create khoj--search-buffer-name))) (progn - (khoj--query-api-and-render-results + (khoj--query-search-api-and-render-results query-url content-type query-title From 72f63a6ef7bbf940898ca25562a7c4962f327577 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 22 Mar 2023 01:13:17 -0600 Subject: [PATCH 2/7] Add basic chat interface for Khoj on Emacs - Query khoj chat API to get Khoj Chat response to user message - Render chat messages as a org-mode list in format: - [sender-name]: *[message]* - /[receive-date]/ - Add references as org links with context visible on hover, but no jump to note - Require dash library for khoj.el to simplify list manipulation. Use `-map-indexed' method from dash --- src/interface/emacs/khoj.el | 84 ++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 29026787..01c3d9db 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Description: Natural, Incremental Search for your Second Brain ;; Keywords: search, org-mode, outlines, markdown, beancount, ledger, image ;; Version: 0.4.1 -;; Package-Requires: ((emacs "27.1") (transient "0.3.0")) +;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1") ;; URL: https://github.com/debanjum/khoj/tree/master/src/interface/emacs ;; This file is NOT part of GNU Emacs. @@ -50,6 +50,7 @@ (require 'json) (require 'transient) (require 'outline) +(require 'dash) (eval-when-compile (require 'subr-x)) ;; for string-trim before Emacs 28.2 @@ -100,6 +101,9 @@ (defconst khoj--search-buffer-name "*🦅Khoj Search*" "Name of buffer to show search results from Khoj.") +(defconst khoj--chat-buffer-name "*🦅Khoj Chat*" + "Name of chat buffer for Khoj.") + (defvar khoj--content-type "org" "The type of content to perform search on.") @@ -284,13 +288,15 @@ Use `which-key` if available, else display simple message in echo area" (mapcar 'intern))))) (defun khoj--construct-search-api-query (query content-type &optional rerank) - "Construct Search API Query from QUERY, CONTENT-TYPE and (optional) RERANK params." + "Construct Search API Query. +Use QUERY, CONTENT-TYPE and (optional) RERANK as query params" (let ((rerank (or rerank "false")) (encoded-query (url-hexify-string query))) (format "%s/api/search?q=%s&t=%s&r=%s&n=%s" khoj-server-url encoded-query content-type rerank khoj-results-count))) (defun khoj--query-search-api-and-render-results (query-url content-type query buffer-name) - "Query Khoj Search with QUERY-URL. Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." + "Query Khoj Search with QUERY-URL. +Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." ;; get json response from api (with-current-buffer buffer-name (let ((inhibit-read-only t) @@ -320,6 +326,67 @@ Use `which-key` if available, else display simple message in echo area" (t (fundamental-mode)))) (read-only-mode t))) + +;; ---------------- +;; Khoj Chat +;; ---------------- + +(defun khoj--chat () + "Chat with Khoj." + (let ((query (read-string "Query: "))) + (khoj--query-chat-api-and-render-messages query khoj--chat-buffer-name) + (switch-to-buffer khoj--chat-buffer-name))) + +(defun khoj--query-chat-api-and-render-messages (query buffer-name) + "Send QUERY to Khoj Chat. Render the chat messages from exchange in BUFFER-NAME." + ;; render json response into formatted chat messages + (with-current-buffer (get-buffer-create buffer-name) + (let ((inhibit-read-only t) + (json-response (khoj--query-chat-api query))) + (goto-char (point-max)) + (insert + (khoj--render-chat-message query "you") + (khoj--render-chat-response json-response))) + (progn (org-mode) + (visual-line-mode)) + (read-only-mode t))) + +(defun khoj--query-chat-api (query) + "Send QUERY to Khoj Chat API." + (let* ((url-request-method "GET") + (encoded-query (url-hexify-string query)) + (query-url (format "%s/api/chat?q=%s" khoj-server-url encoded-query))) + (with-temp-buffer + (erase-buffer) + (url-insert-file-contents query-url) + (json-parse-buffer :object-type 'alist)))) + +(defun khoj--render-chat-message (message sender &optional receive-date) + "Render chat messages as `org-mode' list item. +MESSAGE is the text of the chat message. +SENDER is the message sender. +RECEIVE-DATE is the message receive date." + (let ((emojified-by (if (equal sender "you") "🤔 You" "🦅 Khoj")) + (received (or receive-date (format-time-string "%H:%M %d-%m-%Y")))) + (format "- %s: %s\n /%s/\n\n" emojified-by message received))) + +(defun khoj--generate-reference (index reference) + "Create `org-mode' links with REFERENCE as link and INDEX as link description." + (format "[[[%s][%s]]]" (format "%s" reference) (format "%s" index))) + +(defun khoj--render-chat-response (json-response) + "Render chat message using JSON-RESPONSE from Khoj Chat API." + (let* ((context (or (cdr (assoc 'context json-response)) "")) + (reference-texts (split-string context "\n\n# " t)) + (reference-links (-map-indexed #'khoj--generate-reference reference-texts))) + (thread-first + ;; extract khoj message from API response and make it bold + (format "*%s*" (cdr (assoc 'response json-response))) + ;; append references to khoj message + (concat " " (string-join reference-links " ")) + ;; Set query as heading in rendered results buffer + (khoj--render-chat-message "khoj")))) + ;; ------------------ ;; Incremental Search @@ -503,15 +570,20 @@ Paragraph only starts at first text after blank line." (setq khoj--content-type content-type) (url-retrieve update-url (lambda (_) (message "Khoj %s index %supdated!" content-type (if (member "--force-update" args) "force " ""))))))) +(transient-define-suffix khoj--chat-command (&optional args) + "Command to Chat with Khoj." + (interactive (list (transient-args transient-current-command))) + (khoj--chat)) + (transient-define-prefix khoj-menu () "Create Khoj Menu to Configure and Execute Commands." - [["Configure General" + [["Configure Search" + ("n" "Results Count" "--results-count=" :init-value (lambda (obj) (oset obj value (format "%s" khoj-results-count)))) ("t" "Content Type" khoj--content-type-switch)] - ["Configure Search" - ("n" "Results Count" "--results-count=" :init-value (lambda (obj) (oset obj value (format "%s" khoj-results-count))))] ["Configure Update" ("-f" "Force Update" "--force-update")]] [["Act" + ("c" "Chat" khoj--chat-command) ("s" "Search" khoj--search-command) ("f" "Find Similar" khoj--find-similar-command) ("u" "Update" khoj--update-command) From 36b52fdd0ad55bbbe82ccf6c9b9ff0c5f64486b7 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 22 Mar 2023 02:13:00 -0600 Subject: [PATCH 3/7] Properly escape reference links before rendering - Use org-insert-link method to improve link rendering robustness Previous simple mechanism to crete org-links would result in links escaping out of formating. Use a user-facing org-mode method to remove/reduce probability of this - Replace newlines with space to render reference notes as links --- src/interface/emacs/khoj.el | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 01c3d9db..6d656740 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Description: Natural, Incremental Search for your Second Brain ;; Keywords: search, org-mode, outlines, markdown, beancount, ledger, image ;; Version: 0.4.1 -;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1") +;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1") (org "9.0.0")) ;; URL: https://github.com/debanjum/khoj/tree/master/src/interface/emacs ;; This file is NOT part of GNU Emacs. @@ -51,6 +51,8 @@ (require 'transient) (require 'outline) (require 'dash) +(require 'org) + (eval-when-compile (require 'subr-x)) ;; for string-trim before Emacs 28.2 @@ -372,7 +374,12 @@ RECEIVE-DATE is the message receive date." (defun khoj--generate-reference (index reference) "Create `org-mode' links with REFERENCE as link and INDEX as link description." - (format "[[[%s][%s]]]" (format "%s" reference) (format "%s" index))) + (with-temp-buffer + (org-insert-link + nil + (format "%s" (replace-regexp-in-string "\n" " " reference)) + (format "%s" index)) + (format "[%s]" (buffer-substring-no-properties (point-min) (point-max))))) (defun khoj--render-chat-response (json-response) "Render chat message using JSON-RESPONSE from Khoj Chat API." From 364e6c11af9bb8ceb1b2e1ff9d83b97c7713cd7b Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 22 Mar 2023 11:08:17 -0600 Subject: [PATCH 4/7] Render chat history from API in chat buffer on first run - Generalize the render-chat-response method to handle rendering history or chat response from chat API reponse - Trigger rendering of khoj chat history if Khoj chat buffer not created for this session yet --- src/interface/emacs/khoj.el | 47 ++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 6d656740..818aecbe 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -339,19 +339,35 @@ Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." (khoj--query-chat-api-and-render-messages query khoj--chat-buffer-name) (switch-to-buffer khoj--chat-buffer-name))) +(defun khoj--load-chat-history (buffer-name) + (let ((json-response (cdr (assoc 'response (khoj--query-chat-api ""))))) + (with-current-buffer (get-buffer-create buffer-name) + (erase-buffer) + (thread-last + json-response + ;; generate chat messages from Khoj Chat API response + (mapcar #'khoj--render-chat-response) + ;; insert chat messages into Khoj Chat Buffer + (mapcar #'insert)) + (progn (org-mode) + (visual-line-mode) + (read-only-mode t))))) + (defun khoj--query-chat-api-and-render-messages (query buffer-name) "Send QUERY to Khoj Chat. Render the chat messages from exchange in BUFFER-NAME." ;; render json response into formatted chat messages - (with-current-buffer (get-buffer-create buffer-name) - (let ((inhibit-read-only t) - (json-response (khoj--query-chat-api query))) - (goto-char (point-max)) - (insert - (khoj--render-chat-message query "you") - (khoj--render-chat-response json-response))) - (progn (org-mode) - (visual-line-mode)) - (read-only-mode t))) + (if (not (get-buffer buffer-name)) + (khoj--load-chat-history buffer-name) + (with-current-buffer (get-buffer buffer-name) + (let ((inhibit-read-only t) + (json-response (khoj--query-chat-api query))) + (goto-char (point-max)) + (insert + (khoj--render-chat-message query "you") + (khoj--render-chat-response json-response))) + (progn (org-mode) + (visual-line-mode)) + (read-only-mode t)))) (defun khoj--query-chat-api (query) "Send QUERY to Khoj Chat API." @@ -383,16 +399,19 @@ RECEIVE-DATE is the message receive date." (defun khoj--render-chat-response (json-response) "Render chat message using JSON-RESPONSE from Khoj Chat API." - (let* ((context (or (cdr (assoc 'context json-response)) "")) + (let* ((message (cdr (or (assoc 'response json-response) (assoc 'message json-response)))) + (sender (cdr (assoc 'by json-response))) + (receive-date (cdr (assoc 'created json-response))) + (context (or (cdr (assoc 'context json-response)) "")) (reference-texts (split-string context "\n\n# " t)) (reference-links (-map-indexed #'khoj--generate-reference reference-texts))) (thread-first ;; extract khoj message from API response and make it bold - (format "*%s*" (cdr (assoc 'response json-response))) + (format "*%s*" message) ;; append references to khoj message (concat " " (string-join reference-links " ")) - ;; Set query as heading in rendered results buffer - (khoj--render-chat-message "khoj")))) + ;; Render chat message using data obtained from API + (khoj--render-chat-message sender receive-date)))) ;; ------------------ From 06df394d6cb4b96e5dcc9041db292daaf482af73 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 22 Mar 2023 12:00:43 -0600 Subject: [PATCH 5/7] Style chat messages as org-mode entries in Emacs - Style Message as Org Entries instead of List - Put khoj response as child of user query entry - Improves color coding for readability - Allows folding each back-n-forth - Put timestamp of message received into property drawer - Use standardized time format for new and old chat messages --- src/interface/emacs/khoj.el | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 818aecbe..13e79553 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -343,6 +343,7 @@ Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." (let ((json-response (cdr (assoc 'response (khoj--query-chat-api ""))))) (with-current-buffer (get-buffer-create buffer-name) (erase-buffer) + (insert "#+STARTUP: showall hidestars\n") (thread-last json-response ;; generate chat messages from Khoj Chat API response @@ -384,9 +385,17 @@ Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." MESSAGE is the text of the chat message. SENDER is the message sender. RECEIVE-DATE is the message receive date." - (let ((emojified-by (if (equal sender "you") "🤔 You" "🦅 Khoj")) - (received (or receive-date (format-time-string "%H:%M %d-%m-%Y")))) - (format "- %s: %s\n /%s/\n\n" emojified-by message received))) + (let ((first-message-line (car (split-string message "\n" t))) + (rest-message-lines (string-join (cdr (split-string message "\n" t)) "\n ")) + (heading-level (if (equal sender "you") "**" "***")) + (emojified-by (if (equal sender "you") "🤔 *You*" "🦅 *Khoj*")) + (received (or receive-date (format-time-string "%Y-%m-%d %H:%M:%S")))) + (format "%s %s: %s\n :PROPERTIES:\n :RECEIVED: [%s]\n :END:\n %s\n" + heading-level + emojified-by + first-message-line + received + rest-message-lines))) (defun khoj--generate-reference (index reference) "Create `org-mode' links with REFERENCE as link and INDEX as link description." @@ -407,7 +416,7 @@ RECEIVE-DATE is the message receive date." (reference-links (-map-indexed #'khoj--generate-reference reference-texts))) (thread-first ;; extract khoj message from API response and make it bold - (format "*%s*" message) + (format "%s" message) ;; append references to khoj message (concat " " (string-join reference-links " ")) ;; Render chat message using data obtained from API From e9ca04af0d0a449bc197f66439760ff42bea0a74 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 23 Mar 2023 01:46:26 +0400 Subject: [PATCH 6/7] Require dash, org to run ERT tests for khoj.el --- .github/workflows/test_khoj_el.yml | 5 ++++- src/interface/emacs/tests/khoj-tests.el | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_khoj_el.yml b/.github/workflows/test_khoj_el.yml index 6efdc646..6e22261d 100644 --- a/.github/workflows/test_khoj_el.yml +++ b/.github/workflows/test_khoj_el.yml @@ -42,7 +42,10 @@ jobs: (push '(\"melpa\" . \"https://melpa.org/packages/\") package-archives) \ (package-initialize) \ (unless package-archive-contents (package-refresh-contents)) \ - (unless (package-installed-p 'transient) (package-install 'transient)))" \ + (unless (package-installed-p 'transient) (package-install 'transient)) \ + (unless (package-installed-p 'dash) (package-install 'dash)) \ + (unless (package-installed-p 'org) (package-install 'org)) \ + )" \ -l ert \ -l ./src/interface/emacs/khoj.el \ -l ./src/interface/emacs/tests/khoj-tests.el \ diff --git a/src/interface/emacs/tests/khoj-tests.el b/src/interface/emacs/tests/khoj-tests.el index b780153e..577f2c5c 100644 --- a/src/interface/emacs/tests/khoj-tests.el +++ b/src/interface/emacs/tests/khoj-tests.el @@ -4,7 +4,7 @@ ;; Author: Debanjum Singh Solanky ;; Version: 0.0.0 -;; Package-Requires: ((emacs "27.1") (transient "0.3.0")) +;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1") (org "9.0.0")) ;; URL: https://github.com/debanjum/khoj/tree/master/src/interface/emacs ;;; License: @@ -28,8 +28,10 @@ ;;; Code: +(require 'dash) (require 'ert) (require 'khoj) +(require 'org) From 863933daaaf8bdccb982ced76cf0ba343ec4bb9a Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Thu, 23 Mar 2023 02:25:34 +0400 Subject: [PATCH 7/7] Resolve build issues found by melpazoid --- src/interface/emacs/khoj.el | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 13e79553..64ebfbe4 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Description: Natural, Incremental Search for your Second Brain ;; Keywords: search, org-mode, outlines, markdown, beancount, ledger, image ;; Version: 0.4.1 -;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1") (org "9.0.0")) +;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1")) ;; URL: https://github.com/debanjum/khoj/tree/master/src/interface/emacs ;; This file is NOT part of GNU Emacs. @@ -287,7 +287,7 @@ Use `which-key` if available, else display simple message in echo area" (url-insert-file-contents config-url) (thread-last (json-parse-buffer :object-type 'alist) - (mapcar 'intern))))) + (mapcar #'intern))))) (defun khoj--construct-search-api-query (query content-type &optional rerank) "Construct Search API Query. @@ -349,7 +349,7 @@ Render results in BUFFER-NAME using QUERY, CONTENT-TYPE." ;; generate chat messages from Khoj Chat API response (mapcar #'khoj--render-chat-response) ;; insert chat messages into Khoj Chat Buffer - (mapcar #'insert)) + (mapc #'insert)) (progn (org-mode) (visual-line-mode) (read-only-mode t))))) @@ -389,7 +389,7 @@ RECEIVE-DATE is the message receive date." (rest-message-lines (string-join (cdr (split-string message "\n" t)) "\n ")) (heading-level (if (equal sender "you") "**" "***")) (emojified-by (if (equal sender "you") "🤔 *You*" "🦅 *Khoj*")) - (received (or receive-date (format-time-string "%Y-%m-%d %H:%M:%S")))) + (received (or receive-date (format-time-string "%F %T")))) (format "%s %s: %s\n :PROPERTIES:\n :RECEIVED: [%s]\n :END:\n %s\n" heading-level emojified-by @@ -605,7 +605,7 @@ Paragraph only starts at first text after blank line." (setq khoj--content-type content-type) (url-retrieve update-url (lambda (_) (message "Khoj %s index %supdated!" content-type (if (member "--force-update" args) "force " ""))))))) -(transient-define-suffix khoj--chat-command (&optional args) +(transient-define-suffix khoj--chat-command (&optional _) "Command to Chat with Khoj." (interactive (list (transient-args transient-current-command))) (khoj--chat))