Merge branch 'master' of github.com:khoj-ai/khoj into parallelize-search-across-all-asymmetric-text-content-types

Conflicts:
- src/khoj/routers/api.py: Use theirs
This commit is contained in:
Debanjum Singh Solanky 2023-06-27 16:10:32 -07:00
commit 0636ceaf14
46 changed files with 6976 additions and 454 deletions

View file

@ -33,7 +33,7 @@ jobs:
- name: 🌡️ Validate Khoj.el
env:
# Khoj recipe from https://github.com/melpa/melpa/pull/8321/files
RECIPE: (khoj :fetcher github :repo "debanjum/khoj" :files ("src/interface/emacs/*.el"))
RECIPE: (khoj :fetcher github :repo "khoj-ai/khoj" :files ("src/interface/emacs/*.el"))
EXIST_OK: true
LOCAL_REPO: ${{ github.workspace }}
run: echo $GITHUB_REF && make -C ~/melpazoid

View file

@ -1,14 +1,14 @@
# syntax=docker/dockerfile:1
FROM ubuntu:kinetic
LABEL org.opencontainers.image.source https://github.com/debanjum/khoj
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
# Install System Dependencies
RUN apt update -y && \
apt -y install python3-pip python3-pyqt6
apt -y install python3-pip python3-pyqt6 git
# Install Python Dependencies
RUN pip install --upgrade pip && \
pip install --upgrade --pre khoj-assistant
pip install git+https://github.com/khoj-ai/khoj.git
# Run the Application
# There are more arguments required for the application to run,

View file

@ -1,14 +1,15 @@
# Khoj 🦅
[![test](https://github.com/debanjum/khoj/actions/workflows/test.yml/badge.svg)](https://github.com/debanjum/khoj/actions/workflows/test.yml)
[![dockerize](https://github.com/debanjum/khoj/actions/workflows/dockerize.yml/badge.svg)](https://github.com/debanjum/khoj/pkgs/container/khoj)
[![pypi](https://github.com/debanjum/khoj/actions/workflows/pypi.yml/badge.svg)](https://pypi.org/project/khoj-assistant/)
<h1><img src="src/khoj/interface/web/assets/icons/khoj-logo-sideways.svg" width="330" alt="Khoj Logo"></h1>
[![test](https://github.com/khoj-ai/khoj/actions/workflows/test.yml/badge.svg)](https://github.com/khoj-ai/khoj/actions/workflows/test.yml)
[![dockerize](https://github.com/khoj-ai/khoj/actions/workflows/dockerize.yml/badge.svg)](https://github.com/khoj-ai/khoj/pkgs/container/khoj)
[![pypi](https://github.com/khoj-ai/khoj/actions/workflows/pypi.yml/badge.svg)](https://pypi.org/project/khoj-assistant/)
*An AI personal assistant for your digital brain*
**Supported Plugins**
[![Khoj on Obsidian](https://img.shields.io/badge/Obsidian-%23483699.svg?style=for-the-badge&logo=obsidian&logoColor=white)](https://github.com/debanjum/khoj/tree/master/src/interface/obsidian#readme)
[![Khoj on Emacs](https://img.shields.io/badge/Emacs-%237F5AB6.svg?&style=for-the-badge&logo=gnu-emacs&logoColor=white)](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#readme)
[![Khoj on Obsidian](https://img.shields.io/badge/Obsidian-%23483699.svg?style=for-the-badge&logo=obsidian&logoColor=white)](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#readme)
[![Khoj on Emacs](https://img.shields.io/badge/Emacs-%237F5AB6.svg?&style=for-the-badge&logo=gnu-emacs&logoColor=white)](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#readme)
## Table of Contents
@ -68,7 +69,7 @@
## Demos
### Khoj in Obsidian
https://github.com/debanjum/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b
https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b
<details><summary>Description</summary>
@ -88,9 +89,9 @@ https://user-images.githubusercontent.com/6413477/184735169-92c78bf1-d827-4663-9
- Install Khoj via pip
- Start Khoj app
- Add this readme and [khoj.el readme](https://github.com/debanjum/khoj/tree/master/src/interface/emacs) as org-mode for Khoj to index
- Add this readme and [khoj.el readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs) as org-mode for Khoj to index
- Search \"*Setup editor*\" on the Web and Emacs. Re-rank the results for better accuracy
- Top result is what we are looking for, the [section to Install Khoj.el on Emacs](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#2-Install-Khojel)
- Top result is what we are looking for, the [section to Install Khoj.el on Emacs](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#2-Install-Khojel)
</details>
<details><summary>Analysis</summary>
@ -103,19 +104,19 @@ https://user-images.githubusercontent.com/6413477/184735169-92c78bf1-d827-4663-9
### Interfaces
![](https://github.com/debanjum/khoj/blob/master/docs/interfaces.png?)
![](https://github.com/khoj-ai/khoj/blob/master/docs/interfaces.png?)
## Architecture
![](https://github.com/debanjum/khoj/blob/master/docs/khoj_architecture.png?)
![](https://github.com/khoj-ai/khoj/blob/master/docs/khoj_architecture.png?)
## Setup
These are the general setup instructions for Khoj.
- Make sure [python](https://realpython.com/installing-python/) (version 3.10 or lower) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine
- Check the [Khoj.el Readme](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Setup) to setup Khoj with Emacs<br />
- Check the [Khoj.el Readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Setup) to setup Khoj with Emacs<br />
Its simpler as it can skip the server *install*, *run* and *configure* step below.
- Check the [Khoj Obsidian Readme](https://github.com/debanjum/khoj/tree/master/src/interface/obsidian#Setup) to setup Khoj with Obsidian<br />
- Check the [Khoj Obsidian Readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#Setup) to setup Khoj with Obsidian<br />
Its simpler as it can skip the *configure* step below.
### 1. Install
@ -147,10 +148,10 @@ Khoj exposes a web interface by default.<br />
The optional steps below allow using Khoj from within an existing application like Obsidian or Emacs.
- **Khoj Obsidian**:<br />
[Install](https://github.com/debanjum/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) the Khoj Obsidian plugin
[Install](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) the Khoj Obsidian plugin
- **Khoj Emacs**:<br />
[Install](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#2-Install-Khojel) khoj.el
[Install](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#2-Install-Khojel) khoj.el
## Use
### Khoj Search
@ -201,7 +202,7 @@ Use structured query syntax to filter the natural language search results
2. Type your queries and see response by Khoj from your notes
#### Demo
![](https://github.com/debanjum/khoj/blob/master/docs/khoj_chat_web_interface.png?)
![](https://github.com/khoj-ai/khoj/blob/master/docs/khoj_chat_web_interface.png?)
### Details
1. Your query is used to retrieve the most relevant notes, if any, using Khoj search
@ -221,11 +222,11 @@ pip install --upgrade --pre khoj-assistant
### Upgrade Khoj on Emacs
- Use your Emacs Package Manager to Upgrade
- See [khoj.el readme](https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade) for details
- See [khoj.el readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade) for details
### Upgrade Khoj on Obsidian
- Upgrade via the Community plugins tab on the settings pane in the Obsidian app
- See the [khoj plugin readme](https://github.com/debanjum/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) for details
- See the [khoj plugin readme](https://github.com/khoj-ai/khoj/tree/master/src/interface/obsidian#2-Setup-Plugin) for details
## Uninstall
1. (Optional) Hit `Ctrl-C` in the terminal running the khoj server to stop it
@ -243,7 +244,7 @@ pip install --upgrade --pre khoj-assistant
rustup-init
source ~/.cargo/env
```
- **Refer**: [Issue with Fix](https://github.com/debanjum/khoj/issues/82#issuecomment-1241890946) for more details
- **Refer**: [Issue with Fix](https://github.com/khoj-ai/khoj/issues/82#issuecomment-1241890946) for more details
#### Search starts giving wonky results
- **Fix**: Open [/api/update?force=true](http://localhost:8000/api/update?force=true)[^2] in browser to regenerate index from scratch
@ -264,7 +265,7 @@ pip install --upgrade --pre khoj-assistant
4. Click the [Add to Homescreen](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Add_to_home_screen) button
5. Enjoy exploring your notes, transactions and images from your phone!
![](https://github.com/debanjum/khoj/blob/master/docs/khoj_pwa_android.png?)
![](https://github.com/khoj-ai/khoj/blob/master/docs/khoj_pwa_android.png?)
### Use OpenAI Models for Search
#### Setup
@ -308,7 +309,7 @@ pip install --upgrade --pre khoj-assistant
If you want, Khoj can be configured to use OpenAI for search and chat.<br />
Add your OpenAI API to Khoj by using either of the two options below:
- Open the Khoj desktop GUI, add your [OpenAI API key](https://beta.openai.com/account/api-keys) and click *Configure*
Ensure khoj is started **without** the `--no-gui` flag. Check your system tray to see if Khoj 🦅 is minimized there.
Ensure khoj is started **without** the `--no-gui` flag. Check your system tray to see if Khoj is minimized there.
- Set `openai-api-key` field under `processor.conversation` section in your `khoj.yml`[^1] to your [OpenAI API key](https://beta.openai.com/account/api-keys) and restart khoj:
```diff
processor:
@ -359,7 +360,7 @@ Note that this plugin is currently *only* indexing Markdown files. It will ignor
*[Interactive Visualization](https://mango-dune-07a8b7110.1.azurestaticapps.net/?repo=debanjum%2Fkhoj)*
![](https://github.com/debanjum/khoj/blob/master/docs/khoj_codebase_visualization_0.2.1.png?)
![](https://github.com/khoj-ai/khoj/blob/master/docs/khoj_codebase_visualization_0.2.1.png?)
### Setup
#### Using Pip
@ -367,7 +368,7 @@ Note that this plugin is currently *only* indexing Markdown files. It will ignor
```shell
# Get Khoj Code
git clone https://github.com/debanjum/khoj && cd khoj
git clone https://github.com/khoj-ai/khoj && cd khoj
# Create, Activate Virtual Environment
python3 -m venv .venv && source .venv/bin/activate
@ -396,7 +397,7 @@ pip install -e .[dev]
##### 1. Clone
```shell
git clone https://github.com/debanjum/khoj && cd khoj
git clone https://github.com/khoj-ai/khoj && cd khoj
```
##### 2. Configure
@ -424,7 +425,7 @@ docker-compose build --pull
##### 2. Install Khoj
```shell
git clone https://github.com/debanjum/khoj && cd khoj
git clone https://github.com/khoj-ai/khoj && cd khoj
conda env create -f config/environment.yml
conda activate khoj
python3 -m pip install pyqt6 # As conda does not support pyqt6 yet

View file

@ -1,7 +1,7 @@
version: "3.9"
services:
server:
image: ghcr.io/debanjum/khoj:latest
image: ghcr.io/khoj-ai/khoj:latest
ports:
# If changing the local port (left hand side), no other changes required.
# If changing the remote port (right hand side),

View file

@ -3,7 +3,7 @@
"name": "Khoj",
"version": "0.6.2",
"minAppVersion": "0.15.0",
"description": "An AI Personal Assistant for your Digital Brain 🦅",
"description": "An AI Personal Assistant for your Digital Brain",
"author": "Debanjum Singh Solanky",
"authorUrl": "https://github.com/debanjum",
"isDesktopOnly": false

View file

@ -57,14 +57,15 @@ dependencies = [
"langchain >= 0.0.187",
"pypdf >= 3.9.0",
"requests >= 2.26.0",
"bs4 >= 0.0.1",
]
dynamic = ["version"]
[project.urls]
Homepage = "https://github.com/debanjum/khoj#readme"
Issues = "https://github.com/debanjum/khoj/issues"
Discussions = "https://github.com/debanjum/khoj/discussions"
Releases = "https://github.com/debanjum/khoj/releases"
Homepage = "https://github.com/khoj-ai/khoj#readme"
Issues = "https://github.com/khoj-ai/khoj/issues"
Discussions = "https://github.com/khoj-ai/khoj/discussions"
Releases = "https://github.com/khoj-ai/khoj/releases"
[project.scripts]
khoj = "khoj.main:run"

View file

@ -1,25 +1,27 @@
* Khoj Emacs 🦅
[[https://stable.melpa.org/#/khoj][file:https://stable.melpa.org/packages/khoj-badge.svg]] [[https://melpa.org/#/khoj][file:https://melpa.org/packages/khoj-badge.svg]] [[https://github.com/debanjum/khoj/actions/workflows/build_khoj_el.yml][https://github.com/debanjum/khoj/actions/workflows/build_khoj_el.yml/badge.svg?]] [[https://github.com/debanjum/khoj/actions/workflows/test_khoj_el.yml][https://github.com/debanjum/khoj/actions/workflows/test_khoj_el.yml/badge.svg?]]
[[https://github.com/khoj-ai/khoj/edit/master/src/interface/emacs/README.org][file:/src/khoj/interface/web/assets/icons/khoj-logo-sideways-200.png]] Emacs
[[https://stable.melpa.org/#/khoj][file:https://stable.melpa.org/packages/khoj-badge.svg]] [[https://melpa.org/#/khoj][file:https://melpa.org/packages/khoj-badge.svg]] [[https://github.com/khoj-ai/khoj/actions/workflows/build_khoj_el.yml][https://github.com/khoj-ai/khoj/actions/workflows/build_khoj_el.yml/badge.svg?]] [[https://github.com/khoj-ai/khoj/actions/workflows/test_khoj_el.yml][https://github.com/khoj-ai/khoj/actions/workflows/test_khoj_el.yml/badge.svg?]]
/An AI personal assistant for your digital brain/
** Table of Contents
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#features][Features]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Interface][Interface]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Setup][Setup]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Direct-Install][Direct Install]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Minimal-Install][Minimal Install]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Standard-Install][Standard Install]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#With-Straight.el][With Straight.el]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Use][Use]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Search][Search]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Chat][Chat]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Find-similar-entries][Find Similar Entries]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Advanced-usage][Advanced Usage]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Khoj-menu][Khoj Menu]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade][Upgrade]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade-Khoj-Backend][Upgrade Backend]]
- [[https://github.com/debanjum/khoj/tree/master/src/interface/emacs#Upgrade-Khojel][Upgrade Khoj.el]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#features][Features]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Interface][Interface]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Setup][Setup]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Direct-Install][Direct Install]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Minimal-Install][Minimal Install]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Standard-Install][Standard Install]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#With-Straight.el][With Straight.el]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Use][Use]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Search][Search]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Chat][Chat]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Find-similar-entries][Find Similar Entries]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Advanced-usage][Advanced Usage]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Khoj-menu][Khoj Menu]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade][Upgrade]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade-Khoj-Backend][Upgrade Backend]]
- [[https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs#Upgrade-Khojel][Upgrade Khoj.el]]
** Features
- *Search*
@ -42,7 +44,7 @@
- /Make sure [[https://realpython.com/installing-python/][python]] (version 3.10 or lower) and [[https://pip.pypa.io/en/stable/installation/][pip]] are installed on your machine/
- /khoj.el attempts to automatically install, start and configure the khoj server./
If this fails, follow [[https://github.com/debanjum/khoj/tree/master/#Setup][these instructions]] to manually setup the khoj server.
If this fails, follow [[https://github.com/khoj-ai/khoj/tree/master/#Setup][these instructions]] to manually setup the khoj server.
*** Direct Install
#+begin_src elisp
@ -89,7 +91,7 @@
;; Install Khoj Package using Straight.el
(use-package khoj
:after org
:straight (khoj :type git :host github :repo "debanjum/khoj" :files (:defaults "src/interface/emacs/khoj.el"))
:straight (khoj :type git :host github :repo "khoj-ai/khoj" :files (:defaults "src/interface/emacs/khoj.el"))
:bind ("C-c s" . 'khoj)
:config (setq khoj-org-directories '("~/docs/org-roam" "~/docs/notes")
khoj-org-files '("~/docs/todo.org" "~/docs/work.org")
@ -111,7 +113,7 @@
E.g "When did I file my taxes last year?"
See [[https://github.com/debanjum/khoj/tree/master/#Khoj-Chat][Khoj Chat]] for more details
See [[https://github.com/khoj-ai/khoj/tree/master/#Khoj-Chat][Khoj Chat]] for more details
*** Find Similar Entries
This feature finds entries similar to the one you are currently on.
@ -119,7 +121,7 @@
2. Hit ~C-c s f~ (or ~M-x khoj RET f~) to find similar entries
*** Advanced Usage
- Add [[https://github.com/debanjum/khoj/#query-filters][query filters]] during search to narrow down results further
- Add [[https://github.com/khoj-ai/khoj/#query-filters][query filters]] during search to narrow down results further
e.g `What is the meaning of life? -"god" +"none" dt>"last week"`

View file

@ -7,7 +7,7 @@
;; Keywords: search, chat, org-mode, outlines, markdown, pdf, beancount, image
;; Version: 0.6.2
;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1"))
;; URL: https://github.com/debanjum/khoj/tree/master/src/interface/emacs
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs
;; This file is NOT part of GNU Emacs.
@ -106,13 +106,13 @@
(defvar khoj--minibuffer-window nil
"Minibuffer window used to enter query.")
(defconst khoj--query-prompt "🦅Khoj: "
(defconst khoj--query-prompt "🏮 Khoj: "
"Query prompt shown in the minibuffer.")
(defconst khoj--search-buffer-name "*🦅Khoj Search*"
(defconst khoj--search-buffer-name "*🏮 Khoj Search*"
"Name of buffer to show search results from Khoj.")
(defconst khoj--chat-buffer-name "*🦅Khoj Chat*"
(defconst khoj--chat-buffer-name "*🏮 Khoj Chat*"
"Name of chat buffer for Khoj.")
(defvar khoj--content-type "org"
@ -784,7 +784,7 @@ Render results in BUFFER-NAME using QUERY, CONTENT-TYPE."
(progn
(org-set-startup-visibility)
(visual-line-mode)
(re-search-backward "^\*+ 🦅" nil t))))
(re-search-backward "^\*+ 🏮" nil t))))
(defun khoj--query-chat-api (query)
"Send QUERY to Khoj Chat API."
@ -809,7 +809,7 @@ RECEIVE-DATE is the message receive date."
(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-sender (if (equal sender "you") "🤔 *You*" "🦅 *Khoj*"))
(emojified-sender (if (equal sender "you") "🤔 *You*" "🏮 *Khoj*"))
(suffix-newlines (if (equal sender "khoj") "\n\n" ""))
(received (or receive-date (format-time-string "%F %T"))))
(format "%s %s: %s\n :PROPERTIES:\n :RECEIVED: [%s]\n :END:\n%s\n%s"

View file

@ -5,7 +5,7 @@
;; Author: Debanjum Singh Solanky <debanjum@gmail.com>
;; Version: 0.0.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
;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs
;;; License:

View file

@ -1,5 +1,6 @@
# Khoj Obsidian 🦅
> Natural language search for your Obsidian notes using [Khoj](https://github.com/debanjum/khoj)
<img src="/src/khoj/interface/web/assets/icons/khoj-logo-sideways.svg" width="200" alt="Khoj Logo">Obsidian
> Natural language search for your Obsidian notes using [Khoj](https://github.com/khoj-ai/khoj)
## Table of Contents
@ -35,7 +36,7 @@
## Demo
### Search Demo
https://github.com/debanjum/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b
https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f56b
<details><summary>Description</summary>
@ -51,10 +52,10 @@ https://github.com/debanjum/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0
## Interfaces
### Search Modal
![](https://github.com/debanjum/khoj/blob/master/src/interface/obsidian/docs/khoj_on_obsidian_0.2.5.png?)
![](https://github.com/khoj-ai/khoj/blob/master/src/interface/obsidian/docs/khoj_on_obsidian_0.2.5.png?)
### Chat Modal
![](https://github.com/debanjum/khoj/blob/master/src/interface/obsidian/docs/khoj_chat_on_obsidian_0.6.0.png?)
![](https://github.com/khoj-ai/khoj/blob/master/src/interface/obsidian/docs/khoj_chat_on_obsidian_0.6.0.png?)
## Setup
- *Make sure [python](https://realpython.com/installing-python/) (version 3.10 or lower) and [pip](https://pip.pypa.io/en/stable/installation/) are installed on your machine*
@ -88,9 +89,9 @@ Notes:
- *Using Khoj Chat will result in query relevant notes being shared with OpenAI for ChatGPT to respond.*
- *To use Khoj Chat, ensure you've set your [OpenAI API key](https://platform.openai.com/account/api-keys) in the Khoj plugin settings.*
See [Khoj Chat](https://github.com/debanjum/khoj/tree/master/#Khoj-Chat) for more details
See [Khoj Chat](https://github.com/khoj-ai/khoj/tree/master/#Khoj-Chat) for more details
![](https://github.com/debanjum/khoj/blob/master/src/interface/obsidian/docs/khoj_chat_on_obsidian_0.6.0.png?)
![](https://github.com/khoj-ai/khoj/blob/master/src/interface/obsidian/docs/khoj_chat_on_obsidian_0.6.0.png?)
### Search
Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Search* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)
@ -144,7 +145,7 @@ To see other notes similar to the current one, run *Khoj: Find Similar Notes* fr
So notes across multiple vaults **cannot** be searched at the same time
## Visualize Codebase
<img src="https://github.com/debanjum/khoj/blob/master/src/interface/obsidian/docs/khoj_obsidian_codebase_visualization_0.2.1.png" width="700" />
<img src="https://github.com/khoj-ai/khoj/blob/master/src/interface/obsidian/docs/khoj_obsidian_codebase_visualization_0.2.1.png" width="700" />
## Implementation
The plugin implements the following functionality to search your notes with Khoj:

View file

@ -3,7 +3,7 @@
"name": "Khoj",
"version": "0.6.2",
"minAppVersion": "0.15.0",
"description": "An AI Personal Assistant for your Digital Brain 🦅",
"description": "An AI Personal Assistant for your Digital Brain",
"author": "Debanjum Singh Solanky",
"authorUrl": "https://github.com/debanjum",
"isDesktopOnly": false

View file

@ -1,7 +1,7 @@
{
"name": "Khoj",
"version": "0.6.2",
"description": "An AI Personal Assistant for your Digital Brain 🦅",
"description": "An AI Personal Assistant for your Digital Brain",
"main": "src/main.js",
"scripts": {
"dev": "node esbuild.config.mjs",

View file

@ -49,7 +49,7 @@ export class KhojChatModal extends Modal {
type: "text",
id: "khoj-chat-input",
autofocus: "autofocus",
placeholder: "Chat with Khoj 🦅 [Hit Enter to send message]",
placeholder: "Chat with Khoj [Hit Enter to send message]",
class: "khoj-chat-input option"
}
})
@ -81,7 +81,7 @@ export class KhojChatModal extends Modal {
renderMessage(message: string, sender: string, dt?: Date): Element | null {
let message_time = this.formatDate(dt ?? new Date());
let emojified_sender = sender == "khoj" ? "🦅 Khoj" : "🤔 You";
let emojified_sender = sender == "khoj" ? "🏮 Khoj" : "🤔 You";
// Append message to conversation history HTML element.
// The chat logs should display above the message input box to follow standard UI semantics

View file

@ -63,7 +63,7 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
this.setInstructions(modalInstructions);
// Set Placeholder Text for Modal
this.setPlaceholder('Search with Khoj 🦅...');
this.setPlaceholder('Search with Khoj...');
}
async onOpen() {

View file

@ -258,7 +258,7 @@ def save_chat_session():
@schedule.repeat(schedule.every(59).minutes)
def upload_telemetry():
if not state.config.app.should_log_telemetry or not state.telemetry:
if not state.config or not state.config.app.should_log_telemetry or not state.telemetry:
message = "📡 No telemetry to upload" if not state.telemetry else "📡 Telemetry logging disabled"
logger.debug(message)
return

View file

@ -0,0 +1,693 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.0"
width="299.99649"
height="225.92412"
viewBox="0 0 85.704 64.542"
id="Layer_1"
xml:space="preserve"
sodipodi:docname="Speech_bubble(1).svg"
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview3800"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.0445985"
inkscape:cx="150.77563"
inkscape:cy="112.96206"
inkscape:window-width="1309"
inkscape:window-height="456"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><defs
id="defs8" />
<g
transform="matrix(0.9486962,0,0,0.9486962,2.4834364,1.8361818)"
id="g3">
<path
d="M 45.673,0 C 67.781,0 85.703,12.475 85.703,27.862 C 85.703,43.249 67.781,55.724 45.673,55.724 C 38.742,55.724 32.224,54.497 26.539,52.34 C 15.319,56.564 0,64.542 0,64.542 C 0,64.542 9.989,58.887 14.107,52.021 C 15.159,50.266 15.775,48.426 16.128,46.659 C 9.618,41.704 5.643,35.106 5.643,27.862 C 5.643,12.475 23.565,0 45.673,0 M 45.673,2.22 C 24.824,2.22 7.862,13.723 7.862,27.863 C 7.862,34.129 11.275,40.177 17.472,44.893 L 18.576,45.734 L 18.305,47.094 C 17.86,49.324 17.088,51.366 16.011,53.163 C 15.67,53.73 15.294,54.29 14.891,54.837 C 18.516,53.191 22.312,51.561 25.757,50.264 L 26.542,49.968 L 27.327,50.266 C 32.911,52.385 39.255,53.505 45.673,53.505 C 66.522,53.505 83.484,42.002 83.484,27.862 C 83.484,13.722 66.522,2.22 45.673,2.22 L 45.673,2.22 z "
id="path5" />
</g>
<image
width="75.991768"
height="49.994583"
preserveAspectRatio="none"
xlink:href="data:image/png;base64,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
RU5ErkJggg==
"
id="image3810"
x="9.712183"
y="3.7505856" /></svg>

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 942.19 656.92"><defs><style>.cls-1{fill:#1e1d1e;}</style></defs><title>logotype</title><path class="cls-1" d="M122.48,234.63c7.25,7.25,3.29,38.21,3.29,57.32,0,11.86,2,21.74,2,31,0,9.88-2,19.76-2,29,2,7.9,0,36.89,4,38.87,5.93,4,11.86-15.81,13.83-17.79,5.93-13.18,2-5.27,9.23-21.08,7.9-15.16,15.81-31,21.74-42.17,5.27-11.86,13.18-29,19.11-42.83,5.93-9.22,9.88-27,19.11-34.26,5.92-5.93,25-5.93,42.82-5.93,13.18,2,27,2,31,9.22,5.27,11.86-15.82,42.83-21.74,56-7.25,15.81-13.18,25.7-21.09,38.87-2,4-2,7.91-4,13.84-7.25,19.11-17.13,32.28-27,52-4,7.25-7.25,17.13-13.18,25-4,11.2-9.89,21.08-9.89,29,0,11.2,7.91,27,11.86,34.26,13.18,25.7,27,50.73,40.19,77.75,2,5.27,2,11.2,5.93,17.13,5.93,15.15,13.84,27,23.06,46.12,5.94,13.84,21.09,38.87,17.79,50.07-5.93,13.84-21.74,9.88-38.87,9.88-21.08,0-36.89,0-44.14-4-7.91-5.93-13.84-27-19.77-38.21-13.18-31-19.11-48.76-34.26-81a152.72,152.72,0,0,0-15.81-33c-7.91-9.88-7.91,21.09-9.89,34.92v38.22c0,25,5.93,67.86-5.27,79.06-11.86,9.89-67.86,7.91-75.77-5.93-2-7.24,0-30.31,0-50.07,2-19.11,0-31,0-48.1-2-42.17-2-81-2-123.21,0-15.81,4-31,2-42.83,0-7.9-2-13.17-2-21.08,0-5.93,2-9.88,2-15.81,0-13.18-2-27-2-40.19,0-13.84,2-27,2-38.88,0-17.13-5.27-38.21-2-52,2-5.93,7.91-11.2,9.89-13.18,2,0,2,0,4-2C71.74,222.77,114.57,224.74,122.48,234.63Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M547.58,232.65c3.95,2,3.95,9.23,5.93,17.13,2,15.16,0,46.12,0,63.91-2,29,0,59.3,0,81V523.87c0,34.92,0,83-2,123.87,0,17.13,2,30.31-5.93,36.24-9.22,5.93-63.91,5.93-71.16,0-7.9-7.91-3.95-33-3.95-50.08V610.84c0-36.89-2-71.81,0-114,0-9.88,2-21.08,0-25-5.93-5.93-21.08-2-32.94-2-7.91,0-25-5.93-32.95,2-7.25,9.88-4,36.9-4,54,0,42.17,2,63.91,2,108,0,23.07,2,42.17-9.23,50.08-11.86,7.91-58,5.93-65.89-2-11.86-11.2-9.88-56-9.88-83,0-83,3.95-144.29,2-218.08,0-15.16,2-31,2-42.17,0-7.91-2-15.81-3.95-27,0-5.93,2-13.84,2-19.77,2-21.08-2-48.1,2-56,7.91-11.2,67.87-13.17,77.09-1.31,7.91,11.2,2,34.26,2,48.09-2,15.16,0,34.93,0,56v27c0,9.23,0,23.06,2,27,3.95,5.93,23.71,5.93,32.94,5.93,7.9,0,29,2,32.94-4s4-38.87,4-50.07V264.94c0-11.2-2-23.06,2-30.31,3.95-7.91,13.18-5.93,21.08-7.91C501.46,226.72,539.67,222.77,547.58,232.65Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M716,218.81c31-2,61.93,7.91,81,17.13,5.93,4,19.11,17.8,23.06,23.72,9.88,13.18,15.81,23.06,19.11,40.2,2,3.95,3.95,5.93,3.95,7.9v7.91c2,9.22,5.93,19.11,7.91,25v21.08c0,5.93,2,11.2,2,17.13,4,34.92,2,75.77,0,114,0,25,0,50.07-3.95,73.13-4,23.72-9.88,48.76-15.15,61.94-2,3.95-7.91,13.83-11.86,17.78l-2,2c-5.93,9.23-17.13,17.13-29,25-5.93,3.29-7.91,5.27-13.18,7.25-5.93,2-11.86,2-21.74,4-5.27,0-11.2,3.95-17.13,3.95-13.18,2-31,2-40.19,2-11.86-2-25.7-5.93-36.9-11.86-2,0-4-3.29-4-3.29a107,107,0,0,1-17.14-7.91c-7.9-5.93-17.78-17.79-23.71-25-13.18-23.06-23.07-58-25-90.92-2-25-2-58-2-79.07-2-15.81,0-32.94,2-48.1,0-21.74,0-44.8,2-63.91,2-9.88,4-19.1,5.94-27,0-11.86,0-21.08,2-31,5.93-17.13,19.11-46.12,33-58,3.95-2,9.22-5.93,15.15-9.22,15.81-7.91,38.87-13.84,63.91-15.82ZM677.11,348c-5.93,48.76-4,100.81-2,150.88,2,38.88,4,98.18,21.74,112,0,2,5.27,4,9.22,5.93,15.82,5.28,29,2,40.86-5.93,3.95-4,9.22-9.88,11.19-13.83,5.93-9.23,9.89-36.24,11.86-54,0-15.15,0-34.26,2-56,0-42.17-2-85-5.93-121.24-2-25-3.95-48.09-11.86-59.95C748.93,297.88,741,292,733.12,290c-9.23-3.29-13.18,0-17.13,0h-2C689,293.93,681.07,319,677.11,348Z" transform="translate(-41.67 -218.04)"/><path class="cls-1" d="M975.89,226.44c9.23,9.23,7.91,234.41,7.91,263.4-2,63.25-2,108.05-2,175.26,0,11.86,2,17.79,2,25.69,0,15.16-3.95,38.22-2,61.28,0,23.06,2,51.24-4,64.42-4,17.13-19.77,36.89-34.92,46.12-15.15,9.88-40.85,13.84-71.16,11.86-19.76-2-42.82-4-50.73-15.82-2-5.27-2-21.08-2-32.28,0-15.81,0-27,5.93-31,11.86-5.93,36.9,13.18,56,7.91,7.9-2,15.81-9.88,19.76-23.72,1.32-11.2,0-27.52,0-40.7,0-11.86,1.32-21.08,1.32-32.94V634.79c0-29-3.29-56-3.29-81.7,0-36.24,2-253.51,2-286.46,0-23.06-7.91-38.21,9.22-46.12C923.84,216.56,966,216.56,975.89,226.44Z" transform="translate(-41.67 -218.04)"/></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="208" height="128" viewBox="0 0 208 128"><rect width="198" height="118" x="5" y="5" ry="10" stroke="#000" stroke-width="10" fill="none"/><path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z"/></svg>

After

Width:  |  Height:  |  Size: 283 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 334.371 380.563" version="1.1" viewBox="0 0 14 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
<polygon points="51.791 356.65 51.791 23.99 204.5 23.99 282.65 102.07 282.65 356.65" fill="#fff" stroke-width="212.65"/>
<path d="m201.19 31.99 73.46 73.393v243.26h-214.86v-316.66h141.4m6.623-16h-164.02v348.66h246.85v-265.9z" stroke-width="21.791"/>
</g>
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)">
<polygon points="282.65 356.65 51.791 356.65 51.791 23.99 204.5 23.99 206.31 25.8 206.31 100.33 280.9 100.33 282.65 102.07" fill="#fff" stroke-width="212.65"/>
<path d="m198.31 31.99v76.337h76.337v240.32h-214.86v-316.66h138.52m9.5-16h-164.02v348.66h246.85v-265.9l-6.43-6.424h-69.907v-69.842z" stroke-width="21.791"/>
</g>
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)" stroke-width="21.791">
<polygon points="258.31 87.75 219.64 87.75 219.64 48.667 258.31 86.38"/>
<path d="m227.64 67.646 12.41 12.104h-12.41v-12.104m-5.002-27.229h-10.998v55.333h54.666v-12.742z"/>
</g>
<g transform="matrix(.04589 0 0 .04589 -.66877 -.73379)" fill="#ed1c24" stroke-width="212.65">
<polygon points="311.89 284.49 22.544 284.49 22.544 167.68 37.291 152.94 37.291 171.49 297.15 171.49 297.15 152.94 311.89 167.68"/>
<path d="m303.65 168.63 1.747 1.747v107.62h-276.35v-107.62l1.747-1.747v9.362h272.85v-9.362m-12.999-31.385v27.747h-246.86v-27.747l-27.747 27.747v126h302.35v-126z"/>
</g>
<rect x="1.7219" y="7.9544" width="10.684" height="4.0307" fill="none"/>
<g transform="matrix(.04589 0 0 .04589 1.7219 11.733)" fill="#fff" stroke-width="21.791"><path d="m9.216 0v-83.2h30.464q6.784 0 12.928 1.408 6.144 1.28 10.752 4.608 4.608 3.2 7.296 8.576 2.816 5.248 2.816 13.056 0 7.68-2.816 13.184-2.688 5.504-7.296 9.088-4.608 3.456-10.624 5.248-6.016 1.664-12.544 1.664h-8.96v26.368zm22.016-43.776h7.936q6.528 0 9.6-3.072 3.2-3.072 3.2-8.704t-3.456-7.936-9.856-2.304h-7.424z"/><path d="m87.04 0v-83.2h24.576q9.472 0 17.28 2.304 7.936 2.304 13.568 7.296t8.704 12.8q3.2 7.808 3.2 18.816t-3.072 18.944-8.704 13.056q-5.504 5.12-13.184 7.552-7.552 2.432-16.512 2.432zm22.016-17.664h1.28q4.48 0 8.448-1.024 3.968-1.152 6.784-3.84 2.944-2.688 4.608-7.424t1.664-12.032-1.664-11.904-4.608-7.168q-2.816-2.56-6.784-3.456-3.968-1.024-8.448-1.024h-1.28z"/><path d="m169.22 0v-83.2h54.272v18.432h-32.256v15.872h27.648v18.432h-27.648v30.464z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1,73 @@
/* Amber Light scheme (Default) */
/* Can be forced with data-theme="light" */
[data-theme="light"],
:root:not([data-theme="dark"]) {
--primary: #ffb300;
--primary-hover: #ffa000;
--primary-focus: rgba(255, 179, 0, 0.125);
--primary-inverse: rgba(0, 0, 0, 0.75);
}
/* Amber Dark scheme (Auto) */
/* Automatically enabled if user has Dark mode enabled */
@media only screen and (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--primary: #ffb300;
--primary-hover: #ffc107;
--primary-focus: rgba(255, 179, 0, 0.25);
--primary-inverse: rgba(0, 0, 0, 0.75);
}
}
/* Amber Dark scheme (Forced) */
/* Enabled if forced with data-theme="dark" */
[data-theme="dark"] {
--primary: #ffb300;
--primary-hover: #ffc107;
--primary-focus: rgba(255, 179, 0, 0.25);
--primary-inverse: rgba(0, 0, 0, 0.75);
}
/* Amber (Common styles) */
:root {
--form-element-active-border-color: var(--primary);
--form-element-focus-color: var(--primary-focus);
--switch-color: var(--primary-inverse);
--switch-checked-background-color: var(--primary);
}
.khoj-configure {
display: grid;
grid-template-columns: 1fr;
padding: 0 24px;
}
.khoj-header {
display: grid;
grid-auto-flow: row;
gap: 20px;
padding: 16px 0;
margin: 0 0 16px 0;
}
.khoj-nav {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 32px;
justify-self: center;
}
.khoj-nav a {
color: #333;
text-decoration: none;
font-size: 20px;
font-weight: normal;
padding: 8px;
border-radius: 4px;
justify-self: center;
}
.khoj-nav a:hover {
background-color: var(--primary-hover);
}
.khoj-nav-selected {
background-color: var(--primary);
}
img.khoj-logo {
width: min(60vw, 150px);
justify-self: center;
}

File diff suppressed because one or more lines are too long

View file

@ -1,42 +1,120 @@
<!DOCTYPE html>
<html data-theme="light">
<head>
<meta charset="utf-8">
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
<title>Khoj - Settings</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
<link rel="stylesheet" href="/static/assets/pico.min.css">
<link rel="stylesheet" href="/static/assets/khoj.css">
</head>
<body class="khoj-configure">
<header>
<div>
<h1>Khoj Settings</h1>
<p>Check out our <a href="https://github.com/debanjum/khoj">source code on Github</a></p>
</div>
<div>
<h2>Ready?</h2>
<div id="actions">
<button onclick="window.location.href='/';" >
Search
</button>
<button onclick="window.location.href='/chat';">
Chat
</button>
</div>
</div>
</header>
<div class="khoj-header">
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
<nav class="khoj-nav">
<a href="/chat">Chat</a>
<a href="/">Search</a>
<a class="khoj-nav-selected" href="/config">Settings</a>
</nav>
</div>
<div class=”content”>
{% block content %}
{% endblock %}
</div>
</body>
<style>
header {
html, body {
width: 100%;
margin: 0;
padding: 0;
}
.page {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 12px;
grid-auto-flow: row;
gap: 32px;
}
.section {
display: grid;
justify-self: center;
}
.section-title {
margin: 0;
padding: 0 0 16px 0;
font-size: 32;
font-weight: normal;
}
.section-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
justify-items: start;
}
.card {
display: grid;
grid-template-rows: repeat(3, 1fr);
gap: 8px;
padding: 24px 16px;
width: 320px;
height: 160px;
background: white;
border: 1px solid rgb(229, 229, 229);
border-radius: 4px;
box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.1),0px 1px 2px -1px rgba(0,0,0,0.1);
overflow: hidden;
}
.card-title-row {
display: grid;
grid-template-columns: auto 1fr;
padding: 0;
gap: 12px;
}
.card-icon {
width: 40px;
height: 40px;
}
.card-title {
font-size: 20px;
font-weight: normal;
margin: 0;
padding: 0;
align-self: center;
}
.card-title-text {
vertical-align: middle;
}
.card-description {
margin: 0;
color: grey;
font-size: 16px;
}
.card-button-row {
display: grid;
grid-template-columns: auto;
text-align: right;
}
.card-button {
border: none;
font-weight: bold;
color: rgb(64,64,64);
background: transparent;
font-size: 16px;
cursor: pointer;
margin: 0;
padding: 0;
height: 32px;
text-align: right;
}
.primary-button {
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
@media screen and (max-width: 600px) {
header {
.section-cards {
grid-template-columns: 1fr;
}
}

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Khoj: Data Settings</title>
<link rel=”stylesheet” href=”static/styles.css”>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body class="data-integration">
<header class=”header”>
<h1>Configure your data integrations for Khoj</h1>
</header>
<a href="/config">Go back</a>
<div class=”content”>
{% block content %}
{% endblock %}
</div>
<footer class=”footer”>
</footer>
</body>
<style>
body.data-integration {
padding: 0 10%
}
</style>
</html>

View file

@ -2,10 +2,11 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
<title>Khoj</title>
<title>Khoj - Chat</title>
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj_chat.webmanifest">
<link rel="stylesheet" href="/static/assets/khoj.css">
</head>
<script>
function formatDate(date) {
@ -25,7 +26,7 @@
function renderMessage(message, by, dt=null) {
let message_time = formatDate(dt ?? new Date());
let by_name = by == "khoj" ? "🦅 Khoj" : "🤔 You";
let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You";
// Generate HTML for Chat Message and Append to Chat Body
document.getElementById("chat-body").innerHTML += `
<div data-meta="${by_name} at ${message_time}" class="chat-message ${by}">
@ -83,12 +84,21 @@
window.onload = function () {
fetch('/api/chat?client=web')
.then(response => response.json())
.then(data => data.response)
.then(chat_logs => {
.then(data => {
if (data.detail) {
// If the server returns a 500 error with detail, render it as a message.
renderMessage(data.detail + " You can configure Khoj chat in your <a class='inline-chat-link' href='/config'>settings</a>.", "khoj");
}
return data.response;
})
.then(response => {
// Render conversation history, if any
chat_logs.forEach(chat_log => {
response.forEach(chat_log => {
renderMessageWithReference(chat_log.message, chat_log.by, chat_log.context, new Date(chat_log.created));
});
})
.catch(err => {
return;
});
// Set welcome message on load
@ -103,8 +113,15 @@
}
</script>
<body>
<!-- Chat Header -->
<h1>Khoj</h1>
<!--Add Header Logo and Nav Pane-->
<div class="khoj-header">
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
<nav class="khoj-nav">
<a class="khoj-nav-selected" href="/chat">Chat</a>
<a href="/">Search</a>
<a href="/config">Settings</a>
</nav>
</div>
<!-- Chat Body -->
<div id="chat-body"></div>
@ -136,11 +153,6 @@
padding: 10px;
margin: 10px;
}
h1 {
font-weight: 200;
color: #017eff;
}
#chat-body {
font-size: medium;
margin: 0px;
@ -153,7 +165,7 @@
display: block;
font-size: x-small;
color: #475569;
margin: -12px 7px 0 -5px;
margin: -8px 4px 0 -5px;
}
/* move message by khoj to left */
.chat-message.khoj {
@ -177,8 +189,8 @@
}
/* color chat bubble by khoj blue */
.chat-message-text.khoj {
color: #f8fafc;
background: #017eff;
color: var(--primary-inverse);
background: var(--primary);
margin-left: auto;
white-space: pre-line;
}
@ -189,7 +201,7 @@
bottom: -2px;
left: -7px;
border: 10px solid transparent;
border-top-color: #017eff;
border-top-color: var(--primary);
border-bottom: 0;
transform: rotate(-60deg);
}
@ -232,6 +244,12 @@
font-size: medium;
}
a.inline-chat-link {
color: #475569;
text-decoration: none;
border-bottom: 1px dotted #475569;
}
@media (pointer: coarse), (hover: none) {
abbr[title] {
position: relative;

View file

@ -1,70 +1,125 @@
{% extends "base_config.html" %}
{% block content %}
<h2>Content Types</h2>
<div id="content-configuration">
<button onclick="window.location.href='/config/content_type/pdf';">
PDF
</button>
<button onclick="window.location.href='/config/content_type/markdown';">
Markdown
</button>
<button onclick="window.location.href='/config/content_type/org';">
Org
</button>
<button onclick="window.location.href='/config/content_type/ledger';">
Ledger
</button>
<button onclick="window.location.href='/config/content_type/github';">
GitHub
</button>
<div class="page">
<div class="section">
<h2 class="section-title">Plugins</h2>
<div class="section-cards">
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/github.svg" alt="Github">
<h3 class="card-title">Github</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set repositories for Khoj to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/github">
Setup
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
</a>
</div>
</div>
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/markdown.svg" alt="markdown">
<h3 class="card-title">Markdown</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set markdown files for Khoj to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/markdown">
Setup
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
</a>
</div>
</div>
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/org.svg" alt="org">
<h3 class="card-title">Org</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set org files for Khoj to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/org">
Setup
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
</a>
</div>
</div>
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/pdf.svg" alt="PDF">
<h3 class="card-title">PDF</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set PDF files for Khoj to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content_type/pdf">
Setup
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
</a>
</div>
</div>
</div>
</div>
<div class="section">
<h2 class="section-title">Features</h2>
<div class="section-cards">
<div class="card">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/chat.svg" alt="Chat">
<h3 class="card-title">Chat</h3>
</div>
<div class="card-description-row">
<p class="card-description">Setup Khoj Chat</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/processor/conversation">
Setup
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"></path></svg>
</a>
</div>
</div>
</div>
</div>
<div class="section">
<div id="status" style="display: none;"></div>
<button id="configure" type="submit">⚙️ Configure</button>
</div>
</div>
<h2>Processors</h2>
<button onclick="window.location.href='/config/processor/conversation/';">
Conversation
</button>
<h1>Finalize</h1>
<button id="regenerate" type="submit">Regenerate</button>
<style>
body.khoj-configure {
padding: 0 10%
}
div#content-configuration, div#actions {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 12px;
}
button#regenerate {
background-color: #4CAF50;
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
</style>
<script>
var regenerate = document.getElementById("regenerate");
regenerate.addEventListener("click", function(event) {
var configure = document.getElementById("configure");
configure.addEventListener("click", function(event) {
event.preventDefault();
regenerate.disabled = true;
regenerate.innerHTML = "Regenerating...";
configure.disabled = true;
configure.innerHTML = "Configuring...";
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
fetch('/api/update?force=true&client=web', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
alert("Regenerated!");
regenerate.disabled = false;
regenerate.innerHTML = "Regenerate";
document.getElementById("status").innerHTML = "Configured successfully!";
document.getElementById("status").style.display = "block";
configure.disabled = false;
configure.innerHTML = "⚙️ Configured";
})
.catch((error) => {
console.error('Error:', error);
alert("Regeneration was not successful. Check debug logs.");
regenerate.disabled = false;
regenerate.innerHTML = "Regenerate";
document.getElementById("status").innerHTML = "Unable to save configuration. Raise issue on Khoj Discord or Github.";
document.getElementById("status").style.display = "block";
configure.disabled = false;
configure.innerHTML = "⚙️ Configure";
});
});
</script>

View file

@ -1,85 +1,158 @@
{% extends "base_data_integration.html" %}
{% extends "base_config.html" %}
{% block content %}
<h2>Github</h2>
<form id="config-form">
<div id="success" style="display: none;"></div>
<table>
<tr>
<td>
<label for="pat-token">Personal Access Token</label>
</td>
<td>
<input type="text" id="pat-token" name="pat" value="{{ current_config['pat_token'] }}">
</td>
</tr>
<tr>
<td>
<label for="repo-owner">Repository Owner</label>
</td>
<td>
<input type="text" id="repo-owner" name="repo_owner" value="{{ current_config['repo_owner'] }}">
</td>
</tr>
<tr>
<td>
<label for="repo-name">Repository Name</label>
</td>
<td>
<input type="text" id="repo-name" name="repo_name" value="{{ current_config['repo_name'] }}">
</td>
</tr>
<tr>
<td>
<label for="repo-branch">Repository Branch</label>
</td>
<td>
<input type="text" id="repo-branch" name="repo_branch" value="{{ current_config['repo_branch'] }}">
</td>
</tr>
</table>
<h4>You probably don't need to edit these.</h4>
<div class="page">
<div class="section">
<h2 class="section-title">
<img class="card-icon" src="/static/assets/icons/github.svg" alt="Github">
<span class="card-title-text">Github</span>
</h2>
<form>
<table>
<tr>
<td>
<label for="pat-token">Personal Access Token</label>
</td>
<td>
<input type="text" id="pat-token" name="pat" value="{{ current_config['pat_token'] }}">
</td>
</tr>
</table>
<h4>Repositories</h4>
<div id="repositories" class="section-cards">
{% for repo in current_config['repos'] %}
<div class="card repo" id="repo-card-{{loop.index}}">
<label for="repo-owner">Repository Owner</label>
<input type="text" id="repo-owner-{{loop.index}}" name="repo_owner" value="{{ repo.owner }}">
<label for="repo-name">Repository Name</label>
<input type="text" id="repo-name-{{loop.index}}" name="repo_name" value="{{ repo.name}}">
<label for="repo-branch">Repository Branch</label>
<input type="text" id="repo-branch-{{loop.index}}" name="repo_branch" value="{{ repo.branch }}">
<button type="button"
class="remove-repo-button"
onclick="remove_repo({{loop.index}})"
id="remove-repo-button-{{loop.index}}">Remove Repository</button>
</div>
{% endfor %}
</div>
<button type="button" id="add-repository-button">Add Repository</button>
<h4>You probably don't need to edit these.</h4>
<table>
<tr>
<td>
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
</td>
<td>
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
</td>
</tr>
<tr>
<td>
<label for="embeddings-file">Embeddings File (Output)</label>
</td>
<td>
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
</td>
</tr>
</table>
<button id="submit" type="submit">Submit</button>
</form>
<table>
<tr>
<td>
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
</td>
<td>
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
</td>
</tr>
<tr>
<td>
<label for="embeddings-file">Embeddings File (Output)</label>
</td>
<td>
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
</td>
</tr>
</table>
<div class="section">
<div id="success" style="display: none;"></div>
<button id="submit" type="submit">Save</button>
</div>
</form>
</div>
</div>
<style>
div.repo {
width: 100%;
height: 100%;
grid-template-rows: none;
}
div#repositories {
margin-bottom: 12px;
}
button.remove-repo-button {
background-color: gainsboro;
}
</style>
<script>
const add_repo_button = document.getElementById("add-repository-button");
add_repo_button.addEventListener("click", function(event) {
event.preventDefault();
var repo = document.createElement("div");
repo.classList.add("card");
repo.classList.add("repo");
const id = Date.now();
repo.id = "repo-card-" + id;
repo.innerHTML = `
<label for="repo-owner">Repository Owner</label>
<input type="text" id="repo-owner" name="repo_owner">
<label for="repo-name">Repository Name</label>
<input type="text" id="repo-name" name="repo_name">
<label for="repo-branch">Repository Branch</label>
<input type="text" id="repo-branch" name="repo_branch">
<button type="button"
class="remove-repo-button"
onclick="remove_repo(${id})"
id="remove-repo-button-${id}">Remove Repository</button>
`;
document.getElementById("repositories").appendChild(repo);
})
function remove_repo(index) {
document.getElementById("repo-card-" + index).remove();
}
submit.addEventListener("click", function(event) {
event.preventDefault();
var compressed_jsonl = document.getElementById("compressed-jsonl").value;
var embeddings_file = document.getElementById("embeddings-file").value;
var pat_token = document.getElementById("pat-token").value;
var repo_owner = document.getElementById("repo-owner").value;
var repo_name = document.getElementById("repo-name").value;
var repo_branch = document.getElementById("repo-branch").value;
const compressed_jsonl = document.getElementById("compressed-jsonl").value;
const embeddings_file = document.getElementById("embeddings-file").value;
const pat_token = document.getElementById("pat-token").value;
if (pat_token == "") {
document.getElementById("success").innerHTML = "❌ Please enter a Personal Access Token.";
document.getElementById("success").style.display = "block";
return;
}
var cards = document.getElementById("repositories").getElementsByClassName("repo");
var repos = [];
for (var i = 0; i < cards.length; i++) {
var card = cards[i];
var owner = card.getElementsByTagName("input")[0].value;
var name = card.getElementsByTagName("input")[1].value;
var branch = card.getElementsByTagName("input")[2].value;
if (owner == "" || name == "" || branch == "") {
continue;
}
repos.push({
"owner": owner,
"name": name,
"branch": branch,
});
}
if (repos.length == 0) {
document.getElementById("success").innerHTML = "❌ Please add at least one repository.";
document.getElementById("success").style.display = "block";
return;
}
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
fetch('/api/config/data/content_type/github', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken,
},
body: JSON.stringify({
"pat_token": pat_token,
"repo_owner": repo_owner,
"repo_name": repo_name,
"repo_branch": repo_branch,
"repos": repos,
"compressed_jsonl": compressed_jsonl,
"embeddings_file": embeddings_file,
})
@ -87,7 +160,7 @@
.then(response => response.json())
.then(data => {
if (data["status"] == "ok") {
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to <a href='/config'>your settings</a> to regenerate your index.";
document.getElementById("success").innerHTML = "✅ Successfully updated. Click Configure on your <a href='/config'>settings page</a> to complete your Khoj setup.";
document.getElementById("success").style.display = "block";
} else {
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";

View file

@ -1,75 +1,84 @@
{% extends "base_data_integration.html" %}
{% extends "base_config.html" %}
{% block content %}
<h2>{{ content_type }}</h2>
<form id="config-form">
<div id="success" style="display: none;" ></div>
<table>
<tr>
<td>
<label for="input-files">Input Files</label>
</td>
<td id="input-files-cell">
{% if current_config['input_files'] is none %}
<input type="text" id="input-files" name="input-files">
{% else %}
{% for input_file in current_config['input_files'] %}
<input type="text" id="input-files" name="input-files" value="{{ input_file }}">
{% endfor %}
{% endif %}
</td>
<td>
<button type="button" id="input-files-button">Add</button>
</td>
</tr>
<tr>
<td>
<label for="input-filter">Input Filter</label>
</td>
<td id="input-filter-cell">
{% if current_config['input_filter'] is none %}
<input type="text" id="input-filter" name="input-filter">
{% else %}
{% for input_filter in current_config['input_filter'] %}
<input type="text" id="input-filter" name="input-filter" value="{{ input_filter }}">
{% endfor %}
{% endif %}
</td>
<td>
<button type="button" id="input-filter-button">Add</button>
</td>
</tr>
</table>
<div class="page">
<div class="section">
<h2 class="section-title">
<img class="card-icon" src="/static/assets/icons/{{ content_type }}.svg" alt="{{ content_type|capitalize }}">
<span class="card-title-text">{{ content_type|capitalize }}</span>
</h2>
<form id="config-form">
<table>
<tr>
<td>
<label for="input-files">Input Files</label>
</td>
<td id="input-files-cell">
{% if current_config['input_files'] is none %}
<input type="text" id="input-files" name="input-files">
{% else %}
{% for input_file in current_config['input_files'] %}
<input type="text" id="input-files" name="input-files" value="{{ input_file }}">
{% endfor %}
{% endif %}
</td>
<td>
<button type="button" id="input-files-button">Add</button>
</td>
</tr>
<tr>
<td>
<label for="input-filter">Input Filter</label>
</td>
<td id="input-filter-cell">
{% if current_config['input_filter'] is none %}
<input type="text" id="input-filter" name="input-filter">
{% else %}
{% for input_filter in current_config['input_filter'] %}
<input type="text" id="input-filter" name="input-filter" value="{{ input_filter }}">
{% endfor %}
{% endif %}
</td>
<td>
<button type="button" id="input-filter-button">Add</button>
</td>
</tr>
</table>
<h4>You probably don't need to edit these.</h4>
<h4>You probably don't need to edit these.</h4>
<table>
<tr>
<td>
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
</td>
<td>
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
</td>
</tr>
<tr>
<td>
<label for="embeddings-file">Embeddings File (Output)</label>
</td>
<td>
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
</td>
</tr>
<tr>
<td>
<label for="index-heading-entries">Index Heading Entries</label>
</td>
<td>
<input type="text" id="index-heading-entries" name="index-heading-entries" value="{{ current_config['index_heading_entries'] }}">
</td>
</tr>
</table>
<button id="submit" type="submit">Submit</button>
</form>
<table>
<tr>
<td>
<label for="compressed-jsonl">Compressed JSONL (Output)</label>
</td>
<td>
<input type="text" id="compressed-jsonl" name="compressed-jsonl" value="{{ current_config['compressed_jsonl'] }}">
</td>
</tr>
<tr>
<td>
<label for="embeddings-file">Embeddings File (Output)</label>
</td>
<td>
<input type="text" id="embeddings-file" name="embeddings-file" value="{{ current_config['embeddings_file'] }}">
</td>
</tr>
<tr>
<td>
<label for="index-heading-entries">Index Heading Entries</label>
</td>
<td>
<input type="text" id="index-heading-entries" name="index-heading-entries" value="{{ current_config['index_heading_entries'] }}">
</td>
</tr>
</table>
<div class="section">
<div id="success" style="display: none;" ></div>
<button id="submit" type="submit">Save</button>
</div>
</form>
</div>
</div>
<script>
function addButtonEventListener(fieldName) {
var button = document.getElementById(fieldName + "-button");
@ -122,10 +131,12 @@
var embeddings_file = document.getElementById("embeddings-file").value;
var index_heading_entries = document.getElementById("index-heading-entries").value;
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
fetch('/api/config/data/content_type/{{ content_type }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
"input_files": input_files,
@ -138,7 +149,7 @@
.then(response => response.json())
.then(data => {
if (data["status"] == "ok") {
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to <a href='/config'>your settings</a> to regenerate your index.";
document.getElementById("success").innerHTML = "✅ Successfully updated. Click Configure on your <a href='/config'>settings page</a> to complete your Khoj setup.";
document.getElementById("success").style.display = "block";
} else {
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";

View file

@ -2,13 +2,14 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0">
<title>Khoj</title>
<title>Khoj - Search</title>
<link rel="icon" type="image/png" sizes="128x128" href="/static/assets/icons/favicon-128x128.png">
<link rel="manifest" href="/static/khoj.webmanifest">
<link rel="stylesheet" href="/static/assets/khoj.css">
</head>
<script type="text/javascript" src="static/assets/org.min.js"></script>
<script type="text/javascript" src="static/assets/markdown-it.min.js"></script>
<script type="text/javascript" src="/static/assets/org.min.js"></script>
<script type="text/javascript" src="/static/assets/markdown-it.min.js"></script>
<script>
function render_image(item) {
@ -56,6 +57,27 @@
}).join("\n") + `</div>`;
}
function render_mutliple(query, data, type) {
let org_files = data.filter((item) => item.additional.file.endsWith(".org"));
let md_files = data.filter((item) => item.additional.file.endsWith(".md"));
let pdf_files = data.filter((item) => item.additional.file.endsWith(".pdf"));
let html = "";
if (org_files.length > 0) {
html += render_org(query, org_files, type);
}
if (md_files.length > 0) {
html += render_markdown(query, md_files);
}
if (pdf_files.length > 0) {
html += render_pdf(query, pdf_files);
}
return html;
}
function render_json(data, query, type) {
if (type === "markdown") {
return render_markdown(query, data);
@ -70,7 +92,7 @@
} else if (type === "pdf") {
return render_pdf(query, data);
} else if (type == "github") {
return render_markdown(query, data);
return render_mutliple(query, data, type);
} else {
return `<div id="results-plugin">`
+ data.map((item) => `<p>${item.entry}</p>`).join("\n")
@ -197,7 +219,15 @@
</script>
<body>
<h1>Khoj</h1>
<!--Add Header Logo and Nav Pane-->
<div class="khoj-header">
<img class="khoj-logo" src="/static/assets/icons/khoj-logo-sideways.svg" alt="Khoj"></img>
<nav class="khoj-nav">
<a href="/chat">Chat</a>
<a class="khoj-nav-selected" href="/">Search</a>
<a href="/config">Settings</a>
</nav>
</div>
<!--Add Text Box To Enter Query, Trigger Incremental Search OnChange -->
<input type="text" id="query" class="option" onkeyup=incrementalSearch(event) autofocus="autofocus" placeholder="What is the meaning of life?">
@ -211,7 +241,7 @@
<!--Add Results Count Input To Set Results Count -->
<input type="number" id="results-count" min="1" max="100" value="6" placeholder="results count" onchange="setCountFieldInUrl(this)">
</div>
</div>
<!-- Section to Render Results -->
<div id="results"></div>
@ -222,7 +252,7 @@
body {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr 1fr minmax(80px, 100%);
grid-template-rows: 1fr auto auto minmax(80px, 100%);
}
body > * {
grid-column: 1;
@ -232,14 +262,13 @@
body {
display: grid;
grid-template-columns: 1fr min(70vw, 100%) 1fr;
grid-template-rows: 1fr 1fr 1fr minmax(80px, 100%);
grid-template-rows: 1fr auto auto minmax(80px, 100%);
padding-top: 60vw;
}
body > * {
grid-column: 2;
}
}
body {
padding: 0px;
margin: 0px;
@ -255,11 +284,6 @@
padding: 10px;
margin: 10px;
}
h1 {
font-weight: 200;
color: #017eff;
}
#options {
padding: 0;
display: grid;
@ -310,7 +334,7 @@
text-align: left;
white-space: pre-line;
}
#results-markdown, #results-github {
#results-markdown, #results-github {
text-align: left;
}
#results-music,
@ -350,6 +374,11 @@
background-color: #ef4444;
font-size: small;
}
pre {
max-width: 100;
}
</style>
</html>

View file

@ -1,49 +1,58 @@
{% extends "base_processor_integration.html" %}
{% extends "base_config.html" %}
{% block content %}
<h2>Conversation</h2>
<form id="config-form">
<div id="success" style="display: none;" ></div>
<table>
<tr>
<td>
<label for="openai-api-key">OpenAI API key</label>
</td>
<td>
<input type="text" id="openai-api-key" name="openai-api-key" value="{{ current_config['openai_api_key'] }}">
</td>
</tr>
</table>
<div class="page">
<div class="section">
<h2 class="section-title">
<img class="card-icon" src="/static/assets/icons/chat.svg" alt="Chat">
<span class="card-title-text">Chat</span>
</h2>
<form id="config-form">
<table>
<tr>
<td>
<label for="openai-api-key">OpenAI API key</label>
</td>
<td>
<input type="text" id="openai-api-key" name="openai-api-key" value="{{ current_config['openai_api_key'] }}">
</td>
</tr>
</table>
<h4>You probably don't need to edit these.</h4>
<h4>You probably don't need to edit these.</h4>
<table>
<tr>
<td>
<label for="conversation-logfile">Conversation Logfile</label>
</td>
<td>
<input type="text" id="conversation-logfile" name="conversation-logfile" value="{{ current_config['conversation_logfile'] }}">
</td>
</tr>
<tr>
<td>
<label for="model">Model</label>
</td>
<td>
<input type="text" id="model" name="model" value="{{ current_config['model'] }}">
</td>
</tr>
<tr>
<td>
<label for="chat-model">Chat Model</label>
</td>
<td>
<input type="text" id="chat-model" name="chat-model" value="{{ current_config['chat_model'] }}">
</td>
</tr>
</table>
<button id="submit" type="submit">Submit</button>
</form>
<table>
<tr>
<td>
<label for="conversation-logfile">Conversation Logfile</label>
</td>
<td>
<input type="text" id="conversation-logfile" name="conversation-logfile" value="{{ current_config['conversation_logfile'] }}">
</td>
</tr>
<tr>
<td>
<label for="model">Model</label>
</td>
<td>
<input type="text" id="model" name="model" value="{{ current_config['model'] }}">
</td>
</tr>
<tr>
<td>
<label for="chat-model">Chat Model</label>
</td>
<td>
<input type="text" id="chat-model" name="chat-model" value="{{ current_config['chat_model'] }}">
</td>
</tr>
</table>
<div class="section">
<div id="success" style="display: none;" ></div>
<button id="submit" type="submit">Save</button>
</div>
</form>
</div>
</div>
<script>
submit.addEventListener("click", function(event) {
event.preventDefault();
@ -52,10 +61,12 @@
var model = document.getElementById("model").value;
var chat_model = document.getElementById("chat-model").value;
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
fetch('/api/config/data/processor/conversation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
"openai_api_key": openai_api_key,
@ -67,7 +78,7 @@
.then(response => response.json())
.then(data => {
if (data["status"] == "ok") {
document.getElementById("success").innerHTML = "✅ Successfully updated. Go to <a href='/config'>your settings</a> to regenerate your index.";
document.getElementById("success").innerHTML = "✅ Successfully updated. Click Configure on your <a href='/config'>settings page</a> to complete your Khoj setup.";
document.getElementById("success").style.display = "block";
} else {
document.getElementById("success").innerHTML = "⚠️ Failed to update settings.";

View file

@ -8,8 +8,9 @@ import requests
# Internal Packages
from khoj.utils.helpers import timer
from khoj.utils.rawconfig import Entry, GithubContentConfig
from khoj.utils.rawconfig import Entry, GithubContentConfig, GithubRepoConfig
from khoj.processor.markdown.markdown_to_jsonl import MarkdownToJsonl
from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl
from khoj.processor.text_to_jsonl import TextToJsonl
from khoj.utils.jsonl import dump_jsonl, compress_jsonl_data
@ -21,7 +22,6 @@ class GithubToJsonl(TextToJsonl):
def __init__(self, config: GithubContentConfig):
super().__init__(config)
self.config = config
self.repo_url = f"https://api.github.com/repos/{self.config.repo_owner}/{self.config.repo_name}"
@staticmethod
def wait_for_rate_limit_reset(response, func, *args, **kwargs):
@ -34,26 +34,43 @@ class GithubToJsonl(TextToJsonl):
return
def process(self, previous_entries=None):
current_entries = []
for repo in self.config.repos:
current_entries += self.process_repo(repo, previous_entries)
return self.update_entries_with_ids(current_entries, previous_entries)
def process_repo(self, repo: GithubRepoConfig, previous_entries=None):
repo_url = f"https://api.github.com/repos/{repo.owner}/{repo.name}"
repo_shorthand = f"{repo.owner}/{repo.name}"
logger.info(f"Processing github repo {repo_shorthand}")
with timer("Download markdown files from github repo", logger):
try:
docs = self.get_markdown_files()
markdown_files, org_files = self.get_files(repo_url, repo)
except Exception as e:
logger.error(f"Unable to download github repo {self.config.repo_owner}/{self.config.repo_name}")
logger.error(f"Unable to download github repo {repo_shorthand}")
raise e
logger.info(f"Found {len(docs)} documents in github repo {self.config.repo_owner}/{self.config.repo_name}")
logger.info(f"Found {len(markdown_files)} markdown files in github repo {repo_shorthand}")
logger.info(f"Found {len(org_files)} org files in github repo {repo_shorthand}")
with timer("Extract markdown entries from github repo", logger):
with timer(f"Extract markdown entries from github repo {repo_shorthand}", logger):
current_entries = MarkdownToJsonl.convert_markdown_entries_to_maps(
*GithubToJsonl.extract_markdown_entries(docs)
*GithubToJsonl.extract_markdown_entries(markdown_files)
)
with timer("Extract commit messages from github repo", logger):
current_entries += self.convert_commits_to_entries(self.get_commits())
with timer(f"Extract org entries from github repo {repo_shorthand}", logger):
current_entries += OrgToJsonl.convert_org_nodes_to_entries(*GithubToJsonl.extract_org_entries(org_files))
with timer("Split entries by max token size supported by model", logger):
with timer(f"Extract commit messages from github repo {repo_shorthand}", logger):
current_entries += self.convert_commits_to_entries(self.get_commits(repo_url), repo)
with timer(f"Split entries by max token size supported by model {repo_shorthand}", logger):
current_entries = TextToJsonl.split_entries_by_max_tokens(current_entries, max_tokens=256)
return current_entries
def update_entries_with_ids(self, current_entries, previous_entries):
# Identify, mark and merge any new entries with previous entries
with timer("Identify new or updated entries", logger):
if not previous_entries:
@ -76,31 +93,40 @@ class GithubToJsonl(TextToJsonl):
return entries_with_ids
def get_markdown_files(self):
def get_files(self, repo_url: str, repo: GithubRepoConfig):
# Get the contents of the repository
repo_content_url = f"{self.repo_url}/git/trees/{self.config.repo_branch}"
repo_content_url = f"{repo_url}/git/trees/{repo.branch}"
headers = {"Authorization": f"token {self.config.pat_token}"}
params = {"recursive": "true"}
response = requests.get(repo_content_url, headers=headers, params=params)
contents = response.json()
# Wait for rate limit reset if needed
result = self.wait_for_rate_limit_reset(response, self.get_markdown_files)
result = self.wait_for_rate_limit_reset(response, self.get_files)
if result is not None:
return result
# Extract markdown files from the repository
markdown_files = []
org_files = []
for item in contents["tree"]:
# Find all markdown files in the repository
if item["type"] == "blob" and item["path"].endswith(".md"):
# Create URL for each markdown file on Github
url_path = f'https://github.com/{self.config.repo_owner}/{self.config.repo_name}/blob/{self.config.repo_branch}/{item["path"]}'
url_path = f'https://github.com/{repo.owner}/{repo.name}/blob/{repo.branch}/{item["path"]}'
# Add markdown file contents and URL to list
markdown_files += [{"content": self.get_file_contents(item["url"]), "path": url_path}]
return markdown_files
# Find all org files in the repository
elif item["type"] == "blob" and item["path"].endswith(".org"):
# Create URL for each org file on Github
url_path = f'https://github.com/{repo.owner}/{repo.name}/blob/{repo.branch}/{item["path"]}'
# Add org file contents and URL to list
org_files += [{"content": self.get_file_contents(item["url"]), "path": url_path}]
return markdown_files, org_files
def get_file_contents(self, file_url):
# Get text from each markdown file
@ -114,9 +140,9 @@ class GithubToJsonl(TextToJsonl):
return response.content.decode("utf-8")
def get_commits(self) -> List[Dict]:
def get_commits(self, repo_url: str) -> List[Dict]:
# Get commit messages from the repository using the Github API
commits_url = f"{self.repo_url}/commits"
commits_url = f"{repo_url}/commits"
headers = {"Authorization": f"token {self.config.pat_token}"}
params = {"per_page": 100}
commits = []
@ -140,10 +166,10 @@ class GithubToJsonl(TextToJsonl):
return commits
def convert_commits_to_entries(self, commits) -> List[Entry]:
def convert_commits_to_entries(self, commits, repo: GithubRepoConfig) -> List[Entry]:
entries: List[Entry] = []
for commit in commits:
compiled = f'Commit message from {self.config.repo_owner}/{self.config.repo_name}:\n{commit["content"]}'
compiled = f'Commit message from {repo.owner}/{repo.name}:\n{commit["content"]}'
entries.append(
Entry(
compiled=compiled,
@ -164,3 +190,14 @@ class GithubToJsonl(TextToJsonl):
doc["content"], doc["path"], entries, entry_to_file_map
)
return entries, dict(entry_to_file_map)
@staticmethod
def extract_org_entries(org_files):
entries = []
entry_to_file_map = []
for doc in org_files:
entries, entry_to_file_map = OrgToJsonl.process_single_org_file(
doc["content"], doc["path"], entries, entry_to_file_map
)
return entries, dict(entry_to_file_map)

View file

@ -10,13 +10,17 @@ from khoj.processor.text_to_jsonl import TextToJsonl
from khoj.utils.helpers import get_absolute_path, is_none_or_empty, timer
from khoj.utils.constants import empty_escape_sequences
from khoj.utils.jsonl import dump_jsonl, compress_jsonl_data
from khoj.utils.rawconfig import Entry
from khoj.utils.rawconfig import Entry, TextContentConfig
logger = logging.getLogger(__name__)
class MarkdownToJsonl(TextToJsonl):
def __init__(self, config: TextContentConfig):
super().__init__(config)
self.config = config
# Define Functions
def process(self, previous_entries=None):
# Extract required fields from config

View file

@ -9,7 +9,7 @@ from khoj.processor.org_mode import orgnode
from khoj.processor.text_to_jsonl import TextToJsonl
from khoj.utils.helpers import get_absolute_path, is_none_or_empty, timer
from khoj.utils.jsonl import dump_jsonl, compress_jsonl_data
from khoj.utils.rawconfig import Entry
from khoj.utils.rawconfig import Entry, TextContentConfig
from khoj.utils import state
@ -17,6 +17,10 @@ logger = logging.getLogger(__name__)
class OrgToJsonl(TextToJsonl):
def __init__(self, config: TextContentConfig):
super().__init__(config)
self.config = config
# Define Functions
def process(self, previous_entries: List[Entry] = None):
# Extract required fields from config
@ -96,12 +100,20 @@ class OrgToJsonl(TextToJsonl):
entries = []
entry_to_file_map = []
for org_file in org_files:
org_file_entries = orgnode.makelist(str(org_file))
org_file_entries = orgnode.makelist_with_filepath(str(org_file))
entry_to_file_map += zip(org_file_entries, [org_file] * len(org_file_entries))
entries.extend(org_file_entries)
return entries, dict(entry_to_file_map)
@staticmethod
def process_single_org_file(org_content: str, org_file: str, entries: List, entry_to_file_map: List):
# Process single org file. The org parser assumes that the file is a single org file and reads it from a buffer. We'll split the raw conetnt of this file by new line to mimic the same behavior.
org_file_entries = orgnode.makelist(org_content.split("\n"), org_file)
entry_to_file_map += zip(org_file_entries, [org_file] * len(org_file_entries))
entries.extend(org_file_entries)
return entries, entry_to_file_map
@staticmethod
def convert_org_nodes_to_entries(
parsed_entries: List[orgnode.Orgnode], entry_to_file_map, index_heading_entries=False

View file

@ -53,14 +53,19 @@ def normalize_filename(filename):
return escaped_filename
def makelist(filename):
def makelist_with_filepath(filename):
f = open(filename, "r")
return makelist(f, filename)
def makelist(file, filename):
"""
Read an org-mode file and return a list of Orgnode objects
created from this file.
"""
ctr = 0
f = open(filename, "r")
f = file
todos = {
"TODO": "",

View file

@ -23,8 +23,10 @@ from khoj.search_filter.file_filter import FileFilter
from khoj.search_filter.word_filter import WordFilter
from khoj.utils.helpers import log_telemetry, timer
from khoj.utils.rawconfig import (
ContentConfig,
FullConfig,
ProcessorConfig,
SearchConfig,
SearchResponse,
TextContentConfig,
ConversationProcessorConfig,
@ -82,7 +84,15 @@ async def set_config_data(updated_config: FullConfig):
@api.post("/config/data/content_type/github", status_code=200)
async def set_content_config_github_data(updated_config: GithubContentConfig):
state.config.content_type.github = updated_config
if not state.config:
state.config = FullConfig()
state.config.search_type = SearchConfig.parse_obj(constants.default_config["search-type"])
if not state.config.content_type:
state.config.content_type = ContentConfig(**{"github": updated_config})
else:
state.config.content_type.github = updated_config
try:
save_config_to_file_updated_state()
return {"status": "ok"}
@ -92,7 +102,15 @@ async def set_content_config_github_data(updated_config: GithubContentConfig):
@api.post("/config/data/content_type/{content_type}", status_code=200)
async def set_content_config_data(content_type: str, updated_config: TextContentConfig):
state.config.content_type[content_type] = updated_config
if not state.config:
state.config = FullConfig()
state.config.search_type = SearchConfig.parse_obj(constants.default_config["search-type"])
if not state.config.content_type:
state.config.content_type = ContentConfig(**{content_type: updated_config})
else:
state.config.content_type[content_type] = updated_config
try:
save_config_to_file_updated_state()
return {"status": "ok"}
@ -102,10 +120,10 @@ async def set_content_config_data(content_type: str, updated_config: TextContent
@api.post("/config/data/processor/conversation", status_code=200)
async def set_processor_conversation_config_data(updated_config: ConversationProcessorConfig):
if state.config.processor is None:
state.config.processor = ProcessorConfig(conversation=updated_config)
else:
state.config.processor.conversation = updated_config
if not state.config:
state.config = FullConfig()
state.config.search_type = SearchConfig.parse_obj(constants.default_config["search-type"])
state.config.processor = ProcessorConfig(conversation=updated_config)
try:
save_config_to_file_updated_state()
return {"status": "ok"}

View file

@ -41,7 +41,9 @@ def github_config_page(request: Request):
)
current_config = (
state.config.content_type.github if state.config.content_type.github is not None else default_config
state.config.content_type.github
if state.config and state.config.content_type and state.config.content_type.github
else default_config
)
current_config = json.loads(current_config.json())
@ -63,9 +65,10 @@ def content_config_page(request: Request, content_type: str):
compressed_jsonl=default_content_type["compressed-jsonl"],
embeddings_file=default_content_type["embeddings-file"],
)
current_config = (
state.config.content_type[content_type]
if state.config.content_type[content_type] is not None
if state.config and state.config.content_type and state.config.content_type[content_type] # type: ignore
else default_config
)
current_config = json.loads(current_config.json())
@ -93,7 +96,7 @@ def conversation_processor_config_page(request: Request):
current_processor_conversation_config = (
state.config.processor.conversation
if state.config.processor.conversation is not None
if state.config and state.config.processor and state.config.processor.conversation
else default_processor_config
)
current_processor_conversation_config = json.loads(current_processor_conversation_config.json())

View file

@ -49,9 +49,7 @@ default_config = {
},
"github": {
"pat-token": None,
"repo-name": None,
"repo-owner": None,
"repo-branch": "master",
"repos": [],
"compressed-jsonl": "~/.khoj/content/github/github.jsonl.gz",
"embeddings-file": "~/.khoj/content/github/github_embeddings.pt",
},

View file

@ -41,11 +41,15 @@ class TextContentConfig(TextConfigBase):
return input_filter
class GithubRepoConfig(ConfigBase):
name: str
owner: str
branch: Optional[str] = "master"
class GithubContentConfig(TextConfigBase):
pat_token: str
repo_name: str
repo_owner: str
repo_branch: Optional[str] = "master"
repos: List[GithubRepoConfig]
class ImageContentConfig(ConfigBase):

View file

@ -1,6 +1,6 @@
# Get Base Image
FROM tiangolo/uvicorn-gunicorn:python3.11-slim
LABEL org.opencontainers.image.source https://github.com/debanjum/khoj
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
# Install Telemetry Server Dependencies
COPY requirements.txt /tmp/requirements.txt

View file

@ -17,6 +17,7 @@ from khoj.utils.rawconfig import (
ProcessorConfig,
TextContentConfig,
GithubContentConfig,
GithubRepoConfig,
ImageContentConfig,
SearchConfig,
TextSearchConfig,
@ -92,9 +93,13 @@ def content_config(tmp_path_factory, search_config: SearchConfig):
content_config.github = GithubContentConfig(
pat_token=os.getenv("GITHUB_PAT_TOKEN", ""),
repo_name="lantern",
repo_owner="khoj-ai",
repo_branch="master",
repos=[
GithubRepoConfig(
owner="khoj-ai",
name="lantern",
branch="master",
)
],
compressed_jsonl=content_dir.joinpath("github.jsonl.gz"),
embeddings_file=content_dir.joinpath("github_embeddings.pt"),
)

View file

@ -1,8 +1,8 @@
* Emacs Khoj
/An Emacs interface for [[https://github.com/debanjum/khoj][khoj]]/
/An Emacs interface for [[https://github.com/khoj-ai/khoj][khoj]]/
** Requirements
- Install and Run [[https://github.com/debanjum/khoj][khoj]]
- Install and Run [[https://github.com/khoj-ai/khoj][khoj]]
** Installation
- Direct Install
@ -22,7 +22,7 @@
#+begin_src elisp
;; Khoj Package
(use-package khoj
:quelpa (khoj :fetcher url :url "https://raw.githubusercontent.com/debanjum/khoj/master/interface/emacs/khoj.el")
:quelpa (khoj :fetcher url :url "https://raw.githubusercontent.com/khoj-ai/khoj/master/interface/emacs/khoj.el")
:bind ("C-c s" . 'khoj))
#+end_src

View file

@ -9,7 +9,7 @@
** Install
#+begin_src shell
git clone https://github.com/debanjum/khoj && cd khoj
git clone https://github.com/khoj-ai/khoj && cd khoj
conda env create -f environment.yml
conda activate khoj
#+end_src
@ -23,7 +23,7 @@
** Use
- *Khoj via Emacs*
- [[https://github.com/debanjum/khoj/tree/master/interface/emacs#installation][Install]] [[./interface/emacs/khoj.el][khoj.el]]
- [[https://github.com/khoj-ai/khoj/tree/master/interface/emacs#installation][Install]] [[./interface/emacs/khoj.el][khoj.el]]
- Run ~M-x khoj <user-query>~ or Call ~C-c C-s~
- *Khoj via API*

View file

@ -14,7 +14,7 @@ def test_parse_entry_with_no_headings(tmp_path):
orgfile = create_file(tmp_path, entry)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 1
@ -38,7 +38,7 @@ Body Line 1"""
orgfile = create_file(tmp_path, entry)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 1
@ -71,7 +71,7 @@ Body Line 2"""
orgfile = create_file(tmp_path, entry)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 1
@ -109,7 +109,7 @@ def test_render_entry_with_property_drawer_and_empty_body(tmp_path):
"""
# Act
parsed_entries = orgnode.makelist(orgfile)
parsed_entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert f"{parsed_entries[0]}" == expected_entry
@ -131,7 +131,7 @@ Body Line 2
orgfile = create_file(tmp_path, entry)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
# SOURCE link rendered with Heading
@ -155,7 +155,7 @@ Body Line 1"""
orgfile = create_file(tmp_path, entry, filename="test[1].org")
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 1
@ -197,7 +197,7 @@ Body 2
orgfile = create_file(tmp_path, content)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 2
@ -225,7 +225,7 @@ Body Line 1"""
orgfile = create_file(tmp_path, entry)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 1
@ -248,7 +248,7 @@ Body Line 1"""
orgfile = create_file(tmp_path, entry)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 1
@ -272,7 +272,7 @@ Body Line 1
orgfile = create_file(tmp_path, entry)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 1
@ -298,7 +298,7 @@ entry body
orgfile = create_file(tmp_path, body)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 2
@ -320,7 +320,7 @@ entry body
orgfile = create_file(tmp_path, body)
# Act
entries = orgnode.makelist(orgfile)
entries = orgnode.makelist_with_filepath(orgfile)
# Assert
assert len(entries) == 2