From ac51920859a4ea9a7235a0a4f53998f13c3cf815 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sun, 20 Oct 2024 14:34:42 -0700 Subject: [PATCH 01/13] Start conversation with Agents from within Emacs Exposes a transient switch with available agents as selectable options in the Khoj chat sub-menu. Currently shows agent slugs instead of agent names as options. This isn't the cleanest but gets the job done for now. Only new conversations with a different agent can be started. Existing conversations will continue with the original agent it was created with. The ability to switch the conversation's agent doesn't exist on the server yet. --- src/interface/emacs/khoj.el | 67 ++++++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 02d1dd6f..e27e1a75 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -127,6 +127,11 @@ (const "image") (const "pdf"))) +(defcustom khoj-default-agent "khoj" + "The default agent to chat with. See https://app.khoj.dev/agents for available options." + :group 'khoj + :type 'string) + ;; -------------------------- ;; Khoj Dynamic Configuration @@ -144,6 +149,9 @@ (defconst khoj--chat-buffer-name "*🏮 Khoj Chat*" "Name of chat buffer for Khoj.") +(defvar khoj--selected-agent khoj-default-agent + "Currently selected Khoj agent.") + (defvar khoj--content-type "org" "The type of content to perform search on.") @@ -913,14 +921,16 @@ Call CALLBACK func with response and CBARGS." (let ((selected-session-id (khoj--select-conversation-session "Open"))) (khoj--load-chat-session khoj--chat-buffer-name selected-session-id))) -(defun khoj--create-chat-session () - "Create new chat session." - (khoj--call-api "/api/chat/sessions" "POST")) +(defun khoj--create-chat-session (&optional agent) + "Create new chat session with AGENT." + (khoj--call-api "/api/chat/sessions" + "POST" + (when agent `(("agent_slug" ,agent))))) -(defun khoj--new-conversation-session () - "Create new Khoj conversation session." +(defun khoj--new-conversation-session (&optional agent) + "Create new Khoj conversation session with AGENT." (thread-last - (khoj--create-chat-session) + (khoj--create-chat-session agent) (assoc 'conversation_id) (cdr) (khoj--chat))) @@ -935,6 +945,15 @@ Call CALLBACK func with response and CBARGS." (khoj--select-conversation-session "Delete") (khoj--delete-chat-session))) +(defun khoj--get-agents () + "Get list of available Khoj agents." + (let* ((response (khoj--call-api "/api/agents" "GET")) + (agents (mapcar (lambda (agent) + (cons (cdr (assoc 'name agent)) + (cdr (assoc 'slug agent)))) + response))) + agents)) + (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. @@ -1246,6 +1265,20 @@ Paragraph only starts at first text after blank line." ;; dynamically set choices to content types enabled on khoj backend :choices (or (ignore-errors (mapcar #'symbol-name (khoj--get-enabled-content-types))) '("all" "org" "markdown" "pdf" "image"))) + (transient-define-argument khoj--agent-switch () + :class 'transient-switches + :argument-format "--agent=%s" + :argument-regexp ".+" + :init-value (lambda (obj) + (oset obj value (format "--agent=%s" khoj--selected-agent))) + :choices (or (ignore-errors (mapcar #'cdr (khoj--get-agents))) '("khoj")) + :reader (lambda (prompt initial-input history) + (let* ((agents (khoj--get-agents)) + (selected (completing-read prompt agents nil t initial-input history)) + (slug (cdr (assoc selected agents)))) + (setq khoj--selected-agent slug) + slug))) + (transient-define-suffix khoj--search-command (&optional args) (interactive (list (transient-args transient-current-command))) (progn @@ -1287,10 +1320,11 @@ Paragraph only starts at first text after blank line." (interactive (list (transient-args transient-current-command))) (khoj--open-conversation-session)) - (transient-define-suffix khoj--new-conversation-session-command (&optional _) + (transient-define-suffix khoj--new-conversation-session-command (&optional args) "Command to select Khoj conversation sessions to open." (interactive (list (transient-args transient-current-command))) - (khoj--new-conversation-session)) + (let ((agent-slug (transient-arg-value "--agent=" args))) + (khoj--new-conversation-session agent-slug))) (transient-define-suffix khoj--delete-conversation-session-command (&optional _) "Command to select Khoj conversation sessions to delete." @@ -1298,14 +1332,15 @@ Paragraph only starts at first text after blank line." (khoj--delete-conversation-session)) (transient-define-prefix khoj--chat-menu () - "Open the Khoj chat menu." - ["Act" - ("c" "Chat" khoj--chat-command) - ("o" "Open Conversation" khoj--open-conversation-session-command) - ("n" "New Conversation" khoj--new-conversation-session-command) - ("d" "Delete Conversation" khoj--delete-conversation-session-command) - ("q" "Quit" transient-quit-one) - ]) + "Create the Khoj Chat Menu and Execute Commands." + [["Configure" + ("a" "Select Agent" khoj--agent-switch)]] + [["Act" + ("c" "Chat" khoj--chat-command) + ("o" "Open Conversation" khoj--open-conversation-session-command) + ("n" "New Conversation" khoj--new-conversation-session-command) + ("d" "Delete Conversation" khoj--delete-conversation-session-command) + ("q" "Quit" transient-quit-one)]]) (transient-define-prefix khoj--menu () "Create Khoj Menu to Configure and Execute Commands." From 9ffd7267996fe33005de9cd6a3dc11203b12c8b6 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sun, 20 Oct 2024 14:39:46 -0700 Subject: [PATCH 02/13] Allow making sync api requests with body from khoj.el --- src/interface/emacs/khoj.el | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index e27e1a75..960d06c0 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -664,13 +664,15 @@ Simplified fork of `org-cycle-content' from Emacs 29.1 to work with >=27.1." ;; -------------- ;; Query Khoj API ;; -------------- -(defun khoj--call-api (path &optional method params callback &rest cbargs) - "Sync call API at PATH with METHOD and query PARAMS as kv assoc list. +(defun khoj--call-api (path &optional method params body callback &rest cbargs) + "Sync call API at PATH with METHOD, query PARAMS and BODY as kv assoc list. Optionally apply CALLBACK with JSON parsed response and CBARGS." (let* ((url-request-method (or method "GET")) (url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key)))) - (param-string (if params (url-build-query-string params) "")) - (query-url (format "%s%s?%s&client=emacs" khoj-server-url path param-string)) + (url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key)) ("Content-Type" . "application/json"))) + (url-request-data (if body (json-encode body) nil)) + (param-string (url-build-query-string (append params '((client "emacs"))))) + (query-url (format "%s%s?%s" khoj-server-url path param-string)) (cbargs (if (and (listp cbargs) (listp (car cbargs))) (car cbargs) cbargs))) ; normalize cbargs to (a b) from ((a b)) if required (with-temp-buffer (condition-case ex @@ -690,8 +692,8 @@ Optionally apply CALLBACK with JSON parsed response and CBARGS." (url-request-extra-headers `(("Authorization" . ,(format "Bearer %s" khoj-api-key)) ("Content-Type" . "application/json"))) (url-request-data (if body (json-encode body) nil)) (param-string (url-build-query-string (append params '((client "emacs"))))) - (cbargs (if (and (listp cbargs) (listp (car cbargs))) (car cbargs) cbargs)) ; normalize cbargs to (a b) from ((a b)) if required - (query-url (format "%s%s?%s" khoj-server-url path param-string))) + (query-url (format "%s%s?%s" khoj-server-url path param-string)) + (cbargs (if (and (listp cbargs) (listp (car cbargs))) (car cbargs) cbargs))) ; normalize cbargs to (a b) from ((a b)) if required (url-retrieve query-url (lambda (status) (if (plist-get status :error) @@ -707,7 +709,7 @@ Optionally apply CALLBACK with JSON parsed response and CBARGS." (defun khoj--get-enabled-content-types () "Get content types enabled for search from API." - (khoj--call-api "/api/content/types" "GET" nil `(lambda (item) (mapcar #'intern item)))) + (khoj--call-api "/api/content/types" "GET" nil nil `(lambda (item) (mapcar #'intern item)))) (defun khoj--query-search-api-and-render-results (query content-type buffer-name &optional rerank is-find-similar) "Query Khoj Search API with QUERY, CONTENT-TYPE and RERANK as query params. From 2b68d61fef1fd40d943a93f194436a0aae66a9ac Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 20 Oct 2024 16:21:51 -0700 Subject: [PATCH 03/13] Release Khoj version 1.26.1 --- manifest.json | 2 +- src/interface/desktop/package.json | 2 +- src/interface/emacs/khoj.el | 2 +- src/interface/obsidian/manifest.json | 2 +- src/interface/obsidian/package.json | 2 +- src/interface/obsidian/versions.json | 3 ++- src/interface/web/package.json | 2 +- versions.json | 3 ++- 8 files changed, 10 insertions(+), 8 deletions(-) diff --git a/manifest.json b/manifest.json index e4fab58e..f7026fdb 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.0", + "version": "1.26.1", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/desktop/package.json b/src/interface/desktop/package.json index 87904130..8db64a1c 100644 --- a/src/interface/desktop/package.json +++ b/src/interface/desktop/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.0", + "version": "1.26.1", "description": "Your Second Brain", "author": "Khoj Inc. ", "license": "GPL-3.0-or-later", diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 960d06c0..da963234 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Saba Imran ;; Description: Your Second Brain ;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image -;; Version: 1.26.0 +;; Version: 1.26.1 ;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1")) ;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs diff --git a/src/interface/obsidian/manifest.json b/src/interface/obsidian/manifest.json index e4fab58e..f7026fdb 100644 --- a/src/interface/obsidian/manifest.json +++ b/src/interface/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.0", + "version": "1.26.1", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/obsidian/package.json b/src/interface/obsidian/package.json index 42ee8c26..4a2591fa 100644 --- a/src/interface/obsidian/package.json +++ b/src/interface/obsidian/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.0", + "version": "1.26.1", "description": "Your Second Brain", "author": "Debanjum Singh Solanky, Saba Imran ", "license": "GPL-3.0-or-later", diff --git a/src/interface/obsidian/versions.json b/src/interface/obsidian/versions.json index 8c9fd580..3a4bc6a8 100644 --- a/src/interface/obsidian/versions.json +++ b/src/interface/obsidian/versions.json @@ -78,5 +78,6 @@ "1.24.0": "0.15.0", "1.24.1": "0.15.0", "1.25.0": "0.15.0", - "1.26.0": "0.15.0" + "1.26.0": "0.15.0", + "1.26.1": "0.15.0" } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index 4be40e9e..eb92818c 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -1,6 +1,6 @@ { "name": "khoj-ai", - "version": "1.26.0", + "version": "1.26.1", "private": true, "scripts": { "dev": "next dev", diff --git a/versions.json b/versions.json index 8c9fd580..3a4bc6a8 100644 --- a/versions.json +++ b/versions.json @@ -78,5 +78,6 @@ "1.24.0": "0.15.0", "1.24.1": "0.15.0", "1.25.0": "0.15.0", - "1.26.0": "0.15.0" + "1.26.0": "0.15.0", + "1.26.1": "0.15.0" } From 046de57571920b6e104af7f31c4a93bba9d42502 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 20 Oct 2024 18:03:14 -0700 Subject: [PATCH 04/13] Improve error handling when documents not searched with stack trace - Stop extract OCR content from PDFs - Only use agent knowledge base when user not provided --- src/khoj/database/adapters/__init__.py | 22 +++++++++++++------ .../processor/content/pdf/pdf_to_entries.py | 2 +- src/khoj/routers/api_chat.py | 2 +- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index 28946557..e704b18f 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -1463,12 +1463,15 @@ class EntryAdapters: file_filters = EntryAdapters.file_filter.get_filter_terms(query) date_filters = EntryAdapters.date_filter.get_query_date_range(query) - user_or_agent = Q(user=user) + owner_filter = Q() + + if user != None: + owner_filter = Q(user=user) if agent != None: - user_or_agent |= Q(agent=agent) + owner_filter |= Q(agent=agent) if len(word_filters) == 0 and len(file_filters) == 0 and len(date_filters) == 0: - return Entry.objects.filter(user_or_agent) + return Entry.objects.filter(owner_filter) for term in word_filters: if term.startswith("+"): @@ -1504,7 +1507,7 @@ class EntryAdapters: formatted_max_date = date.fromtimestamp(max_date).strftime("%Y-%m-%d") q_filter_terms &= Q(embeddings_dates__date__lte=formatted_max_date) - relevant_entries = Entry.objects.filter(user_or_agent).filter(q_filter_terms) + relevant_entries = Entry.objects.filter(owner_filter).filter(q_filter_terms) if file_type_filter: relevant_entries = relevant_entries.filter(file_type=file_type_filter) return relevant_entries @@ -1519,13 +1522,18 @@ class EntryAdapters: max_distance: float = math.inf, agent: Agent = None, ): - user_or_agent = Q(user=user) + owner_filter = Q() + if user != None: + owner_filter = Q(user=user) if agent != None: - user_or_agent |= Q(agent=agent) + owner_filter |= Q(agent=agent) + + if owner_filter == Q(): + return Entry.objects.none() relevant_entries = EntryAdapters.apply_filters(user, raw_query, file_type_filter, agent) - relevant_entries = relevant_entries.filter(user_or_agent).annotate( + relevant_entries = relevant_entries.filter(owner_filter).annotate( distance=CosineDistance("embeddings", embeddings) ) relevant_entries = relevant_entries.filter(distance__lte=max_distance) diff --git a/src/khoj/processor/content/pdf/pdf_to_entries.py b/src/khoj/processor/content/pdf/pdf_to_entries.py index 59ffc388..063d1e74 100644 --- a/src/khoj/processor/content/pdf/pdf_to_entries.py +++ b/src/khoj/processor/content/pdf/pdf_to_entries.py @@ -67,7 +67,7 @@ class PdfToEntries(TextToEntries): bytes = pdf_files[pdf_file] f.write(bytes) try: - loader = PyMuPDFLoader(f"{tmp_file}", extract_images=True) + loader = PyMuPDFLoader(f"{tmp_file}", extract_images=False) pdf_entries_per_file = [page.page_content for page in loader.load()] except ImportError: loader = PyMuPDFLoader(f"{tmp_file}") diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index d57b5530..0d7320ff 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -859,7 +859,7 @@ async def chat( defiltered_query = result[2] except Exception as e: error_message = f"Error searching knowledge base: {e}. Attempting to respond without document references." - logger.warning(error_message) + logger.error(error_message, exc_info=True) async for result in send_event( ChatEvent.STATUS, "Document search failed. I'll try respond without document references" ): From fc70f255831bd2fe17e86f17f9dd39fd79ce2ec6 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 20 Oct 2024 18:03:36 -0700 Subject: [PATCH 05/13] Release Khoj version 1.26.2 --- manifest.json | 2 +- src/interface/desktop/package.json | 2 +- src/interface/emacs/khoj.el | 2 +- src/interface/obsidian/manifest.json | 2 +- src/interface/obsidian/package.json | 2 +- src/interface/obsidian/versions.json | 3 ++- src/interface/web/package.json | 2 +- versions.json | 3 ++- 8 files changed, 10 insertions(+), 8 deletions(-) diff --git a/manifest.json b/manifest.json index f7026fdb..bc7bfbc1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.1", + "version": "1.26.2", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/desktop/package.json b/src/interface/desktop/package.json index 8db64a1c..03315905 100644 --- a/src/interface/desktop/package.json +++ b/src/interface/desktop/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.1", + "version": "1.26.2", "description": "Your Second Brain", "author": "Khoj Inc. ", "license": "GPL-3.0-or-later", diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index da963234..a5ad882e 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Saba Imran ;; Description: Your Second Brain ;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image -;; Version: 1.26.1 +;; Version: 1.26.2 ;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1")) ;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs diff --git a/src/interface/obsidian/manifest.json b/src/interface/obsidian/manifest.json index f7026fdb..bc7bfbc1 100644 --- a/src/interface/obsidian/manifest.json +++ b/src/interface/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.1", + "version": "1.26.2", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/obsidian/package.json b/src/interface/obsidian/package.json index 4a2591fa..006f695c 100644 --- a/src/interface/obsidian/package.json +++ b/src/interface/obsidian/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.1", + "version": "1.26.2", "description": "Your Second Brain", "author": "Debanjum Singh Solanky, Saba Imran ", "license": "GPL-3.0-or-later", diff --git a/src/interface/obsidian/versions.json b/src/interface/obsidian/versions.json index 3a4bc6a8..16385422 100644 --- a/src/interface/obsidian/versions.json +++ b/src/interface/obsidian/versions.json @@ -79,5 +79,6 @@ "1.24.1": "0.15.0", "1.25.0": "0.15.0", "1.26.0": "0.15.0", - "1.26.1": "0.15.0" + "1.26.1": "0.15.0", + "1.26.2": "0.15.0" } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index eb92818c..cbe269cc 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -1,6 +1,6 @@ { "name": "khoj-ai", - "version": "1.26.1", + "version": "1.26.2", "private": true, "scripts": { "dev": "next dev", diff --git a/versions.json b/versions.json index 3a4bc6a8..16385422 100644 --- a/versions.json +++ b/versions.json @@ -79,5 +79,6 @@ "1.24.1": "0.15.0", "1.25.0": "0.15.0", "1.26.0": "0.15.0", - "1.26.1": "0.15.0" + "1.26.1": "0.15.0", + "1.26.2": "0.15.0" } From a979457442c7f0ec4c372a25451e0e93d9c95eb6 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 20 Oct 2024 20:04:50 -0700 Subject: [PATCH 06/13] Add unit tests for agents - Add permutations of testing for with, without knowledge base. Private, public, different users. --- src/khoj/database/adapters/__init__.py | 10 ++ src/khoj/routers/api.py | 11 +- tests/conftest.py | 7 + tests/test_agents.py | 211 +++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 tests/test_agents.py diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index e704b18f..d025459e 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -640,6 +640,16 @@ class AgentAdapters: agents = await sync_to_async(AgentAdapters.get_all_accessible_agents)(user) return await sync_to_async(list)(agents) + @staticmethod + async def ais_agent_accessible(agent: Agent, user: KhojUser) -> bool: + if agent.privacy_level == Agent.PrivacyLevel.PUBLIC: + return True + if agent.creator == user: + return True + if agent.privacy_level == Agent.PrivacyLevel.PROTECTED: + return True + return False + @staticmethod def get_conversation_agent_by_id(agent_id: int): agent = Agent.objects.filter(id=agent_id).first() diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 59948b47..60e40d1b 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -21,6 +21,7 @@ from starlette.authentication import has_required_scope, requires from khoj.configure import initialize_content from khoj.database import adapters from khoj.database.adapters import ( + AgentAdapters, AutomationAdapters, ConversationAdapters, EntryAdapters, @@ -114,10 +115,16 @@ async def execute_search( dedupe: Optional[bool] = True, agent: Optional[Agent] = None, ): - start_time = time.time() - # Run validation checks results: List[SearchResponse] = [] + + start_time = time.time() + + # Ensure the agent, if present, is accessible by the user + if user and agent and not await AgentAdapters.ais_agent_accessible(agent, user): + logger.error(f"Agent {agent.slug} is not accessible by user {user}") + return results + if q is None or q == "": logger.warning(f"No query param (q) passed in API call to initiate search") return results diff --git a/tests/conftest.py b/tests/conftest.py index ad691b52..54b4db86 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -178,6 +178,13 @@ def api_user4(default_user4): ) +@pytest.mark.django_db +@pytest.fixture +def default_openai_chat_model_option(): + chat_model = ChatModelOptionsFactory(chat_model="gpt-4o-mini", model_type="openai") + return chat_model + + @pytest.mark.django_db @pytest.fixture def offline_agent(): diff --git a/tests/test_agents.py b/tests/test_agents.py new file mode 100644 index 00000000..da0b2357 --- /dev/null +++ b/tests/test_agents.py @@ -0,0 +1,211 @@ +# tests/test_agents.py +import os + +import pytest +from asgiref.sync import sync_to_async + +from khoj.database.adapters import AgentAdapters +from khoj.database.models import Agent, ChatModelOptions, Entry, KhojUser +from khoj.routers.api import execute_search +from khoj.utils.helpers import get_absolute_path +from tests.helpers import ChatModelOptionsFactory + + +def test_create_default_agent(default_user: KhojUser): + ChatModelOptionsFactory() + + agent = AgentAdapters.create_default_agent(default_user) + assert agent is not None + assert agent.input_tools == [] + assert agent.output_modes == [] + assert agent.privacy_level == Agent.PrivacyLevel.PUBLIC + assert agent.managed_by_admin == True + + +@pytest.mark.anyio +@pytest.mark.django_db(transaction=True) +async def test_create_or_update_agent(default_user: KhojUser, default_openai_chat_model_option: ChatModelOptions): + new_agent = await AgentAdapters.aupdate_agent( + default_user, + "Test Agent", + "Test Personality", + Agent.PrivacyLevel.PRIVATE, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [], + [], + [], + ) + assert new_agent is not None + assert new_agent.name == "Test Agent" + assert new_agent.privacy_level == Agent.PrivacyLevel.PRIVATE + assert new_agent.creator == default_user + + +@pytest.mark.anyio +@pytest.mark.django_db(transaction=True) +async def test_create_or_update_agent_with_knowledge_base( + default_user2: KhojUser, default_openai_chat_model_option: ChatModelOptions, chat_client +): + full_filename = get_absolute_path("tests/data/markdown/having_kids.markdown") + new_agent = await AgentAdapters.aupdate_agent( + default_user2, + "Test Agent", + "Test Personality", + Agent.PrivacyLevel.PRIVATE, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [full_filename], + [], + [], + ) + entries = await sync_to_async(list)(Entry.objects.filter(agent=new_agent)) + file_names = set() + for entry in entries: + file_names.add(entry.file_path) + + assert new_agent is not None + assert new_agent.name == "Test Agent" + assert new_agent.privacy_level == Agent.PrivacyLevel.PRIVATE + assert new_agent.creator == default_user2 + assert len(entries) > 0 + assert full_filename in file_names + assert len(file_names) == 1 + + +@pytest.mark.anyio +@pytest.mark.django_db(transaction=True) +async def test_create_or_update_agent_with_knowledge_base_and_search( + default_user2: KhojUser, default_openai_chat_model_option: ChatModelOptions, chat_client +): + full_filename = get_absolute_path("tests/data/markdown/having_kids.markdown") + new_agent = await AgentAdapters.aupdate_agent( + default_user2, + "Test Agent", + "Test Personality", + Agent.PrivacyLevel.PRIVATE, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [full_filename], + [], + [], + ) + + search_result = await execute_search(user=default_user2, q="having kids", agent=new_agent) + + assert len(search_result) == 5 + + +@pytest.mark.anyio +@pytest.mark.django_db(transaction=True) +async def test_agent_with_knowledge_base_and_search_not_creator( + default_user2: KhojUser, default_openai_chat_model_option: ChatModelOptions, chat_client, default_user3: KhojUser +): + full_filename = get_absolute_path("tests/data/markdown/having_kids.markdown") + new_agent = await AgentAdapters.aupdate_agent( + default_user2, + "Test Agent", + "Test Personality", + Agent.PrivacyLevel.PUBLIC, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [full_filename], + [], + [], + ) + + search_result = await execute_search(user=default_user3, q="having kids", agent=new_agent) + + assert len(search_result) == 5 + + +@pytest.mark.anyio +@pytest.mark.django_db(transaction=True) +async def test_agent_with_knowledge_base_and_search_not_creator_and_private( + default_user2: KhojUser, default_openai_chat_model_option: ChatModelOptions, chat_client, default_user3: KhojUser +): + full_filename = get_absolute_path("tests/data/markdown/having_kids.markdown") + new_agent = await AgentAdapters.aupdate_agent( + default_user2, + "Test Agent", + "Test Personality", + Agent.PrivacyLevel.PRIVATE, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [full_filename], + [], + [], + ) + + search_result = await execute_search(user=default_user3, q="having kids", agent=new_agent) + + assert len(search_result) == 0 + + +@pytest.mark.anyio +@pytest.mark.django_db(transaction=True) +async def test_agent_with_knowledge_base_and_search_not_creator_and_private_accessible_to_none( + default_user2: KhojUser, default_openai_chat_model_option: ChatModelOptions, chat_client +): + full_filename = get_absolute_path("tests/data/markdown/having_kids.markdown") + new_agent = await AgentAdapters.aupdate_agent( + default_user2, + "Test Agent", + "Test Personality", + Agent.PrivacyLevel.PRIVATE, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [full_filename], + [], + [], + ) + + search_result = await execute_search(user=None, q="having kids", agent=new_agent) + + assert len(search_result) == 5 + + +@pytest.mark.anyio +@pytest.mark.django_db(transaction=True) +async def test_multiple_agents_with_knowledge_base_and_users( + default_user2: KhojUser, default_openai_chat_model_option: ChatModelOptions, chat_client, default_user3: KhojUser +): + full_filename = get_absolute_path("tests/data/markdown/having_kids.markdown") + new_agent = await AgentAdapters.aupdate_agent( + default_user2, + "Test Agent", + "Test Personality", + Agent.PrivacyLevel.PUBLIC, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [full_filename], + [], + [], + ) + + full_filename2 = get_absolute_path("tests/data/markdown/Namita.markdown") + new_agent2 = await AgentAdapters.aupdate_agent( + default_user2, + "Test Agent 2", + "Test Personality", + Agent.PrivacyLevel.PUBLIC, + "icon", + "color", + default_openai_chat_model_option.chat_model, + [full_filename2], + [], + [], + ) + + search_result = await execute_search(user=default_user3, q="having kids", agent=new_agent2) + search_result2 = await execute_search(user=default_user3, q="Namita", agent=new_agent2) + + assert len(search_result) == 0 + assert len(search_result2) == 1 From 59fec37943fd3105d0a52773581a65eccb191cab Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 20 Oct 2024 22:24:51 -0700 Subject: [PATCH 07/13] Improve agents management, and limit agents view to private and official agents - Default to None for the input_tools and output_modes so that they can be managed in the admin panel - Hold off on showing off all Public Agents until we have a better experience for user profiles etc. --- src/khoj/database/adapters/__init__.py | 2 + ...nt_input_tools_alter_agent_output_modes.py | 46 +++++++++++++++++++ src/khoj/database/models/__init__.py | 8 +++- 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/khoj/database/migrations/0070_alter_agent_input_tools_alter_agent_output_modes.py diff --git a/src/khoj/database/adapters/__init__.py b/src/khoj/database/adapters/__init__.py index d025459e..eb914ed8 100644 --- a/src/khoj/database/adapters/__init__.py +++ b/src/khoj/database/adapters/__init__.py @@ -622,6 +622,8 @@ class AgentAdapters: @staticmethod def get_all_accessible_agents(user: KhojUser = None): public_query = Q(privacy_level=Agent.PrivacyLevel.PUBLIC) + # TODO Update this to allow any public agent that's officially approved once that experience is launched + public_query &= Q(managed_by_admin=True) if user: return ( Agent.objects.filter(public_query | Q(creator=user)) diff --git a/src/khoj/database/migrations/0070_alter_agent_input_tools_alter_agent_output_modes.py b/src/khoj/database/migrations/0070_alter_agent_input_tools_alter_agent_output_modes.py new file mode 100644 index 00000000..74dfc229 --- /dev/null +++ b/src/khoj/database/migrations/0070_alter_agent_input_tools_alter_agent_output_modes.py @@ -0,0 +1,46 @@ +# Generated by Django 5.0.8 on 2024-10-21 05:16 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0069_webscraper_serverchatsettings_web_scraper"), + ] + + operations = [ + migrations.AlterField( + model_name="agent", + name="input_tools", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("general", "General"), + ("online", "Online"), + ("notes", "Notes"), + ("summarize", "Summarize"), + ("webpage", "Webpage"), + ], + max_length=200, + ), + blank=True, + default=list, + null=True, + size=None, + ), + ), + migrations.AlterField( + model_name="agent", + name="output_modes", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[("text", "Text"), ("image", "Image"), ("automation", "Automation")], max_length=200 + ), + blank=True, + default=list, + null=True, + size=None, + ), + ), + ] diff --git a/src/khoj/database/models/__init__.py b/src/khoj/database/models/__init__.py index 2b2fde2d..6b122dac 100644 --- a/src/khoj/database/models/__init__.py +++ b/src/khoj/database/models/__init__.py @@ -180,8 +180,12 @@ class Agent(BaseModel): ) # Creator will only be null when the agents are managed by admin name = models.CharField(max_length=200) personality = models.TextField() - input_tools = ArrayField(models.CharField(max_length=200, choices=InputToolOptions.choices), default=list) - output_modes = ArrayField(models.CharField(max_length=200, choices=OutputModeOptions.choices), default=list) + input_tools = ArrayField( + models.CharField(max_length=200, choices=InputToolOptions.choices), default=list, null=True, blank=True + ) + output_modes = ArrayField( + models.CharField(max_length=200, choices=OutputModeOptions.choices), default=list, null=True, blank=True + ) managed_by_admin = models.BooleanField(default=False) chat_model = models.ForeignKey(ChatModelOptions, on_delete=models.CASCADE) slug = models.CharField(max_length=200, unique=True) From ad197be70c3d5b5a6e49862bfd9fe761ca65b271 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 20 Oct 2024 22:25:41 -0700 Subject: [PATCH 08/13] Fix PDFs unit test, skip OCR --- tests/test_pdf_to_entries.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_pdf_to_entries.py b/tests/test_pdf_to_entries.py index 3a25b05d..a62eca8b 100644 --- a/tests/test_pdf_to_entries.py +++ b/tests/test_pdf_to_entries.py @@ -1,6 +1,8 @@ import os import re +import pytest + from khoj.processor.content.pdf.pdf_to_entries import PdfToEntries from khoj.utils.fs_syncer import get_pdf_files from khoj.utils.rawconfig import TextContentConfig @@ -37,6 +39,7 @@ def test_multi_page_pdf_to_jsonl(): assert len(entries[1]) == 6 +@pytest.mark.skip(reason="Temporarily disabled OCR due to performance issues") def test_ocr_page_pdf_to_jsonl(): "Convert multiple pages from single PDF file to jsonl." # Arrange From 21e69b506d706dcacf80b3c01850160a311742fe Mon Sep 17 00:00:00 2001 From: sabaimran Date: Mon, 21 Oct 2024 08:19:05 -0700 Subject: [PATCH 09/13] Release Khoj version 1.26.3 --- manifest.json | 2 +- src/interface/desktop/package.json | 2 +- src/interface/emacs/khoj.el | 2 +- src/interface/obsidian/manifest.json | 2 +- src/interface/obsidian/package.json | 2 +- src/interface/obsidian/versions.json | 3 ++- src/interface/web/package.json | 2 +- versions.json | 3 ++- 8 files changed, 10 insertions(+), 8 deletions(-) diff --git a/manifest.json b/manifest.json index bc7bfbc1..9ba9a6e4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.2", + "version": "1.26.3", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/desktop/package.json b/src/interface/desktop/package.json index 03315905..4452f0aa 100644 --- a/src/interface/desktop/package.json +++ b/src/interface/desktop/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.2", + "version": "1.26.3", "description": "Your Second Brain", "author": "Khoj Inc. ", "license": "GPL-3.0-or-later", diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index a5ad882e..92fe9f77 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Saba Imran ;; Description: Your Second Brain ;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image -;; Version: 1.26.2 +;; Version: 1.26.3 ;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1")) ;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs diff --git a/src/interface/obsidian/manifest.json b/src/interface/obsidian/manifest.json index bc7bfbc1..9ba9a6e4 100644 --- a/src/interface/obsidian/manifest.json +++ b/src/interface/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.2", + "version": "1.26.3", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/obsidian/package.json b/src/interface/obsidian/package.json index 006f695c..2714d8fb 100644 --- a/src/interface/obsidian/package.json +++ b/src/interface/obsidian/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.2", + "version": "1.26.3", "description": "Your Second Brain", "author": "Debanjum Singh Solanky, Saba Imran ", "license": "GPL-3.0-or-later", diff --git a/src/interface/obsidian/versions.json b/src/interface/obsidian/versions.json index 16385422..285cc0f4 100644 --- a/src/interface/obsidian/versions.json +++ b/src/interface/obsidian/versions.json @@ -80,5 +80,6 @@ "1.25.0": "0.15.0", "1.26.0": "0.15.0", "1.26.1": "0.15.0", - "1.26.2": "0.15.0" + "1.26.2": "0.15.0", + "1.26.3": "0.15.0" } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index cbe269cc..2b952a16 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -1,6 +1,6 @@ { "name": "khoj-ai", - "version": "1.26.2", + "version": "1.26.3", "private": true, "scripts": { "dev": "next dev", diff --git a/versions.json b/versions.json index 16385422..285cc0f4 100644 --- a/versions.json +++ b/versions.json @@ -80,5 +80,6 @@ "1.25.0": "0.15.0", "1.26.0": "0.15.0", "1.26.1": "0.15.0", - "1.26.2": "0.15.0" + "1.26.2": "0.15.0", + "1.26.3": "0.15.0" } From db959a504da5aaa9f89c6511d8a0f73824ab65cf Mon Sep 17 00:00:00 2001 From: sabaimran Date: Mon, 21 Oct 2024 12:56:51 -0700 Subject: [PATCH 10/13] Fix the version of pymupdf to avert build errors --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 960d8d72..93df0b42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dependencies = [ "requests >= 2.26.0", "tenacity == 8.3.0", "anyio == 3.7.1", - "pymupdf >= 1.23.5", + "pymupdf == 1.24.11", "django == 5.0.9", "authlib == 1.2.1", "llama-cpp-python == 0.2.88", From 892040972fed8b6236c4ad4180f992707a024508 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Mon, 21 Oct 2024 20:47:52 -0700 Subject: [PATCH 11/13] Replace user_id with server_id in telemetry --- src/khoj/configure.py | 2 +- src/khoj/routers/auth.py | 4 ++-- src/khoj/routers/subscription.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/khoj/configure.py b/src/khoj/configure.py index b60c00d1..3fb540bd 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -172,7 +172,7 @@ class UserAuthenticationBackend(AuthenticationBackend): request=request, telemetry_type="api", api="create_user", - metadata={"user_id": str(user.uuid)}, + metadata={"server_id": str(user.uuid)}, ) logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}") else: diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py index 98702041..3002503e 100644 --- a/src/khoj/routers/auth.py +++ b/src/khoj/routers/auth.py @@ -90,7 +90,7 @@ async def login_magic_link(request: Request, form: MagicLinkForm): request=request, telemetry_type="api", api="create_user", - metadata={"user_id": str(user.uuid)}, + metadata={"server_id": str(user.uuid)}, ) logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}") @@ -175,7 +175,7 @@ async def auth(request: Request): request=request, telemetry_type="api", api="create_user", - metadata={"user_id": str(khoj_user.uuid)}, + metadata={"server_id": str(khoj_user.uuid)}, ) logger.log(logging.INFO, f"🥳 New User Created: {khoj_user.uuid}") return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND) diff --git a/src/khoj/routers/subscription.py b/src/khoj/routers/subscription.py index abaac675..14a75da8 100644 --- a/src/khoj/routers/subscription.py +++ b/src/khoj/routers/subscription.py @@ -82,7 +82,7 @@ async def subscribe(request: Request): request=request, telemetry_type="api", api="create_user", - metadata={"user_id": str(user.user.uuid)}, + metadata={"server_id": str(user.user.uuid)}, ) logger.log(logging.INFO, f"🥳 New User Created: {user.user.uuid}") From 1e993d561bbae07659c7243c5df37c19305d691f Mon Sep 17 00:00:00 2001 From: sabaimran Date: Tue, 22 Oct 2024 13:50:08 -0700 Subject: [PATCH 12/13] Release Khoj version 1.26.4 --- manifest.json | 2 +- src/interface/desktop/package.json | 2 +- src/interface/emacs/khoj.el | 2 +- src/interface/obsidian/manifest.json | 2 +- src/interface/obsidian/package.json | 2 +- src/interface/obsidian/versions.json | 3 ++- src/interface/web/package.json | 2 +- versions.json | 3 ++- 8 files changed, 10 insertions(+), 8 deletions(-) diff --git a/manifest.json b/manifest.json index 9ba9a6e4..979c67c7 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.3", + "version": "1.26.4", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/desktop/package.json b/src/interface/desktop/package.json index 4452f0aa..bbae8d2b 100644 --- a/src/interface/desktop/package.json +++ b/src/interface/desktop/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.3", + "version": "1.26.4", "description": "Your Second Brain", "author": "Khoj Inc. ", "license": "GPL-3.0-or-later", diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index 92fe9f77..cad27196 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -6,7 +6,7 @@ ;; Saba Imran ;; Description: Your Second Brain ;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image -;; Version: 1.26.3 +;; Version: 1.26.4 ;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1")) ;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs diff --git a/src/interface/obsidian/manifest.json b/src/interface/obsidian/manifest.json index 9ba9a6e4..979c67c7 100644 --- a/src/interface/obsidian/manifest.json +++ b/src/interface/obsidian/manifest.json @@ -1,7 +1,7 @@ { "id": "khoj", "name": "Khoj", - "version": "1.26.3", + "version": "1.26.4", "minAppVersion": "0.15.0", "description": "Your Second Brain", "author": "Khoj Inc.", diff --git a/src/interface/obsidian/package.json b/src/interface/obsidian/package.json index 2714d8fb..46b74922 100644 --- a/src/interface/obsidian/package.json +++ b/src/interface/obsidian/package.json @@ -1,6 +1,6 @@ { "name": "Khoj", - "version": "1.26.3", + "version": "1.26.4", "description": "Your Second Brain", "author": "Debanjum Singh Solanky, Saba Imran ", "license": "GPL-3.0-or-later", diff --git a/src/interface/obsidian/versions.json b/src/interface/obsidian/versions.json index 285cc0f4..4ed50e51 100644 --- a/src/interface/obsidian/versions.json +++ b/src/interface/obsidian/versions.json @@ -81,5 +81,6 @@ "1.26.0": "0.15.0", "1.26.1": "0.15.0", "1.26.2": "0.15.0", - "1.26.3": "0.15.0" + "1.26.3": "0.15.0", + "1.26.4": "0.15.0" } diff --git a/src/interface/web/package.json b/src/interface/web/package.json index 2b952a16..2cef36b6 100644 --- a/src/interface/web/package.json +++ b/src/interface/web/package.json @@ -1,6 +1,6 @@ { "name": "khoj-ai", - "version": "1.26.3", + "version": "1.26.4", "private": true, "scripts": { "dev": "next dev", diff --git a/versions.json b/versions.json index 285cc0f4..4ed50e51 100644 --- a/versions.json +++ b/versions.json @@ -81,5 +81,6 @@ "1.26.0": "0.15.0", "1.26.1": "0.15.0", "1.26.2": "0.15.0", - "1.26.3": "0.15.0" + "1.26.3": "0.15.0", + "1.26.4": "0.15.0" } From 0dad4212fae7730ac908918b6899f1f5a4a61f10 Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:13:46 -0700 Subject: [PATCH 13/13] Generate dynamic diagrams (via Excalidraw) (#940) Add support for generating dynamic diagrams in flow with Excalidraw (https://github.com/excalidraw/excalidraw). This happens in three steps: 1. Default information collection & intent determination step. 2. Improving the overall guidance of the prompt for generating a JSON, Excalidraw-compatible declaration. 3. Generation of the diagram to output to the final UI. Add support in the web UI. --- src/interface/desktop/chat.html | 14 +- src/interface/desktop/chatutils.js | 15 +- src/interface/obsidian/src/chat_view.ts | 16 +- src/interface/web/app/chat/chat.module.css | 2 +- src/interface/web/app/chat/layout.tsx | 9 +- src/interface/web/app/chat/page.tsx | 7 +- src/interface/web/app/common/chatFunctions.ts | 69 ++++---- src/interface/web/app/common/iconUtils.tsx | 5 + .../chatHistory/chatHistory.module.css | 7 +- .../components/chatHistory/chatHistory.tsx | 35 ++-- .../components/chatMessage/chatMessage.tsx | 53 +++++-- .../app/components/excalidraw/excalidraw.tsx | 24 +++ .../excalidraw/excalidrawWrapper.tsx | 149 ++++++++++++++++++ src/interface/web/app/share/chat/layout.tsx | 9 +- src/interface/web/app/share/chat/page.tsx | 18 ++- .../web/app/share/chat/sharedChat.module.css | 2 +- src/interface/web/package.json | 3 +- src/interface/web/yarn.lock | 5 + src/khoj/processor/conversation/prompts.py | 144 +++++++++++++++++ src/khoj/processor/conversation/utils.py | 5 +- src/khoj/routers/api_chat.py | 52 ++++++ src/khoj/routers/helpers.py | 129 +++++++++++++++ src/khoj/routers/web_client.py | 11 -- src/khoj/utils/helpers.py | 6 +- 24 files changed, 689 insertions(+), 100 deletions(-) create mode 100644 src/interface/web/app/components/excalidraw/excalidraw.tsx create mode 100644 src/interface/web/app/components/excalidraw/excalidrawWrapper.tsx diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index a6ae9a15..4c2258cc 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -326,7 +326,7 @@ entries.forEach(entry => { // If the element is in the viewport, fetch the remaining message and unobserve the element if (entry.isIntersecting) { - fetchRemainingChatMessages(chatHistoryUrl, headers); + fetchRemainingChatMessages(chatHistoryUrl, headers, chatBody.dataset.conversation_id, hostURL); observer.unobserve(entry.target); } }); @@ -342,7 +342,11 @@ new Date(chat_log.created), chat_log.onlineContext, chat_log.intent?.type, - chat_log.intent?.["inferred-queries"]); + chat_log.intent?.["inferred-queries"], + chatBody.dataset.conversationId ?? "", + hostURL, + ); + chatBody.appendChild(messageElement); // When the 4th oldest message is within viewing distance (~60% scrolled up) @@ -421,7 +425,7 @@ } } - function fetchRemainingChatMessages(chatHistoryUrl, headers) { + function fetchRemainingChatMessages(chatHistoryUrl, headers, conversationId, hostURL) { // Create a new IntersectionObserver let observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { @@ -435,7 +439,9 @@ new Date(chat_log.created), chat_log.onlineContext, chat_log.intent?.type, - chat_log.intent?.["inferred-queries"] + chat_log.intent?.["inferred-queries"], + chatBody.dataset.conversationId ?? "", + hostURL, ); entry.target.replaceWith(messageElement); diff --git a/src/interface/desktop/chatutils.js b/src/interface/desktop/chatutils.js index 5213979f..48fb72c3 100644 --- a/src/interface/desktop/chatutils.js +++ b/src/interface/desktop/chatutils.js @@ -189,11 +189,19 @@ function processOnlineReferences(referenceSection, onlineContext) { //same return numOnlineReferences; } -function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null) { //same +function renderMessageWithReference(message, by, context=null, dt=null, onlineContext=null, intentType=null, inferredQueries=null, conversationId=null, hostURL=null) { let chatEl; if (intentType?.includes("text-to-image")) { let imageMarkdown = generateImageMarkdown(message, intentType, inferredQueries); chatEl = renderMessage(imageMarkdown, by, dt, null, false, "return"); + } else if (intentType === "excalidraw") { + let domain = hostURL ?? "https://app.khoj.dev/"; + + if (!domain.endsWith("/")) domain += "/"; + + let excalidrawMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in the web app at ${domain}chat?conversationId=${conversationId}`; + + chatEl = renderMessage(excalidrawMessage, by, dt, null, false, "return"); } else { chatEl = renderMessage(message, by, dt, null, false, "return"); } @@ -312,7 +320,6 @@ function formatHTMLMessage(message, raw=false, willReplace=true) { //same } function createReferenceSection(references, createLinkerSection=false) { - console.log("linker data: ", createLinkerSection); let referenceSection = document.createElement('div'); referenceSection.classList.add("reference-section"); referenceSection.classList.add("collapsed"); @@ -417,7 +424,11 @@ function handleImageResponse(imageJson, rawResponse) { rawResponse += `![generated_image](${imageJson.image})`; } else if (imageJson.intentType === "text-to-image-v3") { rawResponse = `![](data:image/webp;base64,${imageJson.image})`; + } else if (imageJson.intentType === "excalidraw") { + const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in the web app`; + rawResponse += redirectMessage; } + if (inferredQuery) { rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`; } diff --git a/src/interface/obsidian/src/chat_view.ts b/src/interface/obsidian/src/chat_view.ts index ed23bff0..408ce3a1 100644 --- a/src/interface/obsidian/src/chat_view.ts +++ b/src/interface/obsidian/src/chat_view.ts @@ -484,12 +484,13 @@ export class KhojChatView extends KhojPaneView { dt?: Date, intentType?: string, inferredQueries?: string[], + conversationId?: string, ) { if (!message) return; let chatMessageEl; - if (intentType?.includes("text-to-image")) { - let imageMarkdown = this.generateImageMarkdown(message, intentType, inferredQueries); + if (intentType?.includes("text-to-image") || intentType === "excalidraw") { + let imageMarkdown = this.generateImageMarkdown(message, intentType, inferredQueries, conversationId); chatMessageEl = this.renderMessage(chatEl, imageMarkdown, sender, dt); } else { chatMessageEl = this.renderMessage(chatEl, message, sender, dt); @@ -509,7 +510,7 @@ export class KhojChatView extends KhojPaneView { chatMessageBodyEl.appendChild(this.createReferenceSection(references)); } - generateImageMarkdown(message: string, intentType: string, inferredQueries?: string[]) { + generateImageMarkdown(message: string, intentType: string, inferredQueries?: string[], conversationId?: string): string { let imageMarkdown = ""; if (intentType === "text-to-image") { imageMarkdown = `![](data:image/png;base64,${message})`; @@ -517,6 +518,10 @@ export class KhojChatView extends KhojPaneView { imageMarkdown = `![](${message})`; } else if (intentType === "text-to-image-v3") { imageMarkdown = `![](data:image/webp;base64,${message})`; + } else if (intentType === "excalidraw") { + const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`; + const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}chat?conversationId=${conversationId}`; + imageMarkdown = redirectMessage; } if (inferredQueries) { imageMarkdown += "\n\n**Inferred Query**:"; @@ -884,6 +889,7 @@ export class KhojChatView extends KhojPaneView { new Date(chatLog.created), chatLog.intent?.type, chatLog.intent?.["inferred-queries"], + chatBodyEl.dataset.conversationId ?? "", ); // push the user messages to the chat history if(chatLog.by === "you"){ @@ -1354,6 +1360,10 @@ export class KhojChatView extends KhojPaneView { rawResponse += `![generated_image](${imageJson.image})`; } else if (imageJson.intentType === "text-to-image-v3") { rawResponse = `![](data:image/webp;base64,${imageJson.image})`; + } else if (imageJson.intentType === "excalidraw") { + const domain = this.setting.khojUrl.endsWith("/") ? this.setting.khojUrl : `${this.setting.khojUrl}/`; + const redirectMessage = `Hey, I'm not ready to show you diagrams yet here. But you can view it in ${domain}`; + rawResponse += redirectMessage; } if (inferredQuery) { rawResponse += `\n\n**Inferred Query**:\n\n${inferredQuery}`; diff --git a/src/interface/web/app/chat/chat.module.css b/src/interface/web/app/chat/chat.module.css index 942087e6..69be25b4 100644 --- a/src/interface/web/app/chat/chat.module.css +++ b/src/interface/web/app/chat/chat.module.css @@ -79,7 +79,7 @@ div.titleBar { div.chatBoxBody { display: grid; height: 100%; - width: 70%; + width: 95%; margin: auto; } diff --git a/src/interface/web/app/chat/layout.tsx b/src/interface/web/app/chat/layout.tsx index 6688cfc8..09c3afb7 100644 --- a/src/interface/web/app/chat/layout.tsx +++ b/src/interface/web/app/chat/layout.tsx @@ -47,7 +47,14 @@ export default function RootLayout({ child-src 'none'; object-src 'none';" > - {children} + + {children} +