Refactor Config API and Settings pages for Reuse and Consistency (#852)

### Major
- Reuse get config data logic across config pages on web client
- Make config api endpoint urls and response fields consistent
- Rename API path /api/config to /api/configure
- Move Web, Desktop client settings page to be under `/settings` from the previous `/config` url path

### Minor
- Pass isMobileWidth prop to SidePanel via chat share interface
- Turn prettier off instead of throwing error for now
- Do no explicitly add line-clamp plugin as it's in Tailwind by default
This commit is contained in:
Debanjum 2024-07-17 01:03:06 -07:00 committed by GitHub
commit bf815e4463
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 453 additions and 557 deletions

View file

@ -34,4 +34,4 @@ Using LiteLLM with Khoj makes it possible to turn any LLM behind an API into you
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View file

@ -27,4 +27,4 @@ LM Studio can expose an [OpenAI API compatible server](https://lmstudio.ai/docs/
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View file

@ -31,6 +31,6 @@ Ollama exposes a local [OpenAI API compatible server](https://github.com/ollama/
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.
That's it! You should now be able to chat with your Ollama model from Khoj. If you want to add additional models running on Ollama, repeat step 6 for each model.

View file

@ -34,4 +34,4 @@ For specific integrations, see our [Ollama](/advanced/ollama), [LMStudio](/advan
5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel
- Default model: `<name of chat model option you created in step 4>`
- Summarizer model: `<name of chat model option you created in step 4>`
6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown.
6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown.

View file

@ -23,7 +23,7 @@ Khoj will keep these files in sync to provide contextual responses when you sear
## Setup
1. Install the [Khoj Desktop app](https://khoj.dev/downloads) for your OS
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
3. Set your Khoj API Key on the *Settings* page of the Khoj Desktop app
4. [Optional] Add any files, folders you'd like Khoj to be aware of on the *Settings* page and Click *Save*
These files and folders will be automatically kept in sync for you

View file

@ -30,7 +30,7 @@ sidebar_position: 2
| ![khoj search on emacs](/img/khoj_search_on_emacs.png) | ![khoj chat on emacs](/img/khoj_chat_on_emacs.png) |
## Setup
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
2. Add below snippet to your Emacs config file, usually at `~/.emacs.d/init.el`

View file

@ -23,7 +23,7 @@ sidebar_position: 3
1. Open [Khoj](https://obsidian.md/plugins?id=khoj) from the *Community plugins* tab in Obsidian settings panel
2. Click *Install*, then *Enable* on the Khoj plugin page in Obsidian
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients)
3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients)
4. Set your Khoj API Key in the Khoj plugin settings in Obsidian
See the official [Obsidian Plugin Docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for more details on installing Obsidian plugins.

View file

@ -10,7 +10,7 @@ Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan [this QR code](https
Without any desktop clients, you can start chatting with Khoj on WhatsApp. Bear in mind you do need one of the desktop clients in order to share and sync your data with Khoj. The WhatsApp AI bot will work right away for answering generic queries and using Khoj in default mode.
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/config).
In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/settings).
If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://khoj.dev/pricing) on Khoj Cloud.

View file

@ -4,11 +4,11 @@ The Github integration allows you to index as many repositories as you want. It'
# Configure your settings
1. Go to [https://app.khoj.dev/config](https://app.khoj.dev/config) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
1. Go to [https://app.khoj.dev/settings](https://app.khoj.dev/settings) and enter in settings for the data sources you want to index. You'll have to specify the file paths.
## Use the Github plugin
1. Generate a [classic PAT (personal access token)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) from [Github](https://github.com/settings/tokens) with `repo` and `admin:org` scopes at least.
2. Navigate to [https://app.khoj.dev/config/content-source/github](https://app.khoj.dev/config/content-source/github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
2. Navigate to [https://app.khoj.dev/settings/content/github](https://app.khoj.dev/settings/content/github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index.
3. Click `Save`. Go back to the settings page and click `Configure`.
4. Go to [https://app.khoj.dev/](https://app.khoj.dev/) and start searching!

View file

@ -2,7 +2,7 @@
The Notion integration allows you to search/chat with your Notion workspaces. [Notion](https://notion.so/) is a platform people use for taking notes, especially for collaboration.
Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
Go to https://app.khoj.dev/settings to connect your Notion workspace(s) to Khoj.
![notion_integration](https://assets.khoj.dev/notion_integration.gif)
@ -13,7 +13,7 @@ Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj.
![setup_new_integration](https://github.com/khoj-ai/khoj/assets/65192171/b056e057-d4dc-47dc-aad3-57b59a22c68b)
3. Share all the workspaces that you want to integrate with the Khoj integration you just made in the previous step
![enable_workspace](https://github.com/khoj-ai/khoj/assets/65192171/98290303-b5b8-4cb0-b32c-f68c6923a3d0)
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at http://localhost:42110/config/content-source/notion. Click `Save`.
5. Click `Configure` in http://localhost:42110/config to index your Notion workspace(s).
4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at http://localhost:42110/settings/content/notion. Click `Save`.
5. Click `Configure` in http://localhost:42110/settings to index your Notion workspace(s).
That's it! You should be ready to start searching and chatting. Make sure you've configured your [chat settings](/get-started/setup#2-configure).

View file

@ -253,7 +253,7 @@ function pushDataToKhoj (regenerate = false) {
console.error(error);
state["completed"] = false;
if (error?.response?.status === 429 && (BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config')))) {
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/config">Upgrade your plan</a> to unlock more space.`;
state["error"] = `Looks like you're out of space to sync your files. <a href="https://app.khoj.dev/settings">Upgrade your plan</a> to unlock more space.`;
const win = BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config'));
if (win) win.webContents.send('needsSubscription', true);
} else if (error?.code === 'ECONNREFUSED') {

View file

@ -182,7 +182,7 @@ window.updateStateAPI.onUpdateState((event, state) => {
window.needsSubscriptionAPI.onNeedsSubscription((event, needsSubscription) => {
console.log("needs subscription", needsSubscription);
if (needsSubscription) {
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/config");
window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/settings");
needsSubscriptionElement.style.display = 'block';
}
});

View file

@ -212,12 +212,12 @@
const headers = { 'Authorization': `Bearer ${khojToken}` };
// Populate type dropdown field with enabled content types only
fetch(`${hostURL}/api/config/types`, { headers })
fetch(`${hostURL}/api/configure/types`, { headers })
.then(response => response.json())
.then(enabled_types => {
// Show warning if no content types are enabled
if (enabled_types.detail) {
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/config'>settings page</a>.</div>";
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/settings'>settings page</a>.</div>";
document.getElementById("query").setAttribute("disabled", "disabled");
document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search");
return [];

View file

@ -85,7 +85,7 @@ async function populateHeaderPane() {
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
<div class="khoj-nav-username"> ${username} </div>
<a id="github-nav" class="khoj-nav" href="https://github.com/khoj-ai/khoj">GitHub</a>
<a id="settings-nav" class="khoj-nav" href="./config.html"> Settings</a>
<a id="settings-nav" class="khoj-nav" href="./settings.html"> Settings</a>
</div>
</div>
` : ''}

View file

@ -99,7 +99,7 @@
:type 'boolean)
(defcustom khoj-api-key nil
"API Key to your Khoj. Default at https://app.khoj.dev/config#clients."
"API Key to your Khoj. Default at https://app.khoj.dev/settings#clients."
:group 'khoj
:type 'string)
@ -697,7 +697,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/config/types" "GET" nil `(lambda (item) (mapcar #'intern item))))
(khoj--call-api "/api/configure/types" "GET" 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.

View file

@ -201,12 +201,12 @@ export function getBackendStatusMessage(
): string {
// Welcome message with default settings. Khoj cloud always expects an API key.
if (!khojApiKey && khojUrl === 'https://app.khoj.dev')
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/config#clients and set it in the Khoj plugin settings on Obsidian`;
return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/settings#clients and set it in the Khoj plugin settings on Obsidian`;
if (!connectedToServer)
return `Could not connect to Khoj at ${khojUrl}. Ensure your can access it`;
else if (!userEmail)
return `✅ Connected to Khoj. ❗Get a valid API key from ${khojUrl}/config#clients to log in`;
return `✅ Connected to Khoj. ❗Get a valid API key from ${khojUrl}/settings#clients to log in`;
else if (userEmail === 'default@example.com')
// Logged in as default user in anonymous mode
return `✅ Signed in to Khoj`;

View file

@ -6,6 +6,6 @@
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
"prettier/prettier": "off"
}
}

View file

@ -68,8 +68,8 @@ interface ModelPickerProps {
}
export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
const { data: models } = useOptionsRequest('/api/config/data/conversation/model/options');
const { data: selectedModel } = useSelectedModel('/api/config/data/conversation/model');
const { data: models } = useOptionsRequest('/api/configure/chat/model/options');
const { data: selectedModel } = useSelectedModel('/api/configure/chat/model');
const [openLoginDialog, setOpenLoginDialog] = React.useState(false);
let userData = useAuthenticatedData();
@ -94,7 +94,7 @@ export const ModelPicker: React.FC<any> = (props: ModelPickerProps) => {
props.setModelUsed(model);
}
fetch('/api/config/data/conversation/model' + '?id=' + String(model.id), { method: 'POST', body: JSON.stringify(model) })
fetch('/api/configure/chat/model' + '?id=' + String(model.id), { method: 'POST', body: JSON.stringify(model) })
.then((response) => {
if (!response.ok) {
throw new Error('Failed to select model');

View file

@ -128,7 +128,7 @@ export default function NavMenu(props: NavMenuProps) {
<DropdownMenuSeparator />
<DropdownMenuLabel>Profile</DropdownMenuLabel>
<DropdownMenuItem>
<Link href="/config">Settings</Link>
<Link href="/settings">Settings</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="https://docs.khoj.dev">Help</Link>
@ -172,7 +172,7 @@ export default function NavMenu(props: NavMenuProps) {
{userData &&
<>
<MenubarItem>
<Link href="/config">
<Link href="/settings">
Settings
</Link>
</MenubarItem>

View file

@ -148,7 +148,7 @@ interface FilesMenuProps {
function FilesMenu(props: FilesMenuProps) {
// Use SWR to fetch files
const { data: files, error } = useSWR<string[]>(props.conversationId ? '/api/config/data/computer' : null, fetcher);
const { data: files, error } = useSWR<string[]>(props.conversationId ? '/api/configure/content/computer' : null, fetcher);
const { data: selectedFiles, error: selectedFilesError } = useSWR(props.conversationId ? `/api/chat/conversation/file-filters/${props.conversationId}` : null, fetcher);
const [isOpen, setIsOpen] = useState(false);
const [unfilteredFiles, setUnfilteredFiles] = useState<string[]>([]);
@ -604,7 +604,7 @@ function UserProfileComponent(props: UserProfileProps) {
return (
<div className={styles.profile}>
<Link href="/config" target="_blank" rel="noopener noreferrer">
<Link href="/settings">
<Avatar>
<AvatarImage src={props.userProfile.photo} alt="user profile" />
<AvatarFallback>

View file

@ -533,7 +533,7 @@ export default function FactChecker() {
<Button disabled={clickedVerify} onClick={() => onClickVerify()}>Verify</Button>
</div>
<h3 className={`mt-4 mb-4`}>
Try with a particular model. You must be <a href="/config" className="font-medium text-blue-600 dark:text-blue-500 hover:underline">subscribed</a> to configure the model.
Try with a particular model. You must be <a href="/settings" className="font-medium text-blue-600 dark:text-blue-500 hover:underline">subscribed</a> to configure the model.
</h3>
</div>
}

View file

@ -299,7 +299,9 @@ export default function SharedChat() {
<SidePanel
webSocketConnected={!!conversationId ? (chatWS != null) : true}
conversationId={conversationId ?? null}
uploadedFiles={uploadedFiles} />
uploadedFiles={uploadedFiles}
isMobileWidth={isMobileWidth}
/>
</div>
<div className={styles.chatBox}>

View file

@ -35,7 +35,6 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/dompurify": "^3.0.5",
"@types/katex": "^0.16.7",
"@types/markdown-it": "^14.1.1",

View file

@ -76,7 +76,6 @@ const config = {
},
plugins: [
require("tailwindcss-animate"),
require('@tailwindcss/line-clamp'),
],
} satisfies Config

View file

@ -1090,11 +1090,6 @@
"@swc/counter" "^0.1.3"
tslib "^2.4.0"
"@tailwindcss/line-clamp@^0.4.4":
version "0.4.4"
resolved "https://registry.yarnpkg.com/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz#767cf8e5d528a5d90c9740ca66eb079f5e87d423"
integrity sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==
"@ts-morph/common@~0.19.0":
version "0.19.0"
resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.19.0.tgz#927fcd81d1bbc09c89c4a310a84577fb55f3694e"

View file

@ -316,7 +316,7 @@ def configure_routes(app):
app.include_router(api, prefix="/api")
app.include_router(api_chat, prefix="/api/chat")
app.include_router(api_agents, prefix="/api/agents")
app.include_router(api_config, prefix="/api/config")
app.include_router(api_config, prefix="/api/configure")
app.include_router(indexer, prefix="/api/v1/index")
app.include_router(notion_router, prefix="/api/notion")
app.include_router(web_client)
@ -336,7 +336,7 @@ def configure_routes(app):
if is_twilio_enabled():
from khoj.routers.api_phone import api_phone
app.include_router(api_phone, prefix="/api/config/phone")
app.include_router(api_phone, prefix="/api/configure/phone")
logger.info("📞 Enabled Twilio")

View file

@ -44,7 +44,7 @@ Hi, I am Khoj, your open, personal AI 👋🏽. I can:
- 📚 Understand files you drag & drop here
- 👩🏾‍🚀 Be tuned to your conversation needs via [agents](./agents)
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), [Emacs](https://docs.khoj.dev/clients/emacs#setup) apps to search, chat with your 🖥️ computer docs. You can manage all the files you've shared with me at any time by going to [your settings](/config/content-source/computer/).
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), [Emacs](https://docs.khoj.dev/clients/emacs#setup) apps to search, chat with your 🖥️ computer docs. You can manage all the files you've shared with me at any time by going to [your settings](/settings/content/computer/).
To get started, just start typing below. You can also type / to see a list of commands.
`.trim()
@ -1333,7 +1333,7 @@ To get started, just start typing below. You can also type / to see a list of co
- 📚 Understand files you drag & drop here
- 👩🏾‍🚀 Be tuned to your conversation needs via [agents](./agents)
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), or [Emacs](https://docs.khoj.dev/clients/emacs#setup) app to keep your files in sync. You can manage all the files you've shared with me at any time by going to [your settings](/config/content-source/computer/).
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), or [Emacs](https://docs.khoj.dev/clients/emacs#setup) app to keep your files in sync. You can manage all the files you've shared with me at any time by going to [your settings](/settings/content/computer/).
To get started, just start typing below. You can also type / to see a list of commands.
@ -1954,7 +1954,7 @@ To get started, just start typing below. You can also type / to see a list of co
}
var allFiles;
function renderAllFiles() {
fetch('/api/config/data/computer')
fetch('/api/configure/content/computer')
.then(response => response.json())
.then(data => {
var indexedFiles = document.getElementsByClassName("indexed-files")[0];

View file

@ -32,7 +32,7 @@
</style>
<script>
function removeFile(path) {
fetch('/api/config/data/file?filename=' + path, {
fetch('/api/configure/content/file?filename=' + path, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@ -48,7 +48,7 @@
// Get all currently indexed files
function getAllComputerFilenames() {
fetch('/api/config/data/computer')
fetch('/api/configure/content/computer')
.then(response => response.json())
.then(data => {
var indexedFiles = document.getElementsByClassName("indexed-files")[0];
@ -122,7 +122,7 @@
deleteAllComputerFilesButton.textContent = "🗑️ Deleting...";
deleteAllComputerFilesButton.disabled = true;
fetch('/api/config/data/content-source/computer', {
fetch('/api/configure/content/computer', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',

View file

@ -165,7 +165,7 @@
// Save Github config on server
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
fetch('/api/config/data/content-source/github', {
fetch('/api/configure/content/github', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -45,7 +45,7 @@
// Save Notion config on server
const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1];
fetch('/api/config/data/content-source/notion', {
fetch('/api/configure/content/notion', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -34,7 +34,7 @@ Hi, I am Khoj, your open, personal AI 👋🏽. I can:
- 📚 Understand files you drag & drop here
- 👩🏾‍🚀 Be tuned to your conversation needs via [agents](./agents)
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), [Emacs](https://docs.khoj.dev/clients/emacs#setup) apps to search, chat with your 🖥️ computer docs. You can manage all the files you've shared with me at any time by going to [your settings](/config/content-source/computer/).
Get the Khoj [Desktop](https://khoj.dev/downloads), [Obsidian](https://docs.khoj.dev/clients/obsidian#setup), [Emacs](https://docs.khoj.dev/clients/emacs#setup) apps to search, chat with your 🖥️ computer docs. You can manage all the files you've shared with me at any time by going to [your settings](/settings/content/computer/).
To get started, just start typing below. You can also type / to see a list of commands.
`.trim()

View file

@ -209,12 +209,12 @@
function populate_type_dropdown() {
// Populate type dropdown field with enabled content types only
fetch("/api/config/types")
fetch("/api/configure/types")
.then(response => response.json())
.then(enabled_types => {
// Show warning if no content types are enabled, or just one ("all")
if (enabled_types[0] === "all" && enabled_types.length === 1) {
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/config'>settings page</a>.</div>";
document.getElementById("results").innerHTML = "<div id='results-error'>To use Khoj search, setup your content plugins on the Khoj <a class='inline-chat-link' href='/settings'>settings page</a>.</div>";
document.getElementById("query").setAttribute("disabled", "disabled");
document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search");
return [];

View file

@ -34,7 +34,7 @@
<h3 id="card-title-computer" class="card-title">
<span>Files</span>
<img id="configured-icon-computer"
style="display: {% if not current_model_state.computer %}none{% endif %}"
style="display: {% if not enabled_content_source.computer %}none{% endif %}"
class="configured-icon"
src="/static/assets/icons/confirm-icon.svg"
alt="Configured">
@ -44,8 +44,8 @@
<p class="card-description">Manage files from your computer</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content-source/computer">
{% if current_model_state.computer %}
<a class="card-button" href="/settings/content/computer">
{% if enabled_content_source.computer %}
Update
{% else %}
Setup
@ -53,7 +53,7 @@
<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 id="clear-computer" class="card-action-row"
style="display: {% if not current_model_state.computer %}none{% endif %}">
style="display: {% if not enabled_content_source.computer %}none{% endif %}">
<button class="card-button" onclick="clearContentType('computer')">
Disable
</button>
@ -69,15 +69,15 @@
class="configured-icon"
src="/static/assets/icons/confirm-icon.svg"
alt="Configured"
style="display: {% if not current_model_state.github %}none{% endif %}">
style="display: {% if not enabled_content_source.github %}none{% endif %}">
</h3>
</div>
<div class="card-description-row">
<p class="card-description">Set repositories to index</p>
</div>
<div class="card-action-row">
<a class="card-button" href="/config/content-source/github">
{% if current_model_state.github %}
<a class="card-button" href="/settings/content/github">
{% if enabled_content_source.github %}
Update
{% else %}
Setup
@ -86,7 +86,7 @@
</a>
<div id="clear-github"
class="card-action-row"
style="display: {% if not current_model_state.github %}none{% endif %}">
style="display: {% if not enabled_content_source.github %}none{% endif %}">
<button class="card-button" onclick="clearContentType('github')">
Disable
</button>
@ -102,15 +102,15 @@
class="configured-icon"
src="/static/assets/icons/confirm-icon.svg"
alt="Configured"
style="display: {% if not current_model_state.notion %}none{% endif %}">
style="display: {% if not enabled_content_source.notion %}none{% endif %}">
</h3>
</div>
<div class="card-description-row">
<p class="card-description">Sync your Notion pages</p>
</div>
<div class="card-action-row">
{% if current_model_state.notion %}
<a class="card-button" href="/config/content-source/notion">
{% if enabled_content_source.notion %}
<a class="card-button" href="/settings/content/notion">
Update
<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>
@ -120,7 +120,7 @@
<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>
{% else %}
<a class="card-button" href="/config/content-source/notion">
<a class="card-button" href="/settings/content/notion">
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>
@ -128,7 +128,7 @@
<div id="clear-notion"
class="card-action-row"
style="display: {% if not current_model_state.notion %}none{% endif %}">
style="display: {% if not enabled_content_source.notion %}none{% endif %}">
<button class="card-button" onclick="clearContentType('notion')">
Disable
</button>
@ -181,8 +181,8 @@
</div>
<div class="card-description-row">
<select id="chat-models">
{% for option in conversation_options %}
<option value="{{ option.id }}" {% if option.id == selected_conversation_config %}selected{% endif %}>{{ option.chat_model }}</option>
{% for option in chat_model_options %}
<option value="{{ option.id }}" {% if option.id == selected_chat_model_config %}selected{% endif %}>{{ option.name }}</option>
{% endfor %}
</select>
</div>
@ -208,7 +208,7 @@
<div class="card-description-row">
<select id="paint-models">
{% for option in paint_model_options %}
<option value="{{ option.id }}" {% if option.id == selected_paint_model_config %}selected{% endif %}>{{ option.model_name }}</option>
<option value="{{ option.id }}" {% if option.id == selected_paint_model_config %}selected{% endif %}>{{ option.name }}</option>
{% endfor %}
</select>
</div>
@ -394,7 +394,7 @@
function saveProfileGivenName() {
const givenName = document.getElementById("profile_given_name").value;
fetch('/api/config/user/name?name=' + givenName, {
fetch('/api/configure/user/name?name=' + givenName, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -421,7 +421,7 @@
saveVoiceModelButton.disabled = true;
saveVoiceModelButton.textContent = "Saving...";
fetch('/api/config/data/voice/model?id=' + voiceModel, {
fetch('/api/configure/voice/model?id=' + voiceModel, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -455,7 +455,7 @@
saveModelButton.innerHTML = "";
saveModelButton.textContent = "Saving...";
fetch('/api/config/data/conversation/model?id=' + chatModel, {
fetch('/api/configure/chat/model?id=' + chatModel, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -494,7 +494,7 @@
saveSearchModelButton.disabled = true;
saveSearchModelButton.textContent = "Saving...";
fetch('/api/config/data/search/model?id=' + searchModel, {
fetch('/api/configure/search/model?id=' + searchModel, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -526,7 +526,7 @@
saveModelButton.disabled = true;
saveModelButton.innerHTML = "Saving...";
fetch('/api/config/data/paint/model?id=' + paintModel, {
fetch('/api/configure/paint/model?id=' + paintModel, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -553,7 +553,7 @@
};
function clearContentType(content_source) {
fetch('/api/config/data/content-source/' + content_source, {
fetch('/api/configure/content/' + content_source, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@ -676,7 +676,7 @@
content_sources = ["computer", "github", "notion"];
content_sources.forEach(content_source => {
fetch(`/api/config/data/${content_source}`, {
fetch(`/api/configure/content/${content_source}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
@ -807,7 +807,7 @@
function getIndexedDataSize() {
document.getElementById("indexed-data-size").textContent = "Calculating...";
fetch('/api/config/index/size')
fetch('/api/configure/content/size')
.then(response => response.json())
.then(data => {
document.getElementById("indexed-data-size").textContent = data.indexed_data_size_in_mb + " MB used";
@ -815,7 +815,7 @@
}
function removeFile(path) {
fetch('/api/config/data/file?filename=' + path, {
fetch('/api/configure/content/file?filename=' + path, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@ -890,7 +890,7 @@
})
phonenumberRemoveButton.addEventListener("click", () => {
fetch('/api/config/phone', {
fetch('/api/configure/phone', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@ -917,7 +917,7 @@
}, 5000);
} else {
const mobileNumber = iti.getNumber();
fetch('/api/config/phone?phone_number=' + mobileNumber, {
fetch('/api/configure/phone?phone_number=' + mobileNumber, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -970,7 +970,7 @@
return;
}
fetch('/api/config/phone/verify?code=' + otp, {
fetch('/api/configure/phone/verify?code=' + otp, {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -36,7 +36,7 @@
{% endif %}
<div id="khoj-nav-menu" class="khoj-nav-dropdown-content">
<div class="khoj-nav-username"> {{ username }} </div>
<a id="settings-nav" class="khoj-nav" href="/config">Settings</a>
<a id="settings-nav" class="khoj-nav" href="/settings">Settings</a>
<a id="github-nav" class="khoj-nav" href="https://github.com/khoj-ai/khoj">GitHub</a>
<a id="help-nav" class="khoj-nav" href="https://docs.khoj.dev" target="_blank">Help</a>
<a class="khoj-nav" href="/auth/logout">Logout</a>

View file

@ -6,7 +6,6 @@ import os
import threading
import time
import uuid
from random import random
from typing import Any, Callable, List, Optional, Union
import cron_descriptor
@ -190,7 +189,7 @@ def update(
):
user = request.user.object
if not state.config:
error_msg = f"🚨 Khoj is not configured.\nConfigure it via http://localhost:42110/config, plugins or by editing {state.config_file}."
error_msg = f"🚨 Khoj is not configured.\nConfigure it via http://localhost:42110/settings, plugins or by editing {state.config_file}."
logger.warning(error_msg)
raise HTTPException(status_code=500, detail=error_msg)
try:

View file

@ -98,9 +98,9 @@ def _initialize_config():
state.config.search_type = SearchConfig.model_validate(constants.default_config["search-type"])
@api_config.post("/data/content-source/github", status_code=200)
@api_config.post("/content/github", status_code=200)
@requires(["authenticated"])
async def set_content_config_github_data(
async def set_content_github(
request: Request,
updated_config: Union[GithubContentConfig, None],
client: Optional[str] = None,
@ -130,9 +130,9 @@ async def set_content_config_github_data(
return {"status": "ok"}
@api_config.post("/data/content-source/notion", status_code=200)
@api_config.post("/content/notion", status_code=200)
@requires(["authenticated"])
async def set_content_config_notion_data(
async def set_content_notion(
request: Request,
updated_config: Union[NotionContentConfig, None],
client: Optional[str] = None,
@ -161,9 +161,9 @@ async def set_content_config_notion_data(
return {"status": "ok"}
@api_config.delete("/data/content-source/{content_source}", status_code=200)
@api_config.delete("/content/{content_source}", status_code=200)
@requires(["authenticated"])
async def remove_content_source_data(
async def delete_content_source(
request: Request,
content_source: str,
client: Optional[str] = None,
@ -189,9 +189,9 @@ async def remove_content_source_data(
return {"status": "ok"}
@api_config.delete("/data/file", status_code=200)
@api_config.delete("/content/file", status_code=201)
@requires(["authenticated"])
async def remove_file_data(
async def delete_content_file(
request: Request,
filename: str,
client: Optional[str] = None,
@ -210,9 +210,9 @@ async def remove_file_data(
return {"status": "ok"}
@api_config.get("/data/{content_source}", response_model=List[str])
@api_config.get("/content/{content_source}", response_model=List[str])
@requires(["authenticated"])
async def get_all_filenames(
async def get_content_source(
request: Request,
content_source: str,
client: Optional[str] = None,
@ -229,7 +229,7 @@ async def get_all_filenames(
return await sync_to_async(list)(EntryAdapters.get_all_filenames_by_source(user, content_source)) # type: ignore[call-arg]
@api_config.get("/data/conversation/model/options", response_model=Dict[str, Union[str, int]])
@api_config.get("/chat/model/options", response_model=Dict[str, Union[str, int]])
def get_chat_model_options(
request: Request,
client: Optional[str] = None,
@ -243,7 +243,7 @@ def get_chat_model_options(
return Response(content=json.dumps(all_conversation_options), media_type="application/json", status_code=200)
@api_config.get("/data/conversation/model")
@api_config.get("/chat/model")
@requires(["authenticated"])
def get_user_chat_model(
request: Request,
@ -259,7 +259,7 @@ def get_user_chat_model(
return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.chat_model}))
@api_config.post("/data/conversation/model", status_code=200)
@api_config.post("/chat/model", status_code=200)
@requires(["authenticated", "premium"])
async def update_chat_model(
request: Request,
@ -284,7 +284,7 @@ async def update_chat_model(
return {"status": "ok"}
@api_config.post("/data/voice/model", status_code=200)
@api_config.post("/voice/model", status_code=200)
@requires(["authenticated", "premium"])
async def update_voice_model(
request: Request,
@ -308,7 +308,7 @@ async def update_voice_model(
return Response(status_code=202, content=json.dumps({"status": "ok"}))
@api_config.post("/data/search/model", status_code=200)
@api_config.post("/search/model", status_code=200)
@requires(["authenticated"])
async def update_search_model(
request: Request,
@ -341,7 +341,7 @@ async def update_search_model(
return {"status": "ok"}
@api_config.post("/data/paint/model", status_code=200)
@api_config.post("/paint/model", status_code=200)
@requires(["authenticated"])
async def update_paint_model(
request: Request,
@ -370,9 +370,9 @@ async def update_paint_model(
return {"status": "ok"}
@api_config.get("/index/size", response_model=Dict[str, int])
@api_config.get("/content/size", response_model=Dict[str, int])
@requires(["authenticated"])
async def get_indexed_data_size(request: Request, common: CommonQueryParams):
async def get_content_size(request: Request, common: CommonQueryParams):
user = request.user.object
indexed_data_size_in_mb = await sync_to_async(EntryAdapters.get_size_of_indexed_data_in_mb)(user)
return Response(

View file

@ -5,6 +5,7 @@ import io
import json
import logging
import math
import os
import re
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta, timezone
@ -35,6 +36,7 @@ from PIL import Image
from starlette.authentication import has_required_scope
from starlette.requests import URL
from khoj.database import adapters
from khoj.database.adapters import (
AgentAdapters,
AutomationAdapters,
@ -42,18 +44,30 @@ from khoj.database.adapters import (
EntryAdapters,
create_khoj_token,
get_khoj_tokens,
get_user_name,
get_user_subscription_state,
run_with_process_lock,
)
from khoj.database.models import (
ChatModelOptions,
ClientApplication,
Conversation,
GithubConfig,
KhojUser,
NotionConfig,
ProcessLock,
Subscription,
TextToImageModelConfig,
UserRequests,
)
from khoj.processor.content.docx.docx_to_entries import DocxToEntries
from khoj.processor.content.github.github_to_entries import GithubToEntries
from khoj.processor.content.images.image_to_entries import ImageToEntries
from khoj.processor.content.markdown.markdown_to_entries import MarkdownToEntries
from khoj.processor.content.notion.notion_to_entries import NotionToEntries
from khoj.processor.content.org_mode.org_to_entries import OrgToEntries
from khoj.processor.content.pdf.pdf_to_entries import PdfToEntries
from khoj.processor.content.plaintext.plaintext_to_entries import PlaintextToEntries
from khoj.processor.conversation import prompts
from khoj.processor.conversation.anthropic.anthropic_chat import (
anthropic_send_message_to_model,
@ -69,11 +83,15 @@ from khoj.processor.conversation.utils import (
generate_chatml_messages_with_context,
save_to_conversation_log,
)
from khoj.processor.speech.text_to_speech import is_eleven_labs_enabled
from khoj.routers.email import is_resend_enabled, send_task_email
from khoj.routers.storage import upload_image
from khoj.routers.twilio import is_twilio_enabled
from khoj.search_type import text_search
from khoj.utils import state
from khoj.utils.config import OfflineChatProcessorModel
from khoj.utils.helpers import (
LRU,
ConversationCommand,
ImageIntentType,
is_none_or_empty,
@ -90,6 +108,11 @@ logger = logging.getLogger(__name__)
executor = ThreadPoolExecutor(max_workers=1)
NOTION_OAUTH_CLIENT_ID = os.getenv("NOTION_OAUTH_CLIENT_ID")
NOTION_OAUTH_CLIENT_SECRET = os.getenv("NOTION_OAUTH_CLIENT_SECRET")
NOTION_REDIRECT_URI = os.getenv("NOTION_REDIRECT_URI")
def is_query_empty(query: str) -> bool:
return is_none_or_empty(query.strip())
@ -902,7 +925,7 @@ class ApiUserRateLimiter:
)
raise HTTPException(
status_code=429,
detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your usage limit via [your settings](https://app.khoj.dev/config).",
detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your usage limit via [your settings](https://app.khoj.dev/settings).",
)
# Add the current request to the cache
@ -941,7 +964,7 @@ class ConversationCommandRateLimiter:
if not subscribed and count_requests >= self.trial_rate_limit:
raise HTTPException(
status_code=429,
detail=f"We're glad you're enjoying Khoj! You've exceeded your `/{conversation_command.value}` command usage limit for today. Subscribe to increase your usage limit via [your settings](https://app.khoj.dev/config).",
detail=f"We're glad you're enjoying Khoj! You've exceeded your `/{conversation_command.value}` command usage limit for today. Subscribe to increase your usage limit via [your settings](https://app.khoj.dev/settings).",
)
await UserRequests.objects.acreate(user=user, slug=command_slug)
return
@ -1186,3 +1209,284 @@ def construct_automation_created_message(automation: Job, crontime: str, query_t
Manage your automations [here](/automations).
""".strip()
def get_user_config(user: KhojUser, request: Request, is_detailed: bool = False):
user_picture = request.session.get("user", {}).get("picture")
is_active = has_required_scope(request, ["premium"])
has_documents = EntryAdapters.user_has_entries(user=user)
if not is_detailed:
return {
"request": request,
"username": user.username if user else None,
"user_photo": user_picture,
"is_active": is_active,
"has_documents": has_documents,
"khoj_version": state.khoj_version,
}
user_subscription_state = get_user_subscription_state(user.email)
user_subscription = adapters.get_user_subscription(user.email)
subscription_renewal_date = (
user_subscription.renewal_date.strftime("%d %b %Y")
if user_subscription and user_subscription.renewal_date
else (user_subscription.created_at + timedelta(days=7)).strftime("%d %b %Y")
)
given_name = get_user_name(user)
enabled_content_sources_set = set(EntryAdapters.get_unique_file_sources(user))
enabled_content_sources = {
"computer": ("computer" in enabled_content_sources_set),
"github": ("github" in enabled_content_sources_set),
"notion": ("notion" in enabled_content_sources_set),
}
selected_chat_model_config = ConversationAdapters.get_conversation_config(user)
chat_models = ConversationAdapters.get_conversation_processor_options().all()
chat_model_options = list()
for chat_model in chat_models:
chat_model_options.append({"name": chat_model.chat_model, "id": chat_model.id})
search_model_options = adapters.get_or_create_search_models().all()
all_search_model_options = list()
for search_model_option in search_model_options:
all_search_model_options.append({"name": search_model_option.name, "id": search_model_option.id})
current_search_model_option = adapters.get_user_search_model_or_default(user)
selected_paint_model_config = ConversationAdapters.get_user_text_to_image_model_config(user)
paint_model_options = ConversationAdapters.get_text_to_image_model_options().all()
all_paint_model_options = list()
for paint_model in paint_model_options:
all_paint_model_options.append({"name": paint_model.model_name, "id": paint_model.id})
notion_oauth_url = get_notion_auth_url(user)
eleven_labs_enabled = is_eleven_labs_enabled()
voice_models = ConversationAdapters.get_voice_model_options()
voice_model_options = list()
for voice_model in voice_models:
voice_model_options.append({"name": voice_model.name, "id": voice_model.model_id})
if len(voice_model_options) == 0:
eleven_labs_enabled = False
selected_voice_config = ConversationAdapters.get_voice_model_config(user)
return {
"request": request,
# user info
"username": user.username if user else None,
"user_photo": user_picture,
"is_active": is_active,
"given_name": given_name,
"phone_number": user.phone_number,
"is_phone_number_verified": user.verified_phone_number,
# user content, model settings
"enabled_content_source": enabled_content_sources,
"has_documents": has_documents,
"search_model_options": all_search_model_options,
"selected_search_model_config": current_search_model_option.id,
"chat_model_options": chat_model_options,
"selected_chat_model_config": selected_chat_model_config.id if selected_chat_model_config else None,
"paint_model_options": all_paint_model_options,
"selected_paint_model_config": selected_paint_model_config.id if selected_paint_model_config else None,
"voice_model_options": voice_model_options,
"selected_voice_config": selected_voice_config.model_id if selected_voice_config else None,
# user billing info
"subscription_state": user_subscription_state,
"subscription_renewal_date": subscription_renewal_date,
# server settings
"khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"),
"billing_enabled": state.billing_enabled,
"is_eleven_labs_enabled": eleven_labs_enabled,
"is_twilio_enabled": is_twilio_enabled(),
"khoj_version": state.khoj_version,
"anonymous_mode": state.anonymous_mode,
"notion_oauth_url": notion_oauth_url,
}
def configure_content(
files: Optional[dict[str, dict[str, str]]],
regenerate: bool = False,
t: Optional[state.SearchType] = state.SearchType.All,
full_corpus: bool = True,
user: KhojUser = None,
) -> bool:
success = True
if t == None:
t = state.SearchType.All
if t is not None and t in [type.value for type in state.SearchType]:
t = state.SearchType(t)
if t is not None and not t.value in [type.value for type in state.SearchType]:
logger.warning(f"🚨 Invalid search type: {t}")
return False
search_type = t.value if t else None
no_documents = all([not files.get(file_type) for file_type in files])
if files is None:
logger.warning(f"🚨 No files to process for {search_type} search.")
return True
try:
# Initialize Org Notes Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Org.value) and files["org"]:
logger.info("🦄 Setting up search for orgmode notes")
# Extract Entries, Generate Notes Embeddings
text_search.setup(
OrgToEntries,
files.get("org"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup org: {e}", exc_info=True)
success = False
try:
# Initialize Markdown Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Markdown.value) and files[
"markdown"
]:
logger.info("💎 Setting up search for markdown notes")
# Extract Entries, Generate Markdown Embeddings
text_search.setup(
MarkdownToEntries,
files.get("markdown"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup markdown: {e}", exc_info=True)
success = False
try:
# Initialize PDF Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Pdf.value) and files["pdf"]:
logger.info("🖨️ Setting up search for pdf")
# Extract Entries, Generate PDF Embeddings
text_search.setup(
PdfToEntries,
files.get("pdf"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup PDF: {e}", exc_info=True)
success = False
try:
# Initialize Plaintext Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Plaintext.value) and files[
"plaintext"
]:
logger.info("📄 Setting up search for plaintext")
# Extract Entries, Generate Plaintext Embeddings
text_search.setup(
PlaintextToEntries,
files.get("plaintext"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup plaintext: {e}", exc_info=True)
success = False
try:
if no_documents:
github_config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first()
if (
search_type == state.SearchType.All.value or search_type == state.SearchType.Github.value
) and github_config is not None:
logger.info("🐙 Setting up search for github")
# Extract Entries, Generate Github Embeddings
text_search.setup(
GithubToEntries,
None,
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
config=github_config,
)
except Exception as e:
logger.error(f"🚨 Failed to setup GitHub: {e}", exc_info=True)
success = False
try:
if no_documents:
# Initialize Notion Search
notion_config = NotionConfig.objects.filter(user=user).first()
if (
search_type == state.SearchType.All.value or search_type == state.SearchType.Notion.value
) and notion_config:
logger.info("🔌 Setting up search for notion")
text_search.setup(
NotionToEntries,
None,
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
config=notion_config,
)
except Exception as e:
logger.error(f"🚨 Failed to setup Notion: {e}", exc_info=True)
success = False
try:
# Initialize Image Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Image.value) and files[
"image"
]:
logger.info("🖼️ Setting up search for images")
# Extract Entries, Generate Image Embeddings
text_search.setup(
ImageToEntries,
files.get("image"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup images: {e}", exc_info=True)
success = False
try:
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Docx.value) and files["docx"]:
logger.info("📄 Setting up search for docx")
text_search.setup(
DocxToEntries,
files.get("docx"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup docx: {e}", exc_info=True)
success = False
# Invalidate Query Cache
if user:
state.query_cache[user.uuid] = LRU()
return success
def get_notion_auth_url(user: KhojUser):
if not NOTION_OAUTH_CLIENT_ID or not NOTION_OAUTH_CLIENT_SECRET or not NOTION_REDIRECT_URI:
return None
return f"https://api.notion.com/v1/oauth/authorize?client_id={NOTION_OAUTH_CLIENT_ID}&redirect_uri={NOTION_REDIRECT_URI}&response_type=code&state={user.uuid}"

View file

@ -6,20 +6,14 @@ from fastapi import APIRouter, Depends, Header, Request, Response, UploadFile
from pydantic import BaseModel
from starlette.authentication import requires
from khoj.database.models import GithubConfig, KhojUser, NotionConfig
from khoj.processor.content.docx.docx_to_entries import DocxToEntries
from khoj.processor.content.github.github_to_entries import GithubToEntries
from khoj.processor.content.images.image_to_entries import ImageToEntries
from khoj.processor.content.markdown.markdown_to_entries import MarkdownToEntries
from khoj.processor.content.notion.notion_to_entries import NotionToEntries
from khoj.processor.content.org_mode.org_to_entries import OrgToEntries
from khoj.processor.content.pdf.pdf_to_entries import PdfToEntries
from khoj.processor.content.plaintext.plaintext_to_entries import PlaintextToEntries
from khoj.routers.helpers import ApiIndexedDataLimiter, update_telemetry_state
from khoj.search_type import text_search
from khoj.routers.helpers import (
ApiIndexedDataLimiter,
configure_content,
update_telemetry_state,
)
from khoj.utils import constants, state
from khoj.utils.config import SearchModels
from khoj.utils.helpers import LRU, get_file_type
from khoj.utils.helpers import get_file_type
from khoj.utils.rawconfig import ContentConfig, FullConfig, SearchConfig
from khoj.utils.yaml import save_config_to_file_updated_state
@ -170,180 +164,3 @@ def configure_search(search_models: SearchModels, search_config: Optional[Search
search_models = SearchModels()
return search_models
def configure_content(
files: Optional[dict[str, dict[str, str]]],
regenerate: bool = False,
t: Optional[state.SearchType] = state.SearchType.All,
full_corpus: bool = True,
user: KhojUser = None,
) -> bool:
success = True
if t == None:
t = state.SearchType.All
if t is not None and t in [type.value for type in state.SearchType]:
t = state.SearchType(t)
if t is not None and not t.value in [type.value for type in state.SearchType]:
logger.warning(f"🚨 Invalid search type: {t}")
return False
search_type = t.value if t else None
no_documents = all([not files.get(file_type) for file_type in files])
if files is None:
logger.warning(f"🚨 No files to process for {search_type} search.")
return True
try:
# Initialize Org Notes Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Org.value) and files["org"]:
logger.info("🦄 Setting up search for orgmode notes")
# Extract Entries, Generate Notes Embeddings
text_search.setup(
OrgToEntries,
files.get("org"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup org: {e}", exc_info=True)
success = False
try:
# Initialize Markdown Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Markdown.value) and files[
"markdown"
]:
logger.info("💎 Setting up search for markdown notes")
# Extract Entries, Generate Markdown Embeddings
text_search.setup(
MarkdownToEntries,
files.get("markdown"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup markdown: {e}", exc_info=True)
success = False
try:
# Initialize PDF Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Pdf.value) and files["pdf"]:
logger.info("🖨️ Setting up search for pdf")
# Extract Entries, Generate PDF Embeddings
text_search.setup(
PdfToEntries,
files.get("pdf"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup PDF: {e}", exc_info=True)
success = False
try:
# Initialize Plaintext Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Plaintext.value) and files[
"plaintext"
]:
logger.info("📄 Setting up search for plaintext")
# Extract Entries, Generate Plaintext Embeddings
text_search.setup(
PlaintextToEntries,
files.get("plaintext"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup plaintext: {e}", exc_info=True)
success = False
try:
if no_documents:
github_config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first()
if (
search_type == state.SearchType.All.value or search_type == state.SearchType.Github.value
) and github_config is not None:
logger.info("🐙 Setting up search for github")
# Extract Entries, Generate Github Embeddings
text_search.setup(
GithubToEntries,
None,
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
config=github_config,
)
except Exception as e:
logger.error(f"🚨 Failed to setup GitHub: {e}", exc_info=True)
success = False
try:
if no_documents:
# Initialize Notion Search
notion_config = NotionConfig.objects.filter(user=user).first()
if (
search_type == state.SearchType.All.value or search_type == state.SearchType.Notion.value
) and notion_config:
logger.info("🔌 Setting up search for notion")
text_search.setup(
NotionToEntries,
None,
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
config=notion_config,
)
except Exception as e:
logger.error(f"🚨 Failed to setup Notion: {e}", exc_info=True)
success = False
try:
# Initialize Image Search
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Image.value) and files[
"image"
]:
logger.info("🖼️ Setting up search for images")
# Extract Entries, Generate Image Embeddings
text_search.setup(
ImageToEntries,
files.get("image"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup images: {e}", exc_info=True)
success = False
try:
if (search_type == state.SearchType.All.value or search_type == state.SearchType.Docx.value) and files["docx"]:
logger.info("📄 Setting up search for docx")
text_search.setup(
DocxToEntries,
files.get("docx"),
regenerate=regenerate,
full_corpus=full_corpus,
user=user,
)
except Exception as e:
logger.error(f"🚨 Failed to setup docx: {e}", exc_info=True)
success = False
# Invalidate Query Cache
if user:
state.query_cache[user.uuid] = LRU()
return success

View file

@ -11,7 +11,7 @@ from starlette.responses import RedirectResponse
from khoj.database.adapters import aget_user_by_uuid
from khoj.database.models import KhojUser, NotionConfig
from khoj.routers.indexer import configure_content
from khoj.routers.helpers import configure_content
from khoj.utils.state import SearchType
NOTION_OAUTH_CLIENT_ID = os.getenv("NOTION_OAUTH_CLIENT_ID")
@ -25,12 +25,6 @@ executor = ThreadPoolExecutor()
logger = logging.getLogger(__name__)
def get_notion_auth_url(user: KhojUser):
if not NOTION_OAUTH_CLIENT_ID or not NOTION_OAUTH_CLIENT_SECRET or not NOTION_REDIRECT_URI:
return None
return f"https://api.notion.com/v1/oauth/authorize?client_id={NOTION_OAUTH_CLIENT_ID}&redirect_uri={NOTION_REDIRECT_URI}&response_type=code&state={user.uuid}"
async def run_in_executor(func, *args):
loop = asyncio.get_event_loop()
return await loop.run_in_executor(executor, func, *args)

View file

@ -1,30 +1,21 @@
# System Packages
import json
import os
from datetime import timedelta
from typing import Optional
from fastapi import APIRouter, Request
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from starlette.authentication import has_required_scope, requires
from starlette.authentication import requires
from khoj.database import adapters
from khoj.database.adapters import (
AgentAdapters,
ConversationAdapters,
EntryAdapters,
PublicConversationAdapters,
get_user_github_config,
get_user_name,
get_user_notion_config,
get_user_subscription_state,
)
from khoj.database.models import KhojUser
from khoj.processor.speech.text_to_speech import is_eleven_labs_enabled
from khoj.routers.helpers import get_next_url
from khoj.routers.notion import get_notion_auth_url
from khoj.routers.twilio import is_twilio_enabled
from khoj.routers.helpers import get_next_url, get_user_config
from khoj.utils import constants, state
from khoj.utils.rawconfig import (
GithubContentConfig,
@ -42,80 +33,36 @@ templates = Jinja2Templates([constants.web_directory, constants.next_js_director
@requires(["authenticated"], redirect="login_page")
def index(request: Request):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_config = get_user_config(user, request)
return templates.TemplateResponse(
"chat.html",
context={
"request": request,
"username": user.username,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
},
)
return templates.TemplateResponse("chat.html", context=user_config)
@web_client.post("/", response_class=FileResponse)
@requires(["authenticated"], redirect="login_page")
def index_post(request: Request):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_config = get_user_config(user, request)
return templates.TemplateResponse(
"chat.html",
context={
"request": request,
"username": user.username,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
},
)
return templates.TemplateResponse("chat.html", context=user_config)
@web_client.get("/search", response_class=FileResponse)
@requires(["authenticated"], redirect="login_page")
def search_page(request: Request):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_config = get_user_config(user, request)
return templates.TemplateResponse(
"search.html",
context={
"request": request,
"username": user.username,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
},
)
return templates.TemplateResponse("search.html", context=user_config)
@web_client.get("/chat", response_class=FileResponse)
@requires(["authenticated"], redirect="login_page")
def chat_page(request: Request):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_config = get_user_config(user, request)
return templates.TemplateResponse(
"chat.html",
context={
"request": request,
"username": user.username,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
},
)
return templates.TemplateResponse("chat.html", context=user_config)
@web_client.get("/experimental", response_class=FileResponse)
@ -169,25 +116,14 @@ def agents_page(request: Request):
@web_client.get("/agent/{agent_slug}", response_class=HTMLResponse)
def agent_page(request: Request, agent_slug: str):
user: KhojUser = request.user.object if request.user.is_authenticated else None
user_picture = request.session.get("user", {}).get("picture") if user else None
user_config = get_user_config(user, request)
agent = AgentAdapters.get_agent_by_slug(agent_slug)
has_documents = EntryAdapters.user_has_entries(user=user)
if agent == None:
return templates.TemplateResponse(
"404.html",
context={
"request": request,
"khoj_version": state.khoj_version,
"username": user.username if user else None,
"has_documents": False,
"is_active": has_required_scope(request, ["premium"]),
"user_photo": user_picture,
},
)
user_config["has_documents"] = False
return templates.TemplateResponse("404.html", context=user_config)
agent_metadata = {
user_config["agent"] = {
"slug": agent.slug,
"avatar": agent.avatar,
"name": agent.name,
@ -199,115 +135,23 @@ def agent_page(request: Request, agent_slug: str):
"creator_not_self": agent.creator != user,
}
return templates.TemplateResponse(
"agent.html",
context={
"request": request,
"agent": agent_metadata,
"khoj_version": state.khoj_version,
"username": user.username if user else None,
"has_documents": has_documents,
"is_active": has_required_scope(request, ["premium"]),
"user_photo": user_picture,
},
)
return templates.TemplateResponse("agent.html", context=user_config)
@web_client.get("/config", response_class=HTMLResponse)
@web_client.get("/settings", response_class=HTMLResponse)
@requires(["authenticated"], redirect="login_page")
def config_page(request: Request):
user: KhojUser = request.user.object
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_config = get_user_config(user, request, is_detailed=True)
user_subscription_state = get_user_subscription_state(user.email)
user_subscription = adapters.get_user_subscription(user.email)
subscription_renewal_date = (
user_subscription.renewal_date.strftime("%d %b %Y")
if user_subscription and user_subscription.renewal_date
else (user_subscription.created_at + timedelta(days=7)).strftime("%d %b %Y")
)
given_name = get_user_name(user)
enabled_content_source = set(EntryAdapters.get_unique_file_sources(user))
successfully_configured = {
"computer": ("computer" in enabled_content_source),
"github": ("github" in enabled_content_source),
"notion": ("notion" in enabled_content_source),
}
selected_conversation_config = ConversationAdapters.get_conversation_config(user)
conversation_options = ConversationAdapters.get_conversation_processor_options().all()
all_conversation_options = list()
for conversation_option in conversation_options:
all_conversation_options.append({"chat_model": conversation_option.chat_model, "id": conversation_option.id})
search_model_options = adapters.get_or_create_search_models().all()
all_search_model_options = list()
for search_model_option in search_model_options:
all_search_model_options.append({"name": search_model_option.name, "id": search_model_option.id})
current_search_model_option = adapters.get_user_search_model_or_default(user)
selected_paint_model_config = ConversationAdapters.get_user_text_to_image_model_config(user)
paint_model_options = ConversationAdapters.get_text_to_image_model_options().all()
all_paint_model_options = list()
for paint_model in paint_model_options:
all_paint_model_options.append({"model_name": paint_model.model_name, "id": paint_model.id})
notion_oauth_url = get_notion_auth_url(user)
eleven_labs_enabled = is_eleven_labs_enabled()
voice_models = ConversationAdapters.get_voice_model_options()
voice_model_options = list()
for voice_model in voice_models:
voice_model_options.append({"name": voice_model.name, "id": voice_model.model_id})
if len(voice_model_options) == 0:
eleven_labs_enabled = False
selected_voice_config = ConversationAdapters.get_voice_model_config(user)
return templates.TemplateResponse(
"config.html",
context={
"request": request,
"current_model_state": successfully_configured,
"anonymous_mode": state.anonymous_mode,
"username": user.username,
"given_name": given_name,
"search_model_options": all_search_model_options,
"selected_search_model_config": current_search_model_option.id,
"conversation_options": all_conversation_options,
"selected_conversation_config": selected_conversation_config.id if selected_conversation_config else None,
"paint_model_options": all_paint_model_options,
"selected_paint_model_config": selected_paint_model_config.id if selected_paint_model_config else None,
"user_photo": user_picture,
"billing_enabled": state.billing_enabled,
"subscription_state": user_subscription_state,
"subscription_renewal_date": subscription_renewal_date,
"khoj_cloud_subscription_url": os.getenv("KHOJ_CLOUD_SUBSCRIPTION_URL"),
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"is_twilio_enabled": is_twilio_enabled(),
"is_eleven_labs_enabled": eleven_labs_enabled,
"voice_model_options": voice_model_options,
"selected_voice_config": selected_voice_config.model_id if selected_voice_config else None,
"phone_number": user.phone_number,
"is_phone_number_verified": user.verified_phone_number,
"khoj_version": state.khoj_version,
"notion_oauth_url": notion_oauth_url,
},
)
return templates.TemplateResponse("settings.html", context=user_config)
@web_client.get("/config/content-source/github", response_class=HTMLResponse)
@web_client.get("/settings/content/github", response_class=HTMLResponse)
@requires(["authenticated"], redirect="login_page")
def github_config_page(request: Request):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_config = get_user_config(user, request)
current_github_config = get_user_github_config(user)
if current_github_config:
@ -329,66 +173,32 @@ def github_config_page(request: Request):
else:
current_config = {} # type: ignore
return templates.TemplateResponse(
"content_source_github_input.html",
context={
"request": request,
"current_config": current_config,
"username": user.username,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
},
)
user_config["current_config"] = current_config
return templates.TemplateResponse("content_source_github_input.html", context=user_config)
@web_client.get("/config/content-source/notion", response_class=HTMLResponse)
@web_client.get("/settings/content/notion", response_class=HTMLResponse)
@requires(["authenticated"], redirect="login_page")
def notion_config_page(request: Request):
user = request.user.object
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user)
user_config = get_user_config(user, request)
current_notion_config = get_user_notion_config(user)
current_config = NotionContentConfig(
token=current_notion_config.token if current_notion_config else "",
)
token = current_notion_config.token if current_notion_config else ""
current_config = NotionContentConfig(token=token)
current_config = json.loads(current_config.model_dump_json())
return templates.TemplateResponse(
"content_source_notion_input.html",
context={
"request": request,
"current_config": current_config,
"username": user.username,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
},
)
user_config["current_config"] = current_config
return templates.TemplateResponse("content_source_notion_input.html", context=user_config)
@web_client.get("/config/content-source/computer", response_class=HTMLResponse)
@web_client.get("/settings/content/computer", response_class=HTMLResponse)
@requires(["authenticated"], redirect="login_page")
def computer_config_page(request: Request):
user = request.user.object if request.user.is_authenticated else None
user_picture = request.session.get("user", {}).get("picture") if user else None
has_documents = EntryAdapters.user_has_entries(user=user) if user else False
user_config = get_user_config(user, request)
return templates.TemplateResponse(
"content_source_computer_input.html",
context={
"request": request,
"username": user.username,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
},
)
return templates.TemplateResponse("content_source_computer_input.html", context=user_config)
@web_client.get("/share/chat/{public_conversation_slug}", response_class=HTMLResponse)
@ -404,8 +214,9 @@ def view_public_conversation(request: Request):
},
)
user = request.user.object if request.user.is_authenticated else None
user_picture = request.session.get("user", {}).get("picture") if user else None
has_documents = EntryAdapters.user_has_entries(user=user) if user else False
user_config = get_user_config(user, request)
user_config["public_conversation_slug"] = public_conversation_slug
user_config["google_client_id"] = os.environ.get("GOOGLE_CLIENT_ID")
all_agents = AgentAdapters.get_all_accessible_agents(request.user.object if request.user.is_authenticated else None)
@ -420,28 +231,15 @@ def view_public_conversation(request: Request):
"name": agent.name,
}
)
user_config["agents"] = agents_packet
google_client_id = os.environ.get("GOOGLE_CLIENT_ID")
redirect_uri = str(request.app.url_path_for("auth"))
next_url = str(
request.app.url_path_for("view_public_conversation", public_conversation_slug=public_conversation_slug)
)
user_config["redirect_uri"] = f"{redirect_uri}?next={next_url}"
return templates.TemplateResponse(
"public_conversation.html",
context={
"request": request,
"username": user.username if user else None,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
"public_conversation_slug": public_conversation_slug,
"agents": agents_packet,
"google_client_id": google_client_id,
"redirect_uri": f"{redirect_uri}?next={next_url}",
},
)
return templates.TemplateResponse("public_conversation.html", context=user_config)
@web_client.get("/automations", response_class=HTMLResponse)
@ -452,20 +250,9 @@ def automations_config_page(
queryToRun: Optional[str] = None,
):
user = request.user.object if request.user.is_authenticated else None
user_picture = request.session.get("user", {}).get("picture")
has_documents = EntryAdapters.user_has_entries(user=user) if user else False
user_config = get_user_config(user, request)
user_config["subject"] = subject if subject else ""
user_config["crontime"] = crontime if crontime else ""
user_config["queryToRun"] = queryToRun if queryToRun else ""
return templates.TemplateResponse(
"config_automation.html",
context={
"request": request,
"username": user.username if user else None,
"user_photo": user_picture,
"is_active": has_required_scope(request, ["premium"]),
"has_documents": has_documents,
"khoj_version": state.khoj_version,
"subject": subject if subject else "",
"crontime": crontime if crontime else "",
"queryToRun": queryToRun if queryToRun else "",
},
)
return templates.TemplateResponse("config_automation.html", context=user_config)

View file

@ -269,7 +269,7 @@ def test_get_api_config_types(client, sample_org_data, default_user: KhojUser):
text_search.setup(OrgToEntries, sample_org_data, regenerate=False, user=default_user)
# Act
response = client.get(f"/api/config/types", headers=headers)
response = client.get(f"/api/configure/types", headers=headers)
# Assert
assert response.status_code == 200
@ -289,7 +289,7 @@ def test_get_configured_types_with_no_content_config(fastapi_app: FastAPI):
client = TestClient(fastapi_app)
# Act
response = client.get(f"/api/config/types")
response = client.get(f"/api/configure/types")
# Assert
assert response.status_code == 200