From c125995d94a9f973cf1aed9500418389d5bdf592 Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Sat, 14 Oct 2023 19:39:13 -0700 Subject: [PATCH 001/194] [Multi-User]: Part 0 - Add support for logging in with Google (#487) * Add concept of user authentication to the request session via GoogleUser --- .gitignore | 1 + Dockerfile | 2 + pyproject.toml | 4 + src/app/__init__.py | 0 src/{khoj => app}/main.py | 36 +++-- src/app/settings.py | 129 ++++++++++++++++++ src/app/urls.py | 25 ++++ src/app/wsgi.py | 16 +++ src/database/__init__.py | 0 src/database/adapters/__init__.py | 78 +++++++++++ src/database/admin.py | 8 ++ src/database/apps.py | 6 + src/database/migrations/0001_khojuser.py | 98 +++++++++++++ src/database/migrations/0002_googleuser.py | 32 +++++ .../0003_user_khoj_configurations_and_more.py | 79 +++++++++++ src/database/migrations/__init__.py | 0 src/database/models/__init__.py | 53 +++++++ src/khoj/configure.py | 47 ++++++- src/khoj/interface/web/index.html | 6 +- src/khoj/routers/auth.py | 59 ++++++++ src/khoj/utils/fs_syncer.py | 4 + src/manage.py | 22 +++ tests/conftest.py | 12 +- tests/test_client.py | 2 +- 24 files changed, 702 insertions(+), 17 deletions(-) create mode 100644 src/app/__init__.py rename src/{khoj => app}/main.py (78%) create mode 100644 src/app/settings.py create mode 100644 src/app/urls.py create mode 100644 src/app/wsgi.py create mode 100644 src/database/__init__.py create mode 100644 src/database/adapters/__init__.py create mode 100644 src/database/admin.py create mode 100644 src/database/apps.py create mode 100644 src/database/migrations/0001_khojuser.py create mode 100644 src/database/migrations/0002_googleuser.py create mode 100644 src/database/migrations/0003_user_khoj_configurations_and_more.py create mode 100644 src/database/migrations/__init__.py create mode 100644 src/database/models/__init__.py create mode 100644 src/khoj/routers/auth.py create mode 100755 src/manage.py diff --git a/.gitignore b/.gitignore index 8e99392c..e3e93428 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ khoj_assistant.egg-info /config/khoj*.yml .pytest_cache khoj.log +static # Obsidian plugin artifacts # --- diff --git a/Dockerfile b/Dockerfile index bdf9647f..af271537 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj # Install System Dependencies RUN apt update -y && apt -y install python3-pip git +WORKDIR /app + # Install Application COPY . . RUN sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && \ diff --git a/pyproject.toml b/pyproject.toml index a52fc9b6..12be01cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,8 +59,12 @@ dependencies = [ "bs4 >= 0.0.1", "anyio == 3.7.1", "pymupdf >= 1.23.3", + "django == 4.2.5", + "authlib == 1.2.1", "gpt4all == 1.0.12; platform_system == 'Linux' and platform_machine == 'x86_64'", "gpt4all == 1.0.12; platform_system == 'Windows' or platform_system == 'Darwin'", + "itsdangerous == 2.1.2", + "httpx == 0.25.0", ] dynamic = ["version"] diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/khoj/main.py b/src/app/main.py similarity index 78% rename from src/khoj/main.py rename to src/app/main.py index 6710ed05..0049f157 100644 --- a/src/khoj/main.py +++ b/src/app/main.py @@ -3,11 +3,6 @@ import os import sys import locale -if sys.stdout is None: - sys.stdout = open(os.devnull, "w") -if sys.stderr is None: - sys.stderr = open(os.devnull, "w") - import logging import threading import warnings @@ -19,18 +14,33 @@ warnings.filterwarnings("ignore", message=r"legacy way to download files from th # External Packages import uvicorn -from fastapi import FastAPI -from rich.logging import RichHandler +import django import schedule +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from rich.logging import RichHandler +from django.core.asgi import get_asgi_application +from django.core.management import call_command + # Internal Packages -from khoj.configure import configure_routes, initialize_server +from khoj.configure import configure_routes, initialize_server, configure_middleware from khoj.utils import state from khoj.utils.cli import cli +# Initialize Django +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") +django.setup() + +# Initialize Django Database +call_command("migrate", "--noinput") + # Initialize the Application Server app = FastAPI() +# Get Django Application +django_app = get_asgi_application() + # Set Locale locale.setlocale(locale.LC_ALL, "") @@ -72,7 +82,15 @@ def run(): # Start Server configure_routes(app) - initialize_server(args.config, required=False) + + # Mount Django and Static Files + app.mount("/django", django_app, name="django") + app.mount("/static", StaticFiles(directory="static"), name="static") + + # Configure Middleware + configure_middleware(app) + + initialize_server(args.config) start_server(app, host=args.host, port=args.port, socket=args.socket) diff --git a/src/app/settings.py b/src/app/settings.py new file mode 100644 index 00000000..74c496a7 --- /dev/null +++ b/src/app/settings.py @@ -0,0 +1,129 @@ +""" +Django settings for app project. + +Generated by 'django-admin startproject' using Django 4.2.5. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path +import os + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.getenv("DJANGO_SECRET_KEY") + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.auth", + "django.contrib.contenttypes", + "database.apps.DatabaseConfig", + "django.contrib.admin", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "app.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "APP_DIRS": True, + "DIRS": [os.path.join(BASE_DIR, "templates"), os.path.join(BASE_DIR, "templates", "account")], + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "app.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + +# User Settings +AUTH_USER_MODEL = "database.KhojUser" + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_ROOT = os.path.join(BASE_DIR, "static") +STATICFILES_DIRS = [os.path.join(BASE_DIR, "khoj/interface/web")] +STATIC_URL = "/static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/src/app/urls.py b/src/app/urls.py new file mode 100644 index 00000000..fbd67a4e --- /dev/null +++ b/src/app/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for app project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +urlpatterns = [ + path("admin/", admin.site.urls), +] + +urlpatterns += staticfiles_urlpatterns() diff --git a/src/app/wsgi.py b/src/app/wsgi.py new file mode 100644 index 00000000..cbdf4342 --- /dev/null +++ b/src/app/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for app project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") + +application = get_wsgi_application() diff --git a/src/database/__init__.py b/src/database/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py new file mode 100644 index 00000000..a72323ae --- /dev/null +++ b/src/database/adapters/__init__.py @@ -0,0 +1,78 @@ +from typing import Type, TypeVar +import uuid + +from django.db import models +from django.contrib.sessions.backends.db import SessionStore + +# Import sync_to_async from Django Channels +from asgiref.sync import sync_to_async + +from fastapi import HTTPException + +from database.models import KhojUser, GoogleUser, NotionConfig + +ModelType = TypeVar("ModelType", bound=models.Model) + + +async def retrieve_object(model_class: Type[ModelType], id: int) -> ModelType: + instance = await model_class.objects.filter(id=id).afirst() + if not instance: + raise HTTPException(status_code=404, detail=f"{model_class.__name__} not found") + return instance + + +async def set_notion_config(token: str, user: KhojUser): + notion_config = await NotionConfig.objects.filter(user=user).afirst() + if not notion_config: + notion_config = await NotionConfig.objects.acreate(token=token, user=user) + else: + notion_config.token = token + await notion_config.asave() + return notion_config + + +async def get_or_create_user(token: dict) -> KhojUser: + user = await get_user_by_token(token) + if not user: + user = await create_google_user(token) + return user + + +async def create_google_user(token: dict) -> KhojUser: + user_info = token.get("userinfo") + user = await KhojUser.objects.acreate( + username=user_info.get("email"), email=user_info.get("email"), uuid=uuid.uuid4() + ) + await user.asave() + await GoogleUser.objects.acreate( + sub=user_info.get("sub"), + azp=user_info.get("azp"), + email=user_info.get("email"), + name=user_info.get("name"), + given_name=user_info.get("given_name"), + family_name=user_info.get("family_name"), + picture=user_info.get("picture"), + locale=user_info.get("locale"), + user=user, + ) + + return user + + +async def get_user_by_token(token: dict) -> KhojUser: + user_info = token.get("userinfo") + google_user = await GoogleUser.objects.filter(sub=user_info.get("sub")).select_related("user").afirst() + if not google_user: + return None + return google_user.user + + +async def retrieve_user(session_id: str) -> KhojUser: + session = SessionStore(session_key=session_id) + if not await sync_to_async(session.exists)(session_key=session_id): + raise HTTPException(status_code=401, detail="Invalid session") + session_data = await sync_to_async(session.load)() + user = await KhojUser.objects.filter(id=session_data.get("_auth_user_id")).afirst() + if not user: + raise HTTPException(status_code=401, detail="Invalid user") + return user diff --git a/src/database/admin.py b/src/database/admin.py new file mode 100644 index 00000000..d09b0ea6 --- /dev/null +++ b/src/database/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + +# Register your models here. + +from database.models import KhojUser + +admin.site.register(KhojUser, UserAdmin) diff --git a/src/database/apps.py b/src/database/apps.py new file mode 100644 index 00000000..a3b71b13 --- /dev/null +++ b/src/database/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DatabaseConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "database" diff --git a/src/database/migrations/0001_khojuser.py b/src/database/migrations/0001_khojuser.py new file mode 100644 index 00000000..f1420575 --- /dev/null +++ b/src/database/migrations/0001_khojuser.py @@ -0,0 +1,98 @@ +# Generated by Django 4.2.5 on 2023-09-14 19:00 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + run_before = [ + ("admin", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="KhojUser", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("password", models.CharField(max_length=128, verbose_name="password")), + ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name="username", + ), + ), + ("first_name", models.CharField(blank=True, max_length=150, verbose_name="first name")), + ("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")), + ("email", models.EmailField(blank=True, max_length=254, verbose_name="email address")), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/src/database/migrations/0002_googleuser.py b/src/database/migrations/0002_googleuser.py new file mode 100644 index 00000000..478770d6 --- /dev/null +++ b/src/database/migrations/0002_googleuser.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.4 on 2023-09-18 23:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0001_khojuser"), + ] + + operations = [ + migrations.CreateModel( + name="GoogleUser", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("sub", models.CharField(max_length=200)), + ("azp", models.CharField(max_length=200)), + ("email", models.CharField(max_length=200)), + ("name", models.CharField(max_length=200)), + ("given_name", models.CharField(max_length=200)), + ("family_name", models.CharField(max_length=200)), + ("picture", models.CharField(max_length=200)), + ("locale", models.CharField(max_length=200)), + ( + "user", + models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ], + ), + ] diff --git a/src/database/migrations/0003_user_khoj_configurations_and_more.py b/src/database/migrations/0003_user_khoj_configurations_and_more.py new file mode 100644 index 00000000..537ba4c4 --- /dev/null +++ b/src/database/migrations/0003_user_khoj_configurations_and_more.py @@ -0,0 +1,79 @@ +# Generated by Django 4.2.5 on 2023-09-27 17:52 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0002_googleuser"), + ] + + operations = [ + migrations.CreateModel( + name="Configuration", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ], + ), + migrations.CreateModel( + name="ConversationProcessorConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("conversation", models.JSONField()), + ("enable_offline_chat", models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name="GithubConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("pat_token", models.CharField(max_length=200)), + ("compressed_jsonl", models.CharField(max_length=300)), + ("embeddings_file", models.CharField(max_length=300)), + ( + "config", + models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="database.configuration"), + ), + ], + ), + migrations.AddField( + model_name="khojuser", + name="uuid", + field=models.UUIDField(verbose_name=models.UUIDField(default=uuid.uuid4, editable=False)), + preserve_default=False, + ), + migrations.CreateModel( + name="NotionConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("token", models.CharField(max_length=200)), + ("compressed_jsonl", models.CharField(max_length=300)), + ("embeddings_file", models.CharField(max_length=300)), + ( + "config", + models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="database.configuration"), + ), + ], + ), + migrations.CreateModel( + name="GithubRepoConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=200)), + ("owner", models.CharField(max_length=200)), + ("branch", models.CharField(max_length=200)), + ( + "github_config", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="database.githubconfig"), + ), + ], + ), + migrations.AddField( + model_name="configuration", + name="user", + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/src/database/migrations/__init__.py b/src/database/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py new file mode 100644 index 00000000..6536671b --- /dev/null +++ b/src/database/models/__init__.py @@ -0,0 +1,53 @@ +import uuid + +from django.db import models +from django.contrib.auth.models import AbstractUser + + +class KhojUser(AbstractUser): + uuid = models.UUIDField(models.UUIDField(default=uuid.uuid4, editable=False)) + + +class GoogleUser(models.Model): + user = models.OneToOneField(KhojUser, on_delete=models.CASCADE) + sub = models.CharField(max_length=200) + azp = models.CharField(max_length=200) + email = models.CharField(max_length=200) + name = models.CharField(max_length=200) + given_name = models.CharField(max_length=200) + family_name = models.CharField(max_length=200) + picture = models.CharField(max_length=200) + locale = models.CharField(max_length=200) + + def __str__(self): + return self.name + + +class Configuration(models.Model): + user = models.OneToOneField(KhojUser, on_delete=models.CASCADE) + + +class NotionConfig(models.Model): + token = models.CharField(max_length=200) + compressed_jsonl = models.CharField(max_length=300) + embeddings_file = models.CharField(max_length=300) + config = models.OneToOneField(Configuration, on_delete=models.CASCADE) + + +class GithubConfig(models.Model): + pat_token = models.CharField(max_length=200) + compressed_jsonl = models.CharField(max_length=300) + embeddings_file = models.CharField(max_length=300) + config = models.OneToOneField(Configuration, on_delete=models.CASCADE) + + +class GithubRepoConfig(models.Model): + name = models.CharField(max_length=200) + owner = models.CharField(max_length=200) + branch = models.CharField(max_length=200) + github_config = models.ForeignKey(GithubConfig, on_delete=models.CASCADE) + + +class ConversationProcessorConfig(models.Model): + conversation = models.JSONField() + enable_offline_chat = models.BooleanField(default=False) diff --git a/src/khoj/configure.py b/src/khoj/configure.py index 7e6cc409..e0a06601 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -5,10 +5,19 @@ import json from enum import Enum from typing import Optional import requests +import os # External Packages import schedule -from fastapi.staticfiles import StaticFiles +from starlette.middleware.sessions import SessionMiddleware +from starlette.middleware.authentication import AuthenticationMiddleware + +from starlette.authentication import ( + AuthCredentials, + AuthenticationBackend, + SimpleUser, + UnauthenticatedUser, +) # Internal Packages from khoj.utils import constants, state @@ -26,8 +35,32 @@ from khoj.routers.indexer import configure_content, load_content, configure_sear logger = logging.getLogger(__name__) -def initialize_server(config: Optional[FullConfig], required=False): - if config is None and required: +class AuthenticatedKhojUser(SimpleUser): + def __init__(self, user): + self.object = user + super().__init__(user.email) + + +class UserAuthenticationBackend(AuthenticationBackend): + def __init__( + self, + ): + from database.models import KhojUser + + self.khojuser_manager = KhojUser.objects + super().__init__() + + async def authenticate(self, request): + current_user = request.session.get("user") + if current_user and current_user.get("email"): + user = await self.khojuser_manager.filter(email=current_user.get("email")).afirst() + if user: + return AuthCredentials(["authenticated"]), AuthenticatedKhojUser(user) + return AuthCredentials(), UnauthenticatedUser() + + +def initialize_server(config: Optional[FullConfig]): + if config is None: logger.error( f"🚨 Exiting as Khoj is not configured.\nConfigure it via http://{state.host}:{state.port}/config or by editing {state.config_file}." ) @@ -99,12 +132,18 @@ def configure_routes(app): from khoj.routers.api_beta import api_beta from khoj.routers.web_client import web_client from khoj.routers.indexer import indexer + from khoj.routers.auth import auth_router - app.mount("/static", StaticFiles(directory=constants.web_directory), name="static") app.include_router(api, prefix="/api") app.include_router(api_beta, prefix="/api/beta") app.include_router(indexer, prefix="/v1/indexer") app.include_router(web_client) + app.include_router(auth_router, prefix="/auth") + + +def configure_middleware(app): + app.add_middleware(AuthenticationMiddleware, backend=UserAuthenticationBackend()) + app.add_middleware(SessionMiddleware, secret_key=os.environ.get("KHOJ_DJANGO_SECRET_KEY", "!secret")) if not state.demo: diff --git a/src/khoj/interface/web/index.html b/src/khoj/interface/web/index.html index cb2bae49..581ed9b8 100644 --- a/src/khoj/interface/web/index.html +++ b/src/khoj/interface/web/index.html @@ -170,7 +170,11 @@ // Execute Search and Render Results url = createRequestUrl(query, type, results_count || 5, rerank); - fetch(url) + fetch(url, { + headers: { + "X-CSRFToken": csrfToken + } + }) .then(response => response.json()) .then(data => { console.log(data); diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py new file mode 100644 index 00000000..41bc8396 --- /dev/null +++ b/src/khoj/routers/auth.py @@ -0,0 +1,59 @@ +import logging +import json +import os +from fastapi import APIRouter +from starlette.config import Config +from starlette.requests import Request +from starlette.responses import HTMLResponse, RedirectResponse +from authlib.integrations.starlette_client import OAuth, OAuthError + +from database.adapters import get_or_create_user + +logger = logging.getLogger(__name__) + +auth_router = APIRouter() + +if not os.environ.get("GOOGLE_CLIENT_ID") or not os.environ.get("GOOGLE_CLIENT_SECRET"): + logger.info("Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables to use Google OAuth") +else: + config = Config(environ=os.environ) + + oauth = OAuth(config) + + CONF_URL = "https://accounts.google.com/.well-known/openid-configuration" + oauth.register(name="google", server_metadata_url=CONF_URL, client_kwargs={"scope": "openid email profile"}) + + +@auth_router.get("/") +async def homepage(request: Request): + user = request.session.get("user") + if user: + data = json.dumps(user) + html = f"
{data}
" 'logout' + return HTMLResponse(html) + return HTMLResponse('login') + + +@auth_router.get("/login") +async def login(request: Request): + redirect_uri = request.url_for("auth") + return await oauth.google.authorize_redirect(request, redirect_uri) + + +@auth_router.get("/redirect") +async def auth(request: Request): + try: + token = await oauth.google.authorize_access_token(request) + except OAuthError as error: + return HTMLResponse(f"

{error.error}

") + khoj_user = await get_or_create_user(token) + user = token.get("userinfo") + if user: + request.session["user"] = dict(user) + return RedirectResponse(url="/") + + +@auth_router.get("/logout") +async def logout(request: Request): + request.session.pop("user", None) + return RedirectResponse(url="/") diff --git a/src/khoj/utils/fs_syncer.py b/src/khoj/utils/fs_syncer.py index d303d39b..6a777bd7 100644 --- a/src/khoj/utils/fs_syncer.py +++ b/src/khoj/utils/fs_syncer.py @@ -13,6 +13,10 @@ logger = logging.getLogger(__name__) def collect_files(config: ContentConfig, search_type: Optional[SearchType] = SearchType.All): files = {} + + if config is None: + return files + if search_type == SearchType.All or search_type == SearchType.Org: files["org"] = get_org_files(config.org) if config.org else {} if search_type == SearchType.All or search_type == SearchType.Markdown: diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 00000000..1a64b14a --- /dev/null +++ b/src/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/conftest.py index d851341d..7c1878a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,14 +4,16 @@ from copy import deepcopy from fastapi.testclient import TestClient from pathlib import Path import pytest +from fastapi.staticfiles import StaticFiles # Internal Packages -from khoj.main import app -from khoj.configure import configure_processor, configure_routes, configure_search_types +from app.main import app +from khoj.configure import configure_processor, configure_routes, configure_search_types, configure_middleware from khoj.processor.markdown.markdown_to_jsonl import MarkdownToJsonl from khoj.processor.plaintext.plaintext_to_jsonl import PlaintextToJsonl from khoj.search_type import image_search, text_search from khoj.utils.config import SearchModels +from khoj.utils.constants import web_directory from khoj.utils.helpers import resolve_absolute_path from khoj.utils.rawconfig import ( ContentConfig, @@ -231,6 +233,8 @@ def chat_client(md_content_config: ContentConfig, search_config: SearchConfig, p state.processor_config = configure_processor(processor_config) configure_routes(app) + configure_middleware(app) + app.mount("/static", StaticFiles(directory=web_directory), name="static") return TestClient(app) @@ -264,6 +268,8 @@ def client(content_config: ContentConfig, search_config: SearchConfig, processor state.processor_config = configure_processor(processor_config) configure_routes(app) + configure_middleware(app) + app.mount("/static", StaticFiles(directory=web_directory), name="static") return TestClient(app) @@ -292,6 +298,8 @@ def client_offline_chat( state.processor_config = configure_processor(processor_config_offline_chat) configure_routes(app) + configure_middleware(app) + app.mount("/static", StaticFiles(directory=web_directory), name="static") return TestClient(app) diff --git a/tests/test_client.py b/tests/test_client.py index d2497f73..784c765c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,7 +8,7 @@ from urllib.parse import quote from fastapi.testclient import TestClient # Internal Packages -from khoj.main import app +from app.main import app from khoj.configure import configure_routes, configure_search_types from khoj.utils import state from khoj.utils.state import search_models, content_index, config From 216acf545fc2211d92c2af651e72125a2bc835bd Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Thu, 26 Oct 2023 09:42:29 -0700 Subject: [PATCH 002/194] [Multi-User Part 1]: Enable storage of settings for plaintext files based on user account (#498) - Partition configuration for indexing local data based on user accounts - Store indexed data in an underlying postgres db using the `pgvector` extension - Add migrations for all relevant user data and embeddings generation. Very little performance optimization has been done for the lookup time - Apply filters using SQL queries - Start removing many server-level configuration settings - Configure GitHub test actions to run during any PR. Update the test action to run in a containerized environment with a DB. - Update the Docker image and docker-compose.yml to work with the new application design --- .github/workflows/pre-commit.yml | 48 +++ .github/workflows/test.yml | 50 ++- Dockerfile | 11 +- docker-compose.yml | 21 + pyproject.toml | 6 +- pytest.ini | 4 + src/app/README.md | 60 +++ src/app/settings.py | 8 +- src/database/adapters/__init__.py | 169 +++++++- .../0003_user_khoj_configurations_and_more.py | 79 ---- .../migrations/0003_vector_extension.py | 10 + ...onprocessorconfig_githubconfig_and_more.py | 193 +++++++++ .../migrations/0005_embeddings_corpus_id.py | 18 + .../migrations/0006_embeddingsdates.py | 33 ++ src/database/models/__init__.py | 98 ++++- src/database/tests.py | 3 + src/khoj/configure.py | 44 +- src/khoj/interface/web/config.html | 52 ++- .../web/content_type_github_input.html | 22 - .../interface/web/content_type_input.html | 34 +- .../web/content_type_notion_input.html | 22 - src/khoj/interface/web/index.html | 6 +- src/{app => khoj}/main.py | 21 +- src/khoj/processor/embeddings.py | 57 +++ src/khoj/processor/github/github_to_jsonl.py | 49 ++- src/khoj/processor/jsonl/__init__.py | 0 src/khoj/processor/jsonl/jsonl_to_jsonl.py | 91 ----- .../processor/markdown/markdown_to_jsonl.py | 41 +- src/khoj/processor/notion/notion_to_jsonl.py | 35 +- src/khoj/processor/org_mode/org_to_jsonl.py | 40 +- src/khoj/processor/pdf/pdf_to_jsonl.py | 41 +- .../processor/plaintext/plaintext_to_jsonl.py | 37 +- src/khoj/processor/text_to_jsonl.py | 120 +++++- src/khoj/routers/api.py | 377 +++++++----------- src/khoj/routers/helpers.py | 3 + src/khoj/routers/indexer.py | 218 +++------- src/khoj/routers/web_client.py | 113 +++--- src/khoj/search_filter/base_filter.py | 13 +- src/khoj/search_filter/date_filter.py | 77 ++-- src/khoj/search_filter/file_filter.py | 59 +-- src/khoj/search_filter/word_filter.py | 61 +-- src/khoj/search_type/text_search.py | 239 ++++------- src/khoj/utils/cli.py | 9 + src/khoj/utils/config.py | 12 +- src/khoj/utils/constants.py | 1 + src/khoj/utils/fs_syncer.py | 28 +- src/khoj/utils/helpers.py | 4 +- src/khoj/utils/rawconfig.py | 27 +- src/khoj/utils/state.py | 8 +- tests/conftest.py | 195 +++++---- tests/data/config.yml | 11 - tests/test_cli.py | 11 - tests/test_client.py | 121 +++--- tests/test_date_filter.py | 45 +-- tests/test_file_filter.py | 26 +- tests/test_jsonl_to_jsonl.py | 78 ---- tests/test_org_to_jsonl.py | 6 +- tests/test_plaintext_to_jsonl.py | 6 +- tests/test_text_search.py | 320 +++++++-------- tests/test_word_filter.py | 28 -- 60 files changed, 1827 insertions(+), 1792 deletions(-) create mode 100644 .github/workflows/pre-commit.yml create mode 100644 pytest.ini create mode 100644 src/app/README.md delete mode 100644 src/database/migrations/0003_user_khoj_configurations_and_more.py create mode 100644 src/database/migrations/0003_vector_extension.py create mode 100644 src/database/migrations/0004_conversationprocessorconfig_githubconfig_and_more.py create mode 100644 src/database/migrations/0005_embeddings_corpus_id.py create mode 100644 src/database/migrations/0006_embeddingsdates.py create mode 100644 src/database/tests.py rename src/{app => khoj}/main.py (88%) create mode 100644 src/khoj/processor/embeddings.py delete mode 100644 src/khoj/processor/jsonl/__init__.py delete mode 100644 src/khoj/processor/jsonl/jsonl_to_jsonl.py delete mode 100644 tests/test_jsonl_to_jsonl.py diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..a571e8a1 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,48 @@ +name: pre-commit + +on: + pull_request: + paths: + - src/** + - tests/** + - config/** + - pyproject.toml + - .pre-commit-config.yml + - .github/workflows/test.yml + push: + branches: + - master + paths: + - src/khoj/** + - tests/** + - config/** + - pyproject.toml + - .pre-commit-config.yml + - .github/workflows/test.yml + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: ⏬️ Install Dependencies + run: | + sudo apt update && sudo apt install -y libegl1 + python -m pip install --upgrade pip + + - name: ⬇️ Install Application + run: pip install --upgrade .[dev] + + - name: 🌡️ Validate Application + run: pre-commit run --hook-stage manual --all diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8aa9be8..84fbb1aa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,10 +2,8 @@ name: test on: pull_request: - branches: - - 'master' paths: - - src/khoj/** + - src/** - tests/** - config/** - pyproject.toml @@ -13,7 +11,7 @@ on: - .github/workflows/test.yml push: branches: - - 'master' + - master paths: - src/khoj/** - tests/** @@ -26,6 +24,7 @@ jobs: test: name: Run Tests runs-on: ubuntu-latest + container: ubuntu:jammy strategy: fail-fast: false matrix: @@ -33,6 +32,17 @@ jobs: - '3.9' - '3.10' - '3.11' + + services: + postgres: + image: ankane/pgvector + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: - uses: actions/checkout@v3 with: @@ -43,17 +53,37 @@ jobs: with: python-version: ${{ matrix.python_version }} - - name: ⏬️ Install Dependencies + - name: Install Git run: | - sudo apt update && sudo apt install -y libegl1 + apt update && apt install -y git + + - name: ⏬️ Install Dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt update && apt install -y libegl1 sqlite3 libsqlite3-dev libsqlite3-0 + + - name: ⬇️ Install Postgres + env: + DEBIAN_FRONTEND: noninteractive + run : | + apt install -y postgresql postgresql-client && apt install -y postgresql-server-dev-14 + + - name: ⬇️ Install pip + run: | + apt install -y python3-pip + python -m ensurepip --upgrade python -m pip install --upgrade pip - name: ⬇️ Install Application - run: pip install --upgrade .[dev] - - - name: 🌡️ Validate Application - run: pre-commit run --hook-stage manual --all + run: sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && pip install --upgrade .[dev] - name: 🧪 Test Application + env: + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres run: pytest timeout-minutes: 10 diff --git a/Dockerfile b/Dockerfile index af271537..9882a236 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,13 +8,20 @@ RUN apt update -y && apt -y install python3-pip git WORKDIR /app # Install Application -COPY . . +COPY pyproject.toml . +COPY README.md . RUN sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && \ pip install --no-cache-dir . +# Copy Source Code +COPY . . + +# Set the PYTHONPATH environment variable in order for it to find the Django app. +ENV PYTHONPATH=/app/src:$PYTHONPATH + # Run the Application # There are more arguments required for the application to run, # but these should be passed in through the docker-compose.yml file. ARG PORT EXPOSE ${PORT} -ENTRYPOINT ["khoj"] +ENTRYPOINT ["python3", "src/khoj/main.py"] diff --git a/docker-compose.yml b/docker-compose.yml index 5f1bb1f9..d6048916 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,21 @@ version: "3.9" services: + database: + image: ankane/pgvector + ports: + - "5432:5432" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + volumes: + - khoj_db:/var/lib/postgresql/data/ server: + # Use the following line to use the latest version of khoj. Otherwise, it will build from source. image: ghcr.io/khoj-ai/khoj:latest + # Uncomment the following line to build from source. This will take a few minutes. Comment the next two lines out if you want to use the offiicial image. + # build: + # context: . ports: # If changing the local port (left hand side), no other changes required. # If changing the remote port (right hand side), @@ -26,8 +40,15 @@ services: - ./tests/data/models/:/root/.khoj/search/ - khoj_config:/root/.khoj/ # Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/ + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_HOST=database + - POSTGRES_PORT=5432 command: --host="0.0.0.0" --port=42110 -vv volumes: khoj_config: + khoj_db: diff --git a/pyproject.toml b/pyproject.toml index 34f15d4b..8732d47a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,13 +59,15 @@ dependencies = [ "requests >= 2.26.0", "bs4 >= 0.0.1", "anyio == 3.7.1", - "pymupdf >= 1.23.3", + "pymupdf >= 1.23.5", "django == 4.2.5", "authlib == 1.2.1", "gpt4all == 1.0.12; platform_system == 'Linux' and platform_machine == 'x86_64'", "gpt4all == 1.0.12; platform_system == 'Windows' or platform_system == 'Darwin'", "itsdangerous == 2.1.2", "httpx == 0.25.0", + "pgvector == 0.2.3", + "psycopg2-binary == 2.9.9", ] dynamic = ["version"] @@ -91,6 +93,8 @@ dev = [ "mypy >= 1.0.1", "black >= 23.1.0", "pre-commit >= 3.0.4", + "pytest-django == 4.5.2", + "pytest-asyncio == 0.21.1", ] [tool.hatch.version] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..eec111ec --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = app.settings +pythonpath = . src +testpaths = tests diff --git a/src/app/README.md b/src/app/README.md new file mode 100644 index 00000000..7a93ee8b --- /dev/null +++ b/src/app/README.md @@ -0,0 +1,60 @@ +# Django App + +Khoj uses Django as the backend framework primarily for its powerful ORM and the admin interface. The Django app is located in the `src/app` directory. We have one installed app, under the `/database/` directory. This app is responsible for all the database related operations and holds all of our models. You can find the extensive Django documentation [here](https://docs.djangoproject.com/en/4.2/) 🌈. + +## Setup (Docker) + +### Prerequisites +1. Ensure you have [Docker](https://docs.docker.com/get-docker/) installed. +2. Ensure you have [Docker Compose](https://docs.docker.com/compose/install/) installed. + +### Run + +Using the `docker-compose.yml` file in the root directory, you can run the Khoj app using the following command: +```bash +docker-compose up +``` + +## Setup (Local) + +### Install dependencies + +```bash +pip install -e '.[dev]' +``` + +### Setup the database + +1. Ensure you have Postgres installed. For MacOS, you can use [Postgres.app](https://postgresapp.com/). +2. If you're not using Postgres.app, you may have to install the pgvector extension manually. You can find the instructions [here](https://github.com/pgvector/pgvector#installation). If you're using Postgres.app, you can skip this step. Reproduced instructions below for convenience. + +```bash +cd /tmp +git clone --branch v0.5.1 https://github.com/pgvector/pgvector.git +cd pgvector +make +make install # may need sudo +``` +3. Create a database + +### Make migrations + +This command will create the migrations for the database app. This command should be run whenever a new model is added to the database app or an existing model is modified (updated or deleted). + +```bash +python3 src/manage.py makemigrations +``` + +### Run migrations + +This command will run any pending migrations in your application. +```bash +python3 src/manage.py migrate +``` + +### Run the server + +While we're using Django for the ORM, we're still using the FastAPI server for the API. This command automatically scaffolds the Django application in the backend. +```bash +python3 src/khoj/main.py +``` diff --git a/src/app/settings.py b/src/app/settings.py index 74c496a7..cfd7cd3c 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -77,8 +77,12 @@ WSGI_APPLICATION = "app.wsgi.application" DATABASES = { "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", + "ENGINE": "django.db.backends.postgresql", + "HOST": os.getenv("POSTGRES_HOST", "localhost"), + "PORT": os.getenv("POSTGRES_PORT", "5432"), + "USER": os.getenv("POSTGRES_USER", "postgres"), + "NAME": os.getenv("POSTGRES_DB", "khoj"), + "PASSWORD": os.getenv("POSTGRES_PASSWORD", "postgres"), } } diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index a72323ae..a7c1c6f9 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -1,15 +1,30 @@ -from typing import Type, TypeVar +from typing import Type, TypeVar, List import uuid +from datetime import date from django.db import models from django.contrib.sessions.backends.db import SessionStore +from pgvector.django import CosineDistance +from django.db.models.manager import BaseManager +from django.db.models import Q +from torch import Tensor # Import sync_to_async from Django Channels from asgiref.sync import sync_to_async from fastapi import HTTPException -from database.models import KhojUser, GoogleUser, NotionConfig +from database.models import ( + KhojUser, + GoogleUser, + NotionConfig, + GithubConfig, + Embeddings, + GithubRepoConfig, +) +from khoj.search_filter.word_filter import WordFilter +from khoj.search_filter.file_filter import FileFilter +from khoj.search_filter.date_filter import DateFilter ModelType = TypeVar("ModelType", bound=models.Model) @@ -40,9 +55,7 @@ async def get_or_create_user(token: dict) -> KhojUser: async def create_google_user(token: dict) -> KhojUser: user_info = token.get("userinfo") - user = await KhojUser.objects.acreate( - username=user_info.get("email"), email=user_info.get("email"), uuid=uuid.uuid4() - ) + user = await KhojUser.objects.acreate(username=user_info.get("email"), email=user_info.get("email")) await user.asave() await GoogleUser.objects.acreate( sub=user_info.get("sub"), @@ -76,3 +89,149 @@ async def retrieve_user(session_id: str) -> KhojUser: if not user: raise HTTPException(status_code=401, detail="Invalid user") return user + + +def get_all_users() -> BaseManager[KhojUser]: + return KhojUser.objects.all() + + +def get_user_github_config(user: KhojUser): + config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first() + if not config: + return None + return config + + +def get_user_notion_config(user: KhojUser): + config = NotionConfig.objects.filter(user=user).first() + if not config: + return None + return config + + +async def set_text_content_config(user: KhojUser, object: Type[models.Model], updated_config): + deduped_files = list(set(updated_config.input_files)) if updated_config.input_files else None + deduped_filters = list(set(updated_config.input_filter)) if updated_config.input_filter else None + await object.objects.filter(user=user).adelete() + await object.objects.acreate( + input_files=deduped_files, + input_filter=deduped_filters, + index_heading_entries=updated_config.index_heading_entries, + user=user, + ) + + +async def set_user_github_config(user: KhojUser, pat_token: str, repos: list): + config = await GithubConfig.objects.filter(user=user).afirst() + + if not config: + config = await GithubConfig.objects.acreate(pat_token=pat_token, user=user) + else: + config.pat_token = pat_token + await config.asave() + await config.githubrepoconfig.all().adelete() + + for repo in repos: + await GithubRepoConfig.objects.acreate( + name=repo["name"], owner=repo["owner"], branch=repo["branch"], github_config=config + ) + return config + + +class EmbeddingsAdapters: + word_filer = WordFilter() + file_filter = FileFilter() + date_filter = DateFilter() + + @staticmethod + def does_embedding_exist(user: KhojUser, hashed_value: str) -> bool: + return Embeddings.objects.filter(user=user, hashed_value=hashed_value).exists() + + @staticmethod + def delete_embedding_by_file(user: KhojUser, file_path: str): + deleted_count, _ = Embeddings.objects.filter(user=user, file_path=file_path).delete() + return deleted_count + + @staticmethod + def delete_all_embeddings(user: KhojUser, file_type: str): + deleted_count, _ = Embeddings.objects.filter(user=user, file_type=file_type).delete() + return deleted_count + + @staticmethod + def get_existing_entry_hashes_by_file(user: KhojUser, file_path: str): + return Embeddings.objects.filter(user=user, file_path=file_path).values_list("hashed_value", flat=True) + + @staticmethod + def delete_embedding_by_hash(user: KhojUser, hashed_values: List[str]): + Embeddings.objects.filter(user=user, hashed_value__in=hashed_values).delete() + + @staticmethod + def get_embeddings_by_date_filter(embeddings: BaseManager[Embeddings], start_date: date, end_date: date): + return embeddings.filter( + embeddingsdates__date__gte=start_date, + embeddingsdates__date__lte=end_date, + ) + + @staticmethod + async def user_has_embeddings(user: KhojUser): + return await Embeddings.objects.filter(user=user).aexists() + + @staticmethod + def apply_filters(user: KhojUser, query: str, file_type_filter: str = None): + q_filter_terms = Q() + + explicit_word_terms = EmbeddingsAdapters.word_filer.get_filter_terms(query) + file_filters = EmbeddingsAdapters.file_filter.get_filter_terms(query) + date_filters = EmbeddingsAdapters.date_filter.get_query_date_range(query) + + if len(explicit_word_terms) == 0 and len(file_filters) == 0 and len(date_filters) == 0: + return Embeddings.objects.filter(user=user) + + for term in explicit_word_terms: + if term.startswith("+"): + q_filter_terms &= Q(raw__icontains=term[1:]) + elif term.startswith("-"): + q_filter_terms &= ~Q(raw__icontains=term[1:]) + + q_file_filter_terms = Q() + + if len(file_filters) > 0: + for term in file_filters: + q_file_filter_terms |= Q(file_path__regex=term) + + q_filter_terms &= q_file_filter_terms + + if len(date_filters) > 0: + min_date, max_date = date_filters + if min_date is not None: + # Convert the min_date timestamp to yyyy-mm-dd format + formatted_min_date = date.fromtimestamp(min_date).strftime("%Y-%m-%d") + q_filter_terms &= Q(embeddings_dates__date__gte=formatted_min_date) + if max_date is not None: + # Convert the max_date timestamp to yyyy-mm-dd format + formatted_max_date = date.fromtimestamp(max_date).strftime("%Y-%m-%d") + q_filter_terms &= Q(embeddings_dates__date__lte=formatted_max_date) + + relevant_embeddings = Embeddings.objects.filter(user=user).filter( + q_filter_terms, + ) + if file_type_filter: + relevant_embeddings = relevant_embeddings.filter(file_type=file_type_filter) + return relevant_embeddings + + @staticmethod + def search_with_embeddings( + user: KhojUser, embeddings: Tensor, max_results: int = 10, file_type_filter: str = None, raw_query: str = None + ): + relevant_embeddings = EmbeddingsAdapters.apply_filters(user, raw_query, file_type_filter) + relevant_embeddings = relevant_embeddings.filter(user=user).annotate( + distance=CosineDistance("embeddings", embeddings) + ) + if file_type_filter: + relevant_embeddings = relevant_embeddings.filter(file_type=file_type_filter) + relevant_embeddings = relevant_embeddings.order_by("distance") + return relevant_embeddings[:max_results] + + @staticmethod + def get_unique_file_types(user: KhojUser): + return Embeddings.objects.filter(user=user).values_list("file_type", flat=True).distinct() diff --git a/src/database/migrations/0003_user_khoj_configurations_and_more.py b/src/database/migrations/0003_user_khoj_configurations_and_more.py deleted file mode 100644 index 537ba4c4..00000000 --- a/src/database/migrations/0003_user_khoj_configurations_and_more.py +++ /dev/null @@ -1,79 +0,0 @@ -# Generated by Django 4.2.5 on 2023-09-27 17:52 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - dependencies = [ - ("database", "0002_googleuser"), - ] - - operations = [ - migrations.CreateModel( - name="Configuration", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ], - ), - migrations.CreateModel( - name="ConversationProcessorConfig", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("conversation", models.JSONField()), - ("enable_offline_chat", models.BooleanField(default=False)), - ], - ), - migrations.CreateModel( - name="GithubConfig", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("pat_token", models.CharField(max_length=200)), - ("compressed_jsonl", models.CharField(max_length=300)), - ("embeddings_file", models.CharField(max_length=300)), - ( - "config", - models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="database.configuration"), - ), - ], - ), - migrations.AddField( - model_name="khojuser", - name="uuid", - field=models.UUIDField(verbose_name=models.UUIDField(default=uuid.uuid4, editable=False)), - preserve_default=False, - ), - migrations.CreateModel( - name="NotionConfig", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("token", models.CharField(max_length=200)), - ("compressed_jsonl", models.CharField(max_length=300)), - ("embeddings_file", models.CharField(max_length=300)), - ( - "config", - models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to="database.configuration"), - ), - ], - ), - migrations.CreateModel( - name="GithubRepoConfig", - fields=[ - ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), - ("name", models.CharField(max_length=200)), - ("owner", models.CharField(max_length=200)), - ("branch", models.CharField(max_length=200)), - ( - "github_config", - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="database.githubconfig"), - ), - ], - ), - migrations.AddField( - model_name="configuration", - name="user", - field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/src/database/migrations/0003_vector_extension.py b/src/database/migrations/0003_vector_extension.py new file mode 100644 index 00000000..9de01df2 --- /dev/null +++ b/src/database/migrations/0003_vector_extension.py @@ -0,0 +1,10 @@ +from django.db import migrations +from pgvector.django import VectorExtension + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0002_googleuser"), + ] + + operations = [VectorExtension()] diff --git a/src/database/migrations/0004_conversationprocessorconfig_githubconfig_and_more.py b/src/database/migrations/0004_conversationprocessorconfig_githubconfig_and_more.py new file mode 100644 index 00000000..294fc620 --- /dev/null +++ b/src/database/migrations/0004_conversationprocessorconfig_githubconfig_and_more.py @@ -0,0 +1,193 @@ +# Generated by Django 4.2.5 on 2023-10-11 22:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import pgvector.django +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0003_vector_extension"), + ] + + operations = [ + migrations.CreateModel( + name="ConversationProcessorConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("conversation", models.JSONField()), + ("enable_offline_chat", models.BooleanField(default=False)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="GithubConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("pat_token", models.CharField(max_length=200)), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="khojuser", + name="uuid", + field=models.UUIDField(default=1234, verbose_name=models.UUIDField(default=uuid.uuid4, editable=False)), + preserve_default=False, + ), + migrations.CreateModel( + name="NotionConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("token", models.CharField(max_length=200)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="LocalPlaintextConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("input_files", models.JSONField(default=list, null=True)), + ("input_filter", models.JSONField(default=list, null=True)), + ("index_heading_entries", models.BooleanField(default=False)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="LocalPdfConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("input_files", models.JSONField(default=list, null=True)), + ("input_filter", models.JSONField(default=list, null=True)), + ("index_heading_entries", models.BooleanField(default=False)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="LocalOrgConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("input_files", models.JSONField(default=list, null=True)), + ("input_filter", models.JSONField(default=list, null=True)), + ("index_heading_entries", models.BooleanField(default=False)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="LocalMarkdownConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("input_files", models.JSONField(default=list, null=True)), + ("input_filter", models.JSONField(default=list, null=True)), + ("index_heading_entries", models.BooleanField(default=False)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="GithubRepoConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("name", models.CharField(max_length=200)), + ("owner", models.CharField(max_length=200)), + ("branch", models.CharField(max_length=200)), + ( + "github_config", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="githubrepoconfig", + to="database.githubconfig", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="githubconfig", + name="user", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name="Embeddings", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("embeddings", pgvector.django.VectorField(dimensions=384)), + ("raw", models.TextField()), + ("compiled", models.TextField()), + ("heading", models.CharField(blank=True, default=None, max_length=1000, null=True)), + ( + "file_type", + models.CharField( + choices=[ + ("image", "Image"), + ("pdf", "Pdf"), + ("plaintext", "Plaintext"), + ("markdown", "Markdown"), + ("org", "Org"), + ("notion", "Notion"), + ("github", "Github"), + ("conversation", "Conversation"), + ], + default="plaintext", + max_length=30, + ), + ), + ("file_path", models.CharField(blank=True, default=None, max_length=400, null=True)), + ("file_name", models.CharField(blank=True, default=None, max_length=400, null=True)), + ("url", models.URLField(blank=True, default=None, max_length=400, null=True)), + ("hashed_value", models.CharField(max_length=100)), + ( + "user", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/src/database/migrations/0005_embeddings_corpus_id.py b/src/database/migrations/0005_embeddings_corpus_id.py new file mode 100644 index 00000000..47f5aa8c --- /dev/null +++ b/src/database/migrations/0005_embeddings_corpus_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.5 on 2023-10-13 02:39 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0004_conversationprocessorconfig_githubconfig_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="embeddings", + name="corpus_id", + field=models.UUIDField(default=uuid.uuid4, editable=False), + ), + ] diff --git a/src/database/migrations/0006_embeddingsdates.py b/src/database/migrations/0006_embeddingsdates.py new file mode 100644 index 00000000..9d988ed8 --- /dev/null +++ b/src/database/migrations/0006_embeddingsdates.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.5 on 2023-10-13 19:28 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0005_embeddings_corpus_id"), + ] + + operations = [ + migrations.CreateModel( + name="EmbeddingsDates", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("date", models.DateField()), + ( + "embeddings", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="embeddings_dates", + to="database.embeddings", + ), + ), + ], + options={ + "indexes": [models.Index(fields=["date"], name="database_em_date_a1ba47_idx")], + }, + ), + ] diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py index 6536671b..9a50d94f 100644 --- a/src/database/models/__init__.py +++ b/src/database/models/__init__.py @@ -2,11 +2,25 @@ import uuid from django.db import models from django.contrib.auth.models import AbstractUser +from pgvector.django import VectorField + + +class BaseModel(models.Model): + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True class KhojUser(AbstractUser): uuid = models.UUIDField(models.UUIDField(default=uuid.uuid4, editable=False)) + def save(self, *args, **kwargs): + if not self.uuid: + self.uuid = uuid.uuid4() + super().save(*args, **kwargs) + class GoogleUser(models.Model): user = models.OneToOneField(KhojUser, on_delete=models.CASCADE) @@ -23,31 +37,85 @@ class GoogleUser(models.Model): return self.name -class Configuration(models.Model): - user = models.OneToOneField(KhojUser, on_delete=models.CASCADE) - - -class NotionConfig(models.Model): +class NotionConfig(BaseModel): token = models.CharField(max_length=200) - compressed_jsonl = models.CharField(max_length=300) - embeddings_file = models.CharField(max_length=300) - config = models.OneToOneField(Configuration, on_delete=models.CASCADE) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) -class GithubConfig(models.Model): +class GithubConfig(BaseModel): pat_token = models.CharField(max_length=200) - compressed_jsonl = models.CharField(max_length=300) - embeddings_file = models.CharField(max_length=300) - config = models.OneToOneField(Configuration, on_delete=models.CASCADE) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) -class GithubRepoConfig(models.Model): +class GithubRepoConfig(BaseModel): name = models.CharField(max_length=200) owner = models.CharField(max_length=200) branch = models.CharField(max_length=200) - github_config = models.ForeignKey(GithubConfig, on_delete=models.CASCADE) + github_config = models.ForeignKey(GithubConfig, on_delete=models.CASCADE, related_name="githubrepoconfig") -class ConversationProcessorConfig(models.Model): +class LocalOrgConfig(BaseModel): + input_files = models.JSONField(default=list, null=True) + input_filter = models.JSONField(default=list, null=True) + index_heading_entries = models.BooleanField(default=False) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + + +class LocalMarkdownConfig(BaseModel): + input_files = models.JSONField(default=list, null=True) + input_filter = models.JSONField(default=list, null=True) + index_heading_entries = models.BooleanField(default=False) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + + +class LocalPdfConfig(BaseModel): + input_files = models.JSONField(default=list, null=True) + input_filter = models.JSONField(default=list, null=True) + index_heading_entries = models.BooleanField(default=False) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + + +class LocalPlaintextConfig(BaseModel): + input_files = models.JSONField(default=list, null=True) + input_filter = models.JSONField(default=list, null=True) + index_heading_entries = models.BooleanField(default=False) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + + +class ConversationProcessorConfig(BaseModel): conversation = models.JSONField() enable_offline_chat = models.BooleanField(default=False) + + +class Embeddings(BaseModel): + class EmbeddingsType(models.TextChoices): + IMAGE = "image" + PDF = "pdf" + PLAINTEXT = "plaintext" + MARKDOWN = "markdown" + ORG = "org" + NOTION = "notion" + GITHUB = "github" + CONVERSATION = "conversation" + + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True) + embeddings = VectorField(dimensions=384) + raw = models.TextField() + compiled = models.TextField() + heading = models.CharField(max_length=1000, default=None, null=True, blank=True) + file_type = models.CharField(max_length=30, choices=EmbeddingsType.choices, default=EmbeddingsType.PLAINTEXT) + file_path = models.CharField(max_length=400, default=None, null=True, blank=True) + file_name = models.CharField(max_length=400, default=None, null=True, blank=True) + url = models.URLField(max_length=400, default=None, null=True, blank=True) + hashed_value = models.CharField(max_length=100) + corpus_id = models.UUIDField(default=uuid.uuid4, editable=False) + + +class EmbeddingsDates(BaseModel): + date = models.DateField() + embeddings = models.ForeignKey(Embeddings, on_delete=models.CASCADE, related_name="embeddings_dates") + + class Meta: + indexes = [ + models.Index(fields=["date"]), + ] diff --git a/src/database/tests.py b/src/database/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/src/database/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/khoj/configure.py b/src/khoj/configure.py index 5f60c663..f65b1056 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -30,6 +30,8 @@ from khoj.utils.helpers import resolve_absolute_path, merge_dicts from khoj.utils.fs_syncer import collect_files from khoj.utils.rawconfig import FullConfig, OfflineChatProcessorConfig, ProcessorConfig, ConversationProcessorConfig from khoj.routers.indexer import configure_content, load_content, configure_search +from database.models import KhojUser +from database.adapters import get_all_users logger = logging.getLogger(__name__) @@ -48,14 +50,28 @@ class UserAuthenticationBackend(AuthenticationBackend): from database.models import KhojUser self.khojuser_manager = KhojUser.objects + self._initialize_default_user() super().__init__() + def _initialize_default_user(self): + if not self.khojuser_manager.filter(username="default").exists(): + self.khojuser_manager.create_user( + username="default", + email="default@example.com", + password="default", + ) + async def authenticate(self, request): current_user = request.session.get("user") if current_user and current_user.get("email"): user = await self.khojuser_manager.filter(email=current_user.get("email")).afirst() if user: return AuthCredentials(["authenticated"]), AuthenticatedKhojUser(user) + elif not state.anonymous_mode: + user = await self.khojuser_manager.filter(username="default").afirst() + if user: + return AuthCredentials(["authenticated"]), AuthenticatedKhojUser(user) + return AuthCredentials(), UnauthenticatedUser() @@ -78,7 +94,11 @@ def initialize_server(config: Optional[FullConfig]): def configure_server( - config: FullConfig, regenerate: bool = False, search_type: Optional[SearchType] = None, init=False + config: FullConfig, + regenerate: bool = False, + search_type: Optional[SearchType] = None, + init=False, + user: KhojUser = None, ): # Update Config state.config = config @@ -95,7 +115,7 @@ def configure_server( state.config_lock.acquire() state.SearchType = configure_search_types(state.config) state.search_models = configure_search(state.search_models, state.config.search_type) - initialize_content(regenerate, search_type, init) + initialize_content(regenerate, search_type, init, user) except Exception as e: logger.error(f"🚨 Failed to configure search models", exc_info=True) raise e @@ -103,7 +123,7 @@ def configure_server( state.config_lock.release() -def initialize_content(regenerate: bool, search_type: Optional[SearchType] = None, init=False): +def initialize_content(regenerate: bool, search_type: Optional[SearchType] = None, init=False, user: KhojUser = None): # Initialize Content from Config if state.search_models: try: @@ -112,7 +132,7 @@ def initialize_content(regenerate: bool, search_type: Optional[SearchType] = Non state.content_index = load_content(state.config.content_type, state.content_index, state.search_models) else: logger.info("📬 Updating content index...") - all_files = collect_files(state.config.content_type) + all_files = collect_files(user=user) state.content_index = configure_content( state.content_index, state.config.content_type, @@ -120,6 +140,7 @@ def initialize_content(regenerate: bool, search_type: Optional[SearchType] = Non state.search_models, regenerate, search_type, + user=user, ) except Exception as e: logger.error(f"🚨 Failed to index content", exc_info=True) @@ -152,9 +173,14 @@ if not state.demo: def update_search_index(): try: logger.info("📬 Updating content index via Scheduler") - all_files = collect_files(state.config.content_type) + for user in get_all_users(): + all_files = collect_files(user=user) + state.content_index = configure_content( + state.content_index, state.config.content_type, all_files, state.search_models, user=user + ) + all_files = collect_files(user=None) state.content_index = configure_content( - state.content_index, state.config.content_type, all_files, state.search_models + state.content_index, state.config.content_type, all_files, state.search_models, user=None ) logger.info("📪 Content index updated via Scheduler") except Exception as e: @@ -164,13 +190,9 @@ if not state.demo: def configure_search_types(config: FullConfig): # Extract core search types core_search_types = {e.name: e.value for e in SearchType} - # Extract configured plugin search types - plugin_search_types = {} - if config.content_type and config.content_type.plugins: - plugin_search_types = {plugin_type: plugin_type for plugin_type in config.content_type.plugins.keys()} # Dynamically generate search type enum by merging core search types with configured plugin search types - return Enum("SearchType", merge_dicts(core_search_types, plugin_search_types)) + return Enum("SearchType", merge_dicts(core_search_types, {})) def configure_processor( diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index d41ca26b..6e3a0223 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -10,12 +10,10 @@ Github

Github - {% if current_config.content_type.github %} - {% if current_model_state.github == False %} - Not Configured - {% else %} - Configured - {% endif %} + {% if current_model_state.github == False %} + Not Configured + {% else %} + Configured {% endif %}

@@ -24,7 +22,7 @@
- {% if current_config.content_type.github %} + {% if current_model_state.github %} Update {% else %} Setup @@ -32,7 +30,7 @@
- {% if current_config.content_type.github %} + {% if current_model_state.github %}
@@ -59,7 +55,7 @@
- {% if current_config.content_type.content %} + {% if current_model_state.content %} Update {% else %} Setup @@ -67,7 +63,7 @@
- {% if current_config.content_type.notion %} + {% if current_model_state.notion %}
- {% if current_config.content_type.markdown %} + {% if current_model_state.markdown %} Update {% else %} Setup @@ -102,7 +98,7 @@
- {% if current_config.content_type.markdown %} + {% if current_model_state.markdown %}
- {% if current_config.content_type.org %} + {% if current_model_state.org %} Update {% else %} Setup @@ -137,7 +133,7 @@
- {% if current_config.content_type.org %} + {% if current_model_state.org %}
- {% if current_config.content_type.pdf %} + {% if current_model_state.pdf %} Update {% else %} Setup @@ -172,7 +168,7 @@
- {% if current_config.content_type.pdf %} + {% if current_model_state.pdf %}
- {% if current_config.content_type.plaintext %} + {% if current_model_state.plaintext %} Update {% else %} Setup @@ -207,7 +203,7 @@
- {% if current_config.content_type.plaintext %} + {% if current_model_state.plaintext %}
- - - - - - - - - -
- - - -
- - - -
@@ -107,8 +89,6 @@ submit.addEventListener("click", function(event) { event.preventDefault(); - 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 == "") { @@ -154,8 +134,6 @@ body: JSON.stringify({ "pat_token": pat_token, "repos": repos, - "compressed_jsonl": compressed_jsonl, - "embeddings_file": embeddings_file, }) }) .then(response => response.json()) diff --git a/src/khoj/interface/web/content_type_input.html b/src/khoj/interface/web/content_type_input.html index 1f0dfa76..f8751ddc 100644 --- a/src/khoj/interface/web/content_type_input.html +++ b/src/khoj/interface/web/content_type_input.html @@ -43,33 +43,6 @@ - - - - - - - - - - - - - - -
- - - -
- - - -
- - - -
@@ -155,9 +128,8 @@ inputFilter = null; } - var compressed_jsonl = document.getElementById("compressed-jsonl").value; - var embeddings_file = document.getElementById("embeddings-file").value; - var index_heading_entries = document.getElementById("index-heading-entries").value; + // var index_heading_entries = document.getElementById("index-heading-entries").value; + var index_heading_entries = true; const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1]; fetch('/api/config/data/content_type/{{ content_type }}', { @@ -169,8 +141,6 @@ body: JSON.stringify({ "input_files": inputFiles, "input_filter": inputFilter, - "compressed_jsonl": compressed_jsonl, - "embeddings_file": embeddings_file, "index_heading_entries": index_heading_entries }) }) diff --git a/src/khoj/interface/web/content_type_notion_input.html b/src/khoj/interface/web/content_type_notion_input.html index dde5f2df..965c1ef5 100644 --- a/src/khoj/interface/web/content_type_notion_input.html +++ b/src/khoj/interface/web/content_type_notion_input.html @@ -20,24 +20,6 @@ - - - - - - - - - -
- - - -
- - - -
@@ -51,8 +33,6 @@ submit.addEventListener("click", function(event) { event.preventDefault(); - const compressed_jsonl = document.getElementById("compressed-jsonl").value; - const embeddings_file = document.getElementById("embeddings-file").value; const token = document.getElementById("token").value; if (token == "") { @@ -70,8 +50,6 @@ }, body: JSON.stringify({ "token": token, - "compressed_jsonl": compressed_jsonl, - "embeddings_file": embeddings_file, }) }) .then(response => response.json()) diff --git a/src/khoj/interface/web/index.html b/src/khoj/interface/web/index.html index 581ed9b8..ccf1ca71 100644 --- a/src/khoj/interface/web/index.html +++ b/src/khoj/interface/web/index.html @@ -172,7 +172,7 @@ url = createRequestUrl(query, type, results_count || 5, rerank); fetch(url, { headers: { - "X-CSRFToken": csrfToken + "Content-Type": "application/json" } }) .then(response => response.json()) @@ -199,8 +199,8 @@ fetch("/api/config/types") .then(response => response.json()) .then(enabled_types => { - // Show warning if no content types are enabled - if (enabled_types.detail) { + // 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 = "
To use Khoj search, setup your content plugins on the Khoj settings page.
"; document.getElementById("query").setAttribute("disabled", "disabled"); document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search"); diff --git a/src/app/main.py b/src/khoj/main.py similarity index 88% rename from src/app/main.py rename to src/khoj/main.py index 16f7cced..a713cc97 100644 --- a/src/app/main.py +++ b/src/khoj/main.py @@ -24,10 +24,15 @@ from rich.logging import RichHandler from django.core.asgi import get_asgi_application from django.core.management import call_command -# Internal Packages -from khoj.configure import configure_routes, initialize_server, configure_middleware -from khoj.utils import state -from khoj.utils.cli import cli +# Initialize Django +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") +django.setup() + +# Initialize Django Database +call_command("migrate", "--noinput") + +# Initialize Django Static Files +call_command("collectstatic", "--noinput") # Initialize Django os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") @@ -54,6 +59,11 @@ app.add_middleware( # Set Locale locale.setlocale(locale.LC_ALL, "") +# Internal Packages. We do this after setting up Django so that Django features are accessible to the app. +from khoj.configure import configure_routes, initialize_server, configure_middleware +from khoj.utils import state +from khoj.utils.cli import cli + # Setup Logger rich_handler = RichHandler(rich_tracebacks=True) rich_handler.setFormatter(fmt=logging.Formatter(fmt="%(message)s", datefmt="[%X]")) @@ -95,6 +105,8 @@ def run(): # Mount Django and Static Files app.mount("/django", django_app, name="django") + if not os.path.exists("static"): + os.mkdir("static") app.mount("/static", StaticFiles(directory="static"), name="static") # Configure Middleware @@ -111,6 +123,7 @@ def set_state(args): state.host = args.host state.port = args.port state.demo = args.demo + state.anonymous_mode = args.anonymous_mode state.khoj_version = version("khoj-assistant") diff --git a/src/khoj/processor/embeddings.py b/src/khoj/processor/embeddings.py new file mode 100644 index 00000000..f0e2df77 --- /dev/null +++ b/src/khoj/processor/embeddings.py @@ -0,0 +1,57 @@ +from typing import List + +import torch +from langchain.embeddings import HuggingFaceEmbeddings +from sentence_transformers import CrossEncoder + +from khoj.utils.rawconfig import SearchResponse + + +class EmbeddingsModel: + def __init__(self): + self.model_name = "sentence-transformers/multi-qa-MiniLM-L6-cos-v1" + encode_kwargs = {"normalize_embeddings": True} + # encode_kwargs = {} + + if torch.cuda.is_available(): + # Use CUDA GPU + device = torch.device("cuda:0") + elif torch.backends.mps.is_available(): + # Use Apple M1 Metal Acceleration + device = torch.device("mps") + else: + device = torch.device("cpu") + + model_kwargs = {"device": device} + self.embeddings_model = HuggingFaceEmbeddings( + model_name=self.model_name, encode_kwargs=encode_kwargs, model_kwargs=model_kwargs + ) + + def embed_query(self, query): + return self.embeddings_model.embed_query(query) + + def embed_documents(self, docs): + return self.embeddings_model.embed_documents(docs) + + +class CrossEncoderModel: + def __init__(self): + self.model_name = "cross-encoder/ms-marco-MiniLM-L-6-v2" + + if torch.cuda.is_available(): + # Use CUDA GPU + device = torch.device("cuda:0") + + elif torch.backends.mps.is_available(): + # Use Apple M1 Metal Acceleration + device = torch.device("mps") + + else: + device = torch.device("cpu") + + self.cross_encoder_model = CrossEncoder(model_name=self.model_name, device=device) + + def predict(self, query, hits: List[SearchResponse]): + cross__inp = [[query, hit.additional["compiled"]] for hit in hits] + cross_scores = self.cross_encoder_model.predict(cross__inp) + return cross_scores diff --git a/src/khoj/processor/github/github_to_jsonl.py b/src/khoj/processor/github/github_to_jsonl.py index bcd2e530..8feb6a31 100644 --- a/src/khoj/processor/github/github_to_jsonl.py +++ b/src/khoj/processor/github/github_to_jsonl.py @@ -2,7 +2,7 @@ import logging import time from datetime import datetime -from typing import Dict, List, Union +from typing import Dict, List, Union, Tuple # External Packages import requests @@ -12,18 +12,31 @@ from khoj.utils.helpers import timer 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 compress_jsonl_data +from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.rawconfig import Entry +from database.models import Embeddings, GithubConfig, KhojUser logger = logging.getLogger(__name__) -class GithubToJsonl(TextToJsonl): - def __init__(self, config: GithubContentConfig): +class GithubToJsonl(TextEmbeddings): + def __init__(self, config: GithubConfig): super().__init__(config) - self.config = config + raw_repos = config.githubrepoconfig.all() + repos = [] + for repo in raw_repos: + repos.append( + GithubRepoConfig( + name=repo.name, + owner=repo.owner, + branch=repo.branch, + ) + ) + self.config = GithubContentConfig( + pat_token=config.pat_token, + repos=repos, + ) self.session = requests.Session() self.session.headers.update({"Authorization": f"token {self.config.pat_token}"}) @@ -37,7 +50,9 @@ class GithubToJsonl(TextToJsonl): else: return - def process(self, previous_entries=[], files=None, full_corpus=True): + def process( + self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False + ) -> Tuple[int, int]: if self.config.pat_token is None or self.config.pat_token == "": logger.error(f"Github PAT token is not set. Skipping github content") raise ValueError("Github PAT token is not set. Skipping github content") @@ -45,7 +60,7 @@ class GithubToJsonl(TextToJsonl): for repo in self.config.repos: current_entries += self.process_repo(repo) - return self.update_entries_with_ids(current_entries, previous_entries) + return self.update_entries_with_ids(current_entries, user=user) def process_repo(self, repo: GithubRepoConfig): repo_url = f"https://api.github.com/repos/{repo.owner}/{repo.name}" @@ -80,26 +95,18 @@ class GithubToJsonl(TextToJsonl): current_entries += issue_entries 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) + current_entries = TextEmbeddings.split_entries_by_max_tokens(current_entries, max_tokens=256) return current_entries - def update_entries_with_ids(self, current_entries, previous_entries): + def update_entries_with_ids(self, current_entries, user: KhojUser = None): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): - entries_with_ids = TextToJsonl.mark_entries_for_update( - current_entries, previous_entries, key="compiled", logger=logger + num_new_embeddings, num_deleted_embeddings = self.update_embeddings( + current_entries, Embeddings.EmbeddingsType.GITHUB, key="compiled", logger=logger, user=user ) - with timer("Write github entries to JSONL file", logger): - # Process Each Entry from All Notes Files - entries = list(map(lambda entry: entry[1], entries_with_ids)) - jsonl_data = MarkdownToJsonl.convert_markdown_maps_to_jsonl(entries) - - # Compress JSONL formatted Data - compress_jsonl_data(jsonl_data, self.config.compressed_jsonl) - - return entries_with_ids + return num_new_embeddings, num_deleted_embeddings def get_files(self, repo_url: str, repo: GithubRepoConfig): # Get the contents of the repository diff --git a/src/khoj/processor/jsonl/__init__.py b/src/khoj/processor/jsonl/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/khoj/processor/jsonl/jsonl_to_jsonl.py b/src/khoj/processor/jsonl/jsonl_to_jsonl.py deleted file mode 100644 index 4a6fab99..00000000 --- a/src/khoj/processor/jsonl/jsonl_to_jsonl.py +++ /dev/null @@ -1,91 +0,0 @@ -# Standard Packages -import glob -import logging -from pathlib import Path -from typing import List - -# Internal Packages -from khoj.processor.text_to_jsonl import TextToJsonl -from khoj.utils.helpers import get_absolute_path, timer -from khoj.utils.jsonl import load_jsonl, compress_jsonl_data -from khoj.utils.rawconfig import Entry - - -logger = logging.getLogger(__name__) - - -class JsonlToJsonl(TextToJsonl): - # Define Functions - def process(self, previous_entries=[], files: dict[str, str] = {}, full_corpus: bool = True): - # Extract required fields from config - input_jsonl_files, input_jsonl_filter, output_file = ( - self.config.input_files, - self.config.input_filter, - self.config.compressed_jsonl, - ) - - # Get Jsonl Input Files to Process - all_input_jsonl_files = JsonlToJsonl.get_jsonl_files(input_jsonl_files, input_jsonl_filter) - - # Extract Entries from specified jsonl files - with timer("Parse entries from jsonl files", logger): - input_jsons = JsonlToJsonl.extract_jsonl_entries(all_input_jsonl_files) - current_entries = list(map(Entry.from_dict, input_jsons)) - - # Split entries by max tokens supported by model - with timer("Split entries by max token size supported by model", logger): - current_entries = self.split_entries_by_max_tokens(current_entries, max_tokens=256) - - # Identify, mark and merge any new entries with previous entries - with timer("Identify new or updated entries", logger): - entries_with_ids = TextToJsonl.mark_entries_for_update( - current_entries, previous_entries, key="compiled", logger=logger - ) - - with timer("Write entries to JSONL file", logger): - # Process Each Entry from All Notes Files - entries = list(map(lambda entry: entry[1], entries_with_ids)) - jsonl_data = JsonlToJsonl.convert_entries_to_jsonl(entries) - - # Compress JSONL formatted Data - compress_jsonl_data(jsonl_data, output_file) - - return entries_with_ids - - @staticmethod - def get_jsonl_files(jsonl_files=None, jsonl_file_filters=None): - "Get all jsonl files to process" - absolute_jsonl_files, filtered_jsonl_files = set(), set() - if jsonl_files: - absolute_jsonl_files = {get_absolute_path(jsonl_file) for jsonl_file in jsonl_files} - if jsonl_file_filters: - filtered_jsonl_files = { - filtered_file - for jsonl_file_filter in jsonl_file_filters - for filtered_file in glob.glob(get_absolute_path(jsonl_file_filter), recursive=True) - } - - all_jsonl_files = sorted(absolute_jsonl_files | filtered_jsonl_files) - - files_with_non_jsonl_extensions = { - jsonl_file for jsonl_file in all_jsonl_files if not jsonl_file.endswith(".jsonl") - } - if any(files_with_non_jsonl_extensions): - print(f"[Warning] There maybe non jsonl files in the input set: {files_with_non_jsonl_extensions}") - - logger.debug(f"Processing files: {all_jsonl_files}") - - return all_jsonl_files - - @staticmethod - def extract_jsonl_entries(jsonl_files): - "Extract entries from specified jsonl files" - entries = [] - for jsonl_file in jsonl_files: - entries.extend(load_jsonl(Path(jsonl_file))) - return entries - - @staticmethod - def convert_entries_to_jsonl(entries: List[Entry]): - "Convert each entry to JSON and collate as JSONL" - return "".join([f"{entry.to_json()}\n" for entry in entries]) diff --git a/src/khoj/processor/markdown/markdown_to_jsonl.py b/src/khoj/processor/markdown/markdown_to_jsonl.py index c2f0f0bf..17136b00 100644 --- a/src/khoj/processor/markdown/markdown_to_jsonl.py +++ b/src/khoj/processor/markdown/markdown_to_jsonl.py @@ -3,29 +3,28 @@ import logging import re import urllib3 from pathlib import Path -from typing import List +from typing import Tuple, List # Internal Packages -from khoj.processor.text_to_jsonl import TextToJsonl +from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer from khoj.utils.constants import empty_escape_sequences -from khoj.utils.jsonl import compress_jsonl_data -from khoj.utils.rawconfig import Entry, TextContentConfig +from khoj.utils.rawconfig import Entry +from database.models import Embeddings, KhojUser logger = logging.getLogger(__name__) -class MarkdownToJsonl(TextToJsonl): - def __init__(self, config: TextContentConfig): - super().__init__(config) - self.config = config +class MarkdownToJsonl(TextEmbeddings): + def __init__(self): + super().__init__() # Define Functions - def process(self, previous_entries=[], files=None, full_corpus: bool = True): + def process( + self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False + ) -> Tuple[int, int]: # Extract required fields from config - output_file = self.config.compressed_jsonl - if not full_corpus: deletion_file_names = set([file for file in files if files[file] == ""]) files_to_process = set(files) - deletion_file_names @@ -45,19 +44,17 @@ class MarkdownToJsonl(TextToJsonl): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): - entries_with_ids = TextToJsonl.mark_entries_for_update( - current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names + num_new_embeddings, num_deleted_embeddings = self.update_embeddings( + current_entries, + Embeddings.EmbeddingsType.MARKDOWN, + "compiled", + logger, + deletion_file_names, + user, + regenerate=regenerate, ) - with timer("Write markdown entries to JSONL file", logger): - # Process Each Entry from All Notes Files - entries = list(map(lambda entry: entry[1], entries_with_ids)) - jsonl_data = MarkdownToJsonl.convert_markdown_maps_to_jsonl(entries) - - # Compress JSONL formatted Data - compress_jsonl_data(jsonl_data, output_file) - - return entries_with_ids + return num_new_embeddings, num_deleted_embeddings @staticmethod def extract_markdown_entries(markdown_files): diff --git a/src/khoj/processor/notion/notion_to_jsonl.py b/src/khoj/processor/notion/notion_to_jsonl.py index 0df56c37..0081350a 100644 --- a/src/khoj/processor/notion/notion_to_jsonl.py +++ b/src/khoj/processor/notion/notion_to_jsonl.py @@ -1,5 +1,6 @@ # Standard Packages import logging +from typing import Tuple # External Packages import requests @@ -7,9 +8,9 @@ import requests # Internal Packages from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry, NotionContentConfig -from khoj.processor.text_to_jsonl import TextToJsonl -from khoj.utils.jsonl import compress_jsonl_data +from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.rawconfig import Entry +from database.models import Embeddings, KhojUser, NotionConfig from enum import Enum @@ -49,10 +50,12 @@ class NotionBlockType(Enum): CALLOUT = "callout" -class NotionToJsonl(TextToJsonl): - def __init__(self, config: NotionContentConfig): +class NotionToJsonl(TextEmbeddings): + def __init__(self, config: NotionConfig): super().__init__(config) - self.config = config + self.config = NotionContentConfig( + token=config.token, + ) self.session = requests.Session() self.session.headers.update({"Authorization": f"Bearer {config.token}", "Notion-Version": "2022-02-22"}) self.unsupported_block_types = [ @@ -80,7 +83,9 @@ class NotionToJsonl(TextToJsonl): self.body_params = {"page_size": 100} - def process(self, previous_entries=[], files=None, full_corpus=True): + def process( + self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False + ) -> Tuple[int, int]: current_entries = [] # Get all pages @@ -112,7 +117,7 @@ class NotionToJsonl(TextToJsonl): page_entries = self.process_page(p_or_d) current_entries.extend(page_entries) - return self.update_entries_with_ids(current_entries, previous_entries) + return self.update_entries_with_ids(current_entries, user) def process_page(self, page): page_id = page["id"] @@ -241,19 +246,11 @@ class NotionToJsonl(TextToJsonl): title = None return title, content - def update_entries_with_ids(self, current_entries, previous_entries): + def update_entries_with_ids(self, current_entries, user: KhojUser = None): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): - entries_with_ids = TextToJsonl.mark_entries_for_update( - current_entries, previous_entries, key="compiled", logger=logger + num_new_embeddings, num_deleted_embeddings = self.update_embeddings( + current_entries, Embeddings.EmbeddingsType.NOTION, key="compiled", logger=logger, user=user ) - with timer("Write Notion entries to JSONL file", logger): - # Process Each Entry from all Notion entries - entries = list(map(lambda entry: entry[1], entries_with_ids)) - jsonl_data = TextToJsonl.convert_text_maps_to_jsonl(entries) - - # Compress JSONL formatted Data - compress_jsonl_data(jsonl_data, self.config.compressed_jsonl) - - return entries_with_ids + return num_new_embeddings, num_deleted_embeddings diff --git a/src/khoj/processor/org_mode/org_to_jsonl.py b/src/khoj/processor/org_mode/org_to_jsonl.py index 2f22add4..90fdc029 100644 --- a/src/khoj/processor/org_mode/org_to_jsonl.py +++ b/src/khoj/processor/org_mode/org_to_jsonl.py @@ -5,28 +5,26 @@ from typing import Iterable, List, Tuple # Internal Packages from khoj.processor.org_mode import orgnode -from khoj.processor.text_to_jsonl import TextToJsonl +from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer -from khoj.utils.jsonl import compress_jsonl_data -from khoj.utils.rawconfig import Entry, TextContentConfig +from khoj.utils.rawconfig import Entry from khoj.utils import state +from database.models import Embeddings, KhojUser logger = logging.getLogger(__name__) -class OrgToJsonl(TextToJsonl): - def __init__(self, config: TextContentConfig): - super().__init__(config) - self.config = config +class OrgToJsonl(TextEmbeddings): + def __init__(self): + super().__init__() # Define Functions def process( - self, previous_entries: List[Entry] = [], files: dict[str, str] = None, full_corpus: bool = True - ) -> List[Tuple[int, Entry]]: + self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False + ) -> Tuple[int, int]: # Extract required fields from config - output_file = self.config.compressed_jsonl - index_heading_entries = self.config.index_heading_entries + index_heading_entries = True if not full_corpus: deletion_file_names = set([file for file in files if files[file] == ""]) @@ -47,19 +45,17 @@ class OrgToJsonl(TextToJsonl): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): - entries_with_ids = TextToJsonl.mark_entries_for_update( - current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names + num_new_embeddings, num_deleted_embeddings = self.update_embeddings( + current_entries, + Embeddings.EmbeddingsType.ORG, + "compiled", + logger, + deletion_file_names, + user, + regenerate=regenerate, ) - # Process Each Entry from All Notes Files - with timer("Write org entries to JSONL file", logger): - entries = map(lambda entry: entry[1], entries_with_ids) - jsonl_data = self.convert_org_entries_to_jsonl(entries) - - # Compress JSONL formatted Data - compress_jsonl_data(jsonl_data, output_file) - - return entries_with_ids + return num_new_embeddings, num_deleted_embeddings @staticmethod def extract_org_entries(org_files: dict[str, str]): diff --git a/src/khoj/processor/pdf/pdf_to_jsonl.py b/src/khoj/processor/pdf/pdf_to_jsonl.py index c24d9940..3a712c68 100644 --- a/src/khoj/processor/pdf/pdf_to_jsonl.py +++ b/src/khoj/processor/pdf/pdf_to_jsonl.py @@ -1,28 +1,31 @@ # Standard Packages import os import logging -from typing import List +from typing import List, Tuple import base64 # External Packages from langchain.document_loaders import PyMuPDFLoader # Internal Packages -from khoj.processor.text_to_jsonl import TextToJsonl +from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer -from khoj.utils.jsonl import compress_jsonl_data from khoj.utils.rawconfig import Entry +from database.models import Embeddings, KhojUser logger = logging.getLogger(__name__) -class PdfToJsonl(TextToJsonl): - # Define Functions - def process(self, previous_entries=[], files: dict[str, str] = None, full_corpus: bool = True): - # Extract required fields from config - output_file = self.config.compressed_jsonl +class PdfToJsonl(TextEmbeddings): + def __init__(self): + super().__init__() + # Define Functions + def process( + self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False + ) -> Tuple[int, int]: + # Extract required fields from config if not full_corpus: deletion_file_names = set([file for file in files if files[file] == ""]) files_to_process = set(files) - deletion_file_names @@ -40,19 +43,17 @@ class PdfToJsonl(TextToJsonl): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): - entries_with_ids = TextToJsonl.mark_entries_for_update( - current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names + num_new_embeddings, num_deleted_embeddings = self.update_embeddings( + current_entries, + Embeddings.EmbeddingsType.PDF, + "compiled", + logger, + deletion_file_names, + user, + regenerate=regenerate, ) - with timer("Write PDF entries to JSONL file", logger): - # Process Each Entry from All Notes Files - entries = list(map(lambda entry: entry[1], entries_with_ids)) - jsonl_data = PdfToJsonl.convert_pdf_maps_to_jsonl(entries) - - # Compress JSONL formatted Data - compress_jsonl_data(jsonl_data, output_file) - - return entries_with_ids + return num_new_embeddings, num_deleted_embeddings @staticmethod def extract_pdf_entries(pdf_files): @@ -62,7 +63,7 @@ class PdfToJsonl(TextToJsonl): entry_to_location_map = [] for pdf_file in pdf_files: try: - # Write the PDF file to a temporary file, as it is stored in byte format in the pdf_file object and the PyPDFLoader expects a file path + # Write the PDF file to a temporary file, as it is stored in byte format in the pdf_file object and the PDF Loader expects a file path tmp_file = f"tmp_pdf_file.pdf" with open(f"{tmp_file}", "wb") as f: bytes = pdf_files[pdf_file] diff --git a/src/khoj/processor/plaintext/plaintext_to_jsonl.py b/src/khoj/processor/plaintext/plaintext_to_jsonl.py index 3acb656e..965a5a7b 100644 --- a/src/khoj/processor/plaintext/plaintext_to_jsonl.py +++ b/src/khoj/processor/plaintext/plaintext_to_jsonl.py @@ -4,22 +4,23 @@ from pathlib import Path from typing import List, Tuple # Internal Packages -from khoj.processor.text_to_jsonl import TextToJsonl +from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer -from khoj.utils.jsonl import compress_jsonl_data -from khoj.utils.rawconfig import Entry +from khoj.utils.rawconfig import Entry, TextContentConfig +from database.models import Embeddings, KhojUser, LocalPlaintextConfig logger = logging.getLogger(__name__) -class PlaintextToJsonl(TextToJsonl): +class PlaintextToJsonl(TextEmbeddings): + def __init__(self): + super().__init__() + # Define Functions def process( - self, previous_entries: List[Entry] = [], files: dict[str, str] = None, full_corpus: bool = True - ) -> List[Tuple[int, Entry]]: - output_file = self.config.compressed_jsonl - + self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False + ) -> Tuple[int, int]: if not full_corpus: deletion_file_names = set([file for file in files if files[file] == ""]) files_to_process = set(files) - deletion_file_names @@ -37,19 +38,17 @@ class PlaintextToJsonl(TextToJsonl): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): - entries_with_ids = TextToJsonl.mark_entries_for_update( - current_entries, previous_entries, key="compiled", logger=logger, deletion_filenames=deletion_file_names + num_new_embeddings, num_deleted_embeddings = self.update_embeddings( + current_entries, + Embeddings.EmbeddingsType.PLAINTEXT, + key="compiled", + logger=logger, + deletion_filenames=deletion_file_names, + user=user, + regenerate=regenerate, ) - with timer("Write entries to JSONL file", logger): - # Process Each Entry from All Notes Files - entries = list(map(lambda entry: entry[1], entries_with_ids)) - plaintext_data = PlaintextToJsonl.convert_entries_to_jsonl(entries) - - # Compress JSONL formatted Data - compress_jsonl_data(plaintext_data, output_file) - - return entries_with_ids + return num_new_embeddings, num_deleted_embeddings @staticmethod def convert_plaintext_entries_to_maps(entry_to_file_map: dict) -> List[Entry]: diff --git a/src/khoj/processor/text_to_jsonl.py b/src/khoj/processor/text_to_jsonl.py index 98f5986f..c83c83b1 100644 --- a/src/khoj/processor/text_to_jsonl.py +++ b/src/khoj/processor/text_to_jsonl.py @@ -2,24 +2,33 @@ from abc import ABC, abstractmethod import hashlib import logging -from typing import Callable, List, Tuple, Set +import uuid +from tqdm import tqdm +from typing import Callable, List, Tuple, Set, Any from khoj.utils.helpers import timer + # Internal Packages -from khoj.utils.rawconfig import Entry, TextConfigBase +from khoj.utils.rawconfig import Entry +from khoj.processor.embeddings import EmbeddingsModel +from khoj.search_filter.date_filter import DateFilter +from database.models import KhojUser, Embeddings, EmbeddingsDates +from database.adapters import EmbeddingsAdapters logger = logging.getLogger(__name__) -class TextToJsonl(ABC): - def __init__(self, config: TextConfigBase): +class TextEmbeddings(ABC): + def __init__(self, config: Any = None): + self.embeddings_model = EmbeddingsModel() self.config = config + self.date_filter = DateFilter() @abstractmethod def process( - self, previous_entries: List[Entry] = [], files: dict[str, str] = None, full_corpus: bool = True - ) -> List[Tuple[int, Entry]]: + self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False + ) -> Tuple[int, int]: ... @staticmethod @@ -38,6 +47,7 @@ class TextToJsonl(ABC): # Drop long words instead of having entry truncated to maintain quality of entry processed by models compiled_entry_words = [word for word in compiled_entry_words if len(word) <= max_word_length] + corpus_id = uuid.uuid4() # Split entry into chunks of max tokens for chunk_index in range(0, len(compiled_entry_words), max_tokens): @@ -57,11 +67,103 @@ class TextToJsonl(ABC): raw=entry.raw, heading=entry.heading, file=entry.file, + corpus_id=corpus_id, ) ) return chunked_entries + def update_embeddings( + self, + current_entries: List[Entry], + file_type: str, + key="compiled", + logger: logging.Logger = None, + deletion_filenames: Set[str] = None, + user: KhojUser = None, + regenerate: bool = False, + ): + with timer("Construct current entry hashes", logger): + hashes_by_file = dict[str, set[str]]() + current_entry_hashes = list(map(TextEmbeddings.hash_func(key), current_entries)) + hash_to_current_entries = dict(zip(current_entry_hashes, current_entries)) + for entry in tqdm(current_entries, desc="Hashing Entries"): + hashes_by_file.setdefault(entry.file, set()).add(TextEmbeddings.hash_func(key)(entry)) + + num_deleted_embeddings = 0 + with timer("Preparing dataset for regeneration", logger): + if regenerate: + logger.info(f"Deleting all embeddings for file type {file_type}") + num_deleted_embeddings = EmbeddingsAdapters.delete_all_embeddings(user, file_type) + + num_new_embeddings = 0 + with timer("Identify hashes for adding new entries", logger): + for file in tqdm(hashes_by_file, desc="Processing file with hashed values"): + hashes_for_file = hashes_by_file[file] + hashes_to_process = set() + existing_entries = Embeddings.objects.filter( + user=user, hashed_value__in=hashes_for_file, file_type=file_type + ) + existing_entry_hashes = set([entry.hashed_value for entry in existing_entries]) + hashes_to_process = hashes_for_file - existing_entry_hashes + # for hashed_val in hashes_for_file: + # if not EmbeddingsAdapters.does_embedding_exist(user, hashed_val): + # hashes_to_process.add(hashed_val) + + entries_to_process = [hash_to_current_entries[hashed_val] for hashed_val in hashes_to_process] + data_to_embed = [getattr(entry, key) for entry in entries_to_process] + embeddings = self.embeddings_model.embed_documents(data_to_embed) + + with timer("Update the database with new vector embeddings", logger): + embeddings_to_create = [] + for hashed_val, embedding in zip(hashes_to_process, embeddings): + entry = hash_to_current_entries[hashed_val] + embeddings_to_create.append( + Embeddings( + user=user, + embeddings=embedding, + raw=entry.raw, + compiled=entry.compiled, + heading=entry.heading, + file_path=entry.file, + file_type=file_type, + hashed_value=hashed_val, + corpus_id=entry.corpus_id, + ) + ) + new_embeddings = Embeddings.objects.bulk_create(embeddings_to_create) + num_new_embeddings += len(new_embeddings) + + dates_to_create = [] + with timer("Create new date associations for new embeddings", logger): + for embedding in new_embeddings: + dates = self.date_filter.extract_dates(embedding.raw) + for date in dates: + dates_to_create.append( + EmbeddingsDates( + date=date, + embeddings=embedding, + ) + ) + new_dates = EmbeddingsDates.objects.bulk_create(dates_to_create) + if len(new_dates) > 0: + logger.info(f"Created {len(new_dates)} new date entries") + + with timer("Identify hashes for removed entries", logger): + for file in hashes_by_file: + existing_entry_hashes = EmbeddingsAdapters.get_existing_entry_hashes_by_file(user, file) + to_delete_entry_hashes = set(existing_entry_hashes) - hashes_by_file[file] + num_deleted_embeddings += len(to_delete_entry_hashes) + EmbeddingsAdapters.delete_embedding_by_hash(user, hashed_values=list(to_delete_entry_hashes)) + + with timer("Identify hashes for deleting entries", logger): + if deletion_filenames is not None: + for file_path in deletion_filenames: + deleted_count = EmbeddingsAdapters.delete_embedding_by_file(user, file_path) + num_deleted_embeddings += deleted_count + + return num_new_embeddings, num_deleted_embeddings + @staticmethod def mark_entries_for_update( current_entries: List[Entry], @@ -72,11 +174,11 @@ class TextToJsonl(ABC): ): # Hash all current and previous entries to identify new entries with timer("Hash previous, current entries", logger): - current_entry_hashes = list(map(TextToJsonl.hash_func(key), current_entries)) - previous_entry_hashes = list(map(TextToJsonl.hash_func(key), previous_entries)) + current_entry_hashes = list(map(TextEmbeddings.hash_func(key), current_entries)) + previous_entry_hashes = list(map(TextEmbeddings.hash_func(key), previous_entries)) if deletion_filenames is not None: deletion_entries = [entry for entry in previous_entries if entry.file in deletion_filenames] - deletion_entry_hashes = list(map(TextToJsonl.hash_func(key), deletion_entries)) + deletion_entry_hashes = list(map(TextEmbeddings.hash_func(key), deletion_entries)) else: deletion_entry_hashes = [] diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 345429e8..7c3e3392 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -2,14 +2,15 @@ import concurrent.futures import math import time -import yaml import logging import json from typing import List, Optional, Union, Any +import asyncio # External Packages -from fastapi import APIRouter, HTTPException, Header, Request -from sentence_transformers import util +from fastapi import APIRouter, HTTPException, Header, Request, Depends +from starlette.authentication import requires +from asgiref.sync import sync_to_async # Internal Packages from khoj.configure import configure_processor, configure_server @@ -20,7 +21,6 @@ from khoj.search_filter.word_filter import WordFilter from khoj.utils.config import TextSearchModel from khoj.utils.helpers import ConversationCommand, is_none_or_empty, timer, command_descriptions from khoj.utils.rawconfig import ( - ContentConfig, FullConfig, ProcessorConfig, SearchConfig, @@ -48,11 +48,74 @@ from khoj.processor.conversation.openai.gpt import extract_questions from khoj.processor.conversation.gpt4all.chat_model import extract_questions_offline from fastapi.requests import Request +from database import adapters +from database.adapters import EmbeddingsAdapters +from database.models import LocalMarkdownConfig, LocalOrgConfig, LocalPdfConfig, LocalPlaintextConfig, KhojUser + # Initialize Router api = APIRouter() logger = logging.getLogger(__name__) + +def map_config_to_object(content_type: str): + if content_type == "org": + return LocalOrgConfig + if content_type == "markdown": + return LocalMarkdownConfig + if content_type == "pdf": + return LocalPdfConfig + if content_type == "plaintext": + return LocalPlaintextConfig + + +async def map_config_to_db(config: FullConfig, user: KhojUser): + if config.content_type: + if config.content_type.org: + await LocalOrgConfig.objects.filter(user=user).adelete() + await LocalOrgConfig.objects.acreate( + input_files=config.content_type.org.input_files, + input_filter=config.content_type.org.input_filter, + index_heading_entries=config.content_type.org.index_heading_entries, + user=user, + ) + if config.content_type.markdown: + await LocalMarkdownConfig.objects.filter(user=user).adelete() + await LocalMarkdownConfig.objects.acreate( + input_files=config.content_type.markdown.input_files, + input_filter=config.content_type.markdown.input_filter, + index_heading_entries=config.content_type.markdown.index_heading_entries, + user=user, + ) + if config.content_type.pdf: + await LocalPdfConfig.objects.filter(user=user).adelete() + await LocalPdfConfig.objects.acreate( + input_files=config.content_type.pdf.input_files, + input_filter=config.content_type.pdf.input_filter, + index_heading_entries=config.content_type.pdf.index_heading_entries, + user=user, + ) + if config.content_type.plaintext: + await LocalPlaintextConfig.objects.filter(user=user).adelete() + await LocalPlaintextConfig.objects.acreate( + input_files=config.content_type.plaintext.input_files, + input_filter=config.content_type.plaintext.input_filter, + index_heading_entries=config.content_type.plaintext.index_heading_entries, + user=user, + ) + if config.content_type.github: + await adapters.set_user_github_config( + user=user, + pat_token=config.content_type.github.pat_token, + repos=config.content_type.github.repos, + ) + if config.content_type.notion: + await adapters.set_notion_config( + user=user, + token=config.content_type.notion.token, + ) + + # If it's a demo instance, prevent updating any of the configuration. if not state.demo: @@ -64,7 +127,10 @@ if not state.demo: state.processor_config = configure_processor(state.config.processor) @api.get("/config/data", response_model=FullConfig) - def get_config_data(): + def get_config_data(request: Request): + user = request.user.object if request.user.is_authenticated else None + enabled_content = EmbeddingsAdapters.get_unique_file_types(user) + return state.config @api.post("/config/data") @@ -73,20 +139,19 @@ if not state.demo: updated_config: FullConfig, client: Optional[str] = None, ): - state.config = updated_config - with open(state.config_file, "w") as outfile: - yaml.dump(yaml.safe_load(state.config.json(by_alias=True)), outfile) - outfile.close() + user = request.user.object if request.user.is_authenticated else None + await map_config_to_db(updated_config, user) - configuration_update_metadata = dict() + configuration_update_metadata = {} + + enabled_content = await sync_to_async(EmbeddingsAdapters.get_unique_file_types)(user) if state.config.content_type is not None: - configuration_update_metadata["github"] = state.config.content_type.github is not None - configuration_update_metadata["notion"] = state.config.content_type.notion is not None - configuration_update_metadata["org"] = state.config.content_type.org is not None - configuration_update_metadata["pdf"] = state.config.content_type.pdf is not None - configuration_update_metadata["markdown"] = state.config.content_type.markdown is not None - configuration_update_metadata["plugins"] = state.config.content_type.plugins is not None + configuration_update_metadata["github"] = "github" in enabled_content + configuration_update_metadata["notion"] = "notion" in enabled_content + configuration_update_metadata["org"] = "org" in enabled_content + configuration_update_metadata["pdf"] = "pdf" in enabled_content + configuration_update_metadata["markdown"] = "markdown" in enabled_content if state.config.processor is not None: configuration_update_metadata["conversation_processor"] = state.config.processor.conversation is not None @@ -101,6 +166,7 @@ if not state.demo: return state.config @api.post("/config/data/content_type/github", status_code=200) + @requires("authenticated") async def set_content_config_github_data( request: Request, updated_config: Union[GithubContentConfig, None], @@ -108,10 +174,13 @@ if not state.demo: ): _initialize_config() - if not state.config.content_type: - state.config.content_type = ContentConfig(**{"github": updated_config}) - else: - state.config.content_type.github = updated_config + user = request.user.object if request.user.is_authenticated else None + + await adapters.set_user_github_config( + user=user, + pat_token=updated_config.pat_token, + repos=updated_config.repos, + ) update_telemetry_state( request=request, @@ -121,11 +190,7 @@ if not state.demo: metadata={"content_type": "github"}, ) - try: - save_config_to_file_updated_state() - return {"status": "ok"} - except Exception as e: - return {"status": "error", "message": str(e)} + return {"status": "ok"} @api.post("/config/data/content_type/notion", status_code=200) async def set_content_config_notion_data( @@ -135,10 +200,12 @@ if not state.demo: ): _initialize_config() - if not state.config.content_type: - state.config.content_type = ContentConfig(**{"notion": updated_config}) - else: - state.config.content_type.notion = updated_config + user = request.user.object if request.user.is_authenticated else None + + await adapters.set_notion_config( + user=user, + token=updated_config.token, + ) update_telemetry_state( request=request, @@ -148,11 +215,7 @@ if not state.demo: metadata={"content_type": "notion"}, ) - try: - save_config_to_file_updated_state() - return {"status": "ok"} - except Exception as e: - return {"status": "error", "message": str(e)} + return {"status": "ok"} @api.post("/delete/config/data/content_type/{content_type}", status_code=200) async def remove_content_config_data( @@ -160,8 +223,7 @@ if not state.demo: content_type: str, client: Optional[str] = None, ): - if not state.config or not state.config.content_type: - return {"status": "ok"} + user = request.user.object if request.user.is_authenticated else None update_telemetry_state( request=request, @@ -171,31 +233,13 @@ if not state.demo: metadata={"content_type": content_type}, ) - if state.config.content_type: - state.config.content_type[content_type] = None + content_object = map_config_to_object(content_type) + await content_object.objects.filter(user=user).adelete() + await sync_to_async(EmbeddingsAdapters.delete_all_embeddings)(user, content_type) - if content_type == "github": - state.content_index.github = None - elif content_type == "notion": - state.content_index.notion = None - elif content_type == "plugins": - state.content_index.plugins = None - elif content_type == "pdf": - state.content_index.pdf = None - elif content_type == "markdown": - state.content_index.markdown = None - elif content_type == "org": - state.content_index.org = None - elif content_type == "plaintext": - state.content_index.plaintext = None - else: - logger.warning(f"Request to delete unknown content type: {content_type} via API") + enabled_content = await sync_to_async(EmbeddingsAdapters.get_unique_file_types)(user) - try: - save_config_to_file_updated_state() - return {"status": "ok"} - except Exception as e: - return {"status": "error", "message": str(e)} + return {"status": "ok"} @api.post("/delete/config/data/processor/conversation/openai", status_code=200) async def remove_processor_conversation_config_data( @@ -228,6 +272,7 @@ if not state.demo: return {"status": "error", "message": str(e)} @api.post("/config/data/content_type/{content_type}", status_code=200) + # @requires("authenticated") async def set_content_config_data( request: Request, content_type: str, @@ -236,10 +281,10 @@ if not state.demo: ): _initialize_config() - if not state.config.content_type: - state.config.content_type = ContentConfig(**{content_type: updated_config}) - else: - state.config.content_type[content_type] = updated_config + user = request.user.object if request.user.is_authenticated else None + + content_object = map_config_to_object(content_type) + await adapters.set_text_content_config(user, content_object, updated_config) update_telemetry_state( request=request, @@ -249,11 +294,7 @@ if not state.demo: metadata={"content_type": content_type}, ) - try: - save_config_to_file_updated_state() - return {"status": "ok"} - except Exception as e: - return {"status": "error", "message": str(e)} + return {"status": "ok"} @api.post("/config/data/processor/conversation/openai", status_code=200) async def set_processor_openai_config_data( @@ -337,24 +378,23 @@ def get_default_config_data(): @api.get("/config/types", response_model=List[str]) -def get_config_types(): - """Get configured content types""" - if state.config is None or state.config.content_type is None: - raise HTTPException( - status_code=500, - detail="Content types not configured. Configure at least one content type on server and restart it.", - ) +def get_config_types( + request: Request, +): + user = request.user.object if request.user.is_authenticated else None + + enabled_file_types = EmbeddingsAdapters.get_unique_file_types(user) + + configured_content_types = list(enabled_file_types) + + if state.config and state.config.content_type: + for ctype in state.config.content_type.dict(exclude_none=True): + configured_content_types.append(ctype) - configured_content_types = state.config.content_type.dict(exclude_none=True) return [ search_type.value for search_type in SearchType - if ( - search_type.value in configured_content_types - and getattr(state.content_index, search_type.value) is not None - ) - or ("plugins" in configured_content_types and search_type.name in configured_content_types["plugins"]) - or search_type == SearchType.All + if (search_type.value in configured_content_types) or search_type == SearchType.All ] @@ -372,6 +412,7 @@ async def search( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ): + user = request.user.object if request.user.is_authenticated else None start_time = time.time() # Run validation checks @@ -390,10 +431,11 @@ async def search( search_futures: List[concurrent.futures.Future] = [] # return cached results, if available - query_cache_key = f"{user_query}-{n}-{t}-{r}-{score_threshold}-{dedupe}" - if query_cache_key in state.query_cache: - logger.debug(f"Return response from query cache") - return state.query_cache[query_cache_key] + if user: + query_cache_key = f"{user_query}-{n}-{t}-{r}-{score_threshold}-{dedupe}" + if query_cache_key in state.query_cache[user.uuid]: + logger.debug(f"Return response from query cache") + return state.query_cache[user.uuid][query_cache_key] # Encode query with filter terms removed defiltered_query = user_query @@ -407,84 +449,31 @@ async def search( ] if text_search_models: with timer("Encoding query took", logger=logger): - encoded_asymmetric_query = util.normalize_embeddings( - text_search_models[0].bi_encoder.encode( - [defiltered_query], - convert_to_tensor=True, - device=state.device, - ) - ) + encoded_asymmetric_query = state.embeddings_model.embed_query(defiltered_query) with concurrent.futures.ThreadPoolExecutor() as executor: - if (t == SearchType.Org or t == SearchType.All) and state.content_index.org and state.search_models.text_search: - # query org-mode notes - search_futures += [ - executor.submit( - text_search.query, - user_query, - state.search_models.text_search, - state.content_index.org, - question_embedding=encoded_asymmetric_query, - rank_results=r or False, - score_threshold=score_threshold, - dedupe=dedupe or True, - ) - ] - - if ( - (t == SearchType.Markdown or t == SearchType.All) - and state.content_index.markdown - and state.search_models.text_search - ): + if t in [ + SearchType.All, + SearchType.Org, + SearchType.Markdown, + SearchType.Github, + SearchType.Notion, + SearchType.Plaintext, + ]: # query markdown notes search_futures += [ executor.submit( text_search.query, + user, user_query, - state.search_models.text_search, - state.content_index.markdown, + t, question_embedding=encoded_asymmetric_query, rank_results=r or False, score_threshold=score_threshold, - dedupe=dedupe or True, ) ] - if ( - (t == SearchType.Github or t == SearchType.All) - and state.content_index.github - and state.search_models.text_search - ): - # query github issues - search_futures += [ - executor.submit( - text_search.query, - user_query, - state.search_models.text_search, - state.content_index.github, - question_embedding=encoded_asymmetric_query, - rank_results=r or False, - score_threshold=score_threshold, - dedupe=dedupe or True, - ) - ] - - if (t == SearchType.Pdf or t == SearchType.All) and state.content_index.pdf and state.search_models.text_search: - # query pdf files - search_futures += [ - executor.submit( - text_search.query, - user_query, - state.search_models.text_search, - state.content_index.pdf, - question_embedding=encoded_asymmetric_query, - rank_results=r or False, - score_threshold=score_threshold, - dedupe=dedupe or True, - ) - ] - - if (t == SearchType.Image) and state.content_index.image and state.search_models.image_search: + elif (t == SearchType.Image) and state.content_index.image and state.search_models.image_search: # query images search_futures += [ executor.submit( @@ -497,70 +486,6 @@ async def search( ) ] - if ( - (t == SearchType.All or t in SearchType) - and state.content_index.plugins - and state.search_models.plugin_search - ): - # query specified plugin type - # Get plugin content, search model for specified search type, or the first one if none specified - plugin_search = state.search_models.plugin_search.get(t.value) or next( - iter(state.search_models.plugin_search.values()) - ) - plugin_content = state.content_index.plugins.get(t.value) or next( - iter(state.content_index.plugins.values()) - ) - search_futures += [ - executor.submit( - text_search.query, - user_query, - plugin_search, - plugin_content, - question_embedding=encoded_asymmetric_query, - rank_results=r or False, - score_threshold=score_threshold, - dedupe=dedupe or True, - ) - ] - - if ( - (t == SearchType.Notion or t == SearchType.All) - and state.content_index.notion - and state.search_models.text_search - ): - # query notion pages - search_futures += [ - executor.submit( - text_search.query, - user_query, - state.search_models.text_search, - state.content_index.notion, - question_embedding=encoded_asymmetric_query, - rank_results=r or False, - score_threshold=score_threshold, - dedupe=dedupe or True, - ) - ] - - if ( - (t == SearchType.Plaintext or t == SearchType.All) - and state.content_index.plaintext - and state.search_models.text_search - ): - # query plaintext files - search_futures += [ - executor.submit( - text_search.query, - user_query, - state.search_models.text_search, - state.content_index.plaintext, - question_embedding=encoded_asymmetric_query, - rank_results=r or False, - score_threshold=score_threshold, - dedupe=dedupe or True, - ) - ] - # Query across each requested content types in parallel with timer("Query took", logger): for search_future in concurrent.futures.as_completed(search_futures): @@ -576,15 +501,19 @@ async def search( count=results_count, ) else: - hits, entries = await search_future.result() + hits = await search_future.result() # Collate results - results += text_search.collate_results(hits, entries, results_count) + results += text_search.collate_results(hits, dedupe=dedupe) - # Sort results across all content types and take top results - results = sorted(results, key=lambda x: float(x.score), reverse=True)[:results_count] + if r: + results = text_search.rerank_and_sort_results(results, query=defiltered_query)[:results_count] + else: + # Sort results across all content types and take top results + results = sorted(results, key=lambda x: float(x.score))[:results_count] # Cache results - state.query_cache[query_cache_key] = results + if user: + state.query_cache[user.uuid][query_cache_key] = results update_telemetry_state( request=request, @@ -596,8 +525,6 @@ async def search( host=host, ) - state.previous_query = user_query - end_time = time.time() logger.debug(f"🔍 Search took: {end_time - start_time:.3f} seconds") @@ -614,12 +541,13 @@ def update( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ): + user = request.user.object if request.user.is_authenticated else None 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}." logger.warning(error_msg) raise HTTPException(status_code=500, detail=error_msg) try: - configure_server(state.config, regenerate=force, search_type=t) + configure_server(state.config, regenerate=force, search_type=t, user=user) except Exception as e: error_msg = f"🚨 Failed to update server via API: {e}" logger.error(error_msg, exc_info=True) @@ -774,6 +702,7 @@ async def extract_references_and_questions( n: int, conversation_type: ConversationCommand = ConversationCommand.Default, ): + user = request.user.object if request.user.is_authenticated else None # Load Conversation History meta_log = state.processor_config.conversation.meta_log @@ -781,7 +710,7 @@ async def extract_references_and_questions( compiled_references: List[Any] = [] inferred_queries: List[str] = [] - if state.content_index is None: + if not EmbeddingsAdapters.user_has_embeddings(user=user): logger.warning( "No content index loaded, so cannot extract references from knowledge base. Please configure your data sources and update the index to chat with your notes." ) diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 6b42f29c..be9e8700 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -10,6 +10,7 @@ from khoj.utils.helpers import ConversationCommand, timer, log_telemetry from khoj.processor.conversation.openai.gpt import converse from khoj.processor.conversation.gpt4all.chat_model import converse_offline from khoj.processor.conversation.utils import reciprocal_conversation_to_chatml, message_to_log, ThreadedGenerator +from database.models import KhojUser logger = logging.getLogger(__name__) @@ -40,11 +41,13 @@ def update_telemetry_state( host: Optional[str] = None, metadata: Optional[dict] = None, ): + user: KhojUser = request.user.object if request.user.is_authenticated else None user_state = { "client_host": request.client.host if request.client else None, "user_agent": user_agent or "unknown", "referer": referer or "unknown", "host": host or "unknown", + "server_id": str(user.uuid) if user else None, } if metadata: diff --git a/src/khoj/routers/indexer.py b/src/khoj/routers/indexer.py index a9656050..c2ef04ff 100644 --- a/src/khoj/routers/indexer.py +++ b/src/khoj/routers/indexer.py @@ -1,6 +1,7 @@ # Standard Packages import logging from typing import Optional, Union, Dict +import asyncio # External Packages from fastapi import APIRouter, HTTPException, Header, Request, Response, UploadFile @@ -9,31 +10,30 @@ from khoj.routers.helpers import update_telemetry_state # Internal Packages from khoj.utils import state, constants -from khoj.processor.jsonl.jsonl_to_jsonl import JsonlToJsonl from khoj.processor.markdown.markdown_to_jsonl import MarkdownToJsonl from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl from khoj.processor.pdf.pdf_to_jsonl import PdfToJsonl from khoj.processor.github.github_to_jsonl import GithubToJsonl from khoj.processor.notion.notion_to_jsonl import NotionToJsonl from khoj.processor.plaintext.plaintext_to_jsonl import PlaintextToJsonl -from khoj.utils.rawconfig import ContentConfig, TextContentConfig from khoj.search_type import text_search, image_search from khoj.utils.yaml import save_config_to_file_updated_state from khoj.utils.config import SearchModels -from khoj.utils.constants import default_config from khoj.utils.helpers import LRU, get_file_type from khoj.utils.rawconfig import ( ContentConfig, FullConfig, SearchConfig, ) -from khoj.search_filter.date_filter import DateFilter -from khoj.search_filter.word_filter import WordFilter -from khoj.search_filter.file_filter import FileFilter from khoj.utils.config import ( ContentIndex, SearchModels, ) +from database.models import ( + KhojUser, + GithubConfig, + NotionConfig, +) logger = logging.getLogger(__name__) @@ -68,14 +68,14 @@ async def update( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ): + user = request.user.object if request.user.is_authenticated else None if x_api_key != "secret": raise HTTPException(status_code=401, detail="Invalid API Key") - state.config_lock.acquire() try: logger.info(f"📬 Updating content index via API call by {client} client") org_files: Dict[str, str] = {} markdown_files: Dict[str, str] = {} - pdf_files: Dict[str, str] = {} + pdf_files: Dict[str, bytes] = {} plaintext_files: Dict[str, str] = {} for file in files: @@ -86,7 +86,7 @@ async def update( elif file_type == "markdown": dict_to_update = markdown_files elif file_type == "pdf": - dict_to_update = pdf_files + dict_to_update = pdf_files # type: ignore elif file_type == "plaintext": dict_to_update = plaintext_files @@ -120,30 +120,31 @@ async def update( github=None, notion=None, plaintext=None, - plugins=None, ) state.config.content_type = default_content_config save_config_to_file_updated_state() configure_search(state.search_models, state.config.search_type) # Extract required fields from config - state.content_index = configure_content( + loop = asyncio.get_event_loop() + state.content_index = await loop.run_in_executor( + None, + configure_content, state.content_index, state.config.content_type, indexer_input.dict(), state.search_models, - regenerate=force, - t=t, - full_corpus=False, + force, + t, + False, + user, ) - + logger.info(f"Finished processing batch indexing request") except Exception as e: logger.error( f"🚨 Failed to {force} update {t} content index triggered via API call by {client} client: {e}", exc_info=True, ) - finally: - state.config_lock.release() update_telemetry_state( request=request, @@ -167,11 +168,6 @@ def configure_search(search_models: SearchModels, search_config: Optional[Search if search_models is None: search_models = SearchModels() - # Initialize Search Models - if search_config.asymmetric: - logger.info("🔍 📜 Setting up text search model") - search_models.text_search = text_search.initialize_model(search_config.asymmetric) - if search_config.image: logger.info("🔍 🌄 Setting up image search model") search_models.image_search = image_search.initialize_model(search_config.image) @@ -187,16 +183,9 @@ def configure_content( regenerate: bool = False, t: Optional[Union[state.SearchType, str]] = None, full_corpus: bool = True, + user: KhojUser = None, ) -> Optional[ContentIndex]: - def has_valid_text_config(config: TextContentConfig): - return config.input_files or config.input_filter - - # Run Validation Checks - if content_config is None: - logger.warning("🚨 No Content configuration available.") - return None - if content_index is None: - content_index = ContentIndex() + content_index = ContentIndex() if t in [type.value for type in state.SearchType]: t = state.SearchType(t).value @@ -209,59 +198,30 @@ def configure_content( try: # Initialize Org Notes Search - if ( - (t == None or t == state.SearchType.Org.value) - and ((content_config.org and has_valid_text_config(content_config.org)) or files["org"]) - and search_models.text_search - ): - if content_config.org == None: - logger.info("🦄 No configuration for orgmode notes. Using default configuration.") - default_configuration = default_config["content-type"]["org"] # type: ignore - content_config.org = TextContentConfig( - compressed_jsonl=default_configuration["compressed-jsonl"], - embeddings_file=default_configuration["embeddings-file"], - ) - + if (t == None or t == state.SearchType.Org.value) and files["org"]: logger.info("🦄 Setting up search for orgmode notes") # Extract Entries, Generate Notes Embeddings - content_index.org = text_search.setup( + text_search.setup( OrgToJsonl, files.get("org"), - content_config.org, - search_models.text_search.bi_encoder, regenerate=regenerate, - filters=[DateFilter(), WordFilter(), FileFilter()], full_corpus=full_corpus, + user=user, ) except Exception as e: logger.error(f"🚨 Failed to setup org: {e}", exc_info=True) try: # Initialize Markdown Search - if ( - (t == None or t == state.SearchType.Markdown.value) - and ((content_config.markdown and has_valid_text_config(content_config.markdown)) or files["markdown"]) - and search_models.text_search - and files["markdown"] - ): - if content_config.markdown == None: - logger.info("💎 No configuration for markdown notes. Using default configuration.") - default_configuration = default_config["content-type"]["markdown"] # type: ignore - content_config.markdown = TextContentConfig( - compressed_jsonl=default_configuration["compressed-jsonl"], - embeddings_file=default_configuration["embeddings-file"], - ) - + if (t == None or t == state.SearchType.Markdown.value) and files["markdown"]: logger.info("💎 Setting up search for markdown notes") # Extract Entries, Generate Markdown Embeddings - content_index.markdown = text_search.setup( + text_search.setup( MarkdownToJsonl, files.get("markdown"), - content_config.markdown, - search_models.text_search.bi_encoder, regenerate=regenerate, - filters=[DateFilter(), WordFilter(), FileFilter()], full_corpus=full_corpus, + user=user, ) except Exception as e: @@ -269,30 +229,15 @@ def configure_content( try: # Initialize PDF Search - if ( - (t == None or t == state.SearchType.Pdf.value) - and ((content_config.pdf and has_valid_text_config(content_config.pdf)) or files["pdf"]) - and search_models.text_search - and files["pdf"] - ): - if content_config.pdf == None: - logger.info("🖨️ No configuration for pdf notes. Using default configuration.") - default_configuration = default_config["content-type"]["pdf"] # type: ignore - content_config.pdf = TextContentConfig( - compressed_jsonl=default_configuration["compressed-jsonl"], - embeddings_file=default_configuration["embeddings-file"], - ) - + if (t == None or t == state.SearchType.Pdf.value) and files["pdf"]: logger.info("🖨️ Setting up search for pdf") # Extract Entries, Generate PDF Embeddings - content_index.pdf = text_search.setup( + text_search.setup( PdfToJsonl, files.get("pdf"), - content_config.pdf, - search_models.text_search.bi_encoder, regenerate=regenerate, - filters=[DateFilter(), WordFilter(), FileFilter()], full_corpus=full_corpus, + user=user, ) except Exception as e: @@ -300,30 +245,15 @@ def configure_content( try: # Initialize Plaintext Search - if ( - (t == None or t == state.SearchType.Plaintext.value) - and ((content_config.plaintext and has_valid_text_config(content_config.plaintext)) or files["plaintext"]) - and search_models.text_search - and files["plaintext"] - ): - if content_config.plaintext == None: - logger.info("📄 No configuration for plaintext notes. Using default configuration.") - default_configuration = default_config["content-type"]["plaintext"] # type: ignore - content_config.plaintext = TextContentConfig( - compressed_jsonl=default_configuration["compressed-jsonl"], - embeddings_file=default_configuration["embeddings-file"], - ) - + if (t == None or t == state.SearchType.Plaintext.value) and files["plaintext"]: logger.info("📄 Setting up search for plaintext") # Extract Entries, Generate Plaintext Embeddings - content_index.plaintext = text_search.setup( + text_search.setup( PlaintextToJsonl, files.get("plaintext"), - content_config.plaintext, - search_models.text_search.bi_encoder, regenerate=regenerate, - filters=[DateFilter(), WordFilter(), FileFilter()], full_corpus=full_corpus, + user=user, ) except Exception as e: @@ -331,7 +261,12 @@ def configure_content( try: # Initialize Image Search - if (t == None or t == state.SearchType.Image.value) and content_config.image and search_models.image_search: + if ( + (t == None or t == state.SearchType.Image.value) + and content_config + and content_config.image + and search_models.image_search + ): logger.info("🌄 Setting up search for images") # Extract Entries, Generate Image Embeddings content_index.image = image_search.setup( @@ -342,17 +277,17 @@ def configure_content( logger.error(f"🚨 Failed to setup images: {e}", exc_info=True) try: - if (t == None or t == state.SearchType.Github.value) and content_config.github and search_models.text_search: + github_config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first() + if (t == None or t == state.SearchType.Github.value) and github_config is not None: logger.info("🐙 Setting up search for github") # Extract Entries, Generate Github Embeddings - content_index.github = text_search.setup( + text_search.setup( GithubToJsonl, None, - content_config.github, - search_models.text_search.bi_encoder, regenerate=regenerate, - filters=[DateFilter(), WordFilter(), FileFilter()], full_corpus=full_corpus, + user=user, + config=github_config, ) except Exception as e: @@ -360,42 +295,24 @@ def configure_content( try: # Initialize Notion Search - if (t == None or t in state.SearchType.Notion.value) and content_config.notion and search_models.text_search: + notion_config = NotionConfig.objects.filter(user=user).first() + if (t == None or t in state.SearchType.Notion.value) and notion_config: logger.info("🔌 Setting up search for notion") - content_index.notion = text_search.setup( + text_search.setup( NotionToJsonl, None, - content_config.notion, - search_models.text_search.bi_encoder, regenerate=regenerate, - filters=[DateFilter(), WordFilter(), FileFilter()], full_corpus=full_corpus, + user=user, + config=notion_config, ) except Exception as e: logger.error(f"🚨 Failed to setup GitHub: {e}", exc_info=True) - try: - # Initialize External Plugin Search - if t == None and content_config.plugins and search_models.text_search: - logger.info("🔌 Setting up search for plugins") - content_index.plugins = {} - for plugin_type, plugin_config in content_config.plugins.items(): - content_index.plugins[plugin_type] = text_search.setup( - JsonlToJsonl, - None, - plugin_config, - search_models.text_search.bi_encoder, - regenerate=regenerate, - filters=[DateFilter(), WordFilter(), FileFilter()], - full_corpus=full_corpus, - ) - - except Exception as e: - logger.error(f"🚨 Failed to setup Plugin: {e}", exc_info=True) - # Invalidate Query Cache - state.query_cache = LRU() + if user: + state.query_cache[user.uuid] = LRU() return content_index @@ -412,44 +329,9 @@ def load_content( if content_index is None: content_index = ContentIndex() - if content_config.org: - logger.info("🦄 Loading orgmode notes") - content_index.org = text_search.load(content_config.org, filters=[DateFilter(), WordFilter(), FileFilter()]) - if content_config.markdown: - logger.info("💎 Loading markdown notes") - content_index.markdown = text_search.load( - content_config.markdown, filters=[DateFilter(), WordFilter(), FileFilter()] - ) - if content_config.pdf: - logger.info("🖨️ Loading pdf") - content_index.pdf = text_search.load(content_config.pdf, filters=[DateFilter(), WordFilter(), FileFilter()]) - if content_config.plaintext: - logger.info("📄 Loading plaintext") - content_index.plaintext = text_search.load( - content_config.plaintext, filters=[DateFilter(), WordFilter(), FileFilter()] - ) if content_config.image: logger.info("🌄 Loading images") content_index.image = image_search.setup( content_config.image, search_models.image_search.image_encoder, regenerate=False ) - if content_config.github: - logger.info("🐙 Loading github") - content_index.github = text_search.load( - content_config.github, filters=[DateFilter(), WordFilter(), FileFilter()] - ) - if content_config.notion: - logger.info("🔌 Loading notion") - content_index.notion = text_search.load( - content_config.notion, filters=[DateFilter(), WordFilter(), FileFilter()] - ) - if content_config.plugins: - logger.info("🔌 Loading plugins") - content_index.plugins = {} - for plugin_type, plugin_config in content_config.plugins.items(): - content_index.plugins[plugin_type] = text_search.load( - plugin_config, filters=[DateFilter(), WordFilter(), FileFilter()] - ) - - state.query_cache = LRU() return content_index diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 492a263c..6c79e061 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -3,10 +3,20 @@ from fastapi import APIRouter from fastapi import Request from fastapi.responses import HTMLResponse, FileResponse from fastapi.templating import Jinja2Templates -from khoj.utils.rawconfig import TextContentConfig, OpenAIProcessorConfig, FullConfig +from starlette.authentication import requires +from khoj.utils.rawconfig import ( + TextContentConfig, + OpenAIProcessorConfig, + FullConfig, + GithubContentConfig, + GithubRepoConfig, + NotionContentConfig, +) # Internal Packages from khoj.utils import constants, state +from database.adapters import EmbeddingsAdapters, get_user_github_config, get_user_notion_config +from database.models import KhojUser, LocalOrgConfig, LocalMarkdownConfig, LocalPdfConfig, LocalPlaintextConfig import json @@ -29,10 +39,23 @@ def chat_page(request: Request): return templates.TemplateResponse("chat.html", context={"request": request, "demo": state.demo}) +def map_config_to_object(content_type: str): + if content_type == "org": + return LocalOrgConfig + if content_type == "markdown": + return LocalMarkdownConfig + if content_type == "pdf": + return LocalPdfConfig + if content_type == "plaintext": + return LocalPlaintextConfig + + if not state.demo: @web_client.get("/config", response_class=HTMLResponse) def config_page(request: Request): + user = request.user.object if request.user.is_authenticated else None + enabled_content = set(EmbeddingsAdapters.get_unique_file_types(user).all()) default_full_config = FullConfig( content_type=None, search_type=None, @@ -41,13 +64,13 @@ if not state.demo: current_config = state.config or json.loads(default_full_config.json()) successfully_configured = { - "pdf": False, - "markdown": False, - "org": False, + "pdf": ("pdf" in enabled_content), + "markdown": ("markdown" in enabled_content), + "org": ("org" in enabled_content), "image": False, - "github": False, - "notion": False, - "plaintext": False, + "github": ("github" in enabled_content), + "notion": ("notion" in enabled_content), + "plaintext": ("plaintext" in enabled_content), "enable_offline_model": False, "conversation_openai": False, "conversation_gpt4all": False, @@ -56,13 +79,7 @@ if not state.demo: if state.content_index: successfully_configured.update( { - "pdf": state.content_index.pdf is not None, - "markdown": state.content_index.markdown is not None, - "org": state.content_index.org is not None, "image": state.content_index.image is not None, - "github": state.content_index.github is not None, - "notion": state.content_index.notion is not None, - "plaintext": state.content_index.plaintext is not None, } ) @@ -84,22 +101,29 @@ if not state.demo: ) @web_client.get("/config/content_type/github", response_class=HTMLResponse) + @requires(["authenticated"]) def github_config_page(request: Request): - default_copy = constants.default_config.copy() - default_github = default_copy["content-type"]["github"] # type: ignore + user = request.user.object if request.user.is_authenticated else None + current_github_config = get_user_github_config(user) - default_config = TextContentConfig( - compressed_jsonl=default_github["compressed-jsonl"], - embeddings_file=default_github["embeddings-file"], - ) - - current_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()) + if current_github_config: + raw_repos = current_github_config.githubrepoconfig.all() + repos = [] + for repo in raw_repos: + repos.append( + GithubRepoConfig( + name=repo.name, + owner=repo.owner, + branch=repo.branch, + ) + ) + current_config = GithubContentConfig( + pat_token=current_github_config.pat_token, + repos=repos, + ) + current_config = json.loads(current_config.json()) + else: + current_config = {} # type: ignore return templates.TemplateResponse( "content_type_github_input.html", context={"request": request, "current_config": current_config} @@ -107,18 +131,11 @@ if not state.demo: @web_client.get("/config/content_type/notion", response_class=HTMLResponse) def notion_config_page(request: Request): - default_copy = constants.default_config.copy() - default_notion = default_copy["content-type"]["notion"] # type: ignore + user = request.user.object if request.user.is_authenticated else None + current_notion_config = get_user_notion_config(user) - default_config = TextContentConfig( - compressed_jsonl=default_notion["compressed-jsonl"], - embeddings_file=default_notion["embeddings-file"], - ) - - current_config = ( - state.config.content_type.notion - if state.config and state.config.content_type and state.config.content_type.notion - else default_config + current_config = NotionContentConfig( + token=current_notion_config.token if current_notion_config else "", ) current_config = json.loads(current_config.json()) @@ -132,18 +149,16 @@ if not state.demo: if content_type not in VALID_TEXT_CONTENT_TYPES: return templates.TemplateResponse("config.html", context={"request": request}) - default_copy = constants.default_config.copy() - default_content_type = default_copy["content-type"][content_type] # type: ignore + object = map_config_to_object(content_type) + user = request.user.object if request.user.is_authenticated else None + config = object.objects.filter(user=user).first() + if config == None: + config = object.objects.create(user=user) - default_config = TextContentConfig( - 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 and state.config.content_type and state.config.content_type[content_type] # type: ignore - else default_config + current_config = TextContentConfig( + input_files=config.input_files, + input_filter=config.input_filter, + index_heading_entries=config.index_heading_entries, ) current_config = json.loads(current_config.json()) diff --git a/src/khoj/search_filter/base_filter.py b/src/khoj/search_filter/base_filter.py index 470f7341..ae596587 100644 --- a/src/khoj/search_filter/base_filter.py +++ b/src/khoj/search_filter/base_filter.py @@ -1,16 +1,9 @@ # Standard Packages from abc import ABC, abstractmethod -from typing import List, Set, Tuple - -# Internal Packages -from khoj.utils.rawconfig import Entry +from typing import List class BaseFilter(ABC): - @abstractmethod - def load(self, entries: List[Entry], *args, **kwargs): - ... - @abstractmethod def get_filter_terms(self, query: str) -> List[str]: ... @@ -18,10 +11,6 @@ class BaseFilter(ABC): def can_filter(self, raw_query: str) -> bool: return len(self.get_filter_terms(raw_query)) > 0 - @abstractmethod - def apply(self, query: str, entries: List[Entry]) -> Tuple[str, Set[int]]: - ... - @abstractmethod def defilter(self, query: str) -> str: ... diff --git a/src/khoj/search_filter/date_filter.py b/src/khoj/search_filter/date_filter.py index 39e7bec3..88c70101 100644 --- a/src/khoj/search_filter/date_filter.py +++ b/src/khoj/search_filter/date_filter.py @@ -25,72 +25,42 @@ class DateFilter(BaseFilter): # - dt>="last week" # - dt:"2 years ago" date_regex = r"dt([:><=]{1,2})[\"'](.*?)[\"']" + raw_date_regex = r"\d{4}-\d{2}-\d{2}" def __init__(self, entry_key="compiled"): self.entry_key = entry_key self.date_to_entry_ids = defaultdict(set) self.cache = LRU() - def load(self, entries, *args, **kwargs): - with timer("Created date filter index", logger): - for id, entry in enumerate(entries): - # Extract dates from entry - for date_in_entry_string in re.findall(r"\d{4}-\d{2}-\d{2}", getattr(entry, self.entry_key)): - # Convert date string in entry to unix timestamp - try: - date_in_entry = datetime.strptime(date_in_entry_string, "%Y-%m-%d").timestamp() - except ValueError: - continue - except OSError: - logger.debug(f"OSError: Ignoring unprocessable date in entry: {date_in_entry_string}") - continue - self.date_to_entry_ids[date_in_entry].add(id) + def extract_dates(self, content): + pattern_matched_dates = re.findall(self.raw_date_regex, content) + + # Filter down to valid dates + valid_dates = [] + for date_str in pattern_matched_dates: + try: + valid_dates.append(datetime.strptime(date_str, "%Y-%m-%d")) + except ValueError: + continue + + return valid_dates def get_filter_terms(self, query: str) -> List[str]: "Get all filter terms in query" return [f"dt{item[0]}'{item[1]}'" for item in re.findall(self.date_regex, query)] + def get_query_date_range(self, query) -> List: + with timer("Extract date range to filter from query", logger): + query_daterange = self.extract_date_range(query) + + return query_daterange + def defilter(self, query): # remove date range filter from query query = re.sub(rf"\s+{self.date_regex}", " ", query) query = re.sub(r"\s{2,}", " ", query).strip() # remove multiple spaces return query - def apply(self, query, entries): - "Find entries containing any dates that fall within date range specified in query" - # extract date range specified in date filter of query - with timer("Extract date range to filter from query", logger): - query_daterange = self.extract_date_range(query) - - # if no date in query, return all entries - if query_daterange == []: - return query, set(range(len(entries))) - - query = self.defilter(query) - - # return results from cache if exists - cache_key = tuple(query_daterange) - if cache_key in self.cache: - logger.debug(f"Return date filter results from cache") - entries_to_include = self.cache[cache_key] - return query, entries_to_include - - if not self.date_to_entry_ids: - self.load(entries) - - # find entries containing any dates that fall with date range specified in query - with timer("Mark entries satisfying filter", logger): - entries_to_include = set() - for date_in_entry in self.date_to_entry_ids.keys(): - # Check if date in entry is within date range specified in query - if query_daterange[0] <= date_in_entry < query_daterange[1]: - entries_to_include |= self.date_to_entry_ids[date_in_entry] - - # cache results - self.cache[cache_key] = entries_to_include - - return query, entries_to_include - def extract_date_range(self, query): # find date range filter in query date_range_matches = re.findall(self.date_regex, query) @@ -138,6 +108,15 @@ class DateFilter(BaseFilter): if effective_date_range == [0, inf] or effective_date_range[0] > effective_date_range[1]: return [] else: + # If the first element is 0, replace it with None + + if effective_date_range[0] == 0: + effective_date_range[0] = None + + # If the second element is inf, replace it with None + if effective_date_range[1] == inf: + effective_date_range[1] = None + return effective_date_range def parse(self, date_str, relative_base=None): diff --git a/src/khoj/search_filter/file_filter.py b/src/khoj/search_filter/file_filter.py index 420bf9e7..291838ea 100644 --- a/src/khoj/search_filter/file_filter.py +++ b/src/khoj/search_filter/file_filter.py @@ -21,62 +21,13 @@ class FileFilter(BaseFilter): self.file_to_entry_map = defaultdict(set) self.cache = LRU() - def load(self, entries, *args, **kwargs): - with timer("Created file filter index", logger): - for id, entry in enumerate(entries): - self.file_to_entry_map[getattr(entry, self.entry_key)].add(id) - def get_filter_terms(self, query: str) -> List[str]: "Get all filter terms in query" - return [f'file:"{term}"' for term in re.findall(self.file_filter_regex, query)] + return [f"{self.convert_to_regex(term)}" for term in re.findall(self.file_filter_regex, query)] + + def convert_to_regex(self, file_filter: str) -> str: + "Convert file filter to regex" + return file_filter.replace(".", r"\.").replace("*", r".*") def defilter(self, query: str) -> str: return re.sub(self.file_filter_regex, "", query).strip() - - def apply(self, query, entries): - # Extract file filters from raw query - with timer("Extract files_to_search from query", logger): - raw_files_to_search = re.findall(self.file_filter_regex, query) - if not raw_files_to_search: - return query, set(range(len(entries))) - - # Convert simple file filters with no path separator into regex - # e.g. "file:notes.org" -> "file:.*notes.org" - files_to_search = [] - for file in sorted(raw_files_to_search): - if "/" not in file and "\\" not in file and "*" not in file: - files_to_search += [f"*{file}"] - else: - files_to_search += [file] - - # Remove filter terms from original query - query = self.defilter(query) - - # Return item from cache if exists - cache_key = tuple(files_to_search) - if cache_key in self.cache: - logger.debug(f"Return file filter results from cache") - included_entry_indices = self.cache[cache_key] - return query, included_entry_indices - - if not self.file_to_entry_map: - self.load(entries, regenerate=False) - - # Mark entries that contain any blocked_words for exclusion - with timer("Mark entries satisfying filter", logger): - included_entry_indices = set.union( - *[ - self.file_to_entry_map[entry_file] - for entry_file in self.file_to_entry_map.keys() - for search_file in files_to_search - if fnmatch.fnmatch(entry_file, search_file) - ], - set(), - ) - if not included_entry_indices: - return query, {} - - # Cache results - self.cache[cache_key] = included_entry_indices - - return query, included_entry_indices diff --git a/src/khoj/search_filter/word_filter.py b/src/khoj/search_filter/word_filter.py index ebf64b34..b2053dbe 100644 --- a/src/khoj/search_filter/word_filter.py +++ b/src/khoj/search_filter/word_filter.py @@ -6,7 +6,7 @@ from typing import List # Internal Packages from khoj.search_filter.base_filter import BaseFilter -from khoj.utils.helpers import LRU, timer +from khoj.utils.helpers import LRU logger = logging.getLogger(__name__) @@ -22,21 +22,6 @@ class WordFilter(BaseFilter): self.word_to_entry_index = defaultdict(set) self.cache = LRU() - def load(self, entries, *args, **kwargs): - with timer("Created word filter index", logger): - self.cache = {} # Clear cache on filter (re-)load - entry_splitter = ( - r",|\.| |\]|\[\(|\)|\{|\}|\<|\>|\t|\n|\:|\;|\?|\!|\(|\)|\&|\^|\$|\@|\%|\+|\=|\/|\\|\||\~|\`|\"|\'" - ) - # Create map of words to entries they exist in - for entry_index, entry in enumerate(entries): - for word in re.split(entry_splitter, getattr(entry, self.entry_key).lower()): - if word == "": - continue - self.word_to_entry_index[word].add(entry_index) - - return self.word_to_entry_index - def get_filter_terms(self, query: str) -> List[str]: "Get all filter terms in query" required_terms = [f"+{required_term}" for required_term in re.findall(self.required_regex, query)] @@ -45,47 +30,3 @@ class WordFilter(BaseFilter): def defilter(self, query: str) -> str: return re.sub(self.blocked_regex, "", re.sub(self.required_regex, "", query)).strip() - - def apply(self, query, entries): - "Find entries containing required and not blocked words specified in query" - # Separate natural query from required, blocked words filters - with timer("Extract required, blocked filters from query", logger): - required_words = set([word.lower() for word in re.findall(self.required_regex, query)]) - blocked_words = set([word.lower() for word in re.findall(self.blocked_regex, query)]) - query = self.defilter(query) - - if len(required_words) == 0 and len(blocked_words) == 0: - return query, set(range(len(entries))) - - # Return item from cache if exists - cache_key = tuple(sorted(required_words)), tuple(sorted(blocked_words)) - if cache_key in self.cache: - logger.debug(f"Return word filter results from cache") - included_entry_indices = self.cache[cache_key] - return query, included_entry_indices - - if not self.word_to_entry_index: - self.load(entries, regenerate=False) - - # mark entries that contain all required_words for inclusion - with timer("Mark entries satisfying filter", logger): - entries_with_all_required_words = set(range(len(entries))) - if len(required_words) > 0: - entries_with_all_required_words = set.intersection( - *[self.word_to_entry_index.get(word, set()) for word in required_words] - ) - - # mark entries that contain any blocked_words for exclusion - entries_with_any_blocked_words = set() - if len(blocked_words) > 0: - entries_with_any_blocked_words = set.union( - *[self.word_to_entry_index.get(word, set()) for word in blocked_words] - ) - - # get entries satisfying inclusion and exclusion filters - included_entry_indices = entries_with_all_required_words - entries_with_any_blocked_words - - # Cache results - self.cache[cache_key] = included_entry_indices - - return query, included_entry_indices diff --git a/src/khoj/search_type/text_search.py b/src/khoj/search_type/text_search.py index 2890baa9..36d6a791 100644 --- a/src/khoj/search_type/text_search.py +++ b/src/khoj/search_type/text_search.py @@ -2,25 +2,39 @@ import logging import math from pathlib import Path -from typing import List, Tuple, Type, Union +from typing import List, Tuple, Type, Union, Dict # External Packages import torch from sentence_transformers import SentenceTransformer, CrossEncoder, util -from khoj.processor.text_to_jsonl import TextToJsonl -from khoj.search_filter.base_filter import BaseFilter + +from asgiref.sync import sync_to_async + # Internal Packages from khoj.utils import state -from khoj.utils.helpers import get_absolute_path, is_none_or_empty, resolve_absolute_path, load_model, timer -from khoj.utils.config import TextContent, TextSearchModel +from khoj.utils.helpers import get_absolute_path, resolve_absolute_path, load_model, timer +from khoj.utils.config import TextSearchModel from khoj.utils.models import BaseEncoder -from khoj.utils.rawconfig import SearchResponse, TextSearchConfig, TextConfigBase, Entry +from khoj.utils.state import SearchType +from khoj.utils.rawconfig import SearchResponse, TextSearchConfig, Entry from khoj.utils.jsonl import load_jsonl - +from khoj.processor.text_to_jsonl import TextEmbeddings +from database.adapters import EmbeddingsAdapters +from database.models import KhojUser, Embeddings logger = logging.getLogger(__name__) +search_type_to_embeddings_type = { + SearchType.Org.value: Embeddings.EmbeddingsType.ORG, + SearchType.Markdown.value: Embeddings.EmbeddingsType.MARKDOWN, + SearchType.Plaintext.value: Embeddings.EmbeddingsType.PLAINTEXT, + SearchType.Pdf.value: Embeddings.EmbeddingsType.PDF, + SearchType.Github.value: Embeddings.EmbeddingsType.GITHUB, + SearchType.Notion.value: Embeddings.EmbeddingsType.NOTION, + SearchType.All.value: None, +} + def initialize_model(search_config: TextSearchConfig): "Initialize model for semantic search on text" @@ -117,171 +131,102 @@ def load_embeddings( async def query( + user: KhojUser, raw_query: str, - search_model: TextSearchModel, - content: TextContent, + type: SearchType = SearchType.All, question_embedding: Union[torch.Tensor, None] = None, rank_results: bool = False, score_threshold: float = -math.inf, - dedupe: bool = True, ) -> Tuple[List[dict], List[Entry]]: "Search for entries that answer the query" - if ( - content.entries is None - or len(content.entries) == 0 - or content.corpus_embeddings is None - or len(content.corpus_embeddings) == 0 - ): - return [], [] - query, entries, corpus_embeddings = raw_query, content.entries, content.corpus_embeddings + file_type = search_type_to_embeddings_type[type.value] - # Filter query, entries and embeddings before semantic search - query, entries, corpus_embeddings = apply_filters(query, entries, corpus_embeddings, content.filters) - - # If no entries left after filtering, return empty results - if entries is None or len(entries) == 0: - return [], [] - # If query only had filters it'll be empty now. So short-circuit and return results. - if query.strip() == "": - hits = [{"corpus_id": id, "score": 1.0} for id, _ in enumerate(entries)] - return hits, entries + query = raw_query # Encode the query using the bi-encoder if question_embedding is None: with timer("Query Encode Time", logger, state.device): - question_embedding = search_model.bi_encoder.encode([query], convert_to_tensor=True, device=state.device) - question_embedding = util.normalize_embeddings(question_embedding) + question_embedding = state.embeddings_model.embed_query(query) # Find relevant entries for the query - top_k = min(len(entries), search_model.top_k or 10) # top_k hits can't be more than the total entries in corpus + top_k = 10 with timer("Search Time", logger, state.device): - hits = util.semantic_search(question_embedding, corpus_embeddings, top_k, score_function=util.dot_score)[0] + hits = EmbeddingsAdapters.search_with_embeddings( + user=user, + embeddings=question_embedding, + max_results=top_k, + file_type_filter=file_type, + raw_query=raw_query, + ).all() + hits = await sync_to_async(list)(hits) # type: ignore[call-arg] + return hits + + +def collate_results(hits, dedupe=True): + hit_ids = set() + for hit in hits: + if dedupe and hit.corpus_id in hit_ids: + continue + + else: + hit_ids.add(hit.corpus_id) + yield SearchResponse.parse_obj( + { + "entry": hit.raw, + "score": hit.distance, + "additional": { + "file": hit.file_path, + "compiled": hit.compiled, + "heading": hit.heading, + }, + } + ) + + +def rerank_and_sort_results(hits, query): # Score all retrieved entries using the cross-encoder - if rank_results and search_model.cross_encoder: - hits = cross_encoder_score(search_model.cross_encoder, query, entries, hits) + hits = cross_encoder_score(query, hits) - # Filter results by score threshold - hits = [hit for hit in hits if hit.get("cross-score", hit.get("score")) >= score_threshold] + # Sort results by cross-encoder score followed by bi-encoder score + hits = sort_results(rank_results=True, hits=hits) - # Order results by cross-encoder score followed by bi-encoder score - hits = sort_results(rank_results, hits) - - # Deduplicate entries by raw entry text before showing to users - if dedupe: - hits = deduplicate_results(entries, hits) - - return hits, entries - - -def collate_results(hits, entries: List[Entry], count=5) -> List[SearchResponse]: - return [ - SearchResponse.parse_obj( - { - "entry": entries[hit["corpus_id"]].raw, - "score": f"{hit.get('cross-score') or hit.get('score')}", - "additional": { - "file": entries[hit["corpus_id"]].file, - "compiled": entries[hit["corpus_id"]].compiled, - "heading": entries[hit["corpus_id"]].heading, - }, - } - ) - for hit in hits[0:count] - ] + return hits def setup( - text_to_jsonl: Type[TextToJsonl], + text_to_jsonl: Type[TextEmbeddings], files: dict[str, str], - config: TextConfigBase, - bi_encoder: BaseEncoder, regenerate: bool, - filters: List[BaseFilter] = [], - normalize: bool = True, full_corpus: bool = True, -) -> TextContent: - # Map notes in text files to (compressed) JSONL formatted file - config.compressed_jsonl = resolve_absolute_path(config.compressed_jsonl) - previous_entries = [] - if config.compressed_jsonl.exists() and not regenerate: - previous_entries = extract_entries(config.compressed_jsonl) - entries_with_indices = text_to_jsonl(config).process( - previous_entries=previous_entries, files=files, full_corpus=full_corpus - ) - - # Extract Updated Entries - entries = extract_entries(config.compressed_jsonl) - if is_none_or_empty(entries): - config_params = ", ".join([f"{key}={value}" for key, value in config.dict().items()]) - raise ValueError( - f"No valid entries found in specified configuration: {config_params}, with files: {files.keys()}" + user: KhojUser = None, + config=None, +) -> None: + if config: + num_new_embeddings, num_deleted_embeddings = text_to_jsonl(config).process( + files=files, full_corpus=full_corpus, user=user, regenerate=regenerate + ) + else: + num_new_embeddings, num_deleted_embeddings = text_to_jsonl().process( + files=files, full_corpus=full_corpus, user=user, regenerate=regenerate ) - # Compute or Load Embeddings - config.embeddings_file = resolve_absolute_path(config.embeddings_file) - corpus_embeddings = compute_embeddings( - entries_with_indices, bi_encoder, config.embeddings_file, regenerate=regenerate, normalize=normalize + file_names = [file_name for file_name in files] + + logger.info( + f"Created {num_new_embeddings} new embeddings. Deleted {num_deleted_embeddings} embeddings for user {user} and files {file_names}" ) - for filter in filters: - filter.load(entries, regenerate=regenerate) - return TextContent(entries, corpus_embeddings, filters) - - -def load( - config: TextConfigBase, - filters: List[BaseFilter] = [], -) -> TextContent: - # Map notes in text files to (compressed) JSONL formatted file - config.compressed_jsonl = resolve_absolute_path(config.compressed_jsonl) - entries = extract_entries(config.compressed_jsonl) - - # Compute or Load Embeddings - config.embeddings_file = resolve_absolute_path(config.embeddings_file) - corpus_embeddings = load_embeddings(config.embeddings_file) - - for filter in filters: - filter.load(entries, regenerate=False) - - return TextContent(entries, corpus_embeddings, filters) - - -def apply_filters( - query: str, entries: List[Entry], corpus_embeddings: torch.Tensor, filters: List[BaseFilter] -) -> Tuple[str, List[Entry], torch.Tensor]: - """Filter query, entries and embeddings before semantic search""" - - with timer("Total Filter Time", logger, state.device): - included_entry_indices = set(range(len(entries))) - filters_in_query = [filter for filter in filters if filter.can_filter(query)] - for filter in filters_in_query: - query, included_entry_indices_by_filter = filter.apply(query, entries) - included_entry_indices.intersection_update(included_entry_indices_by_filter) - - # Get entries (and associated embeddings) satisfying all filters - if not included_entry_indices: - return "", [], torch.tensor([], device=state.device) - else: - entries = [entries[id] for id in included_entry_indices] - corpus_embeddings = torch.index_select( - corpus_embeddings, 0, torch.tensor(list(included_entry_indices), device=state.device) - ) - - return query, entries, corpus_embeddings - - -def cross_encoder_score(cross_encoder: CrossEncoder, query: str, entries: List[Entry], hits: List[dict]) -> List[dict]: +def cross_encoder_score(query: str, hits: List[SearchResponse]) -> List[SearchResponse]: """Score all retrieved entries using the cross-encoder""" with timer("Cross-Encoder Predict Time", logger, state.device): - cross_inp = [[query, entries[hit["corpus_id"]].compiled] for hit in hits] - cross_scores = cross_encoder.predict(cross_inp) + cross_scores = state.cross_encoder_model.predict(query, hits) # Store cross-encoder scores in results dictionary for ranking for idx in range(len(cross_scores)): - hits[idx]["cross-score"] = cross_scores[idx] + hits[idx]["cross_score"] = cross_scores[idx] return hits @@ -291,23 +236,5 @@ def sort_results(rank_results: bool, hits: List[dict]) -> List[dict]: with timer("Rank Time", logger, state.device): hits.sort(key=lambda x: x["score"], reverse=True) # sort by bi-encoder score if rank_results: - hits.sort(key=lambda x: x["cross-score"], reverse=True) # sort by cross-encoder score - return hits - - -def deduplicate_results(entries: List[Entry], hits: List[dict]) -> List[dict]: - """Deduplicate entries by raw entry text before showing to users - Compiled entries are split by max tokens supported by ML models. - This can result in duplicate hits, entries shown to user.""" - - with timer("Deduplication Time", logger, state.device): - seen, original_hits_count = set(), len(hits) - hits = [ - hit - for hit in hits - if entries[hit["corpus_id"]].raw not in seen and not seen.add(entries[hit["corpus_id"]].raw) # type: ignore[func-returns-value] - ] - duplicate_hits = original_hits_count - len(hits) - - logger.debug(f"Removed {duplicate_hits} duplicates") + hits.sort(key=lambda x: x["cross_score"], reverse=True) # sort by cross-encoder score return hits diff --git a/src/khoj/utils/cli.py b/src/khoj/utils/cli.py index 1d6106cb..c72320a1 100644 --- a/src/khoj/utils/cli.py +++ b/src/khoj/utils/cli.py @@ -2,6 +2,7 @@ import argparse import pathlib from importlib.metadata import version +import os # Internal Packages from khoj.utils.helpers import resolve_absolute_path @@ -34,6 +35,12 @@ def cli(args=None): ) parser.add_argument("--version", "-V", action="store_true", help="Print the installed Khoj version and exit") parser.add_argument("--demo", action="store_true", default=False, help="Run Khoj in demo mode") + parser.add_argument( + "--anonymous-mode", + action="store_true", + default=False, + help="Run Khoj in anonymous mode. This does not require any login for connecting users.", + ) args = parser.parse_args(args) @@ -51,6 +58,8 @@ def cli(args=None): else: args = run_migrations(args) args.config = parse_config_from_file(args.config_file) + if os.environ.get("DEBUG"): + args.config.app.should_log_telemetry = False return args diff --git a/src/khoj/utils/config.py b/src/khoj/utils/config.py index 5b3b9f6e..ee5b4f9f 100644 --- a/src/khoj/utils/config.py +++ b/src/khoj/utils/config.py @@ -41,9 +41,7 @@ class ProcessorType(str, Enum): @dataclass class TextContent: - entries: List[Entry] - corpus_embeddings: torch.Tensor - filters: List[BaseFilter] + enabled: bool @dataclass @@ -67,21 +65,13 @@ class ImageSearchModel: @dataclass class ContentIndex: - org: Optional[TextContent] = None - markdown: Optional[TextContent] = None - pdf: Optional[TextContent] = None - github: Optional[TextContent] = None - notion: Optional[TextContent] = None image: Optional[ImageContent] = None - plaintext: Optional[TextContent] = None - plugins: Optional[Dict[str, TextContent]] = None @dataclass class SearchModels: text_search: Optional[TextSearchModel] = None image_search: Optional[ImageSearchModel] = None - plugin_search: Optional[Dict[str, TextSearchModel]] = None @dataclass diff --git a/src/khoj/utils/constants.py b/src/khoj/utils/constants.py index 9ed97798..181dee04 100644 --- a/src/khoj/utils/constants.py +++ b/src/khoj/utils/constants.py @@ -5,6 +5,7 @@ web_directory = app_root_directory / "khoj/interface/web/" empty_escape_sequences = "\n|\r|\t| " app_env_filepath = "~/.khoj/env" telemetry_server = "https://khoj.beta.haletic.com/v1/telemetry" +content_directory = "~/.khoj/content/" empty_config = { "content-type": { diff --git a/src/khoj/utils/fs_syncer.py b/src/khoj/utils/fs_syncer.py index 44fc70ad..fc7e4a2d 100644 --- a/src/khoj/utils/fs_syncer.py +++ b/src/khoj/utils/fs_syncer.py @@ -5,29 +5,39 @@ from typing import Optional from bs4 import BeautifulSoup from khoj.utils.helpers import get_absolute_path, is_none_or_empty -from khoj.utils.rawconfig import TextContentConfig, ContentConfig +from khoj.utils.rawconfig import TextContentConfig from khoj.utils.config import SearchType +from database.models import LocalMarkdownConfig, LocalOrgConfig, LocalPdfConfig, LocalPlaintextConfig logger = logging.getLogger(__name__) -def collect_files(config: ContentConfig, search_type: Optional[SearchType] = SearchType.All): +def collect_files(search_type: Optional[SearchType] = SearchType.All, user=None) -> dict: files = {} - if config is None: - return files - if search_type == SearchType.All or search_type == SearchType.Org: - files["org"] = get_org_files(config.org) if config.org else {} + org_config = LocalOrgConfig.objects.filter(user=user).first() + files["org"] = get_org_files(construct_config_from_db(org_config)) if org_config else {} if search_type == SearchType.All or search_type == SearchType.Markdown: - files["markdown"] = get_markdown_files(config.markdown) if config.markdown else {} + markdown_config = LocalMarkdownConfig.objects.filter(user=user).first() + files["markdown"] = get_markdown_files(construct_config_from_db(markdown_config)) if markdown_config else {} if search_type == SearchType.All or search_type == SearchType.Plaintext: - files["plaintext"] = get_plaintext_files(config.plaintext) if config.plaintext else {} + plaintext_config = LocalPlaintextConfig.objects.filter(user=user).first() + files["plaintext"] = get_plaintext_files(construct_config_from_db(plaintext_config)) if plaintext_config else {} if search_type == SearchType.All or search_type == SearchType.Pdf: - files["pdf"] = get_pdf_files(config.pdf) if config.pdf else {} + pdf_config = LocalPdfConfig.objects.filter(user=user).first() + files["pdf"] = get_pdf_files(construct_config_from_db(pdf_config)) if pdf_config else {} return files +def construct_config_from_db(db_config) -> TextContentConfig: + return TextContentConfig( + input_files=db_config.input_files, + input_filter=db_config.input_filter, + index_heading_entries=db_config.index_heading_entries, + ) + + def get_plaintext_files(config: TextContentConfig) -> dict[str, str]: def is_plaintextfile(file: str): "Check if file is plaintext file" diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index 9209ff67..e41791f9 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -209,10 +209,12 @@ def log_telemetry( if not app_config or not app_config.should_log_telemetry: return [] + if properties.get("server_id") is None: + properties["server_id"] = get_server_id() + # Populate telemetry data to log request_body = { "telemetry_type": telemetry_type, - "server_id": get_server_id(), "server_version": version("khoj-assistant"), "os": platform.system(), "timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), diff --git a/src/khoj/utils/rawconfig.py b/src/khoj/utils/rawconfig.py index f7c42266..5d2b3ce4 100644 --- a/src/khoj/utils/rawconfig.py +++ b/src/khoj/utils/rawconfig.py @@ -1,13 +1,14 @@ # System Packages import json from pathlib import Path -from typing import List, Dict, Optional, Union, Any +from typing import List, Dict, Optional +import uuid # External Packages -from pydantic import BaseModel, validator +from pydantic import BaseModel # Internal Packages -from khoj.utils.helpers import to_snake_case_from_dash, is_none_or_empty +from khoj.utils.helpers import to_snake_case_from_dash class ConfigBase(BaseModel): @@ -27,7 +28,7 @@ class TextConfigBase(ConfigBase): embeddings_file: Path -class TextContentConfig(TextConfigBase): +class TextContentConfig(ConfigBase): input_files: Optional[List[Path]] input_filter: Optional[List[str]] index_heading_entries: Optional[bool] = False @@ -39,12 +40,12 @@ class GithubRepoConfig(ConfigBase): branch: Optional[str] = "master" -class GithubContentConfig(TextConfigBase): +class GithubContentConfig(ConfigBase): pat_token: str repos: List[GithubRepoConfig] -class NotionContentConfig(TextConfigBase): +class NotionContentConfig(ConfigBase): token: str @@ -63,7 +64,6 @@ class ContentConfig(ConfigBase): pdf: Optional[TextContentConfig] plaintext: Optional[TextContentConfig] github: Optional[GithubContentConfig] - plugins: Optional[Dict[str, TextContentConfig]] notion: Optional[NotionContentConfig] @@ -122,7 +122,8 @@ class FullConfig(ConfigBase): class SearchResponse(ConfigBase): entry: str - score: str + score: float + cross_score: Optional[float] additional: Optional[dict] @@ -131,14 +132,21 @@ class Entry: compiled: str heading: Optional[str] file: Optional[str] + corpus_id: str def __init__( - self, raw: str = None, compiled: str = None, heading: Optional[str] = None, file: Optional[str] = None + self, + raw: str = None, + compiled: str = None, + heading: Optional[str] = None, + file: Optional[str] = None, + corpus_id: uuid.UUID = None, ): self.raw = raw self.compiled = compiled self.heading = heading self.file = file + self.corpus_id = str(corpus_id) def to_json(self) -> str: return json.dumps(self.__dict__, ensure_ascii=False) @@ -153,4 +161,5 @@ class Entry: compiled=dictionary["compiled"], file=dictionary.get("file", None), heading=dictionary.get("heading", None), + corpus_id=dictionary.get("corpus_id", None), ) diff --git a/src/khoj/utils/state.py b/src/khoj/utils/state.py index 5ac8a838..d6169d2a 100644 --- a/src/khoj/utils/state.py +++ b/src/khoj/utils/state.py @@ -2,6 +2,7 @@ import threading from typing import List, Dict from packaging import version +from collections import defaultdict # External Packages import torch @@ -12,10 +13,13 @@ from khoj.utils import config as utils_config from khoj.utils.config import ContentIndex, SearchModels, ProcessorConfigModel from khoj.utils.helpers import LRU from khoj.utils.rawconfig import FullConfig +from khoj.processor.embeddings import EmbeddingsModel, CrossEncoderModel # Application Global State config = FullConfig() search_models = SearchModels() +embeddings_model = EmbeddingsModel() +cross_encoder_model = CrossEncoderModel() content_index = ContentIndex() processor_config = ProcessorConfigModel() config_file: Path = None @@ -23,14 +27,14 @@ verbose: int = 0 host: str = None port: int = None cli_args: List[str] = None -query_cache = LRU() +query_cache: Dict[str, LRU] = defaultdict(LRU) config_lock = threading.Lock() chat_lock = threading.Lock() SearchType = utils_config.SearchType telemetry: List[Dict[str, str]] = [] -previous_query: str = None demo: bool = False khoj_version: str = None +anonymous_mode: bool = False if torch.cuda.is_available(): # Use CUDA GPU diff --git a/tests/conftest.py b/tests/conftest.py index 4f7dfb10..5f515ef1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,15 +1,19 @@ # External Packages import os -from copy import deepcopy from fastapi.testclient import TestClient from pathlib import Path import pytest from fastapi.staticfiles import StaticFiles +from fastapi import FastAPI +import factory +import os +from fastapi import FastAPI + +app = FastAPI() + # Internal Packages -from app.main import app from khoj.configure import configure_processor, configure_routes, configure_search_types, configure_middleware -from khoj.processor.markdown.markdown_to_jsonl import MarkdownToJsonl from khoj.processor.plaintext.plaintext_to_jsonl import PlaintextToJsonl from khoj.search_type import image_search, text_search from khoj.utils.config import SearchModels @@ -22,8 +26,6 @@ from khoj.utils.rawconfig import ( OpenAIProcessorConfig, ProcessorConfig, TextContentConfig, - GithubContentConfig, - GithubRepoConfig, ImageContentConfig, SearchConfig, TextSearchConfig, @@ -31,11 +33,31 @@ from khoj.utils.rawconfig import ( ) from khoj.utils import state, fs_syncer from khoj.routers.indexer import configure_content -from khoj.processor.jsonl.jsonl_to_jsonl import JsonlToJsonl from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl -from khoj.search_filter.date_filter import DateFilter -from khoj.search_filter.word_filter import WordFilter -from khoj.search_filter.file_filter import FileFilter +from database.models import ( + LocalOrgConfig, + LocalMarkdownConfig, + LocalPlaintextConfig, + LocalPdfConfig, + GithubConfig, + KhojUser, + GithubRepoConfig, +) + + +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(db): + pass + + +class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = KhojUser + + username = factory.Faker("name") + email = factory.Faker("email") + password = factory.Faker("password") + uuid = factory.Faker("uuid4") @pytest.fixture(scope="session") @@ -67,17 +89,28 @@ def search_config() -> SearchConfig: return search_config +@pytest.mark.django_db +@pytest.fixture +def default_user(): + return UserFactory() + + @pytest.fixture(scope="session") def search_models(search_config: SearchConfig): search_models = SearchModels() - search_models.text_search = text_search.initialize_model(search_config.asymmetric) search_models.image_search = image_search.initialize_model(search_config.image) return search_models -@pytest.fixture(scope="session") -def content_config(tmp_path_factory, search_models: SearchModels, search_config: SearchConfig): +@pytest.fixture +def anyio_backend(): + return "asyncio" + + +@pytest.mark.django_db +@pytest.fixture(scope="function") +def content_config(tmp_path_factory, search_models: SearchModels, default_user: KhojUser): content_dir = tmp_path_factory.mktemp("content") # Generate Image Embeddings from Test Images @@ -92,94 +125,45 @@ def content_config(tmp_path_factory, search_models: SearchModels, search_config: image_search.setup(content_config.image, search_models.image_search.image_encoder, regenerate=False) - # Generate Notes Embeddings from Test Notes - content_config.org = TextContentConfig( + LocalOrgConfig.objects.create( input_files=None, input_filter=["tests/data/org/*.org"], - compressed_jsonl=content_dir.joinpath("notes.jsonl.gz"), - embeddings_file=content_dir.joinpath("note_embeddings.pt"), + index_heading_entries=False, + user=default_user, ) - filters = [DateFilter(), WordFilter(), FileFilter()] - text_search.setup( - OrgToJsonl, - get_sample_data("org"), - content_config.org, - search_models.text_search.bi_encoder, - regenerate=False, - filters=filters, - ) - - content_config.plugins = { - "plugin1": TextContentConfig( - input_files=[content_dir.joinpath("notes.jsonl.gz")], - input_filter=None, - compressed_jsonl=content_dir.joinpath("plugin.jsonl.gz"), - embeddings_file=content_dir.joinpath("plugin_embeddings.pt"), - ) - } + text_search.setup(OrgToJsonl, get_sample_data("org"), regenerate=False, user=default_user) if os.getenv("GITHUB_PAT_TOKEN"): - content_config.github = GithubContentConfig( - pat_token=os.getenv("GITHUB_PAT_TOKEN", ""), - 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"), + GithubConfig.objects.create( + pat_token=os.getenv("GITHUB_PAT_TOKEN"), + user=default_user, ) - content_config.plaintext = TextContentConfig( + GithubRepoConfig.objects.create( + owner="khoj-ai", + name="lantern", + branch="master", + github_config=GithubConfig.objects.get(user=default_user), + ) + + LocalPlaintextConfig.objects.create( input_files=None, input_filter=["tests/data/plaintext/*.txt", "tests/data/plaintext/*.md", "tests/data/plaintext/*.html"], - compressed_jsonl=content_dir.joinpath("plaintext.jsonl.gz"), - embeddings_file=content_dir.joinpath("plaintext_embeddings.pt"), - ) - - content_config.github = GithubContentConfig( - pat_token=os.getenv("GITHUB_PAT_TOKEN", ""), - 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"), - ) - - filters = [DateFilter(), WordFilter(), FileFilter()] - text_search.setup( - JsonlToJsonl, - None, - content_config.plugins["plugin1"], - search_models.text_search.bi_encoder, - regenerate=False, - filters=filters, + user=default_user, ) return content_config @pytest.fixture(scope="session") -def md_content_config(tmp_path_factory): - content_dir = tmp_path_factory.mktemp("content") - - # Generate Embeddings for Markdown Content - content_config = ContentConfig() - content_config.markdown = TextContentConfig( +def md_content_config(): + markdown_config = LocalMarkdownConfig.objects.create( input_files=None, input_filter=["tests/data/markdown/*.markdown"], - compressed_jsonl=content_dir.joinpath("markdown.jsonl.gz"), - embeddings_file=content_dir.joinpath("markdown_embeddings.pt"), ) - return content_config + return markdown_config @pytest.fixture(scope="session") @@ -220,19 +204,20 @@ def processor_config_offline_chat(tmp_path_factory): @pytest.fixture(scope="session") def chat_client(md_content_config: ContentConfig, search_config: SearchConfig, processor_config: ProcessorConfig): # Initialize app state - state.config.content_type = md_content_config state.config.search_type = search_config state.SearchType = configure_search_types(state.config) # Index Markdown Content for Search - state.search_models.text_search = text_search.initialize_model(search_config.asymmetric) - all_files = fs_syncer.collect_files(state.config.content_type) + all_files = fs_syncer.collect_files() state.content_index = configure_content( state.content_index, state.config.content_type, all_files, state.search_models ) # Initialize Processor from Config state.processor_config = configure_processor(processor_config) + state.anonymous_mode = True + + app = FastAPI() configure_routes(app) configure_middleware(app) @@ -241,33 +226,45 @@ def chat_client(md_content_config: ContentConfig, search_config: SearchConfig, p @pytest.fixture(scope="function") -def client(content_config: ContentConfig, search_config: SearchConfig, processor_config: ProcessorConfig): +def fastapi_app(): + app = FastAPI() + configure_routes(app) + configure_middleware(app) + app.mount("/static", StaticFiles(directory=web_directory), name="static") + return app + + +@pytest.fixture(scope="function") +def client( + content_config: ContentConfig, + search_config: SearchConfig, + processor_config: ProcessorConfig, + default_user: KhojUser, +): state.config.content_type = content_config state.config.search_type = search_config state.SearchType = configure_search_types(state.config) # These lines help us Mock the Search models for these search types - state.search_models.text_search = text_search.initialize_model(search_config.asymmetric) state.search_models.image_search = image_search.initialize_model(search_config.image) - state.content_index.org = text_search.setup( + text_search.setup( OrgToJsonl, get_sample_data("org"), - content_config.org, - state.search_models.text_search.bi_encoder, regenerate=False, + user=default_user, ) state.content_index.image = image_search.setup( content_config.image, state.search_models.image_search, regenerate=False ) - state.content_index.plaintext = text_search.setup( + text_search.setup( PlaintextToJsonl, get_sample_data("plaintext"), - content_config.plaintext, - state.search_models.text_search.bi_encoder, regenerate=False, + user=default_user, ) state.processor_config = configure_processor(processor_config) + state.anonymous_mode = True configure_routes(app) configure_middleware(app) @@ -288,7 +285,6 @@ def client_offline_chat( state.SearchType = configure_search_types(state.config) # Index Markdown Content for Search - state.search_models.text_search = text_search.initialize_model(search_config.asymmetric) state.search_models.image_search = image_search.initialize_model(search_config.image) all_files = fs_syncer.collect_files(state.config.content_type) @@ -298,6 +294,7 @@ def client_offline_chat( # Initialize Processor from Config state.processor_config = configure_processor(processor_config_offline_chat) + state.anonymous_mode = True configure_routes(app) configure_middleware(app) @@ -306,9 +303,11 @@ def client_offline_chat( @pytest.fixture(scope="function") -def new_org_file(content_config: ContentConfig): +def new_org_file(default_user: KhojUser, content_config: ContentConfig): # Setup - new_org_file = Path(content_config.org.input_filter[0]).parent / "new_file.org" + org_config = LocalOrgConfig.objects.filter(user=default_user).first() + input_filters = org_config.input_filter + new_org_file = Path(input_filters[0]).parent / "new_file.org" new_org_file.touch() yield new_org_file @@ -319,11 +318,9 @@ def new_org_file(content_config: ContentConfig): @pytest.fixture(scope="function") -def org_config_with_only_new_file(content_config: ContentConfig, new_org_file: Path): - new_org_config = deepcopy(content_config.org) - new_org_config.input_files = [f"{new_org_file}"] - new_org_config.input_filter = None - return new_org_config +def org_config_with_only_new_file(new_org_file: Path, default_user: KhojUser): + LocalOrgConfig.objects.update(input_files=[str(new_org_file)], input_filter=None) + return LocalOrgConfig.objects.filter(user=default_user).first() @pytest.fixture(scope="function") diff --git a/tests/data/config.yml b/tests/data/config.yml index 06978cf1..c544eebe 100644 --- a/tests/data/config.yml +++ b/tests/data/config.yml @@ -9,17 +9,6 @@ content-type: input-filter: - '*.org' - ~/notes/*.org - plugins: - content_plugin_1: - compressed-jsonl: content_plugin_1.jsonl.gz - embeddings-file: content_plugin_1_embeddings.pt - input-files: - - content_plugin_1_new.jsonl.gz - content_plugin_2: - compressed-jsonl: content_plugin_2.jsonl.gz - embeddings-file: content_plugin_2_embeddings.pt - input-filter: - - '*2_new.jsonl.gz' enable-offline-chat: false search-type: asymmetric: diff --git a/tests/test_cli.py b/tests/test_cli.py index 9de3a853..cff2a7f3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -48,14 +48,3 @@ def test_cli_config_from_file(): Path("~/first_from_config.org"), Path("~/second_from_config.org"), ] - assert len(actual_args.config.content_type.plugins.keys()) == 2 - assert actual_args.config.content_type.plugins["content_plugin_1"].input_files == [ - Path("content_plugin_1_new.jsonl.gz") - ] - assert actual_args.config.content_type.plugins["content_plugin_2"].input_filter == ["*2_new.jsonl.gz"] - assert actual_args.config.content_type.plugins["content_plugin_1"].compressed_jsonl == Path( - "content_plugin_1.jsonl.gz" - ) - assert actual_args.config.content_type.plugins["content_plugin_2"].embeddings_file == Path( - "content_plugin_2_embeddings.pt" - ) diff --git a/tests/test_client.py b/tests/test_client.py index a5f14882..f63b968c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,22 +2,21 @@ from io import BytesIO from PIL import Image from urllib.parse import quote - +import pytest # External Packages from fastapi.testclient import TestClient -import pytest +from fastapi import FastAPI # Internal Packages -from app.main import app from khoj.configure import configure_routes, configure_search_types from khoj.utils import state from khoj.utils.state import search_models, content_index, config from khoj.search_type import text_search, image_search from khoj.utils.rawconfig import ContentConfig, SearchConfig from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl -from khoj.search_filter.word_filter import WordFilter -from khoj.search_filter.file_filter import FileFilter +from database.models import KhojUser +from database.adapters import EmbeddingsAdapters # Test @@ -35,7 +34,7 @@ def test_search_with_invalid_content_type(client): # ---------------------------------------------------------------------------------------------------- def test_search_with_valid_content_type(client): - for content_type in ["all", "org", "markdown", "image", "pdf", "github", "notion", "plugin1"]: + for content_type in ["all", "org", "markdown", "image", "pdf", "github", "notion"]: # Act response = client.get(f"/api/search?q=random&t={content_type}") # Assert @@ -75,7 +74,7 @@ def test_index_update(client): # ---------------------------------------------------------------------------------------------------- def test_regenerate_with_valid_content_type(client): - for content_type in ["all", "org", "markdown", "image", "pdf", "notion", "plugin1"]: + for content_type in ["all", "org", "markdown", "image", "pdf", "notion"]: # Arrange files = get_sample_files_data() headers = {"x-api-key": "secret"} @@ -102,60 +101,42 @@ def test_regenerate_with_github_fails_without_pat(client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db @pytest.mark.skip(reason="Flaky test on parallel test runs") -def test_get_configured_types_via_api(client): +def test_get_configured_types_via_api(client, sample_org_data): # Act - response = client.get(f"/api/config/types") + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) + + enabled_types = EmbeddingsAdapters.get_unique_file_types(user=None).all().values_list("file_type", flat=True) # Assert - assert response.status_code == 200 - assert response.json() == ["all", "org", "image", "plaintext", "plugin1"] + assert list(enabled_types) == ["org"] # ---------------------------------------------------------------------------------------------------- -def test_get_configured_types_with_only_plugin_content_config(content_config): +@pytest.mark.django_db(transaction=True) +def test_get_api_config_types(client, search_config: SearchConfig, sample_org_data): # Arrange - config.content_type = ContentConfig() - config.content_type.plugins = content_config.plugins - state.SearchType = configure_search_types(config) - - configure_routes(app) - client = TestClient(app) + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) # Act response = client.get(f"/api/config/types") # Assert assert response.status_code == 200 - assert response.json() == ["all", "plugin1"] + assert response.json() == ["all", "org", "markdown", "image"] # ---------------------------------------------------------------------------------------------------- -def test_get_configured_types_with_no_plugin_content_config(content_config): +@pytest.mark.django_db(transaction=True) +def test_get_configured_types_with_no_content_config(fastapi_app: FastAPI): # Arrange - config.content_type = content_config - config.content_type.plugins = None state.SearchType = configure_search_types(config) + original_config = state.config.content_type + state.config.content_type = None - configure_routes(app) - client = TestClient(app) - - # Act - response = client.get(f"/api/config/types") - - # Assert - assert response.status_code == 200 - assert "plugin1" not in response.json() - - -# ---------------------------------------------------------------------------------------------------- -def test_get_configured_types_with_no_content_config(): - # Arrange - config.content_type = ContentConfig() - state.SearchType = configure_search_types(config) - - configure_routes(app) - client = TestClient(app) + configure_routes(fastapi_app) + client = TestClient(fastapi_app) # Act response = client.get(f"/api/config/types") @@ -164,6 +145,9 @@ def test_get_configured_types_with_no_content_config(): assert response.status_code == 200 assert response.json() == ["all"] + # Restore + state.config.content_type = original_config + # ---------------------------------------------------------------------------------------------------- def test_image_search(client, content_config: ContentConfig, search_config: SearchConfig): @@ -192,12 +176,10 @@ def test_image_search(client, content_config: ContentConfig, search_config: Sear # ---------------------------------------------------------------------------------------------------- -def test_notes_search(client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data): +@pytest.mark.django_db(transaction=True) +def test_notes_search(client, search_config: SearchConfig, sample_org_data): # Arrange - search_models.text_search = text_search.initialize_model(search_config.asymmetric) - content_index.org = text_search.setup( - OrgToJsonl, sample_org_data, content_config.org, search_models.text_search.bi_encoder, regenerate=False - ) + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) user_query = quote("How to git install application?") # Act @@ -211,19 +193,15 @@ def test_notes_search(client, content_config: ContentConfig, search_config: Sear # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_notes_search_with_only_filters( client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data ): # Arrange - filters = [WordFilter(), FileFilter()] - search_models.text_search = text_search.initialize_model(search_config.asymmetric) - content_index.org = text_search.setup( + text_search.setup( OrgToJsonl, sample_org_data, - content_config.org, - search_models.text_search.bi_encoder, regenerate=False, - filters=filters, ) user_query = quote('+"Emacs" file:"*.org"') @@ -238,15 +216,10 @@ def test_notes_search_with_only_filters( # ---------------------------------------------------------------------------------------------------- -def test_notes_search_with_include_filter( - client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data -): +@pytest.mark.django_db(transaction=True) +def test_notes_search_with_include_filter(client, sample_org_data): # Arrange - filters = [WordFilter()] - search_models.text_search = text_search.initialize_model(search_config.asymmetric) - content_index.org = text_search.setup( - OrgToJsonl, sample_org_data, content_config.org, search_models.text_search, regenerate=False, filters=filters - ) + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) user_query = quote('How to git install application? +"Emacs"') # Act @@ -260,19 +233,13 @@ def test_notes_search_with_include_filter( # ---------------------------------------------------------------------------------------------------- -def test_notes_search_with_exclude_filter( - client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data -): +@pytest.mark.django_db(transaction=True) +def test_notes_search_with_exclude_filter(client, sample_org_data): # Arrange - filters = [WordFilter()] - search_models.text_search = text_search.initialize_model(search_config.asymmetric) - content_index.org = text_search.setup( + text_search.setup( OrgToJsonl, sample_org_data, - content_config.org, - search_models.text_search.bi_encoder, regenerate=False, - filters=filters, ) user_query = quote('How to git install application? -"clone"') @@ -286,6 +253,22 @@ def test_notes_search_with_exclude_filter( assert "clone" not in search_result +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) +def test_different_user_data_not_accessed(client, sample_org_data, default_user: KhojUser): + # Arrange + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user) + user_query = quote("How to git install application?") + + # Act + response = client.get(f"/api/search?q={user_query}&n=1&t=org") + + # Assert + assert response.status_code == 200 + # assert actual response has no data as the default_user is different from the user making the query (anonymous) + assert len(response.json()) == 0 + + def get_sample_files_data(): return { "files": ("path/to/filename.org", "* practicing piano", "text/org"), diff --git a/tests/test_date_filter.py b/tests/test_date_filter.py index d0d05bc5..f1f26d28 100644 --- a/tests/test_date_filter.py +++ b/tests/test_date_filter.py @@ -1,53 +1,12 @@ # Standard Packages import re from datetime import datetime -from math import inf # External Packages import pytest # Internal Packages from khoj.search_filter.date_filter import DateFilter -from khoj.utils.rawconfig import Entry - - -@pytest.mark.filterwarnings("ignore:The localize method is no longer necessary.") -def test_date_filter(): - entries = [ - Entry(compiled="Entry with no date", raw="Entry with no date"), - Entry(compiled="April Fools entry: 1984-04-01", raw="April Fools entry: 1984-04-01"), - Entry(compiled="Entry with date:1984-04-02", raw="Entry with date:1984-04-02"), - ] - - q_with_no_date_filter = "head tail" - ret_query, entry_indices = DateFilter().apply(q_with_no_date_filter, entries) - assert ret_query == "head tail" - assert entry_indices == {0, 1, 2} - - q_with_dtrange_non_overlapping_at_boundary = 'head dt>"1984-04-01" dt<"1984-04-02" tail' - ret_query, entry_indices = DateFilter().apply(q_with_dtrange_non_overlapping_at_boundary, entries) - assert ret_query == "head tail" - assert entry_indices == set() - - query_with_overlapping_dtrange = 'head dt>"1984-04-01" dt<"1984-04-03" tail' - ret_query, entry_indices = DateFilter().apply(query_with_overlapping_dtrange, entries) - assert ret_query == "head tail" - assert entry_indices == {2} - - query_with_overlapping_dtrange = 'head dt>="1984-04-01" dt<"1984-04-02" tail' - ret_query, entry_indices = DateFilter().apply(query_with_overlapping_dtrange, entries) - assert ret_query == "head tail" - assert entry_indices == {1} - - query_with_overlapping_dtrange = 'head dt>"1984-04-01" dt<="1984-04-02" tail' - ret_query, entry_indices = DateFilter().apply(query_with_overlapping_dtrange, entries) - assert ret_query == "head tail" - assert entry_indices == {2} - - query_with_overlapping_dtrange = 'head dt>="1984-04-01" dt<="1984-04-02" tail' - ret_query, entry_indices = DateFilter().apply(query_with_overlapping_dtrange, entries) - assert ret_query == "head tail" - assert entry_indices == {1, 2} @pytest.mark.filterwarnings("ignore:The localize method is no longer necessary.") @@ -56,8 +15,8 @@ def test_extract_date_range(): datetime(1984, 1, 5, 0, 0, 0).timestamp(), datetime(1984, 1, 7, 0, 0, 0).timestamp(), ] - assert DateFilter().extract_date_range('head dt<="1984-01-01"') == [0, datetime(1984, 1, 2, 0, 0, 0).timestamp()] - assert DateFilter().extract_date_range('head dt>="1984-01-01"') == [datetime(1984, 1, 1, 0, 0, 0).timestamp(), inf] + assert DateFilter().extract_date_range('head dt<="1984-01-01"') == [None, datetime(1984, 1, 2, 0, 0, 0).timestamp()] + assert DateFilter().extract_date_range('head dt>="1984-01-01"') == [datetime(1984, 1, 1, 0, 0, 0).timestamp(), None] assert DateFilter().extract_date_range('head dt:"1984-01-01"') == [ datetime(1984, 1, 1, 0, 0, 0).timestamp(), datetime(1984, 1, 2, 0, 0, 0).timestamp(), diff --git a/tests/test_file_filter.py b/tests/test_file_filter.py index ed632d32..f5a903f8 100644 --- a/tests/test_file_filter.py +++ b/tests/test_file_filter.py @@ -6,97 +6,73 @@ from khoj.utils.rawconfig import Entry def test_no_file_filter(): # Arrange file_filter = FileFilter() - entries = arrange_content() q_with_no_filter = "head tail" # Act can_filter = file_filter.can_filter(q_with_no_filter) - ret_query, entry_indices = file_filter.apply(q_with_no_filter, entries) # Assert assert can_filter == False - assert ret_query == "head tail" - assert entry_indices == {0, 1, 2, 3} def test_file_filter_with_non_existent_file(): # Arrange file_filter = FileFilter() - entries = arrange_content() q_with_no_filter = 'head file:"nonexistent.org" tail' # Act can_filter = file_filter.can_filter(q_with_no_filter) - ret_query, entry_indices = file_filter.apply(q_with_no_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {} def test_single_file_filter(): # Arrange file_filter = FileFilter() - entries = arrange_content() q_with_no_filter = 'head file:"file 1.org" tail' # Act can_filter = file_filter.can_filter(q_with_no_filter) - ret_query, entry_indices = file_filter.apply(q_with_no_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {0, 2} def test_file_filter_with_partial_match(): # Arrange file_filter = FileFilter() - entries = arrange_content() q_with_no_filter = 'head file:"1.org" tail' # Act can_filter = file_filter.can_filter(q_with_no_filter) - ret_query, entry_indices = file_filter.apply(q_with_no_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {0, 2} def test_file_filter_with_regex_match(): # Arrange file_filter = FileFilter() - entries = arrange_content() q_with_no_filter = 'head file:"*.org" tail' # Act can_filter = file_filter.can_filter(q_with_no_filter) - ret_query, entry_indices = file_filter.apply(q_with_no_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {0, 1, 2, 3} def test_multiple_file_filter(): # Arrange file_filter = FileFilter() - entries = arrange_content() q_with_no_filter = 'head tail file:"file 1.org" file:"file2.org"' # Act can_filter = file_filter.can_filter(q_with_no_filter) - ret_query, entry_indices = file_filter.apply(q_with_no_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {0, 1, 2, 3} def test_get_file_filter_terms(): @@ -108,7 +84,7 @@ def test_get_file_filter_terms(): filter_terms = file_filter.get_filter_terms(q_with_filter_terms) # Assert - assert filter_terms == ['file:"file 1.org"', 'file:"/path/to/dir/*.org"'] + assert filter_terms == ["file 1\\.org", "/path/to/dir/.*\\.org"] def arrange_content(): diff --git a/tests/test_jsonl_to_jsonl.py b/tests/test_jsonl_to_jsonl.py deleted file mode 100644 index b52b5fc9..00000000 --- a/tests/test_jsonl_to_jsonl.py +++ /dev/null @@ -1,78 +0,0 @@ -# Internal Packages -from khoj.processor.jsonl.jsonl_to_jsonl import JsonlToJsonl -from khoj.utils.rawconfig import Entry - - -def test_process_entries_from_single_input_jsonl(tmp_path): - "Convert multiple jsonl entries from single file to entries." - # Arrange - input_jsonl = """{"raw": "raw input data 1", "compiled": "compiled input data 1", "heading": null, "file": "source/file/path1"} -{"raw": "raw input data 2", "compiled": "compiled input data 2", "heading": null, "file": "source/file/path2"} -""" - input_jsonl_file = create_file(tmp_path, input_jsonl) - - # Act - # Process Each Entry from All Notes Files - input_jsons = JsonlToJsonl.extract_jsonl_entries([input_jsonl_file]) - entries = list(map(Entry.from_dict, input_jsons)) - output_jsonl = JsonlToJsonl.convert_entries_to_jsonl(entries) - - # Assert - assert len(entries) == 2 - assert output_jsonl == input_jsonl - - -def test_process_entries_from_multiple_input_jsonls(tmp_path): - "Convert multiple jsonl entries from single file to entries." - # Arrange - input_jsonl_1 = """{"raw": "raw input data 1", "compiled": "compiled input data 1", "heading": null, "file": "source/file/path1"}""" - input_jsonl_2 = """{"raw": "raw input data 2", "compiled": "compiled input data 2", "heading": null, "file": "source/file/path2"}""" - input_jsonl_file_1 = create_file(tmp_path, input_jsonl_1, filename="input1.jsonl") - input_jsonl_file_2 = create_file(tmp_path, input_jsonl_2, filename="input2.jsonl") - - # Act - # Process Each Entry from All Notes Files - input_jsons = JsonlToJsonl.extract_jsonl_entries([input_jsonl_file_1, input_jsonl_file_2]) - entries = list(map(Entry.from_dict, input_jsons)) - output_jsonl = JsonlToJsonl.convert_entries_to_jsonl(entries) - - # Assert - assert len(entries) == 2 - assert output_jsonl == f"{input_jsonl_1}\n{input_jsonl_2}\n" - - -def test_get_jsonl_files(tmp_path): - "Ensure JSONL files specified via input-filter, input-files extracted" - # Arrange - # Include via input-filter globs - group1_file1 = create_file(tmp_path, filename="group1-file1.jsonl") - group1_file2 = create_file(tmp_path, filename="group1-file2.jsonl") - group2_file1 = create_file(tmp_path, filename="group2-file1.jsonl") - group2_file2 = create_file(tmp_path, filename="group2-file2.jsonl") - # Include via input-file field - file1 = create_file(tmp_path, filename="notes.jsonl") - # Not included by any filter - create_file(tmp_path, filename="not-included-jsonl.jsonl") - create_file(tmp_path, filename="not-included-text.txt") - - expected_files = sorted(map(str, [group1_file1, group1_file2, group2_file1, group2_file2, file1])) - - # Setup input-files, input-filters - input_files = [tmp_path / "notes.jsonl"] - input_filter = [tmp_path / "group1*.jsonl", tmp_path / "group2*.jsonl"] - - # Act - extracted_org_files = JsonlToJsonl.get_jsonl_files(input_files, input_filter) - - # Assert - assert len(extracted_org_files) == 5 - assert extracted_org_files == expected_files - - -# Helper Functions -def create_file(tmp_path, entry=None, filename="test.jsonl"): - jsonl_file = tmp_path / filename - jsonl_file.touch() - if entry: - jsonl_file.write_text(entry) - return jsonl_file diff --git a/tests/test_org_to_jsonl.py b/tests/test_org_to_jsonl.py index abf20d09..d47c212e 100644 --- a/tests/test_org_to_jsonl.py +++ b/tests/test_org_to_jsonl.py @@ -4,7 +4,7 @@ import os # Internal Packages from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl -from khoj.processor.text_to_jsonl import TextToJsonl +from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import is_none_or_empty from khoj.utils.rawconfig import Entry from khoj.utils.fs_syncer import get_org_files @@ -63,7 +63,7 @@ def test_entry_split_when_exceeds_max_words(tmp_path): # Split each entry from specified Org files by max words jsonl_string = OrgToJsonl.convert_org_entries_to_jsonl( - TextToJsonl.split_entries_by_max_tokens( + TextEmbeddings.split_entries_by_max_tokens( OrgToJsonl.convert_org_nodes_to_entries(entries, entry_to_file_map), max_tokens=4 ) ) @@ -86,7 +86,7 @@ def test_entry_split_drops_large_words(): # Act # Split entry by max words and drop words larger than max word length - processed_entry = TextToJsonl.split_entries_by_max_tokens([entry], max_word_length=5)[0] + processed_entry = TextEmbeddings.split_entries_by_max_tokens([entry], max_word_length=5)[0] # Assert # "Heading" dropped from compiled version because its over the set max word limit diff --git a/tests/test_plaintext_to_jsonl.py b/tests/test_plaintext_to_jsonl.py index a6da30e1..56c68e38 100644 --- a/tests/test_plaintext_to_jsonl.py +++ b/tests/test_plaintext_to_jsonl.py @@ -7,6 +7,7 @@ from pathlib import Path from khoj.utils.fs_syncer import get_plaintext_files from khoj.utils.rawconfig import TextContentConfig from khoj.processor.plaintext.plaintext_to_jsonl import PlaintextToJsonl +from database.models import LocalPlaintextConfig, KhojUser def test_plaintext_file(tmp_path): @@ -91,11 +92,12 @@ def test_get_plaintext_files(tmp_path): assert set(extracted_plaintext_files.keys()) == set(expected_files) -def test_parse_html_plaintext_file(content_config): +def test_parse_html_plaintext_file(content_config, default_user: KhojUser): "Ensure HTML files are parsed correctly" # Arrange # Setup input-files, input-filters - extracted_plaintext_files = get_plaintext_files(content_config.plaintext) + config = LocalPlaintextConfig.objects.filter(user=default_user).first() + extracted_plaintext_files = get_plaintext_files(config=config) # Act maps = PlaintextToJsonl.convert_plaintext_entries_to_maps(extracted_plaintext_files) diff --git a/tests/test_text_search.py b/tests/test_text_search.py index 179718fa..af47ffe5 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -3,23 +3,30 @@ import logging import locale from pathlib import Path import os +import asyncio # External Packages import pytest # Internal Packages -from khoj.utils.state import content_index, search_models from khoj.search_type import text_search +from khoj.utils.rawconfig import ContentConfig, SearchConfig from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl from khoj.processor.github.github_to_jsonl import GithubToJsonl from khoj.utils.config import SearchModels -from khoj.utils.fs_syncer import get_org_files +from khoj.utils.fs_syncer import get_org_files, collect_files +from database.models import LocalOrgConfig, KhojUser, Embeddings, GithubConfig + +logger = logging.getLogger(__name__) from khoj.utils.rawconfig import ContentConfig, SearchConfig, TextContentConfig # Test # ---------------------------------------------------------------------------------------------------- -def test_text_search_setup_with_missing_file_raises_error(org_config_with_only_new_file: TextContentConfig): +@pytest.mark.django_db +def test_text_search_setup_with_missing_file_raises_error( + org_config_with_only_new_file: LocalOrgConfig, search_config: SearchConfig +): # Arrange # Ensure file mentioned in org.input-files is missing single_new_file = Path(org_config_with_only_new_file.input_files[0]) @@ -32,98 +39,126 @@ def test_text_search_setup_with_missing_file_raises_error(org_config_with_only_n # ---------------------------------------------------------------------------------------------------- -def test_get_org_files_with_org_suffixed_dir_doesnt_raise_error(tmp_path: Path): +@pytest.mark.django_db +def test_get_org_files_with_org_suffixed_dir_doesnt_raise_error(tmp_path, default_user: KhojUser): # Arrange orgfile = tmp_path / "directory.org" / "file.org" orgfile.parent.mkdir() with open(orgfile, "w") as f: f.write("* Heading\n- List item\n") - org_content_config = TextContentConfig( - input_filter=[f"{tmp_path}/**/*"], compressed_jsonl="test.jsonl", embeddings_file="test.pt" + + LocalOrgConfig.objects.create( + input_filter=[f"{tmp_path}/**/*"], + input_files=None, + user=default_user, ) + org_files = collect_files(user=default_user)["org"] + # Act # should not raise IsADirectoryError and return orgfile - assert get_org_files(org_content_config) == {f"{orgfile}": "* Heading\n- List item\n"} + assert org_files == {f"{orgfile}": "* Heading\n- List item\n"} # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db def test_text_search_setup_with_empty_file_raises_error( - org_config_with_only_new_file: TextContentConfig, search_config: SearchConfig + org_config_with_only_new_file: LocalOrgConfig, default_user: KhojUser, caplog ): # Arrange data = get_org_files(org_config_with_only_new_file) # Act # Generate notes embeddings during asymmetric setup - with pytest.raises(ValueError, match=r"^No valid entries found*"): - text_search.setup(OrgToJsonl, data, org_config_with_only_new_file, search_config.asymmetric, regenerate=True) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) + + assert "Created 0 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message + verify_embeddings(0, default_user) # ---------------------------------------------------------------------------------------------------- -def test_text_search_setup(content_config: ContentConfig, search_models: SearchModels): +@pytest.mark.django_db +def test_text_search_setup(content_config, default_user: KhojUser, caplog): # Arrange - data = get_org_files(content_config.org) - - # Act - # Regenerate notes embeddings during asymmetric setup - notes_model = text_search.setup( - OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=True - ) + org_config = LocalOrgConfig.objects.filter(user=default_user).first() + data = get_org_files(org_config) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) # Assert - assert len(notes_model.entries) == 10 - assert len(notes_model.corpus_embeddings) == 10 + assert "Deleting all embeddings for file type org" in caplog.records[1].message + assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message # ---------------------------------------------------------------------------------------------------- -def test_text_index_same_if_content_unchanged(content_config: ContentConfig, search_models: SearchModels, caplog): +@pytest.mark.django_db +def test_text_index_same_if_content_unchanged(content_config: ContentConfig, default_user: KhojUser, caplog): # Arrange - caplog.set_level(logging.INFO, logger="khoj") - - data = get_org_files(content_config.org) + org_config = LocalOrgConfig.objects.filter(user=default_user).first() + data = get_org_files(org_config) # Act # Generate initial notes embeddings during asymmetric setup - text_search.setup(OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=True) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) initial_logs = caplog.text caplog.clear() # Clear logs # Run asymmetric setup again with no changes to data source. Ensure index is not updated - text_search.setup(OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=False) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) final_logs = caplog.text # Assert - assert "Creating index from scratch." in initial_logs - assert "Creating index from scratch." not in final_logs + assert "Deleting all embeddings for file type org" in initial_logs + assert "Deleting all embeddings for file type org" not in final_logs # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db @pytest.mark.anyio -async def test_text_search(content_config: ContentConfig, search_config: SearchConfig): +# @pytest.mark.asyncio +async def test_text_search(search_config: SearchConfig): # Arrange - data = get_org_files(content_config.org) - - search_models.text_search = text_search.initialize_model(search_config.asymmetric) - content_index.org = text_search.setup( - OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=True + default_user = await KhojUser.objects.acreate( + username="test_user", password="test_password", email="test@example.com" ) + # Arrange + org_config = await LocalOrgConfig.objects.acreate( + input_files=None, + input_filter=["tests/data/org/*.org"], + index_heading_entries=False, + user=default_user, + ) + data = get_org_files(org_config) + + loop = asyncio.get_event_loop() + await loop.run_in_executor( + None, + text_search.setup, + OrgToJsonl, + data, + True, + True, + default_user, + ) + query = "How to git install application?" # Act - hits, entries = await text_search.query( - query, search_model=search_models.text_search, content=content_index.org, rank_results=True - ) - - results = text_search.collate_results(hits, entries, count=1) + hits = await text_search.query(default_user, query) # Assert + results = text_search.collate_results(hits) + results = sorted(results, key=lambda x: float(x.score))[:1] # search results should contain "git clone" entry search_result = results[0].entry assert "git clone" in search_result # ---------------------------------------------------------------------------------------------------- -def test_entry_chunking_by_max_tokens(org_config_with_only_new_file: TextContentConfig, search_models: SearchModels): +@pytest.mark.django_db +def test_entry_chunking_by_max_tokens(org_config_with_only_new_file: LocalOrgConfig, default_user: KhojUser, caplog): # Arrange # Insert org-mode entry with size exceeding max token limit to new org file max_tokens = 256 @@ -137,47 +172,46 @@ def test_entry_chunking_by_max_tokens(org_config_with_only_new_file: TextContent # Act # reload embeddings, entries, notes model after adding new org-mode file - initial_notes_model = text_search.setup( - OrgToJsonl, data, org_config_with_only_new_file, search_models.text_search.bi_encoder, regenerate=False - ) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) # Assert # verify newly added org-mode entry is split by max tokens - assert len(initial_notes_model.entries) == 2 - assert len(initial_notes_model.corpus_embeddings) == 2 + record = caplog.records[1] + assert "Created 2 new embeddings. Deleted 0 embeddings for user " in record.message # ---------------------------------------------------------------------------------------------------- # @pytest.mark.skip(reason="Flaky due to compressed_jsonl file being rewritten by other tests") +@pytest.mark.django_db def test_entry_chunking_by_max_tokens_not_full_corpus( - org_config_with_only_new_file: TextContentConfig, search_models: SearchModels + org_config_with_only_new_file: LocalOrgConfig, default_user: KhojUser, caplog ): # Arrange # Insert org-mode entry with size exceeding max token limit to new org file data = { "readme.org": """ * Khoj - /Allow natural language search on user content like notes, images using transformer based models/ +/Allow natural language search on user content like notes, images using transformer based models/ - All data is processed locally. User can interface with khoj app via [[./interface/emacs/khoj.el][Emacs]], API or Commandline +All data is processed locally. User can interface with khoj app via [[./interface/emacs/khoj.el][Emacs]], API or Commandline ** Dependencies - - Python3 - - [[https://docs.conda.io/en/latest/miniconda.html#latest-miniconda-installer-links][Miniconda]] +- Python3 +- [[https://docs.conda.io/en/latest/miniconda.html#latest-miniconda-installer-links][Miniconda]] ** Install - #+begin_src shell - git clone https://github.com/khoj-ai/khoj && cd khoj - conda env create -f environment.yml - conda activate khoj - #+end_src""" +#+begin_src shell +git clone https://github.com/khoj-ai/khoj && cd khoj +conda env create -f environment.yml +conda activate khoj +#+end_src""" } text_search.setup( OrgToJsonl, data, - org_config_with_only_new_file, - search_models.text_search.bi_encoder, regenerate=False, + user=default_user, ) max_tokens = 256 @@ -191,64 +225,57 @@ def test_entry_chunking_by_max_tokens_not_full_corpus( # Act # reload embeddings, entries, notes model after adding new org-mode file - initial_notes_model = text_search.setup( - OrgToJsonl, - data, - org_config_with_only_new_file, - search_models.text_search.bi_encoder, - regenerate=False, - full_corpus=False, - ) + with caplog.at_level(logging.INFO): + text_search.setup( + OrgToJsonl, + data, + regenerate=False, + full_corpus=False, + user=default_user, + ) + + record = caplog.records[1] # Assert # verify newly added org-mode entry is split by max tokens - assert len(initial_notes_model.entries) == 5 - assert len(initial_notes_model.corpus_embeddings) == 5 + assert "Created 2 new embeddings. Deleted 0 embeddings for user " in record.message # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db def test_regenerate_index_with_new_entry( - content_config: ContentConfig, search_models: SearchModels, new_org_file: Path + content_config: ContentConfig, new_org_file: Path, default_user: KhojUser, caplog ): # Arrange - data = get_org_files(content_config.org) - initial_notes_model = text_search.setup( - OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=True - ) + org_config = LocalOrgConfig.objects.filter(user=default_user).first() + data = get_org_files(org_config) - assert len(initial_notes_model.entries) == 10 - assert len(initial_notes_model.corpus_embeddings) == 10 + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) + + assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message # append org-mode entry to first org input file in config - content_config.org.input_files = [f"{new_org_file}"] + org_config.input_files = [f"{new_org_file}"] with open(new_org_file, "w") as f: f.write("\n* A Chihuahua doing Tango\n- Saw a super cute video of a chihuahua doing the Tango on Youtube\n") - data = get_org_files(content_config.org) + data = get_org_files(org_config) # Act # regenerate notes jsonl, model embeddings and model to include entry from new file - regenerated_notes_model = text_search.setup( - OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=True - ) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) # Assert - assert len(regenerated_notes_model.entries) == 11 - assert len(regenerated_notes_model.corpus_embeddings) == 11 - - # verify new entry appended to index, without disrupting order or content of existing entries - error_details = compare_index(initial_notes_model, regenerated_notes_model) - if error_details: - pytest.fail(error_details, False) - - # Cleanup - # reset input_files in config to empty list - content_config.org.input_files = [] + assert "Created 11 new embeddings. Deleted 10 embeddings for user " in caplog.records[-1].message + verify_embeddings(11, default_user) # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db def test_update_index_with_duplicate_entries_in_stable_order( - org_config_with_only_new_file: TextContentConfig, search_models: SearchModels + org_config_with_only_new_file: LocalOrgConfig, default_user: KhojUser, caplog ): # Arrange new_file_to_index = Path(org_config_with_only_new_file.input_files[0]) @@ -262,30 +289,26 @@ def test_update_index_with_duplicate_entries_in_stable_order( # Act # load embeddings, entries, notes model after adding new org-mode file - initial_index = text_search.setup( - OrgToJsonl, data, org_config_with_only_new_file, search_models.text_search.bi_encoder, regenerate=True - ) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) data = get_org_files(org_config_with_only_new_file) # update embeddings, entries, notes model after adding new org-mode file - updated_index = text_search.setup( - OrgToJsonl, data, org_config_with_only_new_file, search_models.text_search.bi_encoder, regenerate=False - ) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) # Assert # verify only 1 entry added even if there are multiple duplicate entries - assert len(initial_index.entries) == len(updated_index.entries) == 1 - assert len(initial_index.corpus_embeddings) == len(updated_index.corpus_embeddings) == 1 + assert "Created 1 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message + assert "Created 0 new embeddings. Deleted 0 embeddings for user " in caplog.records[4].message - # verify the same entry is added even when there are multiple duplicate entries - error_details = compare_index(initial_index, updated_index) - if error_details: - pytest.fail(error_details) + verify_embeddings(1, default_user) # ---------------------------------------------------------------------------------------------------- -def test_update_index_with_deleted_entry(org_config_with_only_new_file: TextContentConfig, search_models: SearchModels): +@pytest.mark.django_db +def test_update_index_with_deleted_entry(org_config_with_only_new_file: LocalOrgConfig, default_user: KhojUser, caplog): # Arrange new_file_to_index = Path(org_config_with_only_new_file.input_files[0]) @@ -296,9 +319,8 @@ def test_update_index_with_deleted_entry(org_config_with_only_new_file: TextCont data = get_org_files(org_config_with_only_new_file) # load embeddings, entries, notes model after adding new org file with 2 entries - initial_index = text_search.setup( - OrgToJsonl, data, org_config_with_only_new_file, search_models.text_search.bi_encoder, regenerate=True - ) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) # update embeddings, entries, notes model after removing an entry from the org file with open(new_file_to_index, "w") as f: @@ -307,87 +329,65 @@ def test_update_index_with_deleted_entry(org_config_with_only_new_file: TextCont data = get_org_files(org_config_with_only_new_file) # Act - updated_index = text_search.setup( - OrgToJsonl, data, org_config_with_only_new_file, search_models.text_search.bi_encoder, regenerate=False - ) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) # Assert # verify only 1 entry added even if there are multiple duplicate entries - assert len(initial_index.entries) == len(updated_index.entries) + 1 - assert len(initial_index.corpus_embeddings) == len(updated_index.corpus_embeddings) + 1 + assert "Created 2 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message + assert "Created 0 new embeddings. Deleted 1 embeddings for user " in caplog.records[4].message - # verify the same entry is added even when there are multiple duplicate entries - error_details = compare_index(updated_index, initial_index) - if error_details: - pytest.fail(error_details) + verify_embeddings(1, default_user) # ---------------------------------------------------------------------------------------------------- -def test_update_index_with_new_entry(content_config: ContentConfig, search_models: SearchModels, new_org_file: Path): +@pytest.mark.django_db +def test_update_index_with_new_entry(content_config: ContentConfig, new_org_file: Path, default_user: KhojUser, caplog): # Arrange - data = get_org_files(content_config.org) - initial_notes_model = text_search.setup( - OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=True, normalize=False - ) + org_config = LocalOrgConfig.objects.filter(user=default_user).first() + data = get_org_files(org_config) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) # append org-mode entry to first org input file in config with open(new_org_file, "w") as f: new_entry = "\n* A Chihuahua doing Tango\n- Saw a super cute video of a chihuahua doing the Tango on Youtube\n" f.write(new_entry) - data = get_org_files(content_config.org) + data = get_org_files(org_config) # Act # update embeddings, entries with the newly added note - content_config.org.input_files = [f"{new_org_file}"] - final_notes_model = text_search.setup( - OrgToJsonl, data, content_config.org, search_models.text_search.bi_encoder, regenerate=False, normalize=False - ) + with caplog.at_level(logging.INFO): + text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) # Assert - assert len(final_notes_model.entries) == len(initial_notes_model.entries) + 1 - assert len(final_notes_model.corpus_embeddings) == len(initial_notes_model.corpus_embeddings) + 1 + assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message + assert "Created 1 new embeddings. Deleted 0 embeddings for user " in caplog.records[4].message - # verify new entry appended to index, without disrupting order or content of existing entries - error_details = compare_index(initial_notes_model, final_notes_model) - if error_details: - pytest.fail(error_details, False) - - # Cleanup - # reset input_files in config to empty list - content_config.org.input_files = [] + verify_embeddings(11, default_user) # ---------------------------------------------------------------------------------------------------- @pytest.mark.skipif(os.getenv("GITHUB_PAT_TOKEN") is None, reason="GITHUB_PAT_TOKEN not set") -def test_text_search_setup_github(content_config: ContentConfig, search_models: SearchModels): +def test_text_search_setup_github(content_config: ContentConfig, default_user: KhojUser): + # Arrange + github_config = GithubConfig.objects.filter(user=default_user).first() # Act # Regenerate github embeddings to test asymmetric setup without caching - github_model = text_search.setup( - GithubToJsonl, content_config.github, search_models.text_search.bi_encoder, regenerate=True + text_search.setup( + GithubToJsonl, + {}, + regenerate=True, + user=default_user, + config=github_config, ) # Assert - assert len(github_model.entries) > 1 + embeddings = Embeddings.objects.filter(user=default_user, file_type="github").count() + assert embeddings > 1 -def compare_index(initial_notes_model, final_notes_model): - mismatched_entries, mismatched_embeddings = [], [] - for index in range(len(initial_notes_model.entries)): - if initial_notes_model.entries[index].to_json() != final_notes_model.entries[index].to_json(): - mismatched_entries.append(index) - - # verify new entry embedding appended to embeddings tensor, without disrupting order or content of existing embeddings - for index in range(len(initial_notes_model.corpus_embeddings)): - if not initial_notes_model.corpus_embeddings[index].allclose(final_notes_model.corpus_embeddings[index]): - mismatched_embeddings.append(index) - - error_details = "" - if mismatched_entries: - mismatched_entries_str = ",".join(map(str, mismatched_entries)) - error_details += f"Entries at {mismatched_entries_str} not equal\n" - if mismatched_embeddings: - mismatched_embeddings_str = ", ".join(map(str, mismatched_embeddings)) - error_details += f"Embeddings at {mismatched_embeddings_str} not equal\n" - - return error_details +def verify_embeddings(expected_count, user): + embeddings = Embeddings.objects.filter(user=user, file_type="org").count() + assert embeddings == expected_count diff --git a/tests/test_word_filter.py b/tests/test_word_filter.py index 04f45506..2ede35e7 100644 --- a/tests/test_word_filter.py +++ b/tests/test_word_filter.py @@ -3,68 +3,40 @@ from khoj.search_filter.word_filter import WordFilter from khoj.utils.rawconfig import Entry -def test_no_word_filter(): - # Arrange - word_filter = WordFilter() - entries = arrange_content() - q_with_no_filter = "head tail" - - # Act - can_filter = word_filter.can_filter(q_with_no_filter) - ret_query, entry_indices = word_filter.apply(q_with_no_filter, entries) - - # Assert - assert can_filter == False - assert ret_query == "head tail" - assert entry_indices == {0, 1, 2, 3} - - def test_word_exclude_filter(): # Arrange word_filter = WordFilter() - entries = arrange_content() q_with_exclude_filter = 'head -"exclude_word" tail' # Act can_filter = word_filter.can_filter(q_with_exclude_filter) - ret_query, entry_indices = word_filter.apply(q_with_exclude_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {0, 2} def test_word_include_filter(): # Arrange word_filter = WordFilter() - entries = arrange_content() query_with_include_filter = 'head +"include_word" tail' # Act can_filter = word_filter.can_filter(query_with_include_filter) - ret_query, entry_indices = word_filter.apply(query_with_include_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {2, 3} def test_word_include_and_exclude_filter(): # Arrange word_filter = WordFilter() - entries = arrange_content() query_with_include_and_exclude_filter = 'head +"include_word" -"exclude_word" tail' # Act can_filter = word_filter.can_filter(query_with_include_and_exclude_filter) - ret_query, entry_indices = word_filter.apply(query_with_include_and_exclude_filter, entries) # Assert assert can_filter == True - assert ret_query == "head tail" - assert entry_indices == {2} def test_get_word_filter_terms(): From a8a82d274afe7fcfde95f7323dbfbbe29a334d50 Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Thu, 26 Oct 2023 10:17:29 -0700 Subject: [PATCH 003/194] [Multi-User Part 2]: Add login pages and gate access to application behind login wall (#503) - Make most routes conditional on authentication *if anonymous mode is not enabled*. If anonymous mode is enabled, it scaffolds a default user and uses that for all application interactions. - Add a basic login page and add routes for redirecting the user if logged in --- pyproject.toml | 2 + src/database/adapters/__init__.py | 3 +- src/khoj/configure.py | 9 +- src/khoj/interface/web/base_config.html | 12 ++ src/khoj/interface/web/config.html | 5 + src/khoj/interface/web/login.html | 197 ++++++++++++++++++++++++ src/khoj/main.py | 13 +- src/khoj/routers/api.py | 14 +- src/khoj/routers/auth.py | 45 ++++-- src/khoj/routers/indexer.py | 2 +- src/khoj/routers/web_client.py | 42 ++++- tests/conftest.py | 15 +- tests/test_client.py | 27 ++-- 13 files changed, 327 insertions(+), 59 deletions(-) create mode 100644 src/khoj/interface/web/login.html diff --git a/pyproject.toml b/pyproject.toml index 8732d47a..f9ef020c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,8 @@ dependencies = [ "httpx == 0.25.0", "pgvector == 0.2.3", "psycopg2-binary == 2.9.9", + "google-auth == 2.23.3", + "python-multipart == 0.0.6", ] dynamic = ["version"] diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index a7c1c6f9..fc4f23b1 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -73,8 +73,7 @@ async def create_google_user(token: dict) -> KhojUser: async def get_user_by_token(token: dict) -> KhojUser: - user_info = token.get("userinfo") - google_user = await GoogleUser.objects.filter(sub=user_info.get("sub")).select_related("user").afirst() + google_user = await GoogleUser.objects.filter(sub=token.get("sub")).select_related("user").afirst() if not google_user: return None return google_user.user diff --git a/src/khoj/configure.py b/src/khoj/configure.py index f65b1056..76b2e9f4 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -67,7 +67,7 @@ class UserAuthenticationBackend(AuthenticationBackend): user = await self.khojuser_manager.filter(email=current_user.get("email")).afirst() if user: return AuthCredentials(["authenticated"]), AuthenticatedKhojUser(user) - elif not state.anonymous_mode: + elif state.anonymous_mode: user = await self.khojuser_manager.filter(username="default").afirst() if user: return AuthCredentials(["authenticated"]), AuthenticatedKhojUser(user) @@ -77,11 +77,6 @@ class UserAuthenticationBackend(AuthenticationBackend): def initialize_server(config: Optional[FullConfig]): if config is None: - logger.error( - f"🚨 Exiting as Khoj is not configured.\nConfigure it via http://{state.host}:{state.port}/config or by editing {state.config_file}." - ) - sys.exit(1) - elif config is None: logger.warning( f"🚨 Khoj is not configured.\nConfigure it via http://{state.host}:{state.port}/config, plugins or by editing {state.config_file}." ) @@ -230,6 +225,8 @@ def configure_conversation_processor( conversation_logfile=conversation_logfile, openai=(conversation_config.openai if (conversation_config is not None) else None), offline_chat=conversation_config.offline_chat if conversation_config else OfflineChatProcessorConfig(), + max_prompt_size=conversation_config.max_prompt_size if conversation_config else None, + tokenizer=conversation_config.tokenizer if conversation_config else None, ) ) else: diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index 5b643d58..15c3f678 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -211,6 +211,14 @@ grid-gap: 24px; } + button#logout { + font-size: 16px; + cursor: pointer; + margin: 0; + padding: 0; + height: 32px; + } + @media screen and (max-width: 600px) { .section-cards { grid-template-columns: 1fr; @@ -242,6 +250,10 @@ width: 320px; } + div.khoj-header-wrapper{ + grid-template-columns: auto; + } + } diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index 6e3a0223..6c69c056 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -297,6 +297,11 @@
+ {% if anonymous_mode == False %} +
+ +
+ {% endif %}
diff --git a/src/khoj/interface/web/login.html b/src/khoj/interface/web/login.html new file mode 100644 index 00000000..550991ed --- /dev/null +++ b/src/khoj/interface/web/login.html @@ -0,0 +1,197 @@ + + + + + Khoj - Search + + + + + + + + {% if demo %} + +
+ +

+ Enroll in Khoj cloud to get your own assistant +

+
+ + +
+ {% endif %} + +
+ {% if demo %} + + {% else %} + + {% endif %} +
+ + +
+

Become superhuman with your personal knowledge base copilot

+
+
+
+
+
+ + + + + + + + diff --git a/src/khoj/main.py b/src/khoj/main.py index a713cc97..804e71e5 100644 --- a/src/khoj/main.py +++ b/src/khoj/main.py @@ -14,13 +14,13 @@ warnings.filterwarnings("ignore", message=r"legacy way to download files from th # External Packages import uvicorn +import django from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -import schedule -import django - from fastapi.staticfiles import StaticFiles from rich.logging import RichHandler +import schedule + from django.core.asgi import get_asgi_application from django.core.management import call_command @@ -34,13 +34,6 @@ call_command("migrate", "--noinput") # Initialize Django Static Files call_command("collectstatic", "--noinput") -# Initialize Django -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") -django.setup() - -# Initialize Django Database -call_command("migrate", "--noinput") - # Initialize the Application Server app = FastAPI() diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 7c3e3392..d041fd76 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -127,6 +127,7 @@ if not state.demo: state.processor_config = configure_processor(state.config.processor) @api.get("/config/data", response_model=FullConfig) + @requires(["authenticated"], redirect="login_page") def get_config_data(request: Request): user = request.user.object if request.user.is_authenticated else None enabled_content = EmbeddingsAdapters.get_unique_file_types(user) @@ -134,6 +135,7 @@ if not state.demo: return state.config @api.post("/config/data") + @requires(["authenticated"], redirect="login_page") async def set_config_data( request: Request, updated_config: FullConfig, @@ -166,7 +168,7 @@ if not state.demo: return state.config @api.post("/config/data/content_type/github", status_code=200) - @requires("authenticated") + @requires(["authenticated"], redirect="login_page") async def set_content_config_github_data( request: Request, updated_config: Union[GithubContentConfig, None], @@ -193,6 +195,7 @@ if not state.demo: return {"status": "ok"} @api.post("/config/data/content_type/notion", status_code=200) + @requires(["authenticated"], redirect="login_page") async def set_content_config_notion_data( request: Request, updated_config: Union[NotionContentConfig, None], @@ -218,6 +221,7 @@ if not state.demo: return {"status": "ok"} @api.post("/delete/config/data/content_type/{content_type}", status_code=200) + @requires(["authenticated"], redirect="login_page") async def remove_content_config_data( request: Request, content_type: str, @@ -272,7 +276,7 @@ if not state.demo: return {"status": "error", "message": str(e)} @api.post("/config/data/content_type/{content_type}", status_code=200) - # @requires("authenticated") + @requires(["authenticated"], redirect="login_page") async def set_content_config_data( request: Request, content_type: str, @@ -378,6 +382,7 @@ def get_default_config_data(): @api.get("/config/types", response_model=List[str]) +@requires(["authenticated"], redirect="login_page") def get_config_types( request: Request, ): @@ -399,6 +404,7 @@ def get_config_types( @api.get("/search", response_model=List[SearchResponse]) +@requires(["authenticated"], redirect="login_page") async def search( q: str, request: Request, @@ -532,6 +538,7 @@ async def search( @api.get("/update") +@requires(["authenticated"], redirect="login_page") def update( request: Request, t: Optional[SearchType] = None, @@ -577,6 +584,7 @@ def update( @api.get("/chat/history") +@requires(["authenticated"], redirect="login_page") def chat_history( request: Request, client: Optional[str] = None, @@ -605,6 +613,7 @@ def chat_history( @api.get("/chat/options", response_class=Response) +@requires(["authenticated"], redirect="login_page") async def chat_options( request: Request, client: Optional[str] = None, @@ -629,6 +638,7 @@ async def chat_options( @api.get("/chat", response_class=Response) +@requires(["authenticated"], redirect="login_page") async def chat( request: Request, q: str, diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py index 41bc8396..8c767d8f 100644 --- a/src/khoj/routers/auth.py +++ b/src/khoj/routers/auth.py @@ -4,9 +4,12 @@ import os from fastapi import APIRouter from starlette.config import Config from starlette.requests import Request -from starlette.responses import HTMLResponse, RedirectResponse +from starlette.responses import HTMLResponse, RedirectResponse, Response from authlib.integrations.starlette_client import OAuth, OAuthError +from google.oauth2 import id_token +from google.auth.transport import requests as google_requests + from database.adapters import get_or_create_user logger = logging.getLogger(__name__) @@ -24,32 +27,40 @@ else: oauth.register(name="google", server_metadata_url=CONF_URL, client_kwargs={"scope": "openid email profile"}) -@auth_router.get("/") -async def homepage(request: Request): - user = request.session.get("user") - if user: - data = json.dumps(user) - html = f"
{data}
" 'logout' - return HTMLResponse(html) - return HTMLResponse('login') - - @auth_router.get("/login") +async def login_get(request: Request): + redirect_uri = request.url_for("auth") + return await oauth.google.authorize_redirect(request, redirect_uri) + + +@auth_router.post("/login") async def login(request: Request): redirect_uri = request.url_for("auth") return await oauth.google.authorize_redirect(request, redirect_uri) -@auth_router.get("/redirect") +@auth_router.post("/redirect") async def auth(request: Request): + form = await request.form() + credential = form.get("credential") + + csrf_token_cookie = request.cookies.get("g_csrf_token") + if not csrf_token_cookie: + return Response("Missing CSRF token", status_code=400) + csrf_token_body = form.get("g_csrf_token") + if not csrf_token_body: + return Response("Missing CSRF token", status_code=400) + if csrf_token_cookie != csrf_token_body: + return Response("Invalid CSRF token", status_code=400) + try: - token = await oauth.google.authorize_access_token(request) + idinfo = id_token.verify_oauth2_token(credential, google_requests.Request(), os.environ["GOOGLE_CLIENT_ID"]) except OAuthError as error: return HTMLResponse(f"

{error.error}

") - khoj_user = await get_or_create_user(token) - user = token.get("userinfo") - if user: - request.session["user"] = dict(user) + khoj_user = await get_or_create_user(idinfo) + if khoj_user: + request.session["user"] = dict(idinfo) + return RedirectResponse(url="/") diff --git a/src/khoj/routers/indexer.py b/src/khoj/routers/indexer.py index c2ef04ff..1e73c439 100644 --- a/src/khoj/routers/indexer.py +++ b/src/khoj/routers/indexer.py @@ -141,6 +141,7 @@ async def update( ) logger.info(f"Finished processing batch indexing request") except Exception as e: + logger.error(f"Failed to process batch indexing request: {e}", exc_info=True) logger.error( f"🚨 Failed to {force} update {t} content index triggered via API call by {client} client: {e}", exc_info=True, @@ -156,7 +157,6 @@ async def update( host=host, ) - logger.info(f"📪 Content index updated via API call by {client} client") return Response(content="OK", status_code=200) diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 6c79e061..4122c6d0 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -1,7 +1,11 @@ +# System Packages +import json +import os + # External Packages from fastapi import APIRouter from fastapi import Request -from fastapi.responses import HTMLResponse, FileResponse +from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse from fastapi.templating import Jinja2Templates from starlette.authentication import requires from khoj.utils.rawconfig import ( @@ -16,9 +20,7 @@ from khoj.utils.rawconfig import ( # Internal Packages from khoj.utils import constants, state from database.adapters import EmbeddingsAdapters, get_user_github_config, get_user_notion_config -from database.models import KhojUser, LocalOrgConfig, LocalMarkdownConfig, LocalPdfConfig, LocalPlaintextConfig - -import json +from database.models import LocalOrgConfig, LocalMarkdownConfig, LocalPdfConfig, LocalPlaintextConfig # Initialize Router @@ -30,15 +32,41 @@ VALID_TEXT_CONTENT_TYPES = ["org", "markdown", "pdf", "plaintext"] # Create Routes @web_client.get("/", response_class=FileResponse) +@requires(["authenticated"], redirect="login_page") def index(request: Request): return templates.TemplateResponse("index.html", context={"request": request, "demo": state.demo}) +@web_client.post("/", response_class=FileResponse) +@requires(["authenticated"], redirect="login_page") +def index_post(request: Request): + return templates.TemplateResponse("index.html", context={"request": request, "demo": state.demo}) + + @web_client.get("/chat", response_class=FileResponse) +@requires(["authenticated"], redirect="login_page") def chat_page(request: Request): return templates.TemplateResponse("chat.html", context={"request": request, "demo": state.demo}) +@web_client.get("/login", response_class=FileResponse) +def login_page(request: Request): + if request.user.is_authenticated: + next_url = request.query_params.get("next", "/") + return RedirectResponse(url=next_url) + google_client_id = os.environ.get("GOOGLE_CLIENT_ID") + redirect_uri = request.url_for("auth") + return templates.TemplateResponse( + "login.html", + context={ + "request": request, + "demo": state.demo, + "google_client_id": google_client_id, + "redirect_uri": redirect_uri, + }, + ) + + def map_config_to_object(content_type: str): if content_type == "org": return LocalOrgConfig @@ -53,6 +81,7 @@ def map_config_to_object(content_type: str): if not state.demo: @web_client.get("/config", response_class=HTMLResponse) + @requires(["authenticated"], redirect="login_page") def config_page(request: Request): user = request.user.object if request.user.is_authenticated else None enabled_content = set(EmbeddingsAdapters.get_unique_file_types(user).all()) @@ -97,11 +126,12 @@ if not state.demo: "request": request, "current_config": current_config, "current_model_state": successfully_configured, + "anonymous_mode": state.anonymous_mode, }, ) @web_client.get("/config/content_type/github", response_class=HTMLResponse) - @requires(["authenticated"]) + @requires(["authenticated"], redirect="login_page") def github_config_page(request: Request): user = request.user.object if request.user.is_authenticated else None current_github_config = get_user_github_config(user) @@ -130,6 +160,7 @@ if not state.demo: ) @web_client.get("/config/content_type/notion", response_class=HTMLResponse) + @requires(["authenticated"], redirect="login_page") def notion_config_page(request: Request): user = request.user.object if request.user.is_authenticated else None current_notion_config = get_user_notion_config(user) @@ -145,6 +176,7 @@ if not state.demo: ) @web_client.get("/config/content_type/{content_type}", response_class=HTMLResponse) + @requires(["authenticated"], redirect="login_page") def content_config_page(request: Request, content_type: str): if content_type not in VALID_TEXT_CONTENT_TYPES: return templates.TemplateResponse("config.html", context={"request": request}) diff --git a/tests/conftest.py b/tests/conftest.py index 5f515ef1..ee4b9e57 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,6 @@ from khoj.utils.rawconfig import ( OfflineChatProcessorConfig, OpenAIProcessorConfig, ProcessorConfig, - TextContentConfig, ImageContentConfig, SearchConfig, TextSearchConfig, @@ -38,7 +37,6 @@ from database.models import ( LocalOrgConfig, LocalMarkdownConfig, LocalPlaintextConfig, - LocalPdfConfig, GithubConfig, KhojUser, GithubRepoConfig, @@ -95,6 +93,19 @@ def default_user(): return UserFactory() +@pytest.mark.django_db +@pytest.fixture +def default_user2(): + if KhojUser.objects.filter(username="default").exists(): + return KhojUser.objects.get(username="default") + + return UserFactory( + username="default", + email="default@example.com", + password="default", + ) + + @pytest.fixture(scope="session") def search_models(search_config: SearchConfig): search_models = SearchModels() diff --git a/tests/test_client.py b/tests/test_client.py index f63b968c..b77ba07d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -7,6 +7,7 @@ import pytest # External Packages from fastapi.testclient import TestClient from fastapi import FastAPI +import pytest # Internal Packages from khoj.configure import configure_routes, configure_search_types @@ -115,16 +116,11 @@ def test_get_configured_types_via_api(client, sample_org_data): # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_get_api_config_types(client, search_config: SearchConfig, sample_org_data): +def test_get_api_config_types(client, search_config: SearchConfig, sample_org_data, default_user2: KhojUser): # Arrange - text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) - - # Act + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user2) response = client.get(f"/api/config/types") - - # Assert - assert response.status_code == 200 - assert response.json() == ["all", "org", "markdown", "image"] + assert response.json() == ["all", "org", "image"] # ---------------------------------------------------------------------------------------------------- @@ -150,6 +146,7 @@ def test_get_configured_types_with_no_content_config(fastapi_app: FastAPI): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_image_search(client, content_config: ContentConfig, search_config: SearchConfig): # Arrange search_models.image_search = image_search.initialize_model(search_config.image) @@ -177,9 +174,9 @@ def test_image_search(client, content_config: ContentConfig, search_config: Sear # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_notes_search(client, search_config: SearchConfig, sample_org_data): +def test_notes_search(client, search_config: SearchConfig, sample_org_data, default_user2: KhojUser): # Arrange - text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user2) user_query = quote("How to git install application?") # Act @@ -195,13 +192,14 @@ def test_notes_search(client, search_config: SearchConfig, sample_org_data): # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) def test_notes_search_with_only_filters( - client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data + client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data, default_user2: KhojUser ): # Arrange text_search.setup( OrgToJsonl, sample_org_data, regenerate=False, + user=default_user2, ) user_query = quote('+"Emacs" file:"*.org"') @@ -217,9 +215,9 @@ def test_notes_search_with_only_filters( # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_notes_search_with_include_filter(client, sample_org_data): +def test_notes_search_with_include_filter(client, sample_org_data, default_user2: KhojUser): # Arrange - text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user2) user_query = quote('How to git install application? +"Emacs"') # Act @@ -234,12 +232,13 @@ def test_notes_search_with_include_filter(client, sample_org_data): # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_notes_search_with_exclude_filter(client, sample_org_data): +def test_notes_search_with_exclude_filter(client, sample_org_data, default_user2: KhojUser): # Arrange text_search.setup( OrgToJsonl, sample_org_data, regenerate=False, + user=default_user2, ) user_query = quote('How to git install application? -"clone"') From 4b6ec248a6afb35c41c45578529b03293870ace2 Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:37:41 -0700 Subject: [PATCH 004/194] [Multi-User Part 3]: Separate chat sesssions based on authenticated users (#511) - Add a data model which allows us to store Conversations with users. This does a minimal lift over the current setup, where the underlying data is stored in a JSON file. This maintains parity with that configuration. - There does _seem_ to be some regression in chat quality, which is most likely attributable to search results. This will help us with #275. It should become much easier to maintain multiple Conversations in a given table in the backend now. We will have to do some thinking on the UI. --- pytest.ini | 2 + src/database/adapters/__init__.py | 145 +++++++++++++-- ...onprocessorconfig_conversation_and_more.py | 81 ++++++++ ...008_alter_conversation_conversation_log.py | 17 ++ src/database/models/__init__.py | 22 ++- src/khoj/configure.py | 110 +---------- src/khoj/interface/web/config.html | 13 +- src/khoj/routers/api.py | 175 ++++++++---------- src/khoj/routers/helpers.py | 87 ++++++--- src/khoj/routers/indexer.py | 27 +-- src/khoj/routers/web_client.py | 54 +++--- src/khoj/search_type/image_search.py | 1 + src/khoj/search_type/text_search.py | 56 +++--- src/khoj/utils/config.py | 39 +--- src/khoj/utils/constants.py | 122 ------------ src/khoj/utils/helpers.py | 23 +++ src/khoj/utils/rawconfig.py | 19 +- src/khoj/utils/state.py | 4 +- tests/conftest.py | 134 ++++++-------- tests/helpers.py | 51 +++++ tests/test_client.py | 5 + tests/test_gpt4all_chat_director.py | 76 +++++--- tests/test_openai_chat_director.py | 77 +++++--- tests/test_text_search.py | 5 +- 24 files changed, 719 insertions(+), 626 deletions(-) create mode 100644 src/database/migrations/0007_remove_conversationprocessorconfig_conversation_and_more.py create mode 100644 src/database/migrations/0008_alter_conversation_conversation_log.py create mode 100644 tests/helpers.py diff --git a/pytest.ini b/pytest.ini index eec111ec..b3e418d0 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,5 @@ DJANGO_SETTINGS_MODULE = app.settings pythonpath = . src testpaths = tests +markers = + chatquality: marks tests as chatquality (deselect with '-m "not chatquality"') diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index fc4f23b1..db5e9f77 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -1,5 +1,4 @@ from typing import Type, TypeVar, List -import uuid from datetime import date from django.db import models @@ -21,6 +20,13 @@ from database.models import ( GithubConfig, Embeddings, GithubRepoConfig, + Conversation, + ConversationProcessorConfig, + OpenAIProcessorConversationConfig, + OfflineChatProcessorConversationConfig, +) +from khoj.utils.rawconfig import ( + ConversationProcessorConfig as UserConversationProcessorConfig, ) from khoj.search_filter.word_filter import WordFilter from khoj.search_filter.file_filter import FileFilter @@ -54,18 +60,17 @@ async def get_or_create_user(token: dict) -> KhojUser: async def create_google_user(token: dict) -> KhojUser: - user_info = token.get("userinfo") - user = await KhojUser.objects.acreate(username=user_info.get("email"), email=user_info.get("email")) + user = await KhojUser.objects.acreate(username=token.get("email"), email=token.get("email")) await user.asave() await GoogleUser.objects.acreate( - sub=user_info.get("sub"), - azp=user_info.get("azp"), - email=user_info.get("email"), - name=user_info.get("name"), - given_name=user_info.get("given_name"), - family_name=user_info.get("family_name"), - picture=user_info.get("picture"), - locale=user_info.get("locale"), + sub=token.get("sub"), + azp=token.get("azp"), + email=token.get("email"), + name=token.get("name"), + given_name=token.get("given_name"), + family_name=token.get("family_name"), + picture=token.get("picture"), + locale=token.get("locale"), user=user, ) @@ -137,6 +142,124 @@ async def set_user_github_config(user: KhojUser, pat_token: str, repos: list): return config +class ConversationAdapters: + @staticmethod + def get_conversation_by_user(user: KhojUser): + conversation = Conversation.objects.filter(user=user) + if conversation.exists(): + return conversation.first() + return Conversation.objects.create(user=user) + + @staticmethod + async def aget_conversation_by_user(user: KhojUser): + conversation = Conversation.objects.filter(user=user) + if await conversation.aexists(): + return await conversation.afirst() + return await Conversation.objects.acreate(user=user) + + @staticmethod + def has_any_conversation_config(user: KhojUser): + return ConversationProcessorConfig.objects.filter(user=user).exists() + + @staticmethod + def get_openai_conversation_config(user: KhojUser): + return OpenAIProcessorConversationConfig.objects.filter(user=user).first() + + @staticmethod + def get_offline_chat_conversation_config(user: KhojUser): + return OfflineChatProcessorConversationConfig.objects.filter(user=user).first() + + @staticmethod + def has_valid_offline_conversation_config(user: KhojUser): + return OfflineChatProcessorConversationConfig.objects.filter(user=user, enable_offline_chat=True).exists() + + @staticmethod + def has_valid_openai_conversation_config(user: KhojUser): + return OpenAIProcessorConversationConfig.objects.filter(user=user).exists() + + @staticmethod + def get_conversation_config(user: KhojUser): + return ConversationProcessorConfig.objects.filter(user=user).first() + + @staticmethod + def save_conversation(user: KhojUser, conversation_log: dict): + conversation = Conversation.objects.filter(user=user) + if conversation.exists(): + conversation.update(conversation_log=conversation_log) + else: + Conversation.objects.create(user=user, conversation_log=conversation_log) + + @staticmethod + def set_conversation_processor_config(user: KhojUser, new_config: UserConversationProcessorConfig): + conversation_config, _ = ConversationProcessorConfig.objects.get_or_create(user=user) + conversation_config.max_prompt_size = new_config.max_prompt_size + conversation_config.tokenizer = new_config.tokenizer + conversation_config.save() + + if new_config.openai: + default_values = { + "api_key": new_config.openai.api_key, + } + if new_config.openai.chat_model: + default_values["chat_model"] = new_config.openai.chat_model + + OpenAIProcessorConversationConfig.objects.update_or_create(user=user, defaults=default_values) + + if new_config.offline_chat: + default_values = { + "enable_offline_chat": str(new_config.offline_chat.enable_offline_chat), + } + + if new_config.offline_chat.chat_model: + default_values["chat_model"] = new_config.offline_chat.chat_model + + OfflineChatProcessorConversationConfig.objects.update_or_create(user=user, defaults=default_values) + + @staticmethod + def get_enabled_conversation_settings(user: KhojUser): + openai_config = ConversationAdapters.get_openai_conversation_config(user) + offline_chat_config = ConversationAdapters.get_offline_chat_conversation_config(user) + + return { + "openai": True if openai_config is not None else False, + "offline_chat": True + if (offline_chat_config is not None and offline_chat_config.enable_offline_chat) + else False, + } + + @staticmethod + def clear_conversation_config(user: KhojUser): + ConversationProcessorConfig.objects.filter(user=user).delete() + ConversationAdapters.clear_openai_conversation_config(user) + ConversationAdapters.clear_offline_chat_conversation_config(user) + + @staticmethod + def clear_openai_conversation_config(user: KhojUser): + OpenAIProcessorConversationConfig.objects.filter(user=user).delete() + + @staticmethod + def clear_offline_chat_conversation_config(user: KhojUser): + OfflineChatProcessorConversationConfig.objects.filter(user=user).delete() + + @staticmethod + async def has_offline_chat(user: KhojUser): + return await OfflineChatProcessorConversationConfig.objects.filter( + user=user, enable_offline_chat=True + ).aexists() + + @staticmethod + async def get_offline_chat(user: KhojUser): + return await OfflineChatProcessorConversationConfig.objects.filter(user=user).afirst() + + @staticmethod + async def has_openai_chat(user: KhojUser): + return await OpenAIProcessorConversationConfig.objects.filter(user=user).aexists() + + @staticmethod + async def get_openai_chat(user: KhojUser): + return await OpenAIProcessorConversationConfig.objects.filter(user=user).afirst() + + class EmbeddingsAdapters: word_filer = WordFilter() file_filter = FileFilter() diff --git a/src/database/migrations/0007_remove_conversationprocessorconfig_conversation_and_more.py b/src/database/migrations/0007_remove_conversationprocessorconfig_conversation_and_more.py new file mode 100644 index 00000000..d66b2bd0 --- /dev/null +++ b/src/database/migrations/0007_remove_conversationprocessorconfig_conversation_and_more.py @@ -0,0 +1,81 @@ +# Generated by Django 4.2.5 on 2023-10-18 05:31 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0006_embeddingsdates"), + ] + + operations = [ + migrations.RemoveField( + model_name="conversationprocessorconfig", + name="conversation", + ), + migrations.RemoveField( + model_name="conversationprocessorconfig", + name="enable_offline_chat", + ), + migrations.AddField( + model_name="conversationprocessorconfig", + name="max_prompt_size", + field=models.IntegerField(blank=True, default=None, null=True), + ), + migrations.AddField( + model_name="conversationprocessorconfig", + name="tokenizer", + field=models.CharField(blank=True, default=None, max_length=200, null=True), + ), + migrations.AddField( + model_name="conversationprocessorconfig", + name="user", + field=models.ForeignKey( + default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + preserve_default=False, + ), + migrations.CreateModel( + name="OpenAIProcessorConversationConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("api_key", models.CharField(max_length=200)), + ("chat_model", models.CharField(max_length=200)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="OfflineChatProcessorConversationConfig", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("enable_offline_chat", models.BooleanField(default=False)), + ("chat_model", models.CharField(default="llama-2-7b-chat.ggmlv3.q4_0.bin", max_length=200)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Conversation", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("conversation_log", models.JSONField()), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/src/database/migrations/0008_alter_conversation_conversation_log.py b/src/database/migrations/0008_alter_conversation_conversation_log.py new file mode 100644 index 00000000..8c60489f --- /dev/null +++ b/src/database/migrations/0008_alter_conversation_conversation_log.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-10-18 16:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0007_remove_conversationprocessorconfig_conversation_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="conversation", + name="conversation_log", + field=models.JSONField(default=dict), + ), + ] diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py index 9a50d94f..a9d41e0d 100644 --- a/src/database/models/__init__.py +++ b/src/database/models/__init__.py @@ -82,9 +82,27 @@ class LocalPlaintextConfig(BaseModel): user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) -class ConversationProcessorConfig(BaseModel): - conversation = models.JSONField() +class OpenAIProcessorConversationConfig(BaseModel): + api_key = models.CharField(max_length=200) + chat_model = models.CharField(max_length=200) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + + +class OfflineChatProcessorConversationConfig(BaseModel): enable_offline_chat = models.BooleanField(default=False) + chat_model = models.CharField(max_length=200, default="llama-2-7b-chat.ggmlv3.q4_0.bin") + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + + +class ConversationProcessorConfig(BaseModel): + max_prompt_size = models.IntegerField(default=None, null=True, blank=True) + tokenizer = models.CharField(max_length=200, default=None, null=True, blank=True) + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + + +class Conversation(BaseModel): + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + conversation_log = models.JSONField(default=dict) class Embeddings(BaseModel): diff --git a/src/khoj/configure.py b/src/khoj/configure.py index 76b2e9f4..ac43f9b4 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -23,12 +23,10 @@ from starlette.authentication import ( from khoj.utils import constants, state from khoj.utils.config import ( SearchType, - ProcessorConfigModel, - ConversationProcessorConfigModel, ) -from khoj.utils.helpers import resolve_absolute_path, merge_dicts +from khoj.utils.helpers import merge_dicts from khoj.utils.fs_syncer import collect_files -from khoj.utils.rawconfig import FullConfig, OfflineChatProcessorConfig, ProcessorConfig, ConversationProcessorConfig +from khoj.utils.rawconfig import FullConfig from khoj.routers.indexer import configure_content, load_content, configure_search from database.models import KhojUser from database.adapters import get_all_users @@ -98,13 +96,6 @@ def configure_server( # Update Config state.config = config - # Initialize Processor from Config - try: - state.processor_config = configure_processor(state.config.processor) - except Exception as e: - logger.error(f"🚨 Failed to configure processor", exc_info=True) - raise e - # Initialize Search Models from Config and initialize content try: state.config_lock.acquire() @@ -190,103 +181,6 @@ def configure_search_types(config: FullConfig): return Enum("SearchType", merge_dicts(core_search_types, {})) -def configure_processor( - processor_config: Optional[ProcessorConfig], state_processor_config: Optional[ProcessorConfigModel] = None -): - if not processor_config: - logger.warning("🚨 No Processor configuration available.") - return None - - processor = ProcessorConfigModel() - - # Initialize Conversation Processor - logger.info("💬 Setting up conversation processor") - processor.conversation = configure_conversation_processor(processor_config, state_processor_config) - - return processor - - -def configure_conversation_processor( - processor_config: Optional[ProcessorConfig], state_processor_config: Optional[ProcessorConfigModel] = None -): - if ( - not processor_config - or not processor_config.conversation - or not processor_config.conversation.conversation_logfile - ): - default_config = constants.default_config - default_conversation_logfile = resolve_absolute_path( - default_config["processor"]["conversation"]["conversation-logfile"] # type: ignore - ) - conversation_logfile = resolve_absolute_path(default_conversation_logfile) - conversation_config = processor_config.conversation if processor_config else None - conversation_processor = ConversationProcessorConfigModel( - conversation_config=ConversationProcessorConfig( - conversation_logfile=conversation_logfile, - openai=(conversation_config.openai if (conversation_config is not None) else None), - offline_chat=conversation_config.offline_chat if conversation_config else OfflineChatProcessorConfig(), - max_prompt_size=conversation_config.max_prompt_size if conversation_config else None, - tokenizer=conversation_config.tokenizer if conversation_config else None, - ) - ) - else: - conversation_processor = ConversationProcessorConfigModel( - conversation_config=processor_config.conversation, - ) - conversation_logfile = resolve_absolute_path(conversation_processor.conversation_logfile) - - # Load Conversation Logs from Disk - if state_processor_config and state_processor_config.conversation and state_processor_config.conversation.meta_log: - conversation_processor.meta_log = state_processor_config.conversation.meta_log - conversation_processor.chat_session = state_processor_config.conversation.chat_session - logger.debug(f"Loaded conversation logs from state") - return conversation_processor - - if conversation_logfile.is_file(): - # Load Metadata Logs from Conversation Logfile - with conversation_logfile.open("r") as f: - conversation_processor.meta_log = json.load(f) - logger.debug(f"Loaded conversation logs from {conversation_logfile}") - else: - # Initialize Conversation Logs - conversation_processor.meta_log = {} - conversation_processor.chat_session = [] - - return conversation_processor - - -@schedule.repeat(schedule.every(17).minutes) -def save_chat_session(): - # No need to create empty log file - if not ( - state.processor_config - and state.processor_config.conversation - and state.processor_config.conversation.meta_log - and state.processor_config.conversation.chat_session - ): - return - - # Summarize Conversation Logs for this Session - conversation_log = state.processor_config.conversation.meta_log - session = { - "session-start": conversation_log.get("session", [{"session-end": 0}])[-1]["session-end"], - "session-end": len(conversation_log["chat"]), - } - if "session" in conversation_log: - conversation_log["session"].append(session) - else: - conversation_log["session"] = [session] - - # Save Conversation Metadata Logs to Disk - conversation_logfile = resolve_absolute_path(state.processor_config.conversation.conversation_logfile) - conversation_logfile.parent.mkdir(parents=True, exist_ok=True) # create conversation directory if doesn't exist - with open(conversation_logfile, "w+", encoding="utf-8") as logfile: - json.dump(conversation_log, logfile, indent=2) - - state.processor_config.conversation.chat_session = [] - logger.info("📩 Saved current chat session to conversation logs") - - @schedule.repeat(schedule.every(59).minutes) def upload_telemetry(): if not state.config or not state.config.app or not state.config.app.should_log_telemetry or not state.telemetry: diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index 6c69c056..979bf56c 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -3,6 +3,11 @@
+ {% if anonymous_mode == False %} +
+ Logged in as {{ username }} +
+ {% endif %}

Plugins

@@ -257,8 +262,8 @@ Chat

Offline Chat - Configured - {% if current_config.processor and current_config.processor.conversation and current_config.processor.conversation.offline_chat.enable_offline_chat and not current_model_state.conversation_gpt4all %} + Configured + {% if current_model_state.enable_offline_model and not current_model_state.conversation_gpt4all %} Not Configured {% endif %}

@@ -266,12 +271,12 @@

Setup offline chat

-
+
-
+
diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index d041fd76..b7ba66b6 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -8,21 +8,20 @@ from typing import List, Optional, Union, Any import asyncio # External Packages -from fastapi import APIRouter, HTTPException, Header, Request, Depends +from fastapi import APIRouter, HTTPException, Header, Request from starlette.authentication import requires from asgiref.sync import sync_to_async # Internal Packages -from khoj.configure import configure_processor, configure_server +from khoj.configure import configure_server from khoj.search_type import image_search, text_search from khoj.search_filter.date_filter import DateFilter from khoj.search_filter.file_filter import FileFilter from khoj.search_filter.word_filter import WordFilter -from khoj.utils.config import TextSearchModel +from khoj.utils.config import TextSearchModel, GPT4AllProcessorModel from khoj.utils.helpers import ConversationCommand, is_none_or_empty, timer, command_descriptions from khoj.utils.rawconfig import ( FullConfig, - ProcessorConfig, SearchConfig, SearchResponse, TextContentConfig, @@ -32,16 +31,16 @@ from khoj.utils.rawconfig import ( ConversationProcessorConfig, OfflineChatProcessorConfig, ) -from khoj.utils.helpers import resolve_absolute_path from khoj.utils.state import SearchType from khoj.utils import state, constants -from khoj.utils.yaml import save_config_to_file_updated_state +from khoj.utils.helpers import AsyncIteratorWrapper from fastapi.responses import StreamingResponse, Response from khoj.routers.helpers import ( get_conversation_command, perform_chat_checks, - generate_chat_response, + agenerate_chat_response, update_telemetry_state, + is_ready_to_chat, ) from khoj.processor.conversation.prompts import help_message from khoj.processor.conversation.openai.gpt import extract_questions @@ -49,7 +48,7 @@ from khoj.processor.conversation.gpt4all.chat_model import extract_questions_off from fastapi.requests import Request from database import adapters -from database.adapters import EmbeddingsAdapters +from database.adapters import EmbeddingsAdapters, ConversationAdapters from database.models import LocalMarkdownConfig, LocalOrgConfig, LocalPdfConfig, LocalPlaintextConfig, KhojUser @@ -114,6 +113,8 @@ async def map_config_to_db(config: FullConfig, user: KhojUser): user=user, token=config.content_type.notion.token, ) + if config.processor and config.processor.conversation: + ConversationAdapters.set_conversation_processor_config(user, config.processor.conversation) # If it's a demo instance, prevent updating any of the configuration. @@ -123,8 +124,6 @@ if not state.demo: if state.config is None: state.config = FullConfig() state.config.search_type = SearchConfig.parse_obj(constants.default_config["search-type"]) - if state.processor_config is None: - state.processor_config = configure_processor(state.config.processor) @api.get("/config/data", response_model=FullConfig) @requires(["authenticated"], redirect="login_page") @@ -238,28 +237,24 @@ if not state.demo: ) content_object = map_config_to_object(content_type) + if content_object is None: + raise ValueError(f"Invalid content type: {content_type}") + await content_object.objects.filter(user=user).adelete() await sync_to_async(EmbeddingsAdapters.delete_all_embeddings)(user, content_type) enabled_content = await sync_to_async(EmbeddingsAdapters.get_unique_file_types)(user) - return {"status": "ok"} @api.post("/delete/config/data/processor/conversation/openai", status_code=200) + @requires(["authenticated"], redirect="login_page") async def remove_processor_conversation_config_data( request: Request, client: Optional[str] = None, ): - if ( - not state.config - or not state.config.processor - or not state.config.processor.conversation - or not state.config.processor.conversation.openai - ): - return {"status": "ok"} + user = request.user.object - state.config.processor.conversation.openai = None - state.processor_config = configure_processor(state.config.processor, state.processor_config) + await sync_to_async(ConversationAdapters.clear_openai_conversation_config)(user) update_telemetry_state( request=request, @@ -269,11 +264,7 @@ if not state.demo: metadata={"processor_conversation_type": "openai"}, ) - try: - save_config_to_file_updated_state() - return {"status": "ok"} - except Exception as e: - return {"status": "error", "message": str(e)} + return {"status": "ok"} @api.post("/config/data/content_type/{content_type}", status_code=200) @requires(["authenticated"], redirect="login_page") @@ -301,24 +292,17 @@ if not state.demo: return {"status": "ok"} @api.post("/config/data/processor/conversation/openai", status_code=200) + @requires(["authenticated"], redirect="login_page") async def set_processor_openai_config_data( request: Request, updated_config: Union[OpenAIProcessorConfig, None], client: Optional[str] = None, ): - _initialize_config() + user = request.user.object - if not state.config.processor or not state.config.processor.conversation: - default_config = constants.default_config - default_conversation_logfile = resolve_absolute_path( - default_config["processor"]["conversation"]["conversation-logfile"] # type: ignore - ) - conversation_logfile = resolve_absolute_path(default_conversation_logfile) - state.config.processor = ProcessorConfig(conversation=ConversationProcessorConfig(conversation_logfile=conversation_logfile)) # type: ignore + conversation_config = ConversationProcessorConfig(openai=updated_config) - assert state.config.processor.conversation is not None - state.config.processor.conversation.openai = updated_config - state.processor_config = configure_processor(state.config.processor, state.processor_config) + await sync_to_async(ConversationAdapters.set_conversation_processor_config)(user, conversation_config) update_telemetry_state( request=request, @@ -328,11 +312,7 @@ if not state.demo: metadata={"processor_conversation_type": "conversation"}, ) - try: - save_config_to_file_updated_state() - return {"status": "ok"} - except Exception as e: - return {"status": "error", "message": str(e)} + return {"status": "ok"} @api.post("/config/data/processor/conversation/offline_chat", status_code=200) async def set_processor_enable_offline_chat_config_data( @@ -341,24 +321,26 @@ if not state.demo: offline_chat_model: Optional[str] = None, client: Optional[str] = None, ): - _initialize_config() + user = request.user.object - if not state.config.processor or not state.config.processor.conversation: - default_config = constants.default_config - default_conversation_logfile = resolve_absolute_path( - default_config["processor"]["conversation"]["conversation-logfile"] # type: ignore + if enable_offline_chat: + conversation_config = ConversationProcessorConfig( + offline_chat=OfflineChatProcessorConfig( + enable_offline_chat=enable_offline_chat, + chat_model=offline_chat_model, + ) ) - conversation_logfile = resolve_absolute_path(default_conversation_logfile) - state.config.processor = ProcessorConfig(conversation=ConversationProcessorConfig(conversation_logfile=conversation_logfile)) # type: ignore - assert state.config.processor.conversation is not None - if state.config.processor.conversation.offline_chat is None: - state.config.processor.conversation.offline_chat = OfflineChatProcessorConfig() + await sync_to_async(ConversationAdapters.set_conversation_processor_config)(user, conversation_config) - state.config.processor.conversation.offline_chat.enable_offline_chat = enable_offline_chat - if offline_chat_model is not None: - state.config.processor.conversation.offline_chat.chat_model = offline_chat_model - state.processor_config = configure_processor(state.config.processor, state.processor_config) + offline_chat = await ConversationAdapters.get_offline_chat(user) + chat_model = offline_chat.chat_model + if state.gpt4all_processor_config is None: + state.gpt4all_processor_config = GPT4AllProcessorModel(chat_model=chat_model) + + else: + await sync_to_async(ConversationAdapters.clear_offline_chat_conversation_config)(user) + state.gpt4all_processor_config = None update_telemetry_state( request=request, @@ -368,11 +350,7 @@ if not state.demo: metadata={"processor_conversation_type": f"{'enable' if enable_offline_chat else 'disable'}_local_llm"}, ) - try: - save_config_to_file_updated_state() - return {"status": "ok"} - except Exception as e: - return {"status": "error", "message": str(e)} + return {"status": "ok"} # Create Routes @@ -426,9 +404,6 @@ async def search( if q is None or q == "": logger.warning(f"No query param (q) passed in API call to initiate search") return results - if not state.search_models or not any(state.search_models.__dict__.values()): - logger.warning(f"No search models loaded. Configure a search model before initiating search") - return results # initialize variables user_query = q.strip() @@ -565,8 +540,6 @@ def update( components.append("Search models") if state.content_index: components.append("Content index") - if state.processor_config: - components.append("Conversation processor") components_msg = ", ".join(components) logger.info(f"📪 {components_msg} updated via API") @@ -592,12 +565,11 @@ def chat_history( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ): - perform_chat_checks() + user = request.user.object + perform_chat_checks(user) # Load Conversation History - meta_log = {} - if state.processor_config.conversation: - meta_log = state.processor_config.conversation.meta_log + meta_log = ConversationAdapters.get_conversation_by_user(user=user).conversation_log update_telemetry_state( request=request, @@ -649,30 +621,35 @@ async def chat( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ) -> Response: - perform_chat_checks() + user = request.user.object + + await is_ready_to_chat(user) conversation_command = get_conversation_command(query=q, any_references=True) q = q.replace(f"/{conversation_command.value}", "").strip() + meta_log = (await ConversationAdapters.aget_conversation_by_user(user)).conversation_log + compiled_references, inferred_queries, defiltered_query = await extract_references_and_questions( - request, q, (n or 5), conversation_command + request, meta_log, q, (n or 5), conversation_command ) if conversation_command == ConversationCommand.Default and is_none_or_empty(compiled_references): conversation_command = ConversationCommand.General if conversation_command == ConversationCommand.Help: - model_type = "offline" if state.processor_config.conversation.offline_chat.enable_offline_chat else "openai" + model_type = "offline" if await ConversationAdapters.has_offline_chat(user) else "openai" formatted_help = help_message.format(model=model_type, version=state.khoj_version) return StreamingResponse(iter([formatted_help]), media_type="text/event-stream", status_code=200) # Get the (streamed) chat response from the LLM of choice. - llm_response = generate_chat_response( + llm_response = await agenerate_chat_response( defiltered_query, - meta_log=state.processor_config.conversation.meta_log, - compiled_references=compiled_references, - inferred_queries=inferred_queries, - conversation_command=conversation_command, + meta_log, + compiled_references, + inferred_queries, + conversation_command, + user, ) if llm_response is None: @@ -681,13 +658,14 @@ async def chat( if stream: return StreamingResponse(llm_response, media_type="text/event-stream", status_code=200) + iterator = AsyncIteratorWrapper(llm_response) + # Get the full response from the generator if the stream is not requested. aggregated_gpt_response = "" - while True: - try: - aggregated_gpt_response += next(llm_response) - except StopIteration: + async for item in iterator: + if item is None: break + aggregated_gpt_response += item actual_response = aggregated_gpt_response.split("### compiled references:")[0] @@ -708,44 +686,53 @@ async def chat( async def extract_references_and_questions( request: Request, + meta_log: dict, q: str, n: int, conversation_type: ConversationCommand = ConversationCommand.Default, ): user = request.user.object if request.user.is_authenticated else None - # Load Conversation History - meta_log = state.processor_config.conversation.meta_log # Initialize Variables compiled_references: List[Any] = [] inferred_queries: List[str] = [] - if not EmbeddingsAdapters.user_has_embeddings(user=user): + if conversation_type == ConversationCommand.General: + return compiled_references, inferred_queries, q + + if not await EmbeddingsAdapters.user_has_embeddings(user=user): logger.warning( "No content index loaded, so cannot extract references from knowledge base. Please configure your data sources and update the index to chat with your notes." ) return compiled_references, inferred_queries, q - if conversation_type == ConversationCommand.General: - return compiled_references, inferred_queries, q - # Extract filter terms from user message defiltered_query = q for filter in [DateFilter(), WordFilter(), FileFilter()]: defiltered_query = filter.defilter(defiltered_query) filters_in_query = q.replace(defiltered_query, "").strip() + using_offline_chat = False + # Infer search queries from user message with timer("Extracting search queries took", logger): # If we've reached here, either the user has enabled offline chat or the openai model is enabled. - if state.processor_config.conversation.offline_chat.enable_offline_chat: - loaded_model = state.processor_config.conversation.gpt4all_model.loaded_model + if await ConversationAdapters.has_offline_chat(user): + using_offline_chat = True + offline_chat = await ConversationAdapters.get_offline_chat(user) + chat_model = offline_chat.chat_model + if state.gpt4all_processor_config is None: + state.gpt4all_processor_config = GPT4AllProcessorModel(chat_model=chat_model) + + loaded_model = state.gpt4all_processor_config.loaded_model + inferred_queries = extract_questions_offline( defiltered_query, loaded_model=loaded_model, conversation_log=meta_log, should_extract_questions=False ) - elif state.processor_config.conversation.openai_model: - api_key = state.processor_config.conversation.openai_model.api_key - chat_model = state.processor_config.conversation.openai_model.chat_model + elif await ConversationAdapters.has_openai_chat(user): + openai_chat = await ConversationAdapters.get_openai_chat(user) + api_key = openai_chat.api_key + chat_model = openai_chat.chat_model inferred_queries = extract_questions( defiltered_query, model=chat_model, api_key=api_key, conversation_log=meta_log ) @@ -754,7 +741,7 @@ async def extract_references_and_questions( with timer("Searching knowledge base took", logger): result_list = [] for query in inferred_queries: - n_items = min(n, 3) if state.processor_config.conversation.offline_chat.enable_offline_chat else n + n_items = min(n, 3) if using_offline_chat else n result_list.extend( await search( f"{query} {filters_in_query}", @@ -765,6 +752,8 @@ async def extract_references_and_questions( dedupe=False, ) ) + # Dedupe the results again, as duplicates may be returned across queries. + result_list = text_search.deduplicated_search_responses(result_list) compiled_references = [item.additional["compiled"] for item in result_list] return compiled_references, inferred_queries, defiltered_query diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index be9e8700..8a9e53a7 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -1,34 +1,50 @@ import logging +import asyncio from datetime import datetime from functools import partial from typing import Iterator, List, Optional, Union +from concurrent.futures import ThreadPoolExecutor from fastapi import HTTPException, Request from khoj.utils import state -from khoj.utils.helpers import ConversationCommand, timer, log_telemetry +from khoj.utils.config import GPT4AllProcessorModel +from khoj.utils.helpers import ConversationCommand, log_telemetry from khoj.processor.conversation.openai.gpt import converse from khoj.processor.conversation.gpt4all.chat_model import converse_offline -from khoj.processor.conversation.utils import reciprocal_conversation_to_chatml, message_to_log, ThreadedGenerator +from khoj.processor.conversation.utils import message_to_log, ThreadedGenerator from database.models import KhojUser +from database.adapters import ConversationAdapters logger = logging.getLogger(__name__) +executor = ThreadPoolExecutor(max_workers=1) -def perform_chat_checks(): - if ( - state.processor_config - and state.processor_config.conversation - and ( - state.processor_config.conversation.openai_model - or state.processor_config.conversation.gpt4all_model.loaded_model - ) - ): + +def perform_chat_checks(user: KhojUser): + if ConversationAdapters.has_valid_offline_conversation_config( + user + ) or ConversationAdapters.has_valid_openai_conversation_config(user): return - raise HTTPException( - status_code=500, detail="Set your OpenAI API key or enable Local LLM via Khoj settings and restart it." - ) + raise HTTPException(status_code=500, detail="Set your OpenAI API key or enable Local LLM via Khoj settings.") + + +async def is_ready_to_chat(user: KhojUser): + has_offline_config = await ConversationAdapters.has_offline_chat(user=user) + has_openai_config = await ConversationAdapters.has_openai_chat(user=user) + + if has_offline_config: + offline_chat = await ConversationAdapters.get_offline_chat(user) + chat_model = offline_chat.chat_model + if state.gpt4all_processor_config is None: + state.gpt4all_processor_config = GPT4AllProcessorModel(chat_model=chat_model) + return True + + ready = has_openai_config or has_offline_config + + if not ready: + raise HTTPException(status_code=500, detail="Set your OpenAI API key or enable Local LLM via Khoj settings.") def update_telemetry_state( @@ -74,12 +90,22 @@ def get_conversation_command(query: str, any_references: bool = False) -> Conver return ConversationCommand.Default +async def construct_conversation_logs(user: KhojUser): + return (await ConversationAdapters.aget_conversation_by_user(user)).conversation_log + + +async def agenerate_chat_response(*args): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(executor, generate_chat_response, *args) + + def generate_chat_response( q: str, meta_log: dict, compiled_references: List[str] = [], inferred_queries: List[str] = [], conversation_command: ConversationCommand = ConversationCommand.Default, + user: KhojUser = None, ) -> Union[ThreadedGenerator, Iterator[str]]: def _save_to_conversation_log( q: str, @@ -89,17 +115,14 @@ def generate_chat_response( inferred_queries: List[str], meta_log, ): - state.processor_config.conversation.chat_session += reciprocal_conversation_to_chatml([q, chat_response]) - state.processor_config.conversation.meta_log["chat"] = message_to_log( + updated_conversation = message_to_log( user_message=q, chat_response=chat_response, user_message_metadata={"created": user_message_time}, khoj_message_metadata={"context": compiled_references, "intent": {"inferred-queries": inferred_queries}}, conversation_log=meta_log.get("chat", []), ) - - # Load Conversation History - meta_log = state.processor_config.conversation.meta_log + ConversationAdapters.save_conversation(user, {"chat": updated_conversation}) # Initialize Variables user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -116,8 +139,14 @@ def generate_chat_response( meta_log=meta_log, ) - if state.processor_config.conversation.offline_chat.enable_offline_chat: - loaded_model = state.processor_config.conversation.gpt4all_model.loaded_model + offline_chat_config = ConversationAdapters.get_offline_chat_conversation_config(user=user) + conversation_config = ConversationAdapters.get_conversation_config(user) + openai_chat_config = ConversationAdapters.get_openai_conversation_config(user) + if offline_chat_config: + if state.gpt4all_processor_config.loaded_model is None: + state.gpt4all_processor_config = GPT4AllProcessorModel(offline_chat_config.chat_model) + + loaded_model = state.gpt4all_processor_config.loaded_model chat_response = converse_offline( references=compiled_references, user_query=q, @@ -125,14 +154,14 @@ def generate_chat_response( conversation_log=meta_log, completion_func=partial_completion, conversation_command=conversation_command, - model=state.processor_config.conversation.offline_chat.chat_model, - max_prompt_size=state.processor_config.conversation.max_prompt_size, - tokenizer_name=state.processor_config.conversation.tokenizer, + model=offline_chat_config.chat_model, + max_prompt_size=conversation_config.max_prompt_size, + tokenizer_name=conversation_config.tokenizer, ) - elif state.processor_config.conversation.openai_model: - api_key = state.processor_config.conversation.openai_model.api_key - chat_model = state.processor_config.conversation.openai_model.chat_model + elif openai_chat_config: + api_key = openai_chat_config.api_key + chat_model = openai_chat_config.chat_model chat_response = converse( compiled_references, q, @@ -141,8 +170,8 @@ def generate_chat_response( api_key=api_key, completion_func=partial_completion, conversation_command=conversation_command, - max_prompt_size=state.processor_config.conversation.max_prompt_size, - tokenizer_name=state.processor_config.conversation.tokenizer, + max_prompt_size=conversation_config.max_prompt_size if conversation_config else None, + tokenizer_name=conversation_config.tokenizer if conversation_config else None, ) except Exception as e: diff --git a/src/khoj/routers/indexer.py b/src/khoj/routers/indexer.py index 1e73c439..1125e653 100644 --- a/src/khoj/routers/indexer.py +++ b/src/khoj/routers/indexer.py @@ -92,7 +92,7 @@ async def update( if dict_to_update is not None: dict_to_update[file.filename] = ( - file.file.read().decode("utf-8") if encoding == "utf-8" else file.file.read() + file.file.read().decode("utf-8") if encoding == "utf-8" else file.file.read() # type: ignore ) else: logger.warning(f"Skipped indexing unsupported file type sent by {client} client: {file.filename}") @@ -181,24 +181,25 @@ def configure_content( files: Optional[dict[str, dict[str, str]]], search_models: SearchModels, regenerate: bool = False, - t: Optional[Union[state.SearchType, str]] = None, + t: Optional[state.SearchType] = None, full_corpus: bool = True, user: KhojUser = None, ) -> Optional[ContentIndex]: content_index = ContentIndex() - if t in [type.value for type in state.SearchType]: - t = state.SearchType(t).value + 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 None - assert type(t) == str or t == None, f"Invalid search type: {t}" + search_type = t.value if t else None if files is None: - logger.warning(f"🚨 No files to process for {t} search.") + logger.warning(f"🚨 No files to process for {search_type} search.") return None try: # Initialize Org Notes Search - if (t == None or t == state.SearchType.Org.value) and files["org"]: + if (search_type == None 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( @@ -213,7 +214,7 @@ def configure_content( try: # Initialize Markdown Search - if (t == None or t == state.SearchType.Markdown.value) and files["markdown"]: + if (search_type == None 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( @@ -229,7 +230,7 @@ def configure_content( try: # Initialize PDF Search - if (t == None or t == state.SearchType.Pdf.value) and files["pdf"]: + if (search_type == None 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( @@ -245,7 +246,7 @@ def configure_content( try: # Initialize Plaintext Search - if (t == None or t == state.SearchType.Plaintext.value) and files["plaintext"]: + if (search_type == None 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( @@ -262,7 +263,7 @@ def configure_content( try: # Initialize Image Search if ( - (t == None or t == state.SearchType.Image.value) + (search_type == None or search_type == state.SearchType.Image.value) and content_config and content_config.image and search_models.image_search @@ -278,7 +279,7 @@ def configure_content( try: github_config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first() - if (t == None or t == state.SearchType.Github.value) and github_config is not None: + if (search_type == None 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( @@ -296,7 +297,7 @@ def configure_content( try: # Initialize Notion Search notion_config = NotionConfig.objects.filter(user=user).first() - if (t == None or t in state.SearchType.Notion.value) and notion_config: + if (search_type == None or search_type in state.SearchType.Notion.value) and notion_config: logger.info("🔌 Setting up search for notion") text_search.setup( NotionToJsonl, diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 4122c6d0..333d89fa 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -19,7 +19,7 @@ from khoj.utils.rawconfig import ( # Internal Packages from khoj.utils import constants, state -from database.adapters import EmbeddingsAdapters, get_user_github_config, get_user_notion_config +from database.adapters import EmbeddingsAdapters, get_user_github_config, get_user_notion_config, ConversationAdapters from database.models import LocalOrgConfig, LocalMarkdownConfig, LocalPdfConfig, LocalPlaintextConfig @@ -83,7 +83,7 @@ if not state.demo: @web_client.get("/config", response_class=HTMLResponse) @requires(["authenticated"], redirect="login_page") def config_page(request: Request): - user = request.user.object if request.user.is_authenticated else None + user = request.user.object enabled_content = set(EmbeddingsAdapters.get_unique_file_types(user).all()) default_full_config = FullConfig( content_type=None, @@ -100,9 +100,6 @@ if not state.demo: "github": ("github" in enabled_content), "notion": ("notion" in enabled_content), "plaintext": ("plaintext" in enabled_content), - "enable_offline_model": False, - "conversation_openai": False, - "conversation_gpt4all": False, } if state.content_index: @@ -112,13 +109,17 @@ if not state.demo: } ) - if state.processor_config and state.processor_config.conversation: - successfully_configured.update( - { - "conversation_openai": state.processor_config.conversation.openai_model is not None, - "conversation_gpt4all": state.processor_config.conversation.gpt4all_model.loaded_model is not None, - } - ) + enabled_chat_config = ConversationAdapters.get_enabled_conversation_settings(user) + + successfully_configured.update( + { + "conversation_openai": enabled_chat_config["openai"], + "enable_offline_model": enabled_chat_config["offline_chat"], + "conversation_gpt4all": state.gpt4all_processor_config.loaded_model is not None + if state.gpt4all_processor_config + else False, + } + ) return templates.TemplateResponse( "config.html", @@ -127,6 +128,7 @@ if not state.demo: "current_config": current_config, "current_model_state": successfully_configured, "anonymous_mode": state.anonymous_mode, + "username": user.username if user else None, }, ) @@ -204,22 +206,22 @@ if not state.demo: ) @web_client.get("/config/processor/conversation/openai", response_class=HTMLResponse) + @requires(["authenticated"], redirect="login_page") def conversation_processor_config_page(request: Request): - default_copy = constants.default_config.copy() - default_processor_config = default_copy["processor"]["conversation"]["openai"] # type: ignore - default_openai_config = OpenAIProcessorConfig( - api_key="", - chat_model=default_processor_config["chat-model"], - ) + user = request.user.object + openai_config = ConversationAdapters.get_openai_conversation_config(user) + + if openai_config: + current_processor_openai_config = OpenAIProcessorConfig( + api_key=openai_config.api_key, + chat_model=openai_config.chat_model, + ) + else: + current_processor_openai_config = OpenAIProcessorConfig( + api_key="", + chat_model="gpt-3.5-turbo", + ) - current_processor_openai_config = ( - state.config.processor.conversation.openai - if state.config - and state.config.processor - and state.config.processor.conversation - and state.config.processor.conversation.openai - else default_openai_config - ) current_processor_openai_config = json.loads(current_processor_openai_config.json()) return templates.TemplateResponse( diff --git a/src/khoj/search_type/image_search.py b/src/khoj/search_type/image_search.py index 8b92d9db..d7f486af 100644 --- a/src/khoj/search_type/image_search.py +++ b/src/khoj/search_type/image_search.py @@ -236,6 +236,7 @@ def collate_results(hits, image_names, output_directory, image_files_url, count= "image_score": f"{hit['image_score']:.9f}", "metadata_score": f"{hit['metadata_score']:.9f}", }, + "corpus_id": hit["corpus_id"], } ) ] diff --git a/src/khoj/search_type/text_search.py b/src/khoj/search_type/text_search.py index 36d6a791..dc6593f5 100644 --- a/src/khoj/search_type/text_search.py +++ b/src/khoj/search_type/text_search.py @@ -14,10 +14,9 @@ from asgiref.sync import sync_to_async # Internal Packages from khoj.utils import state from khoj.utils.helpers import get_absolute_path, resolve_absolute_path, load_model, timer -from khoj.utils.config import TextSearchModel from khoj.utils.models import BaseEncoder from khoj.utils.state import SearchType -from khoj.utils.rawconfig import SearchResponse, TextSearchConfig, Entry +from khoj.utils.rawconfig import SearchResponse, Entry from khoj.utils.jsonl import load_jsonl from khoj.processor.text_to_jsonl import TextEmbeddings from database.adapters import EmbeddingsAdapters @@ -36,36 +35,6 @@ search_type_to_embeddings_type = { } -def initialize_model(search_config: TextSearchConfig): - "Initialize model for semantic search on text" - torch.set_num_threads(4) - - # If model directory is configured - if search_config.model_directory: - # Convert model directory to absolute path - search_config.model_directory = resolve_absolute_path(search_config.model_directory) - # Create model directory if it doesn't exist - search_config.model_directory.parent.mkdir(parents=True, exist_ok=True) - - # The bi-encoder encodes all entries to use for semantic search - bi_encoder = load_model( - model_dir=search_config.model_directory, - model_name=search_config.encoder, - model_type=search_config.encoder_type or SentenceTransformer, - device=f"{state.device}", - ) - - # The cross-encoder re-ranks the results to improve quality - cross_encoder = load_model( - model_dir=search_config.model_directory, - model_name=search_config.cross_encoder, - model_type=CrossEncoder, - device=f"{state.device}", - ) - - return TextSearchModel(bi_encoder, cross_encoder) - - def extract_entries(jsonl_file) -> List[Entry]: "Load entries from compressed jsonl" return list(map(Entry.from_dict, load_jsonl(jsonl_file))) @@ -176,6 +145,7 @@ def collate_results(hits, dedupe=True): { "entry": hit.raw, "score": hit.distance, + "corpus_id": str(hit.corpus_id), "additional": { "file": hit.file_path, "compiled": hit.compiled, @@ -185,6 +155,28 @@ def collate_results(hits, dedupe=True): ) +def deduplicated_search_responses(hits: List[SearchResponse]): + hit_ids = set() + for hit in hits: + if hit.corpus_id in hit_ids: + continue + + else: + hit_ids.add(hit.corpus_id) + yield SearchResponse.parse_obj( + { + "entry": hit.entry, + "score": hit.score, + "corpus_id": hit.corpus_id, + "additional": { + "file": hit.additional["file"], + "compiled": hit.additional["compiled"], + "heading": hit.additional["heading"], + }, + } + ) + + def rerank_and_sort_results(hits, query): # Score all retrieved entries using the cross-encoder hits = cross_encoder_score(query, hits) diff --git a/src/khoj/utils/config.py b/src/khoj/utils/config.py index ee5b4f9f..3c084c4f 100644 --- a/src/khoj/utils/config.py +++ b/src/khoj/utils/config.py @@ -5,8 +5,7 @@ from enum import Enum import logging from dataclasses import dataclass -from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Optional, Union, Any +from typing import TYPE_CHECKING, List, Optional, Union, Any from khoj.processor.conversation.gpt4all.utils import download_model # External Packages @@ -19,9 +18,7 @@ logger = logging.getLogger(__name__) # Internal Packages if TYPE_CHECKING: from sentence_transformers import CrossEncoder - from khoj.search_filter.base_filter import BaseFilter from khoj.utils.models import BaseEncoder - from khoj.utils.rawconfig import ConversationProcessorConfig, Entry, OpenAIProcessorConfig class SearchType(str, Enum): @@ -79,31 +76,15 @@ class GPT4AllProcessorConfig: loaded_model: Union[Any, None] = None -class ConversationProcessorConfigModel: +class GPT4AllProcessorModel: def __init__( self, - conversation_config: ConversationProcessorConfig, + chat_model: str = "llama-2-7b-chat.ggmlv3.q4_0.bin", ): - self.openai_model = conversation_config.openai - self.gpt4all_model = GPT4AllProcessorConfig() - self.offline_chat = conversation_config.offline_chat or OfflineChatProcessorConfig() - self.max_prompt_size = conversation_config.max_prompt_size - self.tokenizer = conversation_config.tokenizer - self.conversation_logfile = Path(conversation_config.conversation_logfile) - self.chat_session: List[str] = [] - self.meta_log: dict = {} - - if self.offline_chat.enable_offline_chat: - try: - self.gpt4all_model.loaded_model = download_model(self.offline_chat.chat_model) - except Exception as e: - self.offline_chat.enable_offline_chat = False - self.gpt4all_model.loaded_model = None - logger.error(f"Error while loading offline chat model: {e}", exc_info=True) - else: - self.gpt4all_model.loaded_model = None - - -@dataclass -class ProcessorConfigModel: - conversation: Union[ConversationProcessorConfigModel, None] = None + self.chat_model = chat_model + self.loaded_model = None + try: + self.loaded_model = download_model(self.chat_model) + except ValueError as e: + self.loaded_model = None + logger.error(f"Error while loading offline chat model: {e}", exc_info=True) diff --git a/src/khoj/utils/constants.py b/src/khoj/utils/constants.py index 181dee04..e9d431c6 100644 --- a/src/khoj/utils/constants.py +++ b/src/khoj/utils/constants.py @@ -8,136 +8,14 @@ telemetry_server = "https://khoj.beta.haletic.com/v1/telemetry" content_directory = "~/.khoj/content/" empty_config = { - "content-type": { - "org": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/org/org.jsonl.gz", - "embeddings-file": "~/.khoj/content/org/org_embeddings.pt", - "index-heading-entries": False, - }, - "markdown": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/markdown/markdown.jsonl.gz", - "embeddings-file": "~/.khoj/content/markdown/markdown_embeddings.pt", - }, - "pdf": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/pdf/pdf.jsonl.gz", - "embeddings-file": "~/.khoj/content/pdf/pdf_embeddings.pt", - }, - "plaintext": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/plaintext/plaintext.jsonl.gz", - "embeddings-file": "~/.khoj/content/plaintext/plaintext_embeddings.pt", - }, - }, "search-type": { - "symmetric": { - "encoder": "sentence-transformers/all-MiniLM-L6-v2", - "cross-encoder": "cross-encoder/ms-marco-MiniLM-L-6-v2", - "model_directory": "~/.khoj/search/symmetric/", - }, - "asymmetric": { - "encoder": "sentence-transformers/multi-qa-MiniLM-L6-cos-v1", - "cross-encoder": "cross-encoder/ms-marco-MiniLM-L-6-v2", - "model_directory": "~/.khoj/search/asymmetric/", - }, "image": {"encoder": "sentence-transformers/clip-ViT-B-32", "model_directory": "~/.khoj/search/image/"}, }, - "processor": { - "conversation": { - "openai": { - "api-key": None, - "chat-model": "gpt-3.5-turbo", - }, - "offline-chat": { - "enable-offline-chat": False, - "chat-model": "llama-2-7b-chat.ggmlv3.q4_0.bin", - }, - "tokenizer": None, - "max-prompt-size": None, - "conversation-logfile": "~/.khoj/processor/conversation/conversation_logs.json", - } - }, } # default app config to use default_config = { - "content-type": { - "org": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/org/org.jsonl.gz", - "embeddings-file": "~/.khoj/content/org/org_embeddings.pt", - "index-heading-entries": False, - }, - "markdown": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/markdown/markdown.jsonl.gz", - "embeddings-file": "~/.khoj/content/markdown/markdown_embeddings.pt", - }, - "pdf": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/pdf/pdf.jsonl.gz", - "embeddings-file": "~/.khoj/content/pdf/pdf_embeddings.pt", - }, - "image": { - "input-directories": None, - "input-filter": None, - "embeddings-file": "~/.khoj/content/image/image_embeddings.pt", - "batch-size": 50, - "use-xmp-metadata": False, - }, - "github": { - "pat-token": None, - "repos": [], - "compressed-jsonl": "~/.khoj/content/github/github.jsonl.gz", - "embeddings-file": "~/.khoj/content/github/github_embeddings.pt", - }, - "notion": { - "token": None, - "compressed-jsonl": "~/.khoj/content/notion/notion.jsonl.gz", - "embeddings-file": "~/.khoj/content/notion/notion_embeddings.pt", - }, - "plaintext": { - "input-files": None, - "input-filter": None, - "compressed-jsonl": "~/.khoj/content/plaintext/plaintext.jsonl.gz", - "embeddings-file": "~/.khoj/content/plaintext/plaintext_embeddings.pt", - }, - }, "search-type": { - "symmetric": { - "encoder": "sentence-transformers/all-MiniLM-L6-v2", - "cross-encoder": "cross-encoder/ms-marco-MiniLM-L-6-v2", - "model_directory": "~/.khoj/search/symmetric/", - }, - "asymmetric": { - "encoder": "sentence-transformers/multi-qa-MiniLM-L6-cos-v1", - "cross-encoder": "cross-encoder/ms-marco-MiniLM-L-6-v2", - "model_directory": "~/.khoj/search/asymmetric/", - }, "image": {"encoder": "sentence-transformers/clip-ViT-B-32", "model_directory": "~/.khoj/search/image/"}, }, - "processor": { - "conversation": { - "openai": { - "api-key": None, - "chat-model": "gpt-3.5-turbo", - }, - "offline-chat": { - "enable-offline-chat": False, - "chat-model": "llama-2-7b-chat.ggmlv3.q4_0.bin", - }, - "tokenizer": None, - "max-prompt-size": None, - "conversation-logfile": "~/.khoj/processor/conversation/conversation_logs.json", - } - }, } diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index e41791f9..0269a9e9 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -15,6 +15,7 @@ from time import perf_counter import torch from typing import Optional, Union, TYPE_CHECKING import uuid +from asgiref.sync import sync_to_async # Internal Packages from khoj.utils import constants @@ -29,6 +30,28 @@ if TYPE_CHECKING: from khoj.utils.rawconfig import AppConfig +class AsyncIteratorWrapper: + def __init__(self, obj): + self._it = iter(obj) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + value = await self.next_async() + except StopAsyncIteration: + return + return value + + @sync_to_async + def next_async(self): + try: + return next(self._it) + except StopIteration: + raise StopAsyncIteration + + def is_none_or_empty(item): return item == None or (hasattr(item, "__iter__") and len(item) == 0) or item == "" diff --git a/src/khoj/utils/rawconfig.py b/src/khoj/utils/rawconfig.py index 5d2b3ce4..a469951f 100644 --- a/src/khoj/utils/rawconfig.py +++ b/src/khoj/utils/rawconfig.py @@ -67,13 +67,6 @@ class ContentConfig(ConfigBase): notion: Optional[NotionContentConfig] -class TextSearchConfig(ConfigBase): - encoder: str - cross_encoder: str - encoder_type: Optional[str] - model_directory: Optional[Path] - - class ImageSearchConfig(ConfigBase): encoder: str encoder_type: Optional[str] @@ -81,8 +74,6 @@ class ImageSearchConfig(ConfigBase): class SearchConfig(ConfigBase): - asymmetric: Optional[TextSearchConfig] - symmetric: Optional[TextSearchConfig] image: Optional[ImageSearchConfig] @@ -97,11 +88,10 @@ class OfflineChatProcessorConfig(ConfigBase): class ConversationProcessorConfig(ConfigBase): - conversation_logfile: Path - openai: Optional[OpenAIProcessorConfig] - offline_chat: Optional[OfflineChatProcessorConfig] - max_prompt_size: Optional[int] - tokenizer: Optional[str] + openai: Optional[OpenAIProcessorConfig] = None + offline_chat: Optional[OfflineChatProcessorConfig] = None + max_prompt_size: Optional[int] = None + tokenizer: Optional[str] = None class ProcessorConfig(ConfigBase): @@ -125,6 +115,7 @@ class SearchResponse(ConfigBase): score: float cross_score: Optional[float] additional: Optional[dict] + corpus_id: str class Entry: diff --git a/src/khoj/utils/state.py b/src/khoj/utils/state.py index d6169d2a..40806c51 100644 --- a/src/khoj/utils/state.py +++ b/src/khoj/utils/state.py @@ -10,7 +10,7 @@ from pathlib import Path # Internal Packages from khoj.utils import config as utils_config -from khoj.utils.config import ContentIndex, SearchModels, ProcessorConfigModel +from khoj.utils.config import ContentIndex, SearchModels, GPT4AllProcessorModel from khoj.utils.helpers import LRU from khoj.utils.rawconfig import FullConfig from khoj.processor.embeddings import EmbeddingsModel, CrossEncoderModel @@ -21,7 +21,7 @@ search_models = SearchModels() embeddings_model = EmbeddingsModel() cross_encoder_model = CrossEncoderModel() content_index = ContentIndex() -processor_config = ProcessorConfigModel() +gpt4all_processor_config: GPT4AllProcessorModel = None config_file: Path = None verbose: int = 0 host: str = None diff --git a/tests/conftest.py b/tests/conftest.py index ee4b9e57..12ac4f7b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,6 @@ from pathlib import Path import pytest from fastapi.staticfiles import StaticFiles from fastapi import FastAPI -import factory import os from fastapi import FastAPI @@ -13,7 +12,7 @@ app = FastAPI() # Internal Packages -from khoj.configure import configure_processor, configure_routes, configure_search_types, configure_middleware +from khoj.configure import configure_routes, configure_search_types, configure_middleware from khoj.processor.plaintext.plaintext_to_jsonl import PlaintextToJsonl from khoj.search_type import image_search, text_search from khoj.utils.config import SearchModels @@ -21,13 +20,8 @@ from khoj.utils.constants import web_directory from khoj.utils.helpers import resolve_absolute_path from khoj.utils.rawconfig import ( ContentConfig, - ConversationProcessorConfig, - OfflineChatProcessorConfig, - OpenAIProcessorConfig, - ProcessorConfig, ImageContentConfig, SearchConfig, - TextSearchConfig, ImageSearchConfig, ) from khoj.utils import state, fs_syncer @@ -42,42 +36,25 @@ from database.models import ( GithubRepoConfig, ) +from tests.helpers import ( + UserFactory, + ConversationProcessorConfigFactory, + OpenAIProcessorConversationConfigFactory, + OfflineChatProcessorConversationConfigFactory, +) + @pytest.fixture(autouse=True) def enable_db_access_for_all_tests(db): pass -class UserFactory(factory.django.DjangoModelFactory): - class Meta: - model = KhojUser - - username = factory.Faker("name") - email = factory.Faker("email") - password = factory.Faker("password") - uuid = factory.Faker("uuid4") - - @pytest.fixture(scope="session") def search_config() -> SearchConfig: model_dir = resolve_absolute_path("~/.khoj/search") model_dir.mkdir(parents=True, exist_ok=True) search_config = SearchConfig() - search_config.symmetric = TextSearchConfig( - encoder="sentence-transformers/all-MiniLM-L6-v2", - cross_encoder="cross-encoder/ms-marco-MiniLM-L-6-v2", - model_directory=model_dir / "symmetric/", - encoder_type=None, - ) - - search_config.asymmetric = TextSearchConfig( - encoder="sentence-transformers/multi-qa-MiniLM-L6-cos-v1", - cross_encoder="cross-encoder/ms-marco-MiniLM-L-6-v2", - model_directory=model_dir / "asymmetric/", - encoder_type=None, - ) - search_config.image = ImageSearchConfig( encoder="sentence-transformers/clip-ViT-B-32", model_directory=model_dir / "image/", @@ -177,55 +154,48 @@ def md_content_config(): return markdown_config -@pytest.fixture(scope="session") -def processor_config(tmp_path_factory): - openai_api_key = os.getenv("OPENAI_API_KEY") - processor_dir = tmp_path_factory.mktemp("processor") - - # The conversation processor is the only configured processor - # It needs an OpenAI API key to work. - if not openai_api_key: - return - - # Setup conversation processor, if OpenAI API key is set - processor_config = ProcessorConfig() - processor_config.conversation = ConversationProcessorConfig( - openai=OpenAIProcessorConfig(api_key=openai_api_key), - conversation_logfile=processor_dir.joinpath("conversation_logs.json"), - ) - - return processor_config - - -@pytest.fixture(scope="session") -def processor_config_offline_chat(tmp_path_factory): - processor_dir = tmp_path_factory.mktemp("processor") - - # Setup conversation processor - processor_config = ProcessorConfig() - offline_chat = OfflineChatProcessorConfig(enable_offline_chat=True) - processor_config.conversation = ConversationProcessorConfig( - offline_chat=offline_chat, - conversation_logfile=processor_dir.joinpath("conversation_logs.json"), - ) - - return processor_config - - -@pytest.fixture(scope="session") -def chat_client(md_content_config: ContentConfig, search_config: SearchConfig, processor_config: ProcessorConfig): +@pytest.fixture(scope="function") +def chat_client(search_config: SearchConfig, default_user2: KhojUser): # Initialize app state state.config.search_type = search_config state.SearchType = configure_search_types(state.config) + LocalMarkdownConfig.objects.create( + input_files=None, + input_filter=["tests/data/markdown/*.markdown"], + user=default_user2, + ) + # Index Markdown Content for Search - all_files = fs_syncer.collect_files() + all_files = fs_syncer.collect_files(user=default_user2) state.content_index = configure_content( - state.content_index, state.config.content_type, all_files, state.search_models + state.content_index, state.config.content_type, all_files, state.search_models, user=default_user2 ) # Initialize Processor from Config - state.processor_config = configure_processor(processor_config) + if os.getenv("OPENAI_API_KEY"): + OpenAIProcessorConversationConfigFactory(user=default_user2) + + state.anonymous_mode = True + + app = FastAPI() + + configure_routes(app) + configure_middleware(app) + app.mount("/static", StaticFiles(directory=web_directory), name="static") + return TestClient(app) + + +@pytest.fixture(scope="function") +def chat_client_no_background(search_config: SearchConfig, default_user2: KhojUser): + # Initialize app state + state.config.search_type = search_config + state.SearchType = configure_search_types(state.config) + + # Initialize Processor from Config + if os.getenv("OPENAI_API_KEY"): + OpenAIProcessorConversationConfigFactory(user=default_user2) + state.anonymous_mode = True app = FastAPI() @@ -249,7 +219,6 @@ def fastapi_app(): def client( content_config: ContentConfig, search_config: SearchConfig, - processor_config: ProcessorConfig, default_user: KhojUser, ): state.config.content_type = content_config @@ -274,7 +243,7 @@ def client( user=default_user, ) - state.processor_config = configure_processor(processor_config) + ConversationProcessorConfigFactory(user=default_user) state.anonymous_mode = True configure_routes(app) @@ -286,25 +255,32 @@ def client( @pytest.fixture(scope="function") def client_offline_chat( search_config: SearchConfig, - processor_config_offline_chat: ProcessorConfig, content_config: ContentConfig, - md_content_config, + default_user2: KhojUser, ): # Initialize app state state.config.content_type = md_content_config state.config.search_type = search_config state.SearchType = configure_search_types(state.config) + LocalMarkdownConfig.objects.create( + input_files=None, + input_filter=["tests/data/markdown/*.markdown"], + user=default_user2, + ) + # Index Markdown Content for Search state.search_models.image_search = image_search.initialize_model(search_config.image) - all_files = fs_syncer.collect_files(state.config.content_type) - state.content_index = configure_content( - state.content_index, state.config.content_type, all_files, state.search_models + all_files = fs_syncer.collect_files(user=default_user2) + configure_content( + state.content_index, state.config.content_type, all_files, state.search_models, user=default_user2 ) # Initialize Processor from Config - state.processor_config = configure_processor(processor_config_offline_chat) + ConversationProcessorConfigFactory(user=default_user2) + OfflineChatProcessorConversationConfigFactory(user=default_user2) + state.anonymous_mode = True configure_routes(app) diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 00000000..655c4435 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,51 @@ +import factory +import os + +from database.models import ( + KhojUser, + ConversationProcessorConfig, + OfflineChatProcessorConversationConfig, + OpenAIProcessorConversationConfig, + Conversation, +) + + +class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = KhojUser + + username = factory.Faker("name") + email = factory.Faker("email") + password = factory.Faker("password") + uuid = factory.Faker("uuid4") + + +class ConversationProcessorConfigFactory(factory.django.DjangoModelFactory): + class Meta: + model = ConversationProcessorConfig + + max_prompt_size = 2000 + tokenizer = None + + +class OfflineChatProcessorConversationConfigFactory(factory.django.DjangoModelFactory): + class Meta: + model = OfflineChatProcessorConversationConfig + + enable_offline_chat = True + chat_model = "llama-2-7b-chat.ggmlv3.q4_0.bin" + + +class OpenAIProcessorConversationConfigFactory(factory.django.DjangoModelFactory): + class Meta: + model = OpenAIProcessorConversationConfig + + api_key = os.getenv("OPENAI_API_KEY") + chat_model = "gpt-3.5-turbo" + + +class ConversationFactory(factory.django.DjangoModelFactory): + class Meta: + model = Conversation + + user = factory.SubFactory(UserFactory) diff --git a/tests/test_client.py b/tests/test_client.py index b77ba07d..1a6b1346 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -119,7 +119,12 @@ def test_get_configured_types_via_api(client, sample_org_data): def test_get_api_config_types(client, search_config: SearchConfig, sample_org_data, default_user2: KhojUser): # Arrange text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user2) + + # Act response = client.get(f"/api/config/types") + + # Assert + assert response.status_code == 200 assert response.json() == ["all", "org", "image"] diff --git a/tests/test_gpt4all_chat_director.py b/tests/test_gpt4all_chat_director.py index 3e72a7e2..d978fc99 100644 --- a/tests/test_gpt4all_chat_director.py +++ b/tests/test_gpt4all_chat_director.py @@ -9,8 +9,7 @@ from faker import Faker # Internal Packages from khoj.processor.conversation import prompts from khoj.processor.conversation.utils import message_to_log -from khoj.utils import state - +from tests.helpers import ConversationFactory SKIP_TESTS = True pytestmark = pytest.mark.skipif( @@ -23,7 +22,7 @@ fake = Faker() # Helpers # ---------------------------------------------------------------------------------------------------- -def populate_chat_history(message_list): +def populate_chat_history(message_list, user): # Generate conversation logs conversation_log = {"chat": []} for user_message, llm_message, context in message_list: @@ -33,14 +32,15 @@ def populate_chat_history(message_list): {"context": context, "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}}, ) - # Update Conversation Metadata Logs in Application State - state.processor_config.conversation.meta_log = conversation_log + # Update Conversation Metadata Logs in Database + ConversationFactory(user=user, conversation_log=conversation_log) # Tests # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering this question yet") @pytest.mark.chatquality +@pytest.mark.django_db(transaction=True) def test_chat_with_no_chat_history_or_retrieved_content_gpt4all(client_offline_chat): # Act response = client_offline_chat.get(f'/api/chat?q="Hello, my name is Testatron. Who are you?"&stream=true') @@ -56,13 +56,14 @@ def test_chat_with_no_chat_history_or_retrieved_content_gpt4all(client_offline_c # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality -def test_answer_from_chat_history(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_from_chat_history(client_offline_chat, default_user2): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f'/api/chat?q="What is my name?"&stream=true') @@ -78,7 +79,8 @@ def test_answer_from_chat_history(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality -def test_answer_from_currently_retrieved_content(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_from_currently_retrieved_content(client_offline_chat, default_user2): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), @@ -88,7 +90,7 @@ def test_answer_from_currently_retrieved_content(client_offline_chat): ["Testatron was born on 1st April 1984 in Testville."], ), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f'/api/chat?q="Where was Xi Li born?"') @@ -101,7 +103,8 @@ def test_answer_from_currently_retrieved_content(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality -def test_answer_from_chat_history_and_previously_retrieved_content(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_from_chat_history_and_previously_retrieved_content(client_offline_chat, default_user2): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), @@ -111,7 +114,7 @@ def test_answer_from_chat_history_and_previously_retrieved_content(client_offlin ["Testatron was born on 1st April 1984 in Testville."], ), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f'/api/chat?q="Where was I born?"') @@ -130,13 +133,14 @@ def test_answer_from_chat_history_and_previously_retrieved_content(client_offlin reason="Chat director not capable of answering this question yet because it requires extract_questions", ) @pytest.mark.chatquality -def test_answer_from_chat_history_and_currently_retrieved_content(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_from_chat_history_and_currently_retrieved_content(client_offline_chat, default_user2): # Arrange message_list = [ ("Hello, my name is Xi Li. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f'/api/chat?q="Where was I born?"') @@ -154,14 +158,15 @@ def test_answer_from_chat_history_and_currently_retrieved_content(client_offline # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering this question yet") @pytest.mark.chatquality -def test_no_answer_in_chat_history_or_retrieved_content(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_no_answer_in_chat_history_or_retrieved_content(client_offline_chat, default_user2): "Chat director should say don't know as not enough contexts in chat history or retrieved to answer question" # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f'/api/chat?q="Where was I born?"&stream=true') @@ -177,11 +182,12 @@ def test_no_answer_in_chat_history_or_retrieved_content(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality -def test_answer_using_general_command(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_using_general_command(client_offline_chat, default_user2): # Arrange query = urllib.parse.quote("/general Where was Xi Li born?") message_list = [] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f"/api/chat?q={query}&stream=true") @@ -194,11 +200,12 @@ def test_answer_using_general_command(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality -def test_answer_from_retrieved_content_using_notes_command(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_from_retrieved_content_using_notes_command(client_offline_chat, default_user2): # Arrange query = urllib.parse.quote("/notes Where was Xi Li born?") message_list = [] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f"/api/chat?q={query}&stream=true") @@ -211,12 +218,13 @@ def test_answer_from_retrieved_content_using_notes_command(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality -def test_answer_using_file_filter(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_using_file_filter(client_offline_chat, default_user2): # Arrange no_answer_query = urllib.parse.quote('Where was Xi Li born? file:"Namita.markdown"') answer_query = urllib.parse.quote('Where was Xi Li born? file:"Xi Li.markdown"') message_list = [] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act no_answer_response = client_offline_chat.get(f"/api/chat?q={no_answer_query}&stream=true").content.decode("utf-8") @@ -229,11 +237,12 @@ def test_answer_using_file_filter(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality -def test_answer_not_known_using_notes_command(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_not_known_using_notes_command(client_offline_chat, default_user2): # Arrange query = urllib.parse.quote("/notes Where was Testatron born?") message_list = [] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f"/api/chat?q={query}&stream=true") @@ -247,6 +256,7 @@ def test_answer_not_known_using_notes_command(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering time aware questions yet") @pytest.mark.chatquality +@pytest.mark.django_db(transaction=True) @freeze_time("2023-04-01") def test_answer_requires_current_date_awareness(client_offline_chat): "Chat actor should be able to answer questions relative to current date using provided notes" @@ -265,6 +275,7 @@ def test_answer_requires_current_date_awareness(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering this question yet") @pytest.mark.chatquality +@pytest.mark.django_db(transaction=True) @freeze_time("2023-04-01") def test_answer_requires_date_aware_aggregation_across_provided_notes(client_offline_chat): "Chat director should be able to answer questions that require date aware aggregation across multiple notes" @@ -280,14 +291,15 @@ def test_answer_requires_date_aware_aggregation_across_provided_notes(client_off # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering this question yet") @pytest.mark.chatquality -def test_answer_general_question_not_in_chat_history_or_retrieved_content(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_general_question_not_in_chat_history_or_retrieved_content(client_offline_chat, default_user2): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ("Where was I born?", "You were born Testville.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get( @@ -307,7 +319,8 @@ def test_answer_general_question_not_in_chat_history_or_retrieved_content(client # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(reason="Chat director not consistently capable of asking for clarification yet.") @pytest.mark.chatquality -def test_ask_for_clarification_if_not_enough_context_in_question(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_ask_for_clarification_if_not_enough_context_in_question(client_offline_chat, default_user2): # Act response = client_offline_chat.get(f'/api/chat?q="What is the name of Namitas older son"&stream=true') response_message = response.content.decode("utf-8") @@ -328,14 +341,15 @@ def test_ask_for_clarification_if_not_enough_context_in_question(client_offline_ # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(reason="Chat director not capable of answering this question yet") @pytest.mark.chatquality -def test_answer_in_chat_history_beyond_lookback_window(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_in_chat_history_beyond_lookback_window(client_offline_chat, default_user2): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ("Where was I born?", "You were born Testville.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f'/api/chat?q="What is my name?"&stream=true') @@ -350,11 +364,12 @@ def test_answer_in_chat_history_beyond_lookback_window(client_offline_chat): @pytest.mark.chatquality -def test_answer_chat_history_very_long(client_offline_chat): +@pytest.mark.django_db(transaction=True) +def test_answer_chat_history_very_long(client_offline_chat, default_user2): # Arrange message_list = [(" ".join([fake.paragraph() for _ in range(50)]), fake.sentence(), []) for _ in range(10)] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = client_offline_chat.get(f'/api/chat?q="What is my name?"&stream=true') @@ -368,6 +383,7 @@ def test_answer_chat_history_very_long(client_offline_chat): # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering this question yet") @pytest.mark.chatquality +@pytest.mark.django_db(transaction=True) def test_answer_requires_multiple_independent_searches(client_offline_chat): "Chat director should be able to answer by doing multiple independent searches for required information" # Act diff --git a/tests/test_openai_chat_director.py b/tests/test_openai_chat_director.py index abbd1831..14a73f15 100644 --- a/tests/test_openai_chat_director.py +++ b/tests/test_openai_chat_director.py @@ -9,8 +9,8 @@ from khoj.processor.conversation import prompts # Internal Packages from khoj.processor.conversation.utils import message_to_log -from khoj.utils import state - +from tests.helpers import ConversationFactory +from database.models import KhojUser # Initialize variables for tests api_key = os.getenv("OPENAI_API_KEY") @@ -23,7 +23,7 @@ if api_key is None: # Helpers # ---------------------------------------------------------------------------------------------------- -def populate_chat_history(message_list): +def populate_chat_history(message_list, user=None): # Generate conversation logs conversation_log = {"chat": []} for user_message, gpt_message, context in message_list: @@ -33,13 +33,14 @@ def populate_chat_history(message_list): {"context": context, "intent": {"query": user_message, "inferred-queries": f'["{user_message}"]'}}, ) - # Update Conversation Metadata Logs in Application State - state.processor_config.conversation.meta_log = conversation_log + # Update Conversation Metadata Logs in Database + ConversationFactory(user=user, conversation_log=conversation_log) # Tests # ---------------------------------------------------------------------------------------------------- @pytest.mark.chatquality +@pytest.mark.django_db(transaction=True) def test_chat_with_no_chat_history_or_retrieved_content(chat_client): # Act response = chat_client.get(f'/api/chat?q="Hello, my name is Testatron. Who are you?"&stream=true') @@ -54,14 +55,15 @@ def test_chat_with_no_chat_history_or_retrieved_content(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_from_chat_history(chat_client): +def test_answer_from_chat_history(chat_client, default_user2: KhojUser): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get(f'/api/chat?q="What is my name?"&stream=true') @@ -76,8 +78,9 @@ def test_answer_from_chat_history(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_from_currently_retrieved_content(chat_client): +def test_answer_from_currently_retrieved_content(chat_client, default_user2: KhojUser): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), @@ -87,7 +90,7 @@ def test_answer_from_currently_retrieved_content(chat_client): ["Testatron was born on 1st April 1984 in Testville."], ), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get(f'/api/chat?q="Where was Xi Li born?"') @@ -99,8 +102,9 @@ def test_answer_from_currently_retrieved_content(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_from_chat_history_and_previously_retrieved_content(chat_client): +def test_answer_from_chat_history_and_previously_retrieved_content(chat_client_no_background, default_user2: KhojUser): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), @@ -110,10 +114,10 @@ def test_answer_from_chat_history_and_previously_retrieved_content(chat_client): ["Testatron was born on 1st April 1984 in Testville."], ), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act - response = chat_client.get(f'/api/chat?q="Where was I born?"') + response = chat_client_no_background.get(f'/api/chat?q="Where was I born?"') response_message = response.content.decode("utf-8") # Assert @@ -125,14 +129,15 @@ def test_answer_from_chat_history_and_previously_retrieved_content(chat_client): # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering this question yet") +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_from_chat_history_and_currently_retrieved_content(chat_client): +def test_answer_from_chat_history_and_currently_retrieved_content(chat_client, default_user2: KhojUser): # Arrange message_list = [ ("Hello, my name is Xi Li. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get(f'/api/chat?q="Where was I born?"') @@ -148,15 +153,16 @@ def test_answer_from_chat_history_and_currently_retrieved_content(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_no_answer_in_chat_history_or_retrieved_content(chat_client): +def test_no_answer_in_chat_history_or_retrieved_content(chat_client, default_user2: KhojUser): "Chat director should say don't know as not enough contexts in chat history or retrieved to answer question" # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get(f'/api/chat?q="Where was I born?"&stream=true') @@ -171,12 +177,13 @@ def test_no_answer_in_chat_history_or_retrieved_content(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_using_general_command(chat_client): +def test_answer_using_general_command(chat_client, default_user2: KhojUser): # Arrange query = urllib.parse.quote("/general Where was Xi Li born?") message_list = [] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get(f"/api/chat?q={query}&stream=true") @@ -188,12 +195,13 @@ def test_answer_using_general_command(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_from_retrieved_content_using_notes_command(chat_client): +def test_answer_from_retrieved_content_using_notes_command(chat_client, default_user2: KhojUser): # Arrange query = urllib.parse.quote("/notes Where was Xi Li born?") message_list = [] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get(f"/api/chat?q={query}&stream=true") @@ -205,15 +213,16 @@ def test_answer_from_retrieved_content_using_notes_command(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_not_known_using_notes_command(chat_client): +def test_answer_not_known_using_notes_command(chat_client_no_background, default_user2: KhojUser): # Arrange query = urllib.parse.quote("/notes Where was Testatron born?") message_list = [] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act - response = chat_client.get(f"/api/chat?q={query}&stream=true") + response = chat_client_no_background.get(f"/api/chat?q={query}&stream=true") response_message = response.content.decode("utf-8") # Assert @@ -223,6 +232,7 @@ def test_answer_not_known_using_notes_command(chat_client): # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(AssertionError, reason="Chat director not capable of answering time aware questions yet") +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality @freeze_time("2023-04-01") def test_answer_requires_current_date_awareness(chat_client): @@ -240,11 +250,13 @@ def test_answer_requires_current_date_awareness(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality @freeze_time("2023-04-01") def test_answer_requires_date_aware_aggregation_across_provided_notes(chat_client): "Chat director should be able to answer questions that require date aware aggregation across multiple notes" # Act + response = chat_client.get(f'/api/chat?q="How much did I spend on dining this year?"&stream=true') response_message = response.content.decode("utf-8") @@ -254,15 +266,16 @@ def test_answer_requires_date_aware_aggregation_across_provided_notes(chat_clien # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_general_question_not_in_chat_history_or_retrieved_content(chat_client): +def test_answer_general_question_not_in_chat_history_or_retrieved_content(chat_client, default_user2: KhojUser): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ("Where was I born?", "You were born Testville.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get( @@ -280,10 +293,12 @@ def test_answer_general_question_not_in_chat_history_or_retrieved_content(chat_c # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_ask_for_clarification_if_not_enough_context_in_question(chat_client): +def test_ask_for_clarification_if_not_enough_context_in_question(chat_client_no_background): # Act - response = chat_client.get(f'/api/chat?q="What is the name of Namitas older son"&stream=true') + + response = chat_client_no_background.get(f'/api/chat?q="What is the name of Namitas older son"&stream=true') response_message = response.content.decode("utf-8") # Assert @@ -301,15 +316,16 @@ def test_ask_for_clarification_if_not_enough_context_in_question(chat_client): # ---------------------------------------------------------------------------------------------------- @pytest.mark.xfail(reason="Chat director not capable of answering this question yet") +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality -def test_answer_in_chat_history_beyond_lookback_window(chat_client): +def test_answer_in_chat_history_beyond_lookback_window(chat_client, default_user2: KhojUser): # Arrange message_list = [ ("Hello, my name is Testatron. Who are you?", "Hi, I am Khoj, a personal assistant. How can I help?", []), ("When was I born?", "You were born on 1st April 1984.", []), ("Where was I born?", "You were born Testville.", []), ] - populate_chat_history(message_list) + populate_chat_history(message_list, default_user2) # Act response = chat_client.get(f'/api/chat?q="What is my name?"&stream=true') @@ -324,6 +340,7 @@ def test_answer_in_chat_history_beyond_lookback_window(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) @pytest.mark.chatquality def test_answer_requires_multiple_independent_searches(chat_client): "Chat director should be able to answer by doing multiple independent searches for required information" @@ -340,10 +357,12 @@ def test_answer_requires_multiple_independent_searches(chat_client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_answer_using_file_filter(chat_client): "Chat should be able to use search filters in the query" # Act query = urllib.parse.quote('Is Xi older than Namita? file:"Namita.markdown" file:"Xi Li.markdown"') + response = chat_client.get(f"/api/chat?q={query}&stream=true") response_message = response.content.decode("utf-8") diff --git a/tests/test_text_search.py b/tests/test_text_search.py index af47ffe5..ec8034ef 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -13,12 +13,11 @@ from khoj.search_type import text_search from khoj.utils.rawconfig import ContentConfig, SearchConfig from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl from khoj.processor.github.github_to_jsonl import GithubToJsonl -from khoj.utils.config import SearchModels -from khoj.utils.fs_syncer import get_org_files, collect_files +from khoj.utils.fs_syncer import collect_files, get_org_files from database.models import LocalOrgConfig, KhojUser, Embeddings, GithubConfig logger = logging.getLogger(__name__) -from khoj.utils.rawconfig import ContentConfig, SearchConfig, TextContentConfig +from khoj.utils.rawconfig import ContentConfig, SearchConfig # Test From 9acc722f7f19965ac851b208c774b70d1483201f Mon Sep 17 00:00:00 2001 From: Debanjum Date: Thu, 26 Oct 2023 12:33:03 -0700 Subject: [PATCH 005/194] [Multi-User Part 4]: Authenticate using API Tokens (#513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### ✨ New - Use API keys to authenticate from Desktop, Obsidian, Emacs clients - Create API, UI on web app config page to CRUD API Keys - Create user API keys table and functions to CRUD them in Database ### 🧪 Improve - Default to better search model, [gte-small](https://huggingface.co/thenlper/gte-small), to improve search quality - Only load chat model to GPU if enough space, throw error on load failure - Show encoding progress, truncate headings to max chars supported - Add instruction to create db in Django DB setup Readme ### ⚙️ Fix - Fix error handling when configure offline chat via Web UI - Do not warn in anon mode about Google OAuth env vars not being set - Fix path to load static files when server started from project root --- src/app/README.md | 6 + src/app/settings.py | 6 +- src/app/urls.py | 2 +- src/database/adapters/__init__.py | 22 +++ src/database/migrations/0009_khojapiuser.py | 24 +++ src/database/models/__init__.py | 9 + .../desktop/assets/icons/favicon-20x20.png | Bin 0 -> 1660 bytes src/interface/desktop/assets/icons/key.svg | 4 + src/interface/desktop/assets/icons/link.svg | 5 +- src/interface/desktop/chat.html | 17 +- src/interface/desktop/config.html | 169 ++++++++++-------- src/interface/desktop/index.html | 15 +- src/interface/desktop/main.js | 58 +++++- src/interface/desktop/preload.js | 5 + src/interface/desktop/renderer.js | 11 ++ src/interface/emacs/khoj.el | 145 ++------------- src/interface/obsidian/src/chat_modal.ts | 6 +- src/interface/obsidian/src/main.ts | 16 +- src/interface/obsidian/src/search_modal.ts | 5 +- src/interface/obsidian/src/settings.ts | 29 +-- src/interface/obsidian/src/utils.ts | 118 +----------- src/khoj/configure.py | 15 +- src/khoj/interface/web/config.html | 121 +++++++++---- src/khoj/main.py | 7 +- .../processor/conversation/gpt4all/utils.py | 24 ++- src/khoj/processor/embeddings.py | 33 +--- src/khoj/routers/api.py | 82 +++++---- src/khoj/routers/auth.py | 38 +++- src/khoj/routers/indexer.py | 11 +- src/khoj/routers/web_client.py | 6 +- src/khoj/utils/config.py | 6 +- src/khoj/utils/helpers.py | 40 ++++- src/khoj/utils/state.py | 13 +- tests/conftest.py | 40 +++-- tests/helpers.py | 10 ++ tests/test_client.py | 138 ++++++++++---- 36 files changed, 692 insertions(+), 564 deletions(-) create mode 100644 src/database/migrations/0009_khojapiuser.py create mode 100644 src/interface/desktop/assets/icons/favicon-20x20.png create mode 100644 src/interface/desktop/assets/icons/key.svg diff --git a/src/app/README.md b/src/app/README.md index 7a93ee8b..cbfe5356 100644 --- a/src/app/README.md +++ b/src/app/README.md @@ -37,6 +37,12 @@ make install # may need sudo ``` 3. Create a database +### Create the khoj database + +```bash +createdb khoj -U postgres +``` + ### Make migrations This command will create the migrations for the database app. This command should be run whenever a new model is added to the database app or an existing model is modified (updated or deleted). diff --git a/src/app/settings.py b/src/app/settings.py index cfd7cd3c..9a8b427b 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -14,7 +14,7 @@ from pathlib import Path import os # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +BASE_DIR = Path(__file__).resolve().parent.parent.parent # Quick-start development settings - unsuitable for production @@ -123,8 +123,8 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ -STATIC_ROOT = os.path.join(BASE_DIR, "static") -STATICFILES_DIRS = [os.path.join(BASE_DIR, "khoj/interface/web")] +STATIC_ROOT = BASE_DIR / "static" +STATICFILES_DIRS = [BASE_DIR / "src/khoj/interface/web"] STATIC_URL = "/static/" # Default primary key field type diff --git a/src/app/urls.py b/src/app/urls.py index fbd67a4e..39b4b1ef 100644 --- a/src/app/urls.py +++ b/src/app/urls.py @@ -15,7 +15,7 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path, include +from django.urls import path from django.contrib.staticfiles.urls import staticfiles_urlpatterns urlpatterns = [ diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index db5e9f77..52debdc4 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -1,3 +1,4 @@ +import secrets from typing import Type, TypeVar, List from datetime import date @@ -16,6 +17,7 @@ from fastapi import HTTPException from database.models import ( KhojUser, GoogleUser, + KhojApiUser, NotionConfig, GithubConfig, Embeddings, @@ -25,6 +27,7 @@ from database.models import ( OpenAIProcessorConversationConfig, OfflineChatProcessorConversationConfig, ) +from khoj.utils.helpers import generate_random_name from khoj.utils.rawconfig import ( ConversationProcessorConfig as UserConversationProcessorConfig, ) @@ -52,6 +55,25 @@ async def set_notion_config(token: str, user: KhojUser): return notion_config +async def create_khoj_token(user: KhojUser, name=None): + "Create Khoj API key for user" + token = f"kk-{secrets.token_urlsafe(32)}" + name = name or f"{generate_random_name().title()}'s Secret Key" + api_config = await KhojApiUser.objects.acreate(token=token, user=user, name=name) + await api_config.asave() + return api_config + + +def get_khoj_tokens(user: KhojUser): + "Get all Khoj API keys for user" + return list(KhojApiUser.objects.filter(user=user)) + + +async def delete_khoj_token(user: KhojUser, token: str): + "Delete Khoj API Key for user" + await KhojApiUser.objects.filter(token=token, user=user).adelete() + + async def get_or_create_user(token: dict) -> KhojUser: user = await get_user_by_token(token) if not user: diff --git a/src/database/migrations/0009_khojapiuser.py b/src/database/migrations/0009_khojapiuser.py new file mode 100644 index 00000000..86b09ab3 --- /dev/null +++ b/src/database/migrations/0009_khojapiuser.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.5 on 2023-10-26 17:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0008_alter_conversation_conversation_log"), + ] + + operations = [ + migrations.CreateModel( + name="KhojApiUser", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("token", models.CharField(max_length=50, unique=True)), + ("name", models.CharField(max_length=50)), + ("accessed_at", models.DateTimeField(default=None, null=True)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py index a9d41e0d..7c9c3822 100644 --- a/src/database/models/__init__.py +++ b/src/database/models/__init__.py @@ -37,6 +37,15 @@ class GoogleUser(models.Model): return self.name +class KhojApiUser(models.Model): + """User issued API tokens to authenticate Khoj clients""" + + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) + token = models.CharField(max_length=50, unique=True) + name = models.CharField(max_length=50) + accessed_at = models.DateTimeField(null=True, default=None) + + class NotionConfig(BaseModel): token = models.CharField(max_length=200) user = models.ForeignKey(KhojUser, on_delete=models.CASCADE) diff --git a/src/interface/desktop/assets/icons/favicon-20x20.png b/src/interface/desktop/assets/icons/favicon-20x20.png new file mode 100644 index 0000000000000000000000000000000000000000..1a4ee0be5107de76738ddec7fef0728012be28bd GIT binary patch literal 1660 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE-VV{wqX6T`Z5GB1IgmedH(G+$o^ zEg+kNft68+ff=L(2pJfq7+Aq<1_m!iX*fHGQ3I-miGiU#lYs@QCJIP{fCmr*bwg?9 z1&jz27cjw9i!5M9utC}!pU?dVq&N#aB8wRqxP?KOkzv*x37~0_nIRD+5xzcF$@#f@ zi7EL>sd^Q;1t47vHWgMtW^QUpqC!P(PF}H9g{=};g%ywu64qBz04piUwpEJo4N!2- zFG^J~(=*UBP_pAvP*AWbN=dT{a&d!d2l8x{GD=Dctn~HE%ggo3jrH=2()A53EiLs8 zjP#9+bb%^#i!1X=5-W7`ij^UTz|3(;Elw`VEGWs$&r<-Io0ybeT4JlD1hNPYAnq*5 zOhed|R}A$Q(1ZFQ8GS=N1AVyJK&>_)Q7iwV%v7MwAoJ}EZNMr~#Gv-r=z}araty?$ zU{Rn~?YM08;lXCdB^mdSoq>Tx$J50z#6qw%BsL=^RA655yZE}Y*)z7xPJ6F*Q*9+@ zUegWJjD>a)teQzzCndx$!s~+aMFq++a}&n z$=$r{wcF$!yU)J+Ygs&J=j_{JR~mPwpZil&e(sEA@`3-8JA7yQJgbO~z8$eM#?IYJ z^7a30)4X6Khdt?$&xkF}MwK`w^is#-g311f<-afxp z@!~X|sUo70Q7g;${k&#<@It`>1f->&&?o9iRpEP3hIZ`n`C-PtUy zwkmtM$-RoB$M=74KiZo7{+=&B`<~FG+i$EVrg6S(!61hc9e398&A4;;)nLvhtFJz5Db1&u6#RmpZFe= z&_M1Gjv&8-cW-~?IB-CQMLVTIHEFA)_|aKu!I$GMSm_Jw=WTl6Zsa7QHcyk+svjRL{_-*M4I_XzsdO9^;E$i9jDUWO$D%`l1o{D;{-8kiHn`A=nT*i-@ zOmCRv1lc6hjZKcd$vPof-K6l!t3hlWk*j9~%*YO0I_c;?_5GIm-R|F(rrqDSM=Iyo zW;>=w)hAAMXoW7hC9!m2;{&H%u?O6j&)PC2MA5l9);!_;hL(l7uf)B!@7WP|-^)=Z zI?Ls9^DPmXtaBGP#Ko7~-Iv{*7xDkyp5VDQG4m^*CHn8)#?Z_#U3}J&*YTab)9t6u z{N4WXpp$4C*Xh&!Gnd`;?0;yHn8=b5c}-8zW&iu%^51st71sC=9^F+ge(lJMd2>6I zGbgOQ(sa{x;XWO%$b)j2GF#qjpM%JbanfD7886S?w@QTm{6`|g?t_}7R>V3@n&n&njGEZ*N^5yNvul{C97GAN8 z+xbVyZzl)&R+nD;$w@VtnVIwccJ5TH*;sl-Qmo@}jotb3?sZGwEoPX#MXvG1D*jcE zUS4nxU=f?7>t6RUI*q4J_o8O9!!2eXe}8u`E(x;?j!UluE(-{?O}I40DXPuHM^p8G zg#lMq$_`DlzRF@}Cl-(5cRI2y>zR5(*LKvaSI*lsd*N4ZmpMyr)gCK*bnLf!<4gIU WlFRLj_)onD6~UgaelF{r5}E*ujgQg* literal 0 HcmV?d00001 diff --git a/src/interface/desktop/assets/icons/key.svg b/src/interface/desktop/assets/icons/key.svg new file mode 100644 index 00000000..437688fb --- /dev/null +++ b/src/interface/desktop/assets/icons/key.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/interface/desktop/assets/icons/link.svg b/src/interface/desktop/assets/icons/link.svg index ef484368..43852d95 100644 --- a/src/interface/desktop/assets/icons/link.svg +++ b/src/interface/desktop/assets/icons/link.svg @@ -1,5 +1,4 @@ - + - - + diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 21a1a416..9ae3b365 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -89,6 +89,8 @@ // Generate backend API URL to execute query let url = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true`; + const khojToken = await window.tokenAPI.getToken(); + const headers = { 'Authorization': `Bearer ${khojToken}` }; let chat_body = document.getElementById("chat-body"); let new_response = document.createElement("div"); @@ -113,7 +115,7 @@ chatInput.classList.remove("option-enabled"); // Call specified Khoj API which returns a streamed response of type text/plain - fetch(url) + fetch(url, { headers }) .then(response => { const reader = response.body.getReader(); const decoder = new TextDecoder(); @@ -217,7 +219,10 @@ async function loadChat() { const hostURL = await window.hostURLAPI.getURL(); - fetch(`${hostURL}/api/chat/history?client=web`) + const khojToken = await window.tokenAPI.getToken(); + const headers = { 'Authorization': `Bearer ${khojToken}` }; + + fetch(`${hostURL}/api/chat/history?client=web`, { headers }) .then(response => response.json()) .then(data => { if (data.detail) { @@ -243,7 +248,7 @@ return; }); - fetch(`${hostURL}/api/chat/options`) + fetch(`${hostURL}/api/chat/options`, { headers }) .then(response => response.json()) .then(data => { // Render chat options, if any @@ -272,9 +277,9 @@
diff --git a/src/interface/desktop/config.html b/src/interface/desktop/config.html index 04599bb1..4b79f1a1 100644 --- a/src/interface/desktop/config.html +++ b/src/interface/desktop/config.html @@ -12,66 +12,85 @@ -
- -
- - -
+ +
-
-
- File -

- Host -

+
+
+
+ Khoj Server URL +

+ Server URL +

+
+
+ +
+
+ Khoj Access Key +

+ Access Key +

+
+
+ +
-
- -
-
- File -

- Files -

+
+
+
+ File +

+ Files + +

+
+
+
+
+
+ - +
-
-
-
-
- -
-
- Folder -

- Folders -

+
+
+
+ Folder +

+ Folders + +

+
+
+
+
+
+ - -
-
-
-
-
- +
+
+
@@ -79,11 +98,10 @@
- -
-
-
+
+ +
+
@@ -93,7 +111,7 @@ body { display: grid; grid-template-columns: 1fr; - grid-template-rows: 1fr auto auto auto minmax(80px, 100%); + grid-template-rows: 1fr auto; font-size: small!important; } body > * { @@ -104,8 +122,7 @@ body { display: grid; grid-template-columns: 1fr min(70vw, 100%) 1fr; - grid-template-rows: 1fr auto auto auto minmax(80px, 100%); - padding-top: 60vw; + grid-template-rows: 80px auto; } body > * { grid-column: 2; @@ -126,11 +143,6 @@ margin: 10px; } - div.page { - padding: 0px; - margin: 0px; - } - svg { transition: transform 0.3s ease-in-out; } @@ -167,18 +179,18 @@ } } - #khoj-host-url { + .card-input { padding: 4px; box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2); border: none; + width: 450px; } .card { display: grid; - /* grid-template-rows: repeat(3, 1fr); */ gap: 8px; padding: 24px 16px; - width: 100%; + width: 450px; background: white; border: 1px solid rgb(229, 229, 229); border-radius: 4px; @@ -188,15 +200,15 @@ .section-cards { display: grid; - grid-template-columns: repeat(1, 1fr); gap: 16px; - justify-items: start; + justify-items: center; margin: 0; - width: auto; } - - div.configuration { - width: auto; + .section-action-row { + display: grid; + grid-auto-flow: column; + gap: 16px; + height: fit-content; } .card-title-row { @@ -302,7 +314,6 @@ } div.content-name { - width: 500px; overflow-wrap: break-word; } @@ -347,6 +358,12 @@ background-color: #ffcc00; box-shadow: 0px 3px 0px #f9f5de; } + .sync-force-toggle { + align-content: center; + display: grid; + grid-auto-flow: column; + gap: 4px; + } {% endblock %} diff --git a/src/khoj/main.py b/src/khoj/main.py index 804e71e5..8fe40e76 100644 --- a/src/khoj/main.py +++ b/src/khoj/main.py @@ -98,9 +98,10 @@ def run(): # Mount Django and Static Files app.mount("/django", django_app, name="django") - if not os.path.exists("static"): - os.mkdir("static") - app.mount("/static", StaticFiles(directory="static"), name="static") + static_dir = "static" + if not os.path.exists(static_dir): + os.mkdir(static_dir) + app.mount(f"/{static_dir}", StaticFiles(directory=static_dir), name=static_dir) # Configure Middleware configure_middleware(app) diff --git a/src/khoj/processor/conversation/gpt4all/utils.py b/src/khoj/processor/conversation/gpt4all/utils.py index d5201780..cd9bc9e2 100644 --- a/src/khoj/processor/conversation/gpt4all/utils.py +++ b/src/khoj/processor/conversation/gpt4all/utils.py @@ -6,17 +6,23 @@ logger = logging.getLogger(__name__) def download_model(model_name: str): try: - from gpt4all import GPT4All + import gpt4all except ModuleNotFoundError as e: logger.info("There was an error importing GPT4All. Please run pip install gpt4all in order to install it.") raise e - # Use GPU for Chat Model, if available - try: - model = GPT4All(model_name=model_name, device="gpu") - logger.debug("Loaded chat model to GPU.") - except ValueError: - model = GPT4All(model_name=model_name) - logger.debug("Loaded chat model to CPU.") + # Download the chat model + chat_model_config = gpt4all.GPT4All.retrieve_model(model_name=model_name, allow_download=True) - return model + # Decide whether to load model to GPU or CPU + try: + # Check if machine has GPU and GPU has enough free memory to load the chat model + device = "gpu" if gpt4all.pyllmodel.LLModel().list_gpu(chat_model_config["path"]) else "cpu" + except ValueError: + device = "cpu" + + # Now load the downloaded chat model onto appropriate device + chat_model = gpt4all.GPT4All(model_name=model_name, device=device, allow_download=False) + logger.debug(f"Loaded chat model to {device.upper()}.") + + return chat_model diff --git a/src/khoj/processor/embeddings.py b/src/khoj/processor/embeddings.py index f0e2df77..fbcddb67 100644 --- a/src/khoj/processor/embeddings.py +++ b/src/khoj/processor/embeddings.py @@ -1,28 +1,17 @@ from typing import List -import torch from langchain.embeddings import HuggingFaceEmbeddings from sentence_transformers import CrossEncoder +from khoj.utils.helpers import get_device from khoj.utils.rawconfig import SearchResponse class EmbeddingsModel: def __init__(self): - self.model_name = "sentence-transformers/multi-qa-MiniLM-L6-cos-v1" - encode_kwargs = {"normalize_embeddings": True} - # encode_kwargs = {} - - if torch.cuda.is_available(): - # Use CUDA GPU - device = torch.device("cuda:0") - elif torch.backends.mps.is_available(): - # Use Apple M1 Metal Acceleration - device = torch.device("mps") - else: - device = torch.device("cpu") - - model_kwargs = {"device": device} + self.model_name = "thenlper/gte-small" + encode_kwargs = {"normalize_embeddings": True, "show_progress_bar": True} + model_kwargs = {"device": get_device()} self.embeddings_model = HuggingFaceEmbeddings( model_name=self.model_name, encode_kwargs=encode_kwargs, model_kwargs=model_kwargs ) @@ -37,19 +26,7 @@ class EmbeddingsModel: class CrossEncoderModel: def __init__(self): self.model_name = "cross-encoder/ms-marco-MiniLM-L-6-v2" - - if torch.cuda.is_available(): - # Use CUDA GPU - device = torch.device("cuda:0") - - elif torch.backends.mps.is_available(): - # Use Apple M1 Metal Acceleration - device = torch.device("mps") - - else: - device = torch.device("cpu") - - self.cross_encoder_model = CrossEncoder(model_name=self.model_name, device=device) + self.cross_encoder_model = CrossEncoder(model_name=self.model_name, device=get_device()) def predict(self, query, hits: List[SearchResponse]): cross__inp = [[query, hit.additional["compiled"]] for hit in hits] diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index b7ba66b6..4984ea4c 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -126,21 +126,21 @@ if not state.demo: state.config.search_type = SearchConfig.parse_obj(constants.default_config["search-type"]) @api.get("/config/data", response_model=FullConfig) - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) def get_config_data(request: Request): - user = request.user.object if request.user.is_authenticated else None - enabled_content = EmbeddingsAdapters.get_unique_file_types(user) + user = request.user.object + EmbeddingsAdapters.get_unique_file_types(user) return state.config @api.post("/config/data") - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) async def set_config_data( request: Request, updated_config: FullConfig, client: Optional[str] = None, ): - user = request.user.object if request.user.is_authenticated else None + user = request.user.object await map_config_to_db(updated_config, user) configuration_update_metadata = {} @@ -167,7 +167,7 @@ if not state.demo: return state.config @api.post("/config/data/content_type/github", status_code=200) - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) async def set_content_config_github_data( request: Request, updated_config: Union[GithubContentConfig, None], @@ -175,7 +175,7 @@ if not state.demo: ): _initialize_config() - user = request.user.object if request.user.is_authenticated else None + user = request.user.object await adapters.set_user_github_config( user=user, @@ -194,7 +194,7 @@ if not state.demo: return {"status": "ok"} @api.post("/config/data/content_type/notion", status_code=200) - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) async def set_content_config_notion_data( request: Request, updated_config: Union[NotionContentConfig, None], @@ -202,7 +202,7 @@ if not state.demo: ): _initialize_config() - user = request.user.object if request.user.is_authenticated else None + user = request.user.object await adapters.set_notion_config( user=user, @@ -220,13 +220,13 @@ if not state.demo: return {"status": "ok"} @api.post("/delete/config/data/content_type/{content_type}", status_code=200) - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) async def remove_content_config_data( request: Request, content_type: str, client: Optional[str] = None, ): - user = request.user.object if request.user.is_authenticated else None + user = request.user.object update_telemetry_state( request=request, @@ -247,7 +247,7 @@ if not state.demo: return {"status": "ok"} @api.post("/delete/config/data/processor/conversation/openai", status_code=200) - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) async def remove_processor_conversation_config_data( request: Request, client: Optional[str] = None, @@ -267,7 +267,7 @@ if not state.demo: return {"status": "ok"} @api.post("/config/data/content_type/{content_type}", status_code=200) - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) async def set_content_config_data( request: Request, content_type: str, @@ -276,7 +276,7 @@ if not state.demo: ): _initialize_config() - user = request.user.object if request.user.is_authenticated else None + user = request.user.object content_object = map_config_to_object(content_type) await adapters.set_text_content_config(user, content_object, updated_config) @@ -292,7 +292,7 @@ if not state.demo: return {"status": "ok"} @api.post("/config/data/processor/conversation/openai", status_code=200) - @requires(["authenticated"], redirect="login_page") + @requires(["authenticated"]) async def set_processor_openai_config_data( request: Request, updated_config: Union[OpenAIProcessorConfig, None], @@ -315,6 +315,7 @@ if not state.demo: return {"status": "ok"} @api.post("/config/data/processor/conversation/offline_chat", status_code=200) + @requires(["authenticated"]) async def set_processor_enable_offline_chat_config_data( request: Request, enable_offline_chat: bool, @@ -323,24 +324,29 @@ if not state.demo: ): user = request.user.object - if enable_offline_chat: - conversation_config = ConversationProcessorConfig( - offline_chat=OfflineChatProcessorConfig( - enable_offline_chat=enable_offline_chat, - chat_model=offline_chat_model, + try: + if enable_offline_chat: + conversation_config = ConversationProcessorConfig( + offline_chat=OfflineChatProcessorConfig( + enable_offline_chat=enable_offline_chat, + chat_model=offline_chat_model, + ) ) - ) - await sync_to_async(ConversationAdapters.set_conversation_processor_config)(user, conversation_config) + await sync_to_async(ConversationAdapters.set_conversation_processor_config)(user, conversation_config) - offline_chat = await ConversationAdapters.get_offline_chat(user) - chat_model = offline_chat.chat_model - if state.gpt4all_processor_config is None: - state.gpt4all_processor_config = GPT4AllProcessorModel(chat_model=chat_model) + offline_chat = await ConversationAdapters.get_offline_chat(user) + chat_model = offline_chat.chat_model + if state.gpt4all_processor_config is None: + state.gpt4all_processor_config = GPT4AllProcessorModel(chat_model=chat_model) - else: - await sync_to_async(ConversationAdapters.clear_offline_chat_conversation_config)(user) - state.gpt4all_processor_config = None + else: + await sync_to_async(ConversationAdapters.clear_offline_chat_conversation_config)(user) + state.gpt4all_processor_config = None + + except Exception as e: + logger.error(f"Error updating offline chat config: {e}", exc_info=True) + return {"status": "error", "message": str(e)} update_telemetry_state( request=request, @@ -360,11 +366,11 @@ def get_default_config_data(): @api.get("/config/types", response_model=List[str]) -@requires(["authenticated"], redirect="login_page") +@requires(["authenticated"]) def get_config_types( request: Request, ): - user = request.user.object if request.user.is_authenticated else None + user = request.user.object enabled_file_types = EmbeddingsAdapters.get_unique_file_types(user) @@ -382,7 +388,7 @@ def get_config_types( @api.get("/search", response_model=List[SearchResponse]) -@requires(["authenticated"], redirect="login_page") +@requires(["authenticated"]) async def search( q: str, request: Request, @@ -396,7 +402,7 @@ async def search( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ): - user = request.user.object if request.user.is_authenticated else None + user = request.user.object start_time = time.time() # Run validation checks @@ -513,7 +519,7 @@ async def search( @api.get("/update") -@requires(["authenticated"], redirect="login_page") +@requires(["authenticated"]) def update( request: Request, t: Optional[SearchType] = None, @@ -523,7 +529,7 @@ def update( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ): - user = request.user.object if request.user.is_authenticated else None + 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}." logger.warning(error_msg) @@ -557,7 +563,7 @@ def update( @api.get("/chat/history") -@requires(["authenticated"], redirect="login_page") +@requires(["authenticated"]) def chat_history( request: Request, client: Optional[str] = None, @@ -585,7 +591,7 @@ def chat_history( @api.get("/chat/options", response_class=Response) -@requires(["authenticated"], redirect="login_page") +@requires(["authenticated"]) async def chat_options( request: Request, client: Optional[str] = None, @@ -610,7 +616,7 @@ async def chat_options( @api.get("/chat", response_class=Response) -@requires(["authenticated"], redirect="login_page") +@requires(["authenticated"]) async def chat( request: Request, q: str, diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py index 8c767d8f..ab1964b8 100644 --- a/src/khoj/routers/auth.py +++ b/src/khoj/routers/auth.py @@ -1,22 +1,29 @@ +# Standard Packages import logging -import json import os +from typing import Optional + +# External Packages from fastapi import APIRouter from starlette.config import Config from starlette.requests import Request from starlette.responses import HTMLResponse, RedirectResponse, Response +from starlette.authentication import requires from authlib.integrations.starlette_client import OAuth, OAuthError from google.oauth2 import id_token from google.auth.transport import requests as google_requests -from database.adapters import get_or_create_user +# Internal Packages +from database.adapters import get_khoj_tokens, get_or_create_user, create_khoj_token, delete_khoj_token +from khoj.utils import state + logger = logging.getLogger(__name__) auth_router = APIRouter() -if not os.environ.get("GOOGLE_CLIENT_ID") or not os.environ.get("GOOGLE_CLIENT_SECRET"): +if not state.anonymous_mode and not (os.environ.get("GOOGLE_CLIENT_ID") and os.environ.get("GOOGLE_CLIENT_SECRET")): logger.info("Please set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables to use Google OAuth") else: config = Config(environ=os.environ) @@ -39,6 +46,31 @@ async def login(request: Request): return await oauth.google.authorize_redirect(request, redirect_uri) +@auth_router.post("/token") +@requires(["authenticated"], redirect="login_page") +async def generate_token(request: Request, token_name: Optional[str] = None) -> str: + "Generate API token for given user" + if token_name: + return await create_khoj_token(user=request.user.object, name=token_name) + else: + return await create_khoj_token(user=request.user.object) + + +@auth_router.get("/token") +@requires(["authenticated"], redirect="login_page") +def get_tokens(request: Request): + "Get API tokens enabled for given user" + tokens = get_khoj_tokens(user=request.user.object) + return tokens + + +@auth_router.delete("/token") +@requires(["authenticated"], redirect="login_page") +async def delete_token(request: Request, token: str) -> str: + "Delete API token for given user" + return await delete_khoj_token(user=request.user.object, token=token) + + @auth_router.post("/redirect") async def auth(request: Request): form = await request.form() diff --git a/src/khoj/routers/indexer.py b/src/khoj/routers/indexer.py index 1125e653..e7df65a2 100644 --- a/src/khoj/routers/indexer.py +++ b/src/khoj/routers/indexer.py @@ -4,9 +4,9 @@ from typing import Optional, Union, Dict import asyncio # External Packages -from fastapi import APIRouter, HTTPException, Header, Request, Response, UploadFile +from fastapi import APIRouter, Header, Request, Response, UploadFile from pydantic import BaseModel -from khoj.routers.helpers import update_telemetry_state +from starlette.authentication import requires # Internal Packages from khoj.utils import state, constants @@ -17,6 +17,7 @@ from khoj.processor.github.github_to_jsonl import GithubToJsonl from khoj.processor.notion.notion_to_jsonl import NotionToJsonl from khoj.processor.plaintext.plaintext_to_jsonl import PlaintextToJsonl from khoj.search_type import text_search, image_search +from khoj.routers.helpers import update_telemetry_state from khoj.utils.yaml import save_config_to_file_updated_state from khoj.utils.config import SearchModels from khoj.utils.helpers import LRU, get_file_type @@ -57,10 +58,10 @@ class IndexerInput(BaseModel): @indexer.post("/update") +@requires(["authenticated"]) async def update( request: Request, files: list[UploadFile], - x_api_key: str = Header(None), force: bool = False, t: Optional[Union[state.SearchType, str]] = None, client: Optional[str] = None, @@ -68,9 +69,7 @@ async def update( referer: Optional[str] = Header(None), host: Optional[str] = Header(None), ): - user = request.user.object if request.user.is_authenticated else None - if x_api_key != "secret": - raise HTTPException(status_code=401, detail="Invalid API Key") + user = request.user.object try: logger.info(f"📬 Updating content index via API call by {client} client") org_files: Dict[str, str] = {} diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 333d89fa..06f43430 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -135,7 +135,7 @@ if not state.demo: @web_client.get("/config/content_type/github", response_class=HTMLResponse) @requires(["authenticated"], redirect="login_page") def github_config_page(request: Request): - user = request.user.object if request.user.is_authenticated else None + user = request.user.object current_github_config = get_user_github_config(user) if current_github_config: @@ -164,7 +164,7 @@ if not state.demo: @web_client.get("/config/content_type/notion", response_class=HTMLResponse) @requires(["authenticated"], redirect="login_page") def notion_config_page(request: Request): - user = request.user.object if request.user.is_authenticated else None + user = request.user.object current_notion_config = get_user_notion_config(user) current_config = NotionContentConfig( @@ -184,7 +184,7 @@ if not state.demo: return templates.TemplateResponse("config.html", context={"request": request}) object = map_config_to_object(content_type) - user = request.user.object if request.user.is_authenticated else None + user = request.user.object config = object.objects.filter(user=user).first() if config == None: config = object.objects.create(user=user) diff --git a/src/khoj/utils/config.py b/src/khoj/utils/config.py index 3c084c4f..7795d695 100644 --- a/src/khoj/utils/config.py +++ b/src/khoj/utils/config.py @@ -6,12 +6,13 @@ import logging from dataclasses import dataclass from typing import TYPE_CHECKING, List, Optional, Union, Any -from khoj.processor.conversation.gpt4all.utils import download_model # External Packages import torch -from khoj.utils.rawconfig import OfflineChatProcessorConfig +# Internal Packages +from khoj.processor.conversation.gpt4all.utils import download_model + logger = logging.getLogger(__name__) @@ -88,3 +89,4 @@ class GPT4AllProcessorModel: except ValueError as e: self.loaded_model = None logger.error(f"Error while loading offline chat model: {e}", exc_info=True) + raise e diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index 0269a9e9..f6418fbd 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -10,7 +10,7 @@ from os import path import os from pathlib import Path import platform -import sys +import random from time import perf_counter import torch from typing import Optional, Union, TYPE_CHECKING @@ -254,6 +254,18 @@ def log_telemetry( return request_body +def get_device() -> torch.device: + """Get device to run model on""" + if torch.cuda.is_available(): + # Use CUDA GPU + return torch.device("cuda:0") + elif torch.backends.mps.is_available(): + # Use Apple M1 Metal Acceleration + return torch.device("mps") + else: + return torch.device("cpu") + + class ConversationCommand(str, Enum): Default = "default" General = "general" @@ -267,3 +279,29 @@ command_descriptions = { ConversationCommand.Default: "The default command when no command specified. It intelligently auto-switches between general and notes mode.", ConversationCommand.Help: "Display a help message with all available commands and other metadata.", } + + +def generate_random_name(): + # List of adjectives and nouns to choose from + adjectives = [ + "happy", + "irritated", + "annoyed", + "calm", + "brave", + "scared", + "energetic", + "chivalrous", + "kind", + "grumpy", + ] + nouns = ["dog", "cat", "falcon", "whale", "turtle", "rabbit", "hamster", "snake", "spider", "elephant"] + + # Select two random words from the lists + adjective = random.choice(adjectives) + noun = random.choice(nouns) + + # Combine the words to form a name + name = f"{adjective} {noun}" + + return name diff --git a/src/khoj/utils/state.py b/src/khoj/utils/state.py index 40806c51..e92f19a7 100644 --- a/src/khoj/utils/state.py +++ b/src/khoj/utils/state.py @@ -1,7 +1,6 @@ # Standard Packages import threading from typing import List, Dict -from packaging import version from collections import defaultdict # External Packages @@ -11,7 +10,7 @@ from pathlib import Path # Internal Packages from khoj.utils import config as utils_config from khoj.utils.config import ContentIndex, SearchModels, GPT4AllProcessorModel -from khoj.utils.helpers import LRU +from khoj.utils.helpers import LRU, get_device from khoj.utils.rawconfig import FullConfig from khoj.processor.embeddings import EmbeddingsModel, CrossEncoderModel @@ -35,12 +34,4 @@ telemetry: List[Dict[str, str]] = [] demo: bool = False khoj_version: str = None anonymous_mode: bool = False - -if torch.cuda.is_available(): - # Use CUDA GPU - device = torch.device("cuda:0") -elif version.parse(torch.__version__) >= version.parse("1.13.0.dev") and torch.backends.mps.is_available(): - # Use Apple M1 Metal Acceleration - device = torch.device("mps") -else: - device = torch.device("cpu") +device = get_device() diff --git a/tests/conftest.py b/tests/conftest.py index 12ac4f7b..aad20274 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,7 @@ from khoj.utils import state, fs_syncer from khoj.routers.indexer import configure_content from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl from database.models import ( + KhojApiUser, LocalOrgConfig, LocalMarkdownConfig, LocalPlaintextConfig, @@ -76,13 +77,26 @@ def default_user2(): if KhojUser.objects.filter(username="default").exists(): return KhojUser.objects.get(username="default") - return UserFactory( + return KhojUser.objects.create( username="default", email="default@example.com", password="default", ) +@pytest.mark.django_db +@pytest.fixture +def api_user(default_user): + if KhojApiUser.objects.filter(user=default_user).exists(): + return KhojApiUser.objects.get(user=default_user) + + return KhojApiUser.objects.create( + user=default_user, + name="api-key", + token="kk-secret", + ) + + @pytest.fixture(scope="session") def search_models(search_config: SearchConfig): search_models = SearchModels() @@ -176,7 +190,7 @@ def chat_client(search_config: SearchConfig, default_user2: KhojUser): if os.getenv("OPENAI_API_KEY"): OpenAIProcessorConversationConfigFactory(user=default_user2) - state.anonymous_mode = True + state.anonymous_mode = False app = FastAPI() @@ -219,7 +233,7 @@ def fastapi_app(): def client( content_config: ContentConfig, search_config: SearchConfig, - default_user: KhojUser, + api_user: KhojApiUser, ): state.config.content_type = content_config state.config.search_type = search_config @@ -231,7 +245,7 @@ def client( OrgToJsonl, get_sample_data("org"), regenerate=False, - user=default_user, + user=api_user.user, ) state.content_index.image = image_search.setup( content_config.image, state.search_models.image_search, regenerate=False @@ -240,11 +254,11 @@ def client( PlaintextToJsonl, get_sample_data("plaintext"), regenerate=False, - user=default_user, + user=api_user.user, ) - ConversationProcessorConfigFactory(user=default_user) - state.anonymous_mode = True + ConversationProcessorConfigFactory(user=api_user.user) + state.anonymous_mode = False configure_routes(app) configure_middleware(app) @@ -253,13 +267,8 @@ def client( @pytest.fixture(scope="function") -def client_offline_chat( - search_config: SearchConfig, - content_config: ContentConfig, - default_user2: KhojUser, -): +def client_offline_chat(search_config: SearchConfig, default_user2: KhojUser): # Initialize app state - state.config.content_type = md_content_config state.config.search_type = search_config state.SearchType = configure_search_types(state.config) @@ -269,9 +278,6 @@ def client_offline_chat( user=default_user2, ) - # Index Markdown Content for Search - state.search_models.image_search = image_search.initialize_model(search_config.image) - all_files = fs_syncer.collect_files(user=default_user2) configure_content( state.content_index, state.config.content_type, all_files, state.search_models, user=default_user2 @@ -283,6 +289,8 @@ def client_offline_chat( state.anonymous_mode = True + app = FastAPI() + configure_routes(app) configure_middleware(app) app.mount("/static", StaticFiles(directory=web_directory), name="static") diff --git a/tests/helpers.py b/tests/helpers.py index 655c4435..2f2feddf 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -3,6 +3,7 @@ import os from database.models import ( KhojUser, + KhojApiUser, ConversationProcessorConfig, OfflineChatProcessorConversationConfig, OpenAIProcessorConversationConfig, @@ -20,6 +21,15 @@ class UserFactory(factory.django.DjangoModelFactory): uuid = factory.Faker("uuid4") +class ApiUserFactory(factory.django.DjangoModelFactory): + class Meta: + model = KhojApiUser + + user = None + name = factory.Faker("name") + token = factory.Faker("password") + + class ConversationProcessorConfigFactory(factory.django.DjangoModelFactory): class Meta: model = ConversationProcessorConfig diff --git a/tests/test_client.py b/tests/test_client.py index 1a6b1346..6818c2ba 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -22,49 +22,115 @@ from database.adapters import EmbeddingsAdapters # Test # ---------------------------------------------------------------------------------------------------- -def test_search_with_invalid_content_type(client): +@pytest.mark.django_db(transaction=True) +def test_search_with_no_auth_key(client): # Arrange user_query = quote("How to call Khoj from Emacs?") # Act - response = client.get(f"/api/search?q={user_query}&t=invalid_content_type") + response = client.get(f"/api/search?q={user_query}") + + # Assert + assert response.status_code == 403 + + +@pytest.mark.django_db(transaction=True) +def test_search_with_invalid_auth_key(client): + # Arrange + headers = {"Authorization": "Bearer invalid-token"} + user_query = quote("How to call Khoj from Emacs?") + + # Act + response = client.get(f"/api/search?q={user_query}", headers=headers) + + # Assert + assert response.status_code == 403 + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) +def test_search_with_invalid_content_type(client): + # Arrange + headers = {"Authorization": "Bearer kk-secret"} + user_query = quote("How to call Khoj from Emacs?") + + # Act + response = client.get(f"/api/search?q={user_query}&t=invalid_content_type", headers=headers) # Assert assert response.status_code == 422 # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_search_with_valid_content_type(client): - for content_type in ["all", "org", "markdown", "image", "pdf", "github", "notion"]: + headers = {"Authorization": "Bearer kk-secret"} + for content_type in ["all", "org", "markdown", "image", "pdf", "github", "notion", "plaintext"]: # Act - response = client.get(f"/api/search?q=random&t={content_type}") + response = client.get(f"/api/search?q=random&t={content_type}", headers=headers) # Assert assert response.status_code == 200, f"Returned status: {response.status_code} for content type: {content_type}" # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) +def test_index_update_with_no_auth_key(client): + # Arrange + files = get_sample_files_data() + + # Act + response = client.post("/api/v1/index/update", files=files) + + # Assert + assert response.status_code == 403 + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) +def test_index_update_with_invalid_auth_key(client): + # Arrange + files = get_sample_files_data() + headers = {"Authorization": "Bearer kk-invalid-token"} + + # Act + response = client.post("/api/v1/index/update", files=files, headers=headers) + + # Assert + assert response.status_code == 403 + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_update_with_invalid_content_type(client): + # Arrange + headers = {"Authorization": "Bearer kk-secret"} + # Act - response = client.get(f"/api/update?t=invalid_content_type") + response = client.get(f"/api/update?t=invalid_content_type", headers=headers) # Assert assert response.status_code == 422 # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_regenerate_with_invalid_content_type(client): + # Arrange + headers = {"Authorization": "Bearer kk-secret"} + # Act - response = client.get(f"/api/update?force=true&t=invalid_content_type") + response = client.get(f"/api/update?force=true&t=invalid_content_type", headers=headers) # Assert assert response.status_code == 422 # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_index_update(client): # Arrange files = get_sample_files_data() - headers = {"x-api-key": "secret"} + headers = {"Authorization": "Bearer kk-secret"} # Act response = client.post("/api/v1/index/update", files=files, headers=headers) @@ -74,29 +140,33 @@ def test_index_update(client): # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_regenerate_with_valid_content_type(client): for content_type in ["all", "org", "markdown", "image", "pdf", "notion"]: # Arrange files = get_sample_files_data() - headers = {"x-api-key": "secret"} + headers = {"Authorization": "Bearer kk-secret"} # Act response = client.post(f"/api/v1/index/update?t={content_type}", files=files, headers=headers) + # Assert assert response.status_code == 200, f"Returned status: {response.status_code} for content type: {content_type}" # ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) def test_regenerate_with_github_fails_without_pat(client): # Act - response = client.get(f"/api/update?force=true&t=github") + headers = {"Authorization": "Bearer kk-secret"} + response = client.get(f"/api/update?force=true&t=github", headers=headers) # Arrange files = get_sample_files_data() - headers = {"x-api-key": "secret"} # Act response = client.post(f"/api/v1/index/update?t=github", files=files, headers=headers) + # Assert assert response.status_code == 200, f"Returned status: {response.status_code} for content type: github" @@ -116,16 +186,17 @@ def test_get_configured_types_via_api(client, sample_org_data): # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_get_api_config_types(client, search_config: SearchConfig, sample_org_data, default_user2: KhojUser): +def test_get_api_config_types(client, sample_org_data, default_user: KhojUser): # Arrange - text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user2) + headers = {"Authorization": "Bearer kk-secret"} + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user) # Act - response = client.get(f"/api/config/types") + response = client.get(f"/api/config/types", headers=headers) # Assert assert response.status_code == 200 - assert response.json() == ["all", "org", "image"] + assert response.json() == ["all", "org", "image", "plaintext"] # ---------------------------------------------------------------------------------------------------- @@ -135,6 +206,7 @@ def test_get_configured_types_with_no_content_config(fastapi_app: FastAPI): state.SearchType = configure_search_types(config) original_config = state.config.content_type state.config.content_type = None + state.anonymous_mode = True configure_routes(fastapi_app) client = TestClient(fastapi_app) @@ -154,6 +226,7 @@ def test_get_configured_types_with_no_content_config(fastapi_app: FastAPI): @pytest.mark.django_db(transaction=True) def test_image_search(client, content_config: ContentConfig, search_config: SearchConfig): # Arrange + headers = {"Authorization": "Bearer kk-secret"} search_models.image_search = image_search.initialize_model(search_config.image) content_index.image = image_search.setup( content_config.image, search_models.image_search.image_encoder, regenerate=False @@ -166,7 +239,7 @@ def test_image_search(client, content_config: ContentConfig, search_config: Sear for query, expected_image_name in query_expected_image_pairs: # Act - response = client.get(f"/api/search?q={query}&n=1&t=image") + response = client.get(f"/api/search?q={query}&n=1&t=image", headers=headers) # Assert assert response.status_code == 200 @@ -179,13 +252,14 @@ def test_image_search(client, content_config: ContentConfig, search_config: Sear # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_notes_search(client, search_config: SearchConfig, sample_org_data, default_user2: KhojUser): +def test_notes_search(client, search_config: SearchConfig, sample_org_data, default_user: KhojUser): # Arrange - text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user2) + headers = {"Authorization": "Bearer kk-secret"} + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user) user_query = quote("How to git install application?") # Act - response = client.get(f"/api/search?q={user_query}&n=1&t=org&r=true") + response = client.get(f"/api/search?q={user_query}&n=1&t=org&r=true", headers=headers) # Assert assert response.status_code == 200 @@ -197,19 +271,20 @@ def test_notes_search(client, search_config: SearchConfig, sample_org_data, defa # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) def test_notes_search_with_only_filters( - client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data, default_user2: KhojUser + client, content_config: ContentConfig, search_config: SearchConfig, sample_org_data, default_user: KhojUser ): # Arrange + headers = {"Authorization": "Bearer kk-secret"} text_search.setup( OrgToJsonl, sample_org_data, regenerate=False, - user=default_user2, + user=default_user, ) user_query = quote('+"Emacs" file:"*.org"') # Act - response = client.get(f"/api/search?q={user_query}&n=1&t=org") + response = client.get(f"/api/search?q={user_query}&n=1&t=org", headers=headers) # Assert assert response.status_code == 200 @@ -220,13 +295,14 @@ def test_notes_search_with_only_filters( # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_notes_search_with_include_filter(client, sample_org_data, default_user2: KhojUser): +def test_notes_search_with_include_filter(client, sample_org_data, default_user: KhojUser): # Arrange - text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user2) + headers = {"Authorization": "Bearer kk-secret"} + text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user) user_query = quote('How to git install application? +"Emacs"') # Act - response = client.get(f"/api/search?q={user_query}&n=1&t=org") + response = client.get(f"/api/search?q={user_query}&n=1&t=org", headers=headers) # Assert assert response.status_code == 200 @@ -237,18 +313,19 @@ def test_notes_search_with_include_filter(client, sample_org_data, default_user2 # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db(transaction=True) -def test_notes_search_with_exclude_filter(client, sample_org_data, default_user2: KhojUser): +def test_notes_search_with_exclude_filter(client, sample_org_data, default_user: KhojUser): # Arrange + headers = {"Authorization": "Bearer kk-secret"} text_search.setup( OrgToJsonl, sample_org_data, regenerate=False, - user=default_user2, + user=default_user, ) user_query = quote('How to git install application? -"clone"') # Act - response = client.get(f"/api/search?q={user_query}&n=1&t=org") + response = client.get(f"/api/search?q={user_query}&n=1&t=org", headers=headers) # Assert assert response.status_code == 200 @@ -261,16 +338,17 @@ def test_notes_search_with_exclude_filter(client, sample_org_data, default_user2 @pytest.mark.django_db(transaction=True) def test_different_user_data_not_accessed(client, sample_org_data, default_user: KhojUser): # Arrange + headers = {"Authorization": "Bearer kk-token"} # Token for default_user2 text_search.setup(OrgToJsonl, sample_org_data, regenerate=False, user=default_user) user_query = quote("How to git install application?") # Act - response = client.get(f"/api/search?q={user_query}&n=1&t=org") + response = client.get(f"/api/search?q={user_query}&n=1&t=org", headers=headers) # Assert - assert response.status_code == 200 + assert response.status_code == 403 # assert actual response has no data as the default_user is different from the user making the query (anonymous) - assert len(response.json()) == 0 + assert len(response.json()) == 1 and response.json()["detail"] == "Forbidden" def get_sample_files_data(): From 5f3f6b7c61532bbc7456022fa2442ae16d8998e3 Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:15:31 -0700 Subject: [PATCH 006/194] [Multi-User Part 5]: Add a production Docker file and use a gunicorn configuration with it (#514) - Add a productionized setup for the Khoj server using `gunicorn` with multiple workers for handling requests - Add a new Dockerfile meant for production config at `ghcr.io/khoj-ai/khoj:prod`; the existing Docker config should remain the same --- .github/workflows/dockerize_production.yml | 48 ++++++++++++++++++++++ .gitignore | 2 +- docker-compose.yml | 4 ++ gunicorn-config.py | 10 +++++ prod.Dockerfile | 30 ++++++++++++++ pyproject.toml | 3 +- src/khoj/configure.py | 3 ++ src/khoj/main.py | 9 +++- src/khoj/routers/auth.py | 5 ++- src/khoj/utils/cli.py | 9 +++- tests/test_cli.py | 4 +- tests/test_text_search.py | 1 - 12 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/dockerize_production.yml create mode 100644 gunicorn-config.py create mode 100644 prod.Dockerfile diff --git a/.github/workflows/dockerize_production.yml b/.github/workflows/dockerize_production.yml new file mode 100644 index 00000000..97fc876d --- /dev/null +++ b/.github/workflows/dockerize_production.yml @@ -0,0 +1,48 @@ +name: dockerize-prod + +on: + pull_request: + push: + tags: + - "*" + branches: + - master + paths: + - src/khoj/** + - config/** + - pyproject.toml + - prod.Dockerfile + - .github/workflows/dockerize_production.yml + workflow_dispatch: + +env: + DOCKER_IMAGE_TAG: 'prod' + +jobs: + build: + name: Build Production Docker Image, Push to Container Registry + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.PAT }} + + - name: 📦 Build and Push Docker Image + uses: docker/build-push-action@v2 + with: + context: . + file: prod.Dockerfile + platforms: linux/amd64 + push: true + tags: ghcr.io/${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }} + build-args: | + PORT=42110 diff --git a/.gitignore b/.gitignore index e3e93428..35315263 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ todesktop.json khoj_assistant.egg-info /config/khoj*.yml .pytest_cache -khoj.log +*.log static # Obsidian plugin artifacts diff --git a/docker-compose.yml b/docker-compose.yml index d6048916..c75aa4fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,6 +39,7 @@ services: - ./tests/data/embeddings/:/root/.khoj/content/ - ./tests/data/models/:/root/.khoj/search/ - khoj_config:/root/.khoj/ + - khoj_models:/root/.cache/torch/sentence_transformers # Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/ environment: - POSTGRES_DB=postgres @@ -46,9 +47,12 @@ services: - POSTGRES_PASSWORD=postgres - POSTGRES_HOST=database - POSTGRES_PORT=5432 + - GOOGLE_CLIENT_SECRET=bar + - GOOGLE_CLIENT_ID=foo command: --host="0.0.0.0" --port=42110 -vv volumes: khoj_config: khoj_db: + khoj_models: diff --git a/gunicorn-config.py b/gunicorn-config.py new file mode 100644 index 00000000..1760ae38 --- /dev/null +++ b/gunicorn-config.py @@ -0,0 +1,10 @@ +import multiprocessing + +bind = "0.0.0.0:42110" +workers = 4 +worker_class = "uvicorn.workers.UvicornWorker" +timeout = 120 +keep_alive = 60 +accesslog = "access.log" +errorlog = "error.log" +loglevel = "debug" diff --git a/prod.Dockerfile b/prod.Dockerfile new file mode 100644 index 00000000..3cf6a600 --- /dev/null +++ b/prod.Dockerfile @@ -0,0 +1,30 @@ +# Use Nvidia's latest Ubuntu 22.04 image as the base image +FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 + +LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj + +# Install System Dependencies +RUN apt update -y && apt -y install python3-pip git + +WORKDIR /app + +# Install Application +COPY pyproject.toml . +COPY README.md . +RUN sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && \ + TMPDIR=/home/cache/ pip install --cache-dir=/home/cache/ -e . + +# Copy Source Code +COPY . . + +RUN apt install vim -y + +# Set the PYTHONPATH environment variable in order for it to find the Django app. +ENV PYTHONPATH=/app/src:$PYTHONPATH + +# Run the Application +# There are more arguments required for the application to run, +# but these should be passed in through the docker-compose.yml file. +ARG PORT +EXPOSE ${PORT} +ENTRYPOINT [ "gunicorn", "-c", "gunicorn-config.py", "src.khoj.main:app" ] diff --git a/pyproject.toml b/pyproject.toml index f9ef020c..d5b7f0ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "schedule == 1.1.0", "sentence-transformers == 2.2.2", "transformers >= 4.28.0", - "torch >= 2.0.1", + "torch == 2.0.1", "uvicorn == 0.17.6", "aiohttp == 3.8.5", "langchain >= 0.0.187", @@ -70,6 +70,7 @@ dependencies = [ "psycopg2-binary == 2.9.9", "google-auth == 2.23.3", "python-multipart == 0.0.6", + "gunicorn == 21.2.0", ] dynamic = ["version"] diff --git a/src/khoj/configure.py b/src/khoj/configure.py index 56585328..67ca3543 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -103,6 +103,9 @@ def configure_server( user: KhojUser = None, ): # Update Config + if config == None: + logger.info(f"🚨 Khoj is not configured.\nInitializing it with a default config.") + config = FullConfig() state.config = config # Initialize Search Models from Config and initialize content diff --git a/src/khoj/main.py b/src/khoj/main.py index 8fe40e76..d434c461 100644 --- a/src/khoj/main.py +++ b/src/khoj/main.py @@ -65,7 +65,7 @@ logging.basicConfig(handlers=[rich_handler]) logger = logging.getLogger("khoj") -def run(): +def run(should_start_server=True): # Turn Tokenizers Parallelism Off. App does not support it. os.environ["TOKENIZERS_PARALLELISM"] = "false" @@ -107,7 +107,10 @@ def run(): configure_middleware(app) initialize_server(args.config) - start_server(app, host=args.host, port=args.port, socket=args.socket) + + # If the server is started through gunicorn (external to the script), don't start the server + if should_start_server: + start_server(app, host=args.host, port=args.port, socket=args.socket) def set_state(args): @@ -139,3 +142,5 @@ def poll_task_scheduler(): if __name__ == "__main__": run() +else: + run(should_start_server=False) diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py index ab1964b8..5c375bd0 100644 --- a/src/khoj/routers/auth.py +++ b/src/khoj/routers/auth.py @@ -36,16 +36,17 @@ else: @auth_router.get("/login") async def login_get(request: Request): - redirect_uri = request.url_for("auth") + redirect_uri = str(request.app.url_path_for("auth")) return await oauth.google.authorize_redirect(request, redirect_uri) @auth_router.post("/login") async def login(request: Request): - redirect_uri = request.url_for("auth") + redirect_uri = str(request.app.url_path_for("auth")) return await oauth.google.authorize_redirect(request, redirect_uri) +@auth_router.post("/redirect") @auth_router.post("/token") @requires(["authenticated"], redirect="login_page") async def generate_token(request: Request, token_name: Optional[str] = None) -> str: diff --git a/src/khoj/utils/cli.py b/src/khoj/utils/cli.py index c72320a1..5090e399 100644 --- a/src/khoj/utils/cli.py +++ b/src/khoj/utils/cli.py @@ -3,6 +3,9 @@ import argparse import pathlib from importlib.metadata import version import os +import logging + +logger = logging.getLogger(__name__) # Internal Packages from khoj.utils.helpers import resolve_absolute_path @@ -17,7 +20,7 @@ def cli(args=None): # Setup Argument Parser for the Commandline Interface parser = argparse.ArgumentParser(description="Start Khoj; An AI personal assistant for your Digital Brain") parser.add_argument( - "--config-file", "-c", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj" + "--config-file", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj" ) parser.add_argument( "--regenerate", @@ -42,7 +45,9 @@ def cli(args=None): help="Run Khoj in anonymous mode. This does not require any login for connecting users.", ) - args = parser.parse_args(args) + args, remaining_args = parser.parse_known_args(args) + + logger.debug(f"Ignoring unknown commandline args: {remaining_args}") args.version_no = version("khoj-assistant") if args.version: diff --git a/tests/test_cli.py b/tests/test_cli.py index cff2a7f3..e3daa2c6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -25,7 +25,7 @@ def test_cli_invalid_config_file_path(): non_existent_config_file = f"non-existent-khoj-{random()}.yml" # Act - actual_args = cli([f"-c={non_existent_config_file}"]) + actual_args = cli([f"--config-file={non_existent_config_file}"]) # Assert assert actual_args.config_file == resolve_absolute_path(non_existent_config_file) @@ -35,7 +35,7 @@ def test_cli_invalid_config_file_path(): # ---------------------------------------------------------------------------------------------------- def test_cli_config_from_file(): # Act - actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "-vvv"]) + actual_args = cli(["--config-file=tests/data/config.yml", "--regenerate", "-vvv"]) # Assert assert actual_args.config_file == resolve_absolute_path(Path("tests/data/config.yml")) diff --git a/tests/test_text_search.py b/tests/test_text_search.py index ec8034ef..aeeaa85f 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -17,7 +17,6 @@ from khoj.utils.fs_syncer import collect_files, get_org_files from database.models import LocalOrgConfig, KhojUser, Embeddings, GithubConfig logger = logging.getLogger(__name__) -from khoj.utils.rawconfig import ContentConfig, SearchConfig # Test From 54a387326cef113f407e7678dec5be30cc2bb4bf Mon Sep 17 00:00:00 2001 From: sabaimran <65192171+sabaimran@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:59:53 -0700 Subject: [PATCH 007/194] [Multi-User Part 6]: Address small bugs and upstream PR comments (#518) - 08654163cb227edb8991ad7f77c99b560819f4f9: Add better parsing for XML files - f3acfac7fbec0a3876e7586607c376c9dfac6a4d: Add a try/catch around the dateparser in order to avoid internal server errors in app - 7d43cd62c0d51889978413ca411cec1bd37b024a: Chunk embeddings generation in order to avoid large memory load - e02d751eb3cb9a005f1b529fde3b34ce3c026f1b: Addresses comments from PR #498 - a3f393edb49842bedb4f42fba2dfc831ee51db7f: Addresses comments from PR #503 - 66eb0782867b201a878c7fb13ba662be1258037c: Addresses comments from PR #511 - Address various items in https://github.com/khoj-ai/khoj/issues/527 --- pyproject.toml | 1 + src/database/adapters/__init__.py | 24 +++--- src/interface/desktop/config.html | 2 +- src/interface/desktop/index.html | 11 +++ src/interface/desktop/main.js | 6 ++ src/interface/desktop/renderer.js | 1 + src/khoj/configure.py | 9 +-- .../interface/web/assets/icons/copy-solid.svg | 1 + .../web/assets/icons/trash-solid.svg | 1 + src/khoj/interface/web/base_config.html | 22 +++++- src/khoj/interface/web/config.html | 68 +++++++++++++--- src/khoj/interface/web/index.html | 11 +++ src/khoj/interface/web/login.html | 2 +- .../processor/plaintext/plaintext_to_jsonl.py | 28 ++++++- src/khoj/processor/text_to_jsonl.py | 78 ++++++++++--------- src/khoj/routers/api.py | 2 +- src/khoj/routers/auth.py | 1 - src/khoj/routers/helpers.py | 2 +- src/khoj/routers/indexer.py | 7 +- src/khoj/search_filter/date_filter.py | 20 +++-- src/khoj/utils/helpers.py | 11 +++ tests/test_text_search.py | 70 +++++++++++------ 22 files changed, 264 insertions(+), 114 deletions(-) create mode 100644 src/khoj/interface/web/assets/icons/copy-solid.svg create mode 100644 src/khoj/interface/web/assets/icons/trash-solid.svg diff --git a/pyproject.toml b/pyproject.toml index d5b7f0ce..f4ae57f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,6 +71,7 @@ dependencies = [ "google-auth == 2.23.3", "python-multipart == 0.0.6", "gunicorn == 21.2.0", + "lxml == 4.9.3", ] dynamic = ["version"] diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index 52debdc4..362398d8 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -1,6 +1,9 @@ import secrets from typing import Type, TypeVar, List from datetime import date +import secrets +from typing import Type, TypeVar, List +from datetime import date from django.db import models from django.contrib.sessions.backends.db import SessionStore @@ -8,6 +11,10 @@ from pgvector.django import CosineDistance from django.db.models.manager import BaseManager from django.db.models import Q from torch import Tensor +from pgvector.django import CosineDistance +from django.db.models.manager import BaseManager +from django.db.models import Q +from torch import Tensor # Import sync_to_async from Django Channels from asgiref.sync import sync_to_async @@ -58,7 +65,7 @@ async def set_notion_config(token: str, user: KhojUser): async def create_khoj_token(user: KhojUser, name=None): "Create Khoj API key for user" token = f"kk-{secrets.token_urlsafe(32)}" - name = name or f"{generate_random_name().title()}'s Secret Key" + name = name or f"{generate_random_name().title()}" api_config = await KhojApiUser.objects.acreate(token=token, user=user, name=name) await api_config.asave() return api_config @@ -123,15 +130,11 @@ def get_all_users() -> BaseManager[KhojUser]: def get_user_github_config(user: KhojUser): config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first() - if not config: - return None return config def get_user_notion_config(user: KhojUser): config = NotionConfig.objects.filter(user=user).first() - if not config: - return None return config @@ -240,13 +243,10 @@ class ConversationAdapters: @staticmethod def get_enabled_conversation_settings(user: KhojUser): openai_config = ConversationAdapters.get_openai_conversation_config(user) - offline_chat_config = ConversationAdapters.get_offline_chat_conversation_config(user) return { "openai": True if openai_config is not None else False, - "offline_chat": True - if (offline_chat_config is not None and offline_chat_config.enable_offline_chat) - else False, + "offline_chat": ConversationAdapters.has_offline_chat(user), } @staticmethod @@ -264,7 +264,11 @@ class ConversationAdapters: OfflineChatProcessorConversationConfig.objects.filter(user=user).delete() @staticmethod - async def has_offline_chat(user: KhojUser): + def has_offline_chat(user: KhojUser): + return OfflineChatProcessorConversationConfig.objects.filter(user=user, enable_offline_chat=True).exists() + + @staticmethod + async def ahas_offline_chat(user: KhojUser): return await OfflineChatProcessorConversationConfig.objects.filter( user=user, enable_offline_chat=True ).aexists() diff --git a/src/interface/desktop/config.html b/src/interface/desktop/config.html index 4b79f1a1..b781af26 100644 --- a/src/interface/desktop/config.html +++ b/src/interface/desktop/config.html @@ -2,7 +2,7 @@ - Khoj - Search + Khoj - Settings diff --git a/src/interface/desktop/index.html b/src/interface/desktop/index.html index 283f2477..ce930cec 100644 --- a/src/interface/desktop/index.html +++ b/src/interface/desktop/index.html @@ -94,6 +94,15 @@ }).join("\n"); } + function render_xml(query, data) { + return data.map(function (item) { + return `
` + + `${item.additional.heading}` + + `${item.entry}` + + `
` + }).join("\n"); + } + function render_multiple(query, data, type) { let html = ""; data.forEach(item => { @@ -113,6 +122,8 @@ html += `
` + `${item.additional.heading}` + `

${item.entry}

` + `
`; } else if (item.additional.file.endsWith(".html")) { html += render_html(query, [item]); + } else if (item.additional.file.endsWith(".xml")) { + html += render_xml(query, [item]) } else { html += `
` + `${item.additional.heading}` + `

${item.entry}

` + `
`; } diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index 7c0559c3..d38a9e9b 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -267,6 +267,12 @@ async function getFolders () { } async function setURL (event, url) { + // Sanitize the URL. Remove trailing slash if present. Add http:// if not present. + url = url.replace(/\/$/, ""); + if (!url.match(/^[a-zA-Z]+:\/\//)) { + url = `http://${url}`; + } + store.set('hostURL', url); return store.get('hostURL'); } diff --git a/src/interface/desktop/renderer.js b/src/interface/desktop/renderer.js index 5586758c..b365ceff 100644 --- a/src/interface/desktop/renderer.js +++ b/src/interface/desktop/renderer.js @@ -174,6 +174,7 @@ urlInput.addEventListener('blur', async () => { new URL(urlInputValue); } catch (e) { console.log(e); + alert('Please enter a valid URL'); return; } diff --git a/src/khoj/configure.py b/src/khoj/configure.py index 67ca3543..5dec86e7 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -25,7 +25,6 @@ from khoj.utils import constants, state from khoj.utils.config import ( SearchType, ) -from khoj.utils.helpers import merge_dicts from khoj.utils.fs_syncer import collect_files from khoj.utils.rawconfig import FullConfig from khoj.routers.indexer import configure_content, load_content, configure_search @@ -83,12 +82,6 @@ class UserAuthenticationBackend(AuthenticationBackend): def initialize_server(config: Optional[FullConfig]): - if config is None: - logger.warning( - f"🚨 Khoj is not configured.\nConfigure it via http://{state.host}:{state.port}/config, plugins or by editing {state.config_file}." - ) - return None - try: configure_server(config, init=True) except Exception as e: @@ -190,7 +183,7 @@ def configure_search_types(config: FullConfig): core_search_types = {e.name: e.value for e in SearchType} # Dynamically generate search type enum by merging core search types with configured plugin search types - return Enum("SearchType", merge_dicts(core_search_types, {})) + return Enum("SearchType", core_search_types) @schedule.repeat(schedule.every(59).minutes) diff --git a/src/khoj/interface/web/assets/icons/copy-solid.svg b/src/khoj/interface/web/assets/icons/copy-solid.svg new file mode 100644 index 00000000..da7020be --- /dev/null +++ b/src/khoj/interface/web/assets/icons/copy-solid.svg @@ -0,0 +1 @@ + diff --git a/src/khoj/interface/web/assets/icons/trash-solid.svg b/src/khoj/interface/web/assets/icons/trash-solid.svg new file mode 100644 index 00000000..768d80f8 --- /dev/null +++ b/src/khoj/interface/web/assets/icons/trash-solid.svg @@ -0,0 +1 @@ + diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index 15c3f678..db77787b 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -52,10 +52,24 @@ justify-self: center; } - - div.section.general-settings { - justify-self: center; - } + .api-settings { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr 1fr auto; + justify-items: start; + gap: 8px; + padding: 24px 24px; + 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); + } + #api-settings-card-description { + margin: 8px 0 0 0; + } + #api-settings-keys-table { + margin-bottom: 16px; + } div.instructions { font-size: large; diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index 3f504efa..c65615e1 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -287,11 +287,33 @@
-
-
- -
+
+
+
+ API Key +

API Keys

+
+
+

Manage access to your Khoj from client apps

+
+ + + + + + + + + +
NameKeyActions
+
+ +
+
+
@@ -520,14 +542,32 @@ .then(response => response.json()) .then(tokenObj => { apiKeyList.innerHTML += ` -
- ${tokenObj.token} - -
+ + ${tokenObj.name} + ${tokenObj.token} + + Copy API Key + Delete API Key + + `; }); } + function copyAPIKey(token) { + // Copy API key to clipboard + navigator.clipboard.writeText(token); + // Flash the API key copied message + const copyApiKeyButton = document.getElementById(`api-key-${token}`); + original_html = copyApiKeyButton.innerHTML + setTimeout(function() { + copyApiKeyButton.innerHTML = "✅ Copied to your clipboard!"; + setTimeout(function() { + copyApiKeyButton.innerHTML = original_html; + }, 1000); + }, 100); + } + function deleteAPIKey(token) { const apiKeyList = document.getElementById("api-key-list"); fetch(`/auth/token?token=${token}`, { @@ -548,10 +588,14 @@ .then(tokens => { apiKeyList.innerHTML = tokens.map(tokenObj => ` -
- ${tokenObj.token} - -
+ + ${tokenObj.name} + ${tokenObj.token} + + Copy API Key + Delete API Key + + `) .join(""); }); diff --git a/src/khoj/interface/web/index.html b/src/khoj/interface/web/index.html index ccf1ca71..539c96e0 100644 --- a/src/khoj/interface/web/index.html +++ b/src/khoj/interface/web/index.html @@ -94,6 +94,15 @@ }).join("\n"); } + function render_xml(query, data) { + return data.map(function (item) { + return `
` + + `${item.additional.heading}` + + `${item.entry}` + + `
` + }).join("\n"); + } + function render_multiple(query, data, type) { let html = ""; data.forEach(item => { @@ -113,6 +122,8 @@ html += `
` + `${item.additional.heading}` + `

${item.entry}

` + `
`; } else if (item.additional.file.endsWith(".html")) { html += render_html(query, [item]); + } else if (item.additional.file.endsWith(".xml")) { + html += render_xml(query, [item]) } else { html += `
` + `${item.additional.heading}` + `

${item.entry}

` + `
`; } diff --git a/src/khoj/interface/web/login.html b/src/khoj/interface/web/login.html index 550991ed..1bab4221 100644 --- a/src/khoj/interface/web/login.html +++ b/src/khoj/interface/web/login.html @@ -2,7 +2,7 @@ - Khoj - Search + Khoj - Login diff --git a/src/khoj/processor/plaintext/plaintext_to_jsonl.py b/src/khoj/processor/plaintext/plaintext_to_jsonl.py index 965a5a7b..086808b7 100644 --- a/src/khoj/processor/plaintext/plaintext_to_jsonl.py +++ b/src/khoj/processor/plaintext/plaintext_to_jsonl.py @@ -2,12 +2,14 @@ import logging from pathlib import Path from typing import List, Tuple +from bs4 import BeautifulSoup + # Internal Packages from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer -from khoj.utils.rawconfig import Entry, TextContentConfig -from database.models import Embeddings, KhojUser, LocalPlaintextConfig +from khoj.utils.rawconfig import Entry +from database.models import Embeddings, KhojUser logger = logging.getLogger(__name__) @@ -28,6 +30,19 @@ class PlaintextToJsonl(TextEmbeddings): else: deletion_file_names = None + with timer("Scrub plaintext files and extract text", logger): + for file in files: + try: + plaintext_content = files[file] + if file.endswith(("html", "htm", "xml")): + plaintext_content = PlaintextToJsonl.extract_html_content( + plaintext_content, file.split(".")[-1] + ) + files[file] = plaintext_content + except Exception as e: + logger.warning(f"Unable to read file: {file} as plaintext. Skipping file.") + logger.warning(e, exc_info=True) + # Extract Entries from specified plaintext files with timer("Parse entries from plaintext files", logger): current_entries = PlaintextToJsonl.convert_plaintext_entries_to_maps(files) @@ -50,6 +65,15 @@ class PlaintextToJsonl(TextEmbeddings): return num_new_embeddings, num_deleted_embeddings + @staticmethod + def extract_html_content(markup_content: str, markup_type: str): + "Extract content from HTML" + if markup_type == "xml": + soup = BeautifulSoup(markup_content, "xml") + else: + soup = BeautifulSoup(markup_content, "html.parser") + return soup.get_text(strip=True, separator="\n") + @staticmethod def convert_plaintext_entries_to_maps(entry_to_file_map: dict) -> List[Entry]: "Convert each plaintext entries into a dictionary" diff --git a/src/khoj/processor/text_to_jsonl.py b/src/khoj/processor/text_to_jsonl.py index c83c83b1..831a032f 100644 --- a/src/khoj/processor/text_to_jsonl.py +++ b/src/khoj/processor/text_to_jsonl.py @@ -5,7 +5,7 @@ import logging import uuid from tqdm import tqdm from typing import Callable, List, Tuple, Set, Any -from khoj.utils.helpers import timer +from khoj.utils.helpers import timer, batcher # Internal Packages @@ -93,7 +93,7 @@ class TextEmbeddings(ABC): num_deleted_embeddings = 0 with timer("Preparing dataset for regeneration", logger): if regenerate: - logger.info(f"Deleting all embeddings for file type {file_type}") + logger.debug(f"Deleting all embeddings for file type {file_type}") num_deleted_embeddings = EmbeddingsAdapters.delete_all_embeddings(user, file_type) num_new_embeddings = 0 @@ -106,48 +106,54 @@ class TextEmbeddings(ABC): ) existing_entry_hashes = set([entry.hashed_value for entry in existing_entries]) hashes_to_process = hashes_for_file - existing_entry_hashes - # for hashed_val in hashes_for_file: - # if not EmbeddingsAdapters.does_embedding_exist(user, hashed_val): - # hashes_to_process.add(hashed_val) entries_to_process = [hash_to_current_entries[hashed_val] for hashed_val in hashes_to_process] data_to_embed = [getattr(entry, key) for entry in entries_to_process] embeddings = self.embeddings_model.embed_documents(data_to_embed) with timer("Update the database with new vector embeddings", logger): - embeddings_to_create = [] - for hashed_val, embedding in zip(hashes_to_process, embeddings): - entry = hash_to_current_entries[hashed_val] - embeddings_to_create.append( - Embeddings( - user=user, - embeddings=embedding, - raw=entry.raw, - compiled=entry.compiled, - heading=entry.heading, - file_path=entry.file, - file_type=file_type, - hashed_value=hashed_val, - corpus_id=entry.corpus_id, - ) - ) - new_embeddings = Embeddings.objects.bulk_create(embeddings_to_create) - num_new_embeddings += len(new_embeddings) + num_items = len(hashes_to_process) + assert num_items == len(embeddings) + batch_size = min(200, num_items) + entry_batches = zip(hashes_to_process, embeddings) - dates_to_create = [] - with timer("Create new date associations for new embeddings", logger): - for embedding in new_embeddings: - dates = self.date_filter.extract_dates(embedding.raw) - for date in dates: - dates_to_create.append( - EmbeddingsDates( - date=date, - embeddings=embedding, - ) + for entry_batch in tqdm( + batcher(entry_batches, batch_size), desc="Processing embeddings in batches" + ): + batch_embeddings_to_create = [] + for entry_hash, embedding in entry_batch: + entry = hash_to_current_entries[entry_hash] + batch_embeddings_to_create.append( + Embeddings( + user=user, + embeddings=embedding, + raw=entry.raw, + compiled=entry.compiled, + heading=entry.heading[:1000], # Truncate to max chars of field allowed + file_path=entry.file, + file_type=file_type, + hashed_value=entry_hash, + corpus_id=entry.corpus_id, ) - new_dates = EmbeddingsDates.objects.bulk_create(dates_to_create) - if len(new_dates) > 0: - logger.info(f"Created {len(new_dates)} new date entries") + ) + new_embeddings = Embeddings.objects.bulk_create(batch_embeddings_to_create) + logger.debug(f"Created {len(new_embeddings)} new embeddings") + num_new_embeddings += len(new_embeddings) + + dates_to_create = [] + with timer("Create new date associations for new embeddings", logger): + for embedding in new_embeddings: + dates = self.date_filter.extract_dates(embedding.raw) + for date in dates: + dates_to_create.append( + EmbeddingsDates( + date=date, + embeddings=embedding, + ) + ) + new_dates = EmbeddingsDates.objects.bulk_create(dates_to_create) + if len(new_dates) > 0: + logger.debug(f"Created {len(new_dates)} new date entries") with timer("Identify hashes for removed entries", logger): for file in hashes_by_file: diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 4984ea4c..2607120d 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -723,7 +723,7 @@ async def extract_references_and_questions( # Infer search queries from user message with timer("Extracting search queries took", logger): # If we've reached here, either the user has enabled offline chat or the openai model is enabled. - if await ConversationAdapters.has_offline_chat(user): + if await ConversationAdapters.ahas_offline_chat(user): using_offline_chat = True offline_chat = await ConversationAdapters.get_offline_chat(user) chat_model = offline_chat.chat_model diff --git a/src/khoj/routers/auth.py b/src/khoj/routers/auth.py index 5c375bd0..ebabeb8e 100644 --- a/src/khoj/routers/auth.py +++ b/src/khoj/routers/auth.py @@ -46,7 +46,6 @@ async def login(request: Request): return await oauth.google.authorize_redirect(request, redirect_uri) -@auth_router.post("/redirect") @auth_router.post("/token") @requires(["authenticated"], redirect="login_page") async def generate_token(request: Request, token_name: Optional[str] = None) -> str: diff --git a/src/khoj/routers/helpers.py b/src/khoj/routers/helpers.py index 8a9e53a7..185217ed 100644 --- a/src/khoj/routers/helpers.py +++ b/src/khoj/routers/helpers.py @@ -31,7 +31,7 @@ def perform_chat_checks(user: KhojUser): async def is_ready_to_chat(user: KhojUser): - has_offline_config = await ConversationAdapters.has_offline_chat(user=user) + has_offline_config = await ConversationAdapters.ahas_offline_chat(user=user) has_openai_config = await ConversationAdapters.has_openai_chat(user=user) if has_offline_config: diff --git a/src/khoj/routers/indexer.py b/src/khoj/routers/indexer.py index e7df65a2..590164fb 100644 --- a/src/khoj/routers/indexer.py +++ b/src/khoj/routers/indexer.py @@ -156,18 +156,17 @@ async def update( host=host, ) + logger.info(f"📪 Content index updated via API call by {client} client") + return Response(content="OK", status_code=200) def configure_search(search_models: SearchModels, search_config: Optional[SearchConfig]) -> Optional[SearchModels]: # Run Validation Checks - if search_config is None: - logger.warning("🚨 No Search configuration available.") - return None if search_models is None: search_models = SearchModels() - if search_config.image: + if search_config and search_config.image: logger.info("🔍 🌄 Setting up image search model") search_models.image_search = image_search.initialize_model(search_config.image) diff --git a/src/khoj/search_filter/date_filter.py b/src/khoj/search_filter/date_filter.py index 88c70101..1d90b9f5 100644 --- a/src/khoj/search_filter/date_filter.py +++ b/src/khoj/search_filter/date_filter.py @@ -127,14 +127,18 @@ class DateFilter(BaseFilter): clean_date_str = re.sub("|".join(future_strings), "", date_str) # parse date passed in query date filter - parsed_date = dtparse.parse( - clean_date_str, - settings={ - "RELATIVE_BASE": relative_base or datetime.now(), - "PREFER_DAY_OF_MONTH": "first", - "PREFER_DATES_FROM": prefer_dates_from, - }, - ) + try: + parsed_date = dtparse.parse( + clean_date_str, + settings={ + "RELATIVE_BASE": relative_base or datetime.now(), + "PREFER_DAY_OF_MONTH": "first", + "PREFER_DATES_FROM": prefer_dates_from, + }, + ) + except Exception as e: + logger.error(f"Failed to parse date string: {date_str} with error: {e}") + return None if parsed_date is None: return None diff --git a/src/khoj/utils/helpers.py b/src/khoj/utils/helpers.py index f6418fbd..3bce67a0 100644 --- a/src/khoj/utils/helpers.py +++ b/src/khoj/utils/helpers.py @@ -5,6 +5,7 @@ import datetime from enum import Enum from importlib import import_module from importlib.metadata import version +from itertools import islice import logging from os import path import os @@ -305,3 +306,13 @@ def generate_random_name(): name = f"{adjective} {noun}" return name + + +def batcher(iterable, max_n): + "Split an iterable into chunks of size max_n" + it = iter(iterable) + while True: + chunk = list(islice(it, max_n)) + if not chunk: + return + yield (x for x in chunk if x is not None) diff --git a/tests/test_text_search.py b/tests/test_text_search.py index aeeaa85f..ae7f0c20 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -22,9 +22,7 @@ logger = logging.getLogger(__name__) # Test # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db -def test_text_search_setup_with_missing_file_raises_error( - org_config_with_only_new_file: LocalOrgConfig, search_config: SearchConfig -): +def test_text_search_setup_with_missing_file_raises_error(org_config_with_only_new_file: LocalOrgConfig): # Arrange # Ensure file mentioned in org.input-files is missing single_new_file = Path(org_config_with_only_new_file.input_files[0]) @@ -70,22 +68,39 @@ def test_text_search_setup_with_empty_file_raises_error( with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) - assert "Created 0 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message + assert "Created 0 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message verify_embeddings(0, default_user) # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db -def test_text_search_setup(content_config, default_user: KhojUser, caplog): +def test_text_indexer_deletes_embedding_before_regenerate( + content_config: ContentConfig, default_user: KhojUser, caplog +): # Arrange org_config = LocalOrgConfig.objects.filter(user=default_user).first() data = get_org_files(org_config) - with caplog.at_level(logging.INFO): + with caplog.at_level(logging.DEBUG): text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) # Assert - assert "Deleting all embeddings for file type org" in caplog.records[1].message - assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message + assert "Deleting all embeddings for file type org" in caplog.text + assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db +def test_text_search_setup_batch_processes(content_config: ContentConfig, default_user: KhojUser, caplog): + # Arrange + org_config = LocalOrgConfig.objects.filter(user=default_user).first() + data = get_org_files(org_config) + with caplog.at_level(logging.DEBUG): + text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) + + # Assert + assert "Created 4 new embeddings" in caplog.text + assert "Created 6 new embeddings" in caplog.text + assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message # ---------------------------------------------------------------------------------------------------- @@ -97,13 +112,13 @@ def test_text_index_same_if_content_unchanged(content_config: ContentConfig, def # Act # Generate initial notes embeddings during asymmetric setup - with caplog.at_level(logging.INFO): + with caplog.at_level(logging.DEBUG): text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) initial_logs = caplog.text caplog.clear() # Clear logs # Run asymmetric setup again with no changes to data source. Ensure index is not updated - with caplog.at_level(logging.INFO): + with caplog.at_level(logging.DEBUG): text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) final_logs = caplog.text @@ -175,12 +190,10 @@ def test_entry_chunking_by_max_tokens(org_config_with_only_new_file: LocalOrgCon # Assert # verify newly added org-mode entry is split by max tokens - record = caplog.records[1] - assert "Created 2 new embeddings. Deleted 0 embeddings for user " in record.message + assert "Created 2 new embeddings. Deleted 0 embeddings for user " in caplog.records[-1].message # ---------------------------------------------------------------------------------------------------- -# @pytest.mark.skip(reason="Flaky due to compressed_jsonl file being rewritten by other tests") @pytest.mark.django_db def test_entry_chunking_by_max_tokens_not_full_corpus( org_config_with_only_new_file: LocalOrgConfig, default_user: KhojUser, caplog @@ -232,11 +245,9 @@ conda activate khoj user=default_user, ) - record = caplog.records[1] - # Assert # verify newly added org-mode entry is split by max tokens - assert "Created 2 new embeddings. Deleted 0 embeddings for user " in record.message + assert "Created 2 new embeddings. Deleted 0 embeddings for user " in caplog.records[-1].message # ---------------------------------------------------------------------------------------------------- @@ -251,7 +262,7 @@ def test_regenerate_index_with_new_entry( with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) - assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message + assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message # append org-mode entry to first org input file in config org_config.input_files = [f"{new_org_file}"] @@ -286,20 +297,23 @@ def test_update_index_with_duplicate_entries_in_stable_order( data = get_org_files(org_config_with_only_new_file) # Act - # load embeddings, entries, notes model after adding new org-mode file + # generate embeddings, entries, notes model from scratch after adding new org-mode file with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) + initial_logs = caplog.text + caplog.clear() # Clear logs data = get_org_files(org_config_with_only_new_file) - # update embeddings, entries, notes model after adding new org-mode file + # update embeddings, entries, notes model with no new changes with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) + final_logs = caplog.text # Assert # verify only 1 entry added even if there are multiple duplicate entries - assert "Created 1 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message - assert "Created 0 new embeddings. Deleted 0 embeddings for user " in caplog.records[4].message + assert "Created 1 new embeddings. Deleted 3 embeddings for user " in initial_logs + assert "Created 0 new embeddings. Deleted 0 embeddings for user " in final_logs verify_embeddings(1, default_user) @@ -319,6 +333,8 @@ def test_update_index_with_deleted_entry(org_config_with_only_new_file: LocalOrg # load embeddings, entries, notes model after adding new org file with 2 entries with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) + initial_logs = caplog.text + caplog.clear() # Clear logs # update embeddings, entries, notes model after removing an entry from the org file with open(new_file_to_index, "w") as f: @@ -329,11 +345,12 @@ def test_update_index_with_deleted_entry(org_config_with_only_new_file: LocalOrg # Act with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) + final_logs = caplog.text # Assert # verify only 1 entry added even if there are multiple duplicate entries - assert "Created 2 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message - assert "Created 0 new embeddings. Deleted 1 embeddings for user " in caplog.records[4].message + assert "Created 2 new embeddings. Deleted 3 embeddings for user " in initial_logs + assert "Created 0 new embeddings. Deleted 1 embeddings for user " in final_logs verify_embeddings(1, default_user) @@ -346,6 +363,8 @@ def test_update_index_with_new_entry(content_config: ContentConfig, new_org_file data = get_org_files(org_config) with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=True, user=default_user) + initial_logs = caplog.text + caplog.clear() # Clear logs # append org-mode entry to first org input file in config with open(new_org_file, "w") as f: @@ -358,10 +377,11 @@ def test_update_index_with_new_entry(content_config: ContentConfig, new_org_file # update embeddings, entries with the newly added note with caplog.at_level(logging.INFO): text_search.setup(OrgToJsonl, data, regenerate=False, user=default_user) + final_logs = caplog.text # Assert - assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[2].message - assert "Created 1 new embeddings. Deleted 0 embeddings for user " in caplog.records[4].message + assert "Created 10 new embeddings. Deleted 3 embeddings for user " in initial_logs + assert "Created 1 new embeddings. Deleted 0 embeddings for user " in final_logs verify_embeddings(11, default_user) From bcbee05a9ebd994dcb9fb7a7567817f27fa2a5da Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 31 Oct 2023 18:50:54 -0700 Subject: [PATCH 008/194] Rename DbModels Embeddings, EmbeddingsAdapter to Entry, EntryAdapter Improves readability as name has closer match to underlying constructs - Entry is any atomic item indexed by Khoj. This can be an org-mode entry, a markdown section, a PDF or Notion page etc. - Embeddings are semantic vectors generated by the search ML model that encodes for meaning contained in an entries text. - An "Entry" contains "Embeddings" vectors but also other metadata about the entry like filename etc. --- src/database/adapters/__init__.py | 64 +++++++++---------- .../0010_rename_embeddings_entry_and_more.py | 30 +++++++++ src/database/models/__init__.py | 10 +-- src/khoj/processor/github/github_to_jsonl.py | 5 +- .../processor/markdown/markdown_to_jsonl.py | 4 +- src/khoj/processor/notion/notion_to_jsonl.py | 4 +- src/khoj/processor/org_mode/org_to_jsonl.py | 4 +- src/khoj/processor/pdf/pdf_to_jsonl.py | 4 +- .../processor/plaintext/plaintext_to_jsonl.py | 4 +- src/khoj/processor/text_to_jsonl.py | 22 +++---- src/khoj/routers/api.py | 14 ++-- src/khoj/routers/web_client.py | 4 +- src/khoj/search_type/text_search.py | 22 +++---- tests/test_client.py | 4 +- tests/test_text_search.py | 7 +- 15 files changed, 115 insertions(+), 87 deletions(-) create mode 100644 src/database/migrations/0010_rename_embeddings_entry_and_more.py diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index 362398d8..080e73d7 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -27,7 +27,7 @@ from database.models import ( KhojApiUser, NotionConfig, GithubConfig, - Embeddings, + Entry, GithubRepoConfig, Conversation, ConversationProcessorConfig, @@ -286,54 +286,54 @@ class ConversationAdapters: return await OpenAIProcessorConversationConfig.objects.filter(user=user).afirst() -class EmbeddingsAdapters: +class EntryAdapters: word_filer = WordFilter() file_filter = FileFilter() date_filter = DateFilter() @staticmethod - def does_embedding_exist(user: KhojUser, hashed_value: str) -> bool: - return Embeddings.objects.filter(user=user, hashed_value=hashed_value).exists() + def does_entry_exist(user: KhojUser, hashed_value: str) -> bool: + return Entry.objects.filter(user=user, hashed_value=hashed_value).exists() @staticmethod - def delete_embedding_by_file(user: KhojUser, file_path: str): - deleted_count, _ = Embeddings.objects.filter(user=user, file_path=file_path).delete() + def delete_entry_by_file(user: KhojUser, file_path: str): + deleted_count, _ = Entry.objects.filter(user=user, file_path=file_path).delete() return deleted_count @staticmethod - def delete_all_embeddings(user: KhojUser, file_type: str): - deleted_count, _ = Embeddings.objects.filter(user=user, file_type=file_type).delete() + def delete_all_entries(user: KhojUser, file_type: str): + deleted_count, _ = Entry.objects.filter(user=user, file_type=file_type).delete() return deleted_count @staticmethod def get_existing_entry_hashes_by_file(user: KhojUser, file_path: str): - return Embeddings.objects.filter(user=user, file_path=file_path).values_list("hashed_value", flat=True) + return Entry.objects.filter(user=user, file_path=file_path).values_list("hashed_value", flat=True) @staticmethod - def delete_embedding_by_hash(user: KhojUser, hashed_values: List[str]): - Embeddings.objects.filter(user=user, hashed_value__in=hashed_values).delete() + def delete_entry_by_hash(user: KhojUser, hashed_values: List[str]): + Entry.objects.filter(user=user, hashed_value__in=hashed_values).delete() @staticmethod - def get_embeddings_by_date_filter(embeddings: BaseManager[Embeddings], start_date: date, end_date: date): - return embeddings.filter( - embeddingsdates__date__gte=start_date, - embeddingsdates__date__lte=end_date, + def get_entries_by_date_filter(entry: BaseManager[Entry], start_date: date, end_date: date): + return entry.filter( + entrydates__date__gte=start_date, + entrydates__date__lte=end_date, ) @staticmethod - async def user_has_embeddings(user: KhojUser): - return await Embeddings.objects.filter(user=user).aexists() + async def user_has_entries(user: KhojUser): + return await Entry.objects.filter(user=user).aexists() @staticmethod def apply_filters(user: KhojUser, query: str, file_type_filter: str = None): q_filter_terms = Q() - explicit_word_terms = EmbeddingsAdapters.word_filer.get_filter_terms(query) - file_filters = EmbeddingsAdapters.file_filter.get_filter_terms(query) - date_filters = EmbeddingsAdapters.date_filter.get_query_date_range(query) + explicit_word_terms = EntryAdapters.word_filer.get_filter_terms(query) + file_filters = EntryAdapters.file_filter.get_filter_terms(query) + date_filters = EntryAdapters.date_filter.get_query_date_range(query) if len(explicit_word_terms) == 0 and len(file_filters) == 0 and len(date_filters) == 0: - return Embeddings.objects.filter(user=user) + return Entry.objects.filter(user=user) for term in explicit_word_terms: if term.startswith("+"): @@ -354,32 +354,32 @@ class EmbeddingsAdapters: if min_date is not None: # Convert the min_date timestamp to yyyy-mm-dd format formatted_min_date = date.fromtimestamp(min_date).strftime("%Y-%m-%d") - q_filter_terms &= Q(embeddings_dates__date__gte=formatted_min_date) + q_filter_terms &= Q(entry_dates__date__gte=formatted_min_date) if max_date is not None: # Convert the max_date timestamp to yyyy-mm-dd format formatted_max_date = date.fromtimestamp(max_date).strftime("%Y-%m-%d") - q_filter_terms &= Q(embeddings_dates__date__lte=formatted_max_date) + q_filter_terms &= Q(entry_dates__date__lte=formatted_max_date) - relevant_embeddings = Embeddings.objects.filter(user=user).filter( + relevant_entries = Entry.objects.filter(user=user).filter( q_filter_terms, ) if file_type_filter: - relevant_embeddings = relevant_embeddings.filter(file_type=file_type_filter) - return relevant_embeddings + relevant_entries = relevant_entries.filter(file_type=file_type_filter) + return relevant_entries @staticmethod def search_with_embeddings( user: KhojUser, embeddings: Tensor, max_results: int = 10, file_type_filter: str = None, raw_query: str = None ): - relevant_embeddings = EmbeddingsAdapters.apply_filters(user, raw_query, file_type_filter) - relevant_embeddings = relevant_embeddings.filter(user=user).annotate( + relevant_entries = EntryAdapters.apply_filters(user, raw_query, file_type_filter) + relevant_entries = relevant_entries.filter(user=user).annotate( distance=CosineDistance("embeddings", embeddings) ) if file_type_filter: - relevant_embeddings = relevant_embeddings.filter(file_type=file_type_filter) - relevant_embeddings = relevant_embeddings.order_by("distance") - return relevant_embeddings[:max_results] + relevant_entries = relevant_entries.filter(file_type=file_type_filter) + relevant_entries = relevant_entries.order_by("distance") + return relevant_entries[:max_results] @staticmethod def get_unique_file_types(user: KhojUser): - return Embeddings.objects.filter(user=user).values_list("file_type", flat=True).distinct() + return Entry.objects.filter(user=user).values_list("file_type", flat=True).distinct() diff --git a/src/database/migrations/0010_rename_embeddings_entry_and_more.py b/src/database/migrations/0010_rename_embeddings_entry_and_more.py new file mode 100644 index 00000000..f86b2caa --- /dev/null +++ b/src/database/migrations/0010_rename_embeddings_entry_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.5 on 2023-10-26 23:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0009_khojapiuser"), + ] + + operations = [ + migrations.RenameModel( + old_name="Embeddings", + new_name="Entry", + ), + migrations.RenameModel( + old_name="EmbeddingsDates", + new_name="EntryDates", + ), + migrations.RenameField( + model_name="entrydates", + old_name="embeddings", + new_name="entry", + ), + migrations.RenameIndex( + model_name="entrydates", + new_name="database_en_date_8d823c_idx", + old_name="database_em_date_a1ba47_idx", + ), + ] diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py index 7c9c3822..fe020601 100644 --- a/src/database/models/__init__.py +++ b/src/database/models/__init__.py @@ -114,8 +114,8 @@ class Conversation(BaseModel): conversation_log = models.JSONField(default=dict) -class Embeddings(BaseModel): - class EmbeddingsType(models.TextChoices): +class Entry(BaseModel): + class EntryType(models.TextChoices): IMAGE = "image" PDF = "pdf" PLAINTEXT = "plaintext" @@ -130,7 +130,7 @@ class Embeddings(BaseModel): raw = models.TextField() compiled = models.TextField() heading = models.CharField(max_length=1000, default=None, null=True, blank=True) - file_type = models.CharField(max_length=30, choices=EmbeddingsType.choices, default=EmbeddingsType.PLAINTEXT) + file_type = models.CharField(max_length=30, choices=EntryType.choices, default=EntryType.PLAINTEXT) file_path = models.CharField(max_length=400, default=None, null=True, blank=True) file_name = models.CharField(max_length=400, default=None, null=True, blank=True) url = models.URLField(max_length=400, default=None, null=True, blank=True) @@ -138,9 +138,9 @@ class Embeddings(BaseModel): corpus_id = models.UUIDField(default=uuid.uuid4, editable=False) -class EmbeddingsDates(BaseModel): +class EntryDates(BaseModel): date = models.DateField() - embeddings = models.ForeignKey(Embeddings, on_delete=models.CASCADE, related_name="embeddings_dates") + entry = models.ForeignKey(Entry, on_delete=models.CASCADE, related_name="embeddings_dates") class Meta: indexes = [ diff --git a/src/khoj/processor/github/github_to_jsonl.py b/src/khoj/processor/github/github_to_jsonl.py index 8feb6a31..a548ae1b 100644 --- a/src/khoj/processor/github/github_to_jsonl.py +++ b/src/khoj/processor/github/github_to_jsonl.py @@ -13,8 +13,7 @@ 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 TextEmbeddings -from khoj.utils.rawconfig import Entry -from database.models import Embeddings, GithubConfig, KhojUser +from database.models import Entry as DbEntry, GithubConfig, KhojUser logger = logging.getLogger(__name__) @@ -103,7 +102,7 @@ class GithubToJsonl(TextEmbeddings): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( - current_entries, Embeddings.EmbeddingsType.GITHUB, key="compiled", logger=logger, user=user + current_entries, DbEntry.EntryType.GITHUB, key="compiled", logger=logger, user=user ) return num_new_embeddings, num_deleted_embeddings diff --git a/src/khoj/processor/markdown/markdown_to_jsonl.py b/src/khoj/processor/markdown/markdown_to_jsonl.py index 17136b00..921f2213 100644 --- a/src/khoj/processor/markdown/markdown_to_jsonl.py +++ b/src/khoj/processor/markdown/markdown_to_jsonl.py @@ -10,7 +10,7 @@ from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer from khoj.utils.constants import empty_escape_sequences from khoj.utils.rawconfig import Entry -from database.models import Embeddings, KhojUser +from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) @@ -46,7 +46,7 @@ class MarkdownToJsonl(TextEmbeddings): with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, - Embeddings.EmbeddingsType.MARKDOWN, + DbEntry.EntryType.MARKDOWN, "compiled", logger, deletion_file_names, diff --git a/src/khoj/processor/notion/notion_to_jsonl.py b/src/khoj/processor/notion/notion_to_jsonl.py index 0081350a..15c21b23 100644 --- a/src/khoj/processor/notion/notion_to_jsonl.py +++ b/src/khoj/processor/notion/notion_to_jsonl.py @@ -10,7 +10,7 @@ from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry, NotionContentConfig from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.rawconfig import Entry -from database.models import Embeddings, KhojUser, NotionConfig +from database.models import Entry as DbEntry, KhojUser, NotionConfig from enum import Enum @@ -250,7 +250,7 @@ class NotionToJsonl(TextEmbeddings): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( - current_entries, Embeddings.EmbeddingsType.NOTION, key="compiled", logger=logger, user=user + current_entries, DbEntry.EntryType.NOTION, key="compiled", logger=logger, user=user ) return num_new_embeddings, num_deleted_embeddings diff --git a/src/khoj/processor/org_mode/org_to_jsonl.py b/src/khoj/processor/org_mode/org_to_jsonl.py index 90fdc029..9bf85660 100644 --- a/src/khoj/processor/org_mode/org_to_jsonl.py +++ b/src/khoj/processor/org_mode/org_to_jsonl.py @@ -9,7 +9,7 @@ from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry from khoj.utils import state -from database.models import Embeddings, KhojUser +from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) @@ -47,7 +47,7 @@ class OrgToJsonl(TextEmbeddings): with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, - Embeddings.EmbeddingsType.ORG, + DbEntry.EntryType.ORG, "compiled", logger, deletion_file_names, diff --git a/src/khoj/processor/pdf/pdf_to_jsonl.py b/src/khoj/processor/pdf/pdf_to_jsonl.py index 3a712c68..feed12d7 100644 --- a/src/khoj/processor/pdf/pdf_to_jsonl.py +++ b/src/khoj/processor/pdf/pdf_to_jsonl.py @@ -11,7 +11,7 @@ from langchain.document_loaders import PyMuPDFLoader from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry -from database.models import Embeddings, KhojUser +from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) @@ -45,7 +45,7 @@ class PdfToJsonl(TextEmbeddings): with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, - Embeddings.EmbeddingsType.PDF, + DbEntry.EntryType.PDF, "compiled", logger, deletion_file_names, diff --git a/src/khoj/processor/plaintext/plaintext_to_jsonl.py b/src/khoj/processor/plaintext/plaintext_to_jsonl.py index 086808b7..a657ff2f 100644 --- a/src/khoj/processor/plaintext/plaintext_to_jsonl.py +++ b/src/khoj/processor/plaintext/plaintext_to_jsonl.py @@ -9,7 +9,7 @@ from bs4 import BeautifulSoup from khoj.processor.text_to_jsonl import TextEmbeddings from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry -from database.models import Embeddings, KhojUser +from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ class PlaintextToJsonl(TextEmbeddings): with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, - Embeddings.EmbeddingsType.PLAINTEXT, + DbEntry.EntryType.PLAINTEXT, key="compiled", logger=logger, deletion_filenames=deletion_file_names, diff --git a/src/khoj/processor/text_to_jsonl.py b/src/khoj/processor/text_to_jsonl.py index 831a032f..3aa6a5b1 100644 --- a/src/khoj/processor/text_to_jsonl.py +++ b/src/khoj/processor/text_to_jsonl.py @@ -12,8 +12,8 @@ from khoj.utils.helpers import timer, batcher from khoj.utils.rawconfig import Entry from khoj.processor.embeddings import EmbeddingsModel from khoj.search_filter.date_filter import DateFilter -from database.models import KhojUser, Embeddings, EmbeddingsDates -from database.adapters import EmbeddingsAdapters +from database.models import KhojUser, Entry as DbEntry, EntryDates +from database.adapters import EntryAdapters logger = logging.getLogger(__name__) @@ -94,14 +94,14 @@ class TextEmbeddings(ABC): with timer("Preparing dataset for regeneration", logger): if regenerate: logger.debug(f"Deleting all embeddings for file type {file_type}") - num_deleted_embeddings = EmbeddingsAdapters.delete_all_embeddings(user, file_type) + num_deleted_embeddings = EntryAdapters.delete_all_entries(user, file_type) num_new_embeddings = 0 with timer("Identify hashes for adding new entries", logger): for file in tqdm(hashes_by_file, desc="Processing file with hashed values"): hashes_for_file = hashes_by_file[file] hashes_to_process = set() - existing_entries = Embeddings.objects.filter( + existing_entries = DbEntry.objects.filter( user=user, hashed_value__in=hashes_for_file, file_type=file_type ) existing_entry_hashes = set([entry.hashed_value for entry in existing_entries]) @@ -124,7 +124,7 @@ class TextEmbeddings(ABC): for entry_hash, embedding in entry_batch: entry = hash_to_current_entries[entry_hash] batch_embeddings_to_create.append( - Embeddings( + DbEntry( user=user, embeddings=embedding, raw=entry.raw, @@ -136,7 +136,7 @@ class TextEmbeddings(ABC): corpus_id=entry.corpus_id, ) ) - new_embeddings = Embeddings.objects.bulk_create(batch_embeddings_to_create) + new_embeddings = DbEntry.objects.bulk_create(batch_embeddings_to_create) logger.debug(f"Created {len(new_embeddings)} new embeddings") num_new_embeddings += len(new_embeddings) @@ -146,26 +146,26 @@ class TextEmbeddings(ABC): dates = self.date_filter.extract_dates(embedding.raw) for date in dates: dates_to_create.append( - EmbeddingsDates( + EntryDates( date=date, embeddings=embedding, ) ) - new_dates = EmbeddingsDates.objects.bulk_create(dates_to_create) + new_dates = EntryDates.objects.bulk_create(dates_to_create) if len(new_dates) > 0: logger.debug(f"Created {len(new_dates)} new date entries") with timer("Identify hashes for removed entries", logger): for file in hashes_by_file: - existing_entry_hashes = EmbeddingsAdapters.get_existing_entry_hashes_by_file(user, file) + existing_entry_hashes = EntryAdapters.get_existing_entry_hashes_by_file(user, file) to_delete_entry_hashes = set(existing_entry_hashes) - hashes_by_file[file] num_deleted_embeddings += len(to_delete_entry_hashes) - EmbeddingsAdapters.delete_embedding_by_hash(user, hashed_values=list(to_delete_entry_hashes)) + EntryAdapters.delete_entry_by_hash(user, hashed_values=list(to_delete_entry_hashes)) with timer("Identify hashes for deleting entries", logger): if deletion_filenames is not None: for file_path in deletion_filenames: - deleted_count = EmbeddingsAdapters.delete_embedding_by_file(user, file_path) + deleted_count = EntryAdapters.delete_entry_by_file(user, file_path) num_deleted_embeddings += deleted_count return num_new_embeddings, num_deleted_embeddings diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 2607120d..8f6af0bf 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -48,7 +48,7 @@ from khoj.processor.conversation.gpt4all.chat_model import extract_questions_off from fastapi.requests import Request from database import adapters -from database.adapters import EmbeddingsAdapters, ConversationAdapters +from database.adapters import EntryAdapters, ConversationAdapters from database.models import LocalMarkdownConfig, LocalOrgConfig, LocalPdfConfig, LocalPlaintextConfig, KhojUser @@ -129,7 +129,7 @@ if not state.demo: @requires(["authenticated"]) def get_config_data(request: Request): user = request.user.object - EmbeddingsAdapters.get_unique_file_types(user) + EntryAdapters.get_unique_file_types(user) return state.config @@ -145,7 +145,7 @@ if not state.demo: configuration_update_metadata = {} - enabled_content = await sync_to_async(EmbeddingsAdapters.get_unique_file_types)(user) + enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user) if state.config.content_type is not None: configuration_update_metadata["github"] = "github" in enabled_content @@ -241,9 +241,9 @@ if not state.demo: raise ValueError(f"Invalid content type: {content_type}") await content_object.objects.filter(user=user).adelete() - await sync_to_async(EmbeddingsAdapters.delete_all_embeddings)(user, content_type) + await sync_to_async(EntryAdapters.delete_all_entries)(user, content_type) - enabled_content = await sync_to_async(EmbeddingsAdapters.get_unique_file_types)(user) + enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user) return {"status": "ok"} @api.post("/delete/config/data/processor/conversation/openai", status_code=200) @@ -372,7 +372,7 @@ def get_config_types( ): user = request.user.object - enabled_file_types = EmbeddingsAdapters.get_unique_file_types(user) + enabled_file_types = EntryAdapters.get_unique_file_types(user) configured_content_types = list(enabled_file_types) @@ -706,7 +706,7 @@ async def extract_references_and_questions( if conversation_type == ConversationCommand.General: return compiled_references, inferred_queries, q - if not await EmbeddingsAdapters.user_has_embeddings(user=user): + if not await EntryAdapters.user_has_entries(user=user): logger.warning( "No content index loaded, so cannot extract references from knowledge base. Please configure your data sources and update the index to chat with your notes." ) diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 06f43430..ef0abe18 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -19,7 +19,7 @@ from khoj.utils.rawconfig import ( # Internal Packages from khoj.utils import constants, state -from database.adapters import EmbeddingsAdapters, get_user_github_config, get_user_notion_config, ConversationAdapters +from database.adapters import EntryAdapters, get_user_github_config, get_user_notion_config, ConversationAdapters from database.models import LocalOrgConfig, LocalMarkdownConfig, LocalPdfConfig, LocalPlaintextConfig @@ -84,7 +84,7 @@ if not state.demo: @requires(["authenticated"], redirect="login_page") def config_page(request: Request): user = request.user.object - enabled_content = set(EmbeddingsAdapters.get_unique_file_types(user).all()) + enabled_content = set(EntryAdapters.get_unique_file_types(user).all()) default_full_config = FullConfig( content_type=None, search_type=None, diff --git a/src/khoj/search_type/text_search.py b/src/khoj/search_type/text_search.py index dc6593f5..db3b313c 100644 --- a/src/khoj/search_type/text_search.py +++ b/src/khoj/search_type/text_search.py @@ -6,31 +6,31 @@ from typing import List, Tuple, Type, Union, Dict # External Packages import torch -from sentence_transformers import SentenceTransformer, CrossEncoder, util +from sentence_transformers import util from asgiref.sync import sync_to_async # Internal Packages from khoj.utils import state -from khoj.utils.helpers import get_absolute_path, resolve_absolute_path, load_model, timer +from khoj.utils.helpers import get_absolute_path, timer from khoj.utils.models import BaseEncoder from khoj.utils.state import SearchType from khoj.utils.rawconfig import SearchResponse, Entry from khoj.utils.jsonl import load_jsonl from khoj.processor.text_to_jsonl import TextEmbeddings -from database.adapters import EmbeddingsAdapters -from database.models import KhojUser, Embeddings +from database.adapters import EntryAdapters +from database.models import KhojUser, Entry as DbEntry logger = logging.getLogger(__name__) search_type_to_embeddings_type = { - SearchType.Org.value: Embeddings.EmbeddingsType.ORG, - SearchType.Markdown.value: Embeddings.EmbeddingsType.MARKDOWN, - SearchType.Plaintext.value: Embeddings.EmbeddingsType.PLAINTEXT, - SearchType.Pdf.value: Embeddings.EmbeddingsType.PDF, - SearchType.Github.value: Embeddings.EmbeddingsType.GITHUB, - SearchType.Notion.value: Embeddings.EmbeddingsType.NOTION, + SearchType.Org.value: DbEntry.EntryType.ORG, + SearchType.Markdown.value: DbEntry.EntryType.MARKDOWN, + SearchType.Plaintext.value: DbEntry.EntryType.PLAINTEXT, + SearchType.Pdf.value: DbEntry.EntryType.PDF, + SearchType.Github.value: DbEntry.EntryType.GITHUB, + SearchType.Notion.value: DbEntry.EntryType.NOTION, SearchType.All.value: None, } @@ -121,7 +121,7 @@ async def query( # Find relevant entries for the query top_k = 10 with timer("Search Time", logger, state.device): - hits = EmbeddingsAdapters.search_with_embeddings( + hits = EntryAdapters.search_with_embeddings( user=user, embeddings=question_embedding, max_results=top_k, diff --git a/tests/test_client.py b/tests/test_client.py index 6818c2ba..a1013017 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -17,7 +17,7 @@ from khoj.search_type import text_search, image_search from khoj.utils.rawconfig import ContentConfig, SearchConfig from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl from database.models import KhojUser -from database.adapters import EmbeddingsAdapters +from database.adapters import EntryAdapters # Test @@ -178,7 +178,7 @@ def test_get_configured_types_via_api(client, sample_org_data): # Act text_search.setup(OrgToJsonl, sample_org_data, regenerate=False) - enabled_types = EmbeddingsAdapters.get_unique_file_types(user=None).all().values_list("file_type", flat=True) + enabled_types = EntryAdapters.get_unique_file_types(user=None).all().values_list("file_type", flat=True) # Assert assert list(enabled_types) == ["org"] diff --git a/tests/test_text_search.py b/tests/test_text_search.py index ae7f0c20..db26ea7b 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -1,6 +1,5 @@ # System Packages import logging -import locale from pathlib import Path import os import asyncio @@ -14,7 +13,7 @@ from khoj.utils.rawconfig import ContentConfig, SearchConfig from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl from khoj.processor.github.github_to_jsonl import GithubToJsonl from khoj.utils.fs_syncer import collect_files, get_org_files -from database.models import LocalOrgConfig, KhojUser, Embeddings, GithubConfig +from database.models import LocalOrgConfig, KhojUser, Entry, GithubConfig logger = logging.getLogger(__name__) @@ -402,10 +401,10 @@ def test_text_search_setup_github(content_config: ContentConfig, default_user: K ) # Assert - embeddings = Embeddings.objects.filter(user=default_user, file_type="github").count() + embeddings = Entry.objects.filter(user=default_user, file_type="github").count() assert embeddings > 1 def verify_embeddings(expected_count, user): - embeddings = Embeddings.objects.filter(user=user, file_type="org").count() + embeddings = Entry.objects.filter(user=user, file_type="org").count() assert embeddings == expected_count From 87e6b1eab97ef129da86b37fe2ad8a3740141ab4 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 31 Oct 2023 18:55:59 -0700 Subject: [PATCH 009/194] Rename TextEmbeddings to TextEntries for improved readability Improves readability as name has closer match to underlying constructs --- src/khoj/processor/github/github_to_jsonl.py | 6 +++--- src/khoj/processor/markdown/markdown_to_jsonl.py | 4 ++-- src/khoj/processor/notion/notion_to_jsonl.py | 4 ++-- src/khoj/processor/org_mode/org_to_jsonl.py | 4 ++-- src/khoj/processor/pdf/pdf_to_jsonl.py | 4 ++-- src/khoj/processor/plaintext/plaintext_to_jsonl.py | 4 ++-- src/khoj/processor/text_to_jsonl.py | 12 ++++++------ src/khoj/search_type/text_search.py | 4 ++-- tests/test_org_to_jsonl.py | 6 +++--- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/khoj/processor/github/github_to_jsonl.py b/src/khoj/processor/github/github_to_jsonl.py index a548ae1b..98e771dc 100644 --- a/src/khoj/processor/github/github_to_jsonl.py +++ b/src/khoj/processor/github/github_to_jsonl.py @@ -12,14 +12,14 @@ from khoj.utils.helpers import timer 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 TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from database.models import Entry as DbEntry, GithubConfig, KhojUser logger = logging.getLogger(__name__) -class GithubToJsonl(TextEmbeddings): +class GithubToJsonl(TextEntries): def __init__(self, config: GithubConfig): super().__init__(config) raw_repos = config.githubrepoconfig.all() @@ -94,7 +94,7 @@ class GithubToJsonl(TextEmbeddings): current_entries += issue_entries with timer(f"Split entries by max token size supported by model {repo_shorthand}", logger): - current_entries = TextEmbeddings.split_entries_by_max_tokens(current_entries, max_tokens=256) + current_entries = TextEntries.split_entries_by_max_tokens(current_entries, max_tokens=256) return current_entries diff --git a/src/khoj/processor/markdown/markdown_to_jsonl.py b/src/khoj/processor/markdown/markdown_to_jsonl.py index 921f2213..86acc4b3 100644 --- a/src/khoj/processor/markdown/markdown_to_jsonl.py +++ b/src/khoj/processor/markdown/markdown_to_jsonl.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import Tuple, List # Internal Packages -from khoj.processor.text_to_jsonl import TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from khoj.utils.helpers import timer from khoj.utils.constants import empty_escape_sequences from khoj.utils.rawconfig import Entry @@ -16,7 +16,7 @@ from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) -class MarkdownToJsonl(TextEmbeddings): +class MarkdownToJsonl(TextEntries): def __init__(self): super().__init__() diff --git a/src/khoj/processor/notion/notion_to_jsonl.py b/src/khoj/processor/notion/notion_to_jsonl.py index 15c21b23..048642ef 100644 --- a/src/khoj/processor/notion/notion_to_jsonl.py +++ b/src/khoj/processor/notion/notion_to_jsonl.py @@ -8,7 +8,7 @@ import requests # Internal Packages from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry, NotionContentConfig -from khoj.processor.text_to_jsonl import TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from khoj.utils.rawconfig import Entry from database.models import Entry as DbEntry, KhojUser, NotionConfig @@ -50,7 +50,7 @@ class NotionBlockType(Enum): CALLOUT = "callout" -class NotionToJsonl(TextEmbeddings): +class NotionToJsonl(TextEntries): def __init__(self, config: NotionConfig): super().__init__(config) self.config = NotionContentConfig( diff --git a/src/khoj/processor/org_mode/org_to_jsonl.py b/src/khoj/processor/org_mode/org_to_jsonl.py index 9bf85660..fbb43f55 100644 --- a/src/khoj/processor/org_mode/org_to_jsonl.py +++ b/src/khoj/processor/org_mode/org_to_jsonl.py @@ -5,7 +5,7 @@ from typing import Iterable, List, Tuple # Internal Packages from khoj.processor.org_mode import orgnode -from khoj.processor.text_to_jsonl import TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry from khoj.utils import state @@ -15,7 +15,7 @@ from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) -class OrgToJsonl(TextEmbeddings): +class OrgToJsonl(TextEntries): def __init__(self): super().__init__() diff --git a/src/khoj/processor/pdf/pdf_to_jsonl.py b/src/khoj/processor/pdf/pdf_to_jsonl.py index feed12d7..034e51f4 100644 --- a/src/khoj/processor/pdf/pdf_to_jsonl.py +++ b/src/khoj/processor/pdf/pdf_to_jsonl.py @@ -8,7 +8,7 @@ import base64 from langchain.document_loaders import PyMuPDFLoader # Internal Packages -from khoj.processor.text_to_jsonl import TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry from database.models import Entry as DbEntry, KhojUser @@ -17,7 +17,7 @@ from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) -class PdfToJsonl(TextEmbeddings): +class PdfToJsonl(TextEntries): def __init__(self): super().__init__() diff --git a/src/khoj/processor/plaintext/plaintext_to_jsonl.py b/src/khoj/processor/plaintext/plaintext_to_jsonl.py index a657ff2f..1094baa2 100644 --- a/src/khoj/processor/plaintext/plaintext_to_jsonl.py +++ b/src/khoj/processor/plaintext/plaintext_to_jsonl.py @@ -6,7 +6,7 @@ from bs4 import BeautifulSoup # Internal Packages -from khoj.processor.text_to_jsonl import TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from khoj.utils.helpers import timer from khoj.utils.rawconfig import Entry from database.models import Entry as DbEntry, KhojUser @@ -15,7 +15,7 @@ from database.models import Entry as DbEntry, KhojUser logger = logging.getLogger(__name__) -class PlaintextToJsonl(TextEmbeddings): +class PlaintextToJsonl(TextEntries): def __init__(self): super().__init__() diff --git a/src/khoj/processor/text_to_jsonl.py b/src/khoj/processor/text_to_jsonl.py index 3aa6a5b1..763db9df 100644 --- a/src/khoj/processor/text_to_jsonl.py +++ b/src/khoj/processor/text_to_jsonl.py @@ -19,7 +19,7 @@ from database.adapters import EntryAdapters logger = logging.getLogger(__name__) -class TextEmbeddings(ABC): +class TextEntries(ABC): def __init__(self, config: Any = None): self.embeddings_model = EmbeddingsModel() self.config = config @@ -85,10 +85,10 @@ class TextEmbeddings(ABC): ): with timer("Construct current entry hashes", logger): hashes_by_file = dict[str, set[str]]() - current_entry_hashes = list(map(TextEmbeddings.hash_func(key), current_entries)) + current_entry_hashes = list(map(TextEntries.hash_func(key), current_entries)) hash_to_current_entries = dict(zip(current_entry_hashes, current_entries)) for entry in tqdm(current_entries, desc="Hashing Entries"): - hashes_by_file.setdefault(entry.file, set()).add(TextEmbeddings.hash_func(key)(entry)) + hashes_by_file.setdefault(entry.file, set()).add(TextEntries.hash_func(key)(entry)) num_deleted_embeddings = 0 with timer("Preparing dataset for regeneration", logger): @@ -180,11 +180,11 @@ class TextEmbeddings(ABC): ): # Hash all current and previous entries to identify new entries with timer("Hash previous, current entries", logger): - current_entry_hashes = list(map(TextEmbeddings.hash_func(key), current_entries)) - previous_entry_hashes = list(map(TextEmbeddings.hash_func(key), previous_entries)) + current_entry_hashes = list(map(TextEntries.hash_func(key), current_entries)) + previous_entry_hashes = list(map(TextEntries.hash_func(key), previous_entries)) if deletion_filenames is not None: deletion_entries = [entry for entry in previous_entries if entry.file in deletion_filenames] - deletion_entry_hashes = list(map(TextEmbeddings.hash_func(key), deletion_entries)) + deletion_entry_hashes = list(map(TextEntries.hash_func(key), deletion_entries)) else: deletion_entry_hashes = [] diff --git a/src/khoj/search_type/text_search.py b/src/khoj/search_type/text_search.py index db3b313c..e1da9043 100644 --- a/src/khoj/search_type/text_search.py +++ b/src/khoj/search_type/text_search.py @@ -18,7 +18,7 @@ from khoj.utils.models import BaseEncoder from khoj.utils.state import SearchType from khoj.utils.rawconfig import SearchResponse, Entry from khoj.utils.jsonl import load_jsonl -from khoj.processor.text_to_jsonl import TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from database.adapters import EntryAdapters from database.models import KhojUser, Entry as DbEntry @@ -188,7 +188,7 @@ def rerank_and_sort_results(hits, query): def setup( - text_to_jsonl: Type[TextEmbeddings], + text_to_jsonl: Type[TextEntries], files: dict[str, str], regenerate: bool, full_corpus: bool = True, diff --git a/tests/test_org_to_jsonl.py b/tests/test_org_to_jsonl.py index d47c212e..c9ccf0d6 100644 --- a/tests/test_org_to_jsonl.py +++ b/tests/test_org_to_jsonl.py @@ -4,7 +4,7 @@ import os # Internal Packages from khoj.processor.org_mode.org_to_jsonl import OrgToJsonl -from khoj.processor.text_to_jsonl import TextEmbeddings +from khoj.processor.text_to_jsonl import TextEntries from khoj.utils.helpers import is_none_or_empty from khoj.utils.rawconfig import Entry from khoj.utils.fs_syncer import get_org_files @@ -63,7 +63,7 @@ def test_entry_split_when_exceeds_max_words(tmp_path): # Split each entry from specified Org files by max words jsonl_string = OrgToJsonl.convert_org_entries_to_jsonl( - TextEmbeddings.split_entries_by_max_tokens( + TextEntries.split_entries_by_max_tokens( OrgToJsonl.convert_org_nodes_to_entries(entries, entry_to_file_map), max_tokens=4 ) ) @@ -86,7 +86,7 @@ def test_entry_split_drops_large_words(): # Act # Split entry by max words and drop words larger than max word length - processed_entry = TextEmbeddings.split_entries_by_max_tokens([entry], max_word_length=5)[0] + processed_entry = TextEntries.split_entries_by_max_tokens([entry], max_word_length=5)[0] # Assert # "Heading" dropped from compiled version because its over the set max word limit From f77336ba61e8cc055d3996134a0775c288f2b6f4 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 31 Oct 2023 19:01:09 -0700 Subject: [PATCH 010/194] Add key icon for API keys table in Web client config page --- src/khoj/interface/web/assets/icons/key.svg | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/khoj/interface/web/assets/icons/key.svg diff --git a/src/khoj/interface/web/assets/icons/key.svg b/src/khoj/interface/web/assets/icons/key.svg new file mode 100644 index 00000000..437688fb --- /dev/null +++ b/src/khoj/interface/web/assets/icons/key.svg @@ -0,0 +1,4 @@ + + + + From 9cebd7f856bf97ae5226901ad3bfbbf574018507 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 31 Oct 2023 22:38:44 -0700 Subject: [PATCH 011/194] Add emoji icons to Search, Chat, Settings items in nav menu of Web client Emoji icons have already been added to the Search, Chat and Settings top navigation menu in the desktop client. This change adds these to the web client as well --- src/khoj/interface/web/base_config.html | 6 +++--- src/khoj/interface/web/chat.html | 6 +++--- src/khoj/interface/web/index.html | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index db77787b..b2748b6d 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -16,9 +16,9 @@
diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 2230e901..5e3f23b4 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -281,10 +281,10 @@ {% endif %}
diff --git a/src/khoj/interface/web/index.html b/src/khoj/interface/web/index.html index 539c96e0..d137f458 100644 --- a/src/khoj/interface/web/index.html +++ b/src/khoj/interface/web/index.html @@ -293,10 +293,10 @@ {% endif %}
From 58a7171911e39eefabb8c9b6b1c7c6994bbd1dd0 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 31 Oct 2023 23:10:26 -0700 Subject: [PATCH 012/194] Show truncated API key for identification & restrict table width - Use a function to generate API Key table row HTML, to dedup logic - Show delete, copy icon hints on hover - Reduce length of copied message to not expand table width - Truncating API key helps keep the API key table width within width of smaller width displays --- src/khoj/interface/web/config.html | 44 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index c65615e1..0fc7d8e9 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -541,16 +541,7 @@ }) .then(response => response.json()) .then(tokenObj => { - apiKeyList.innerHTML += ` - - ${tokenObj.name} - ${tokenObj.token} - - Copy API Key - Delete API Key - - - `; + apiKeyList.innerHTML += generateTokenRow(tokenObj); }); } @@ -561,7 +552,7 @@ const copyApiKeyButton = document.getElementById(`api-key-${token}`); original_html = copyApiKeyButton.innerHTML setTimeout(function() { - copyApiKeyButton.innerHTML = "✅ Copied to your clipboard!"; + copyApiKeyButton.innerHTML = "✅ Copied!"; setTimeout(function() { copyApiKeyButton.innerHTML = original_html; }, 1000); @@ -581,23 +572,30 @@ }); } + function generateTokenRow(tokenObj) { + let token = tokenObj.token; + let tokenName = tokenObj.name; + let truncatedToken = token.slice(0, 4) + "..." + token.slice(-4); + let tokenId = `${tokenName}-${truncatedToken}`; + return ` + + ${tokenName} + ${truncatedToken} + + Copy API Key + Delete API Key + + + `; + + } + function listApiKeys() { const apiKeyList = document.getElementById("api-key-list"); fetch('/auth/token') .then(response => response.json()) .then(tokens => { - apiKeyList.innerHTML = tokens.map(tokenObj => - ` - - ${tokenObj.name} - ${tokenObj.token} - - Copy API Key - Delete API Key - - - `) - .join(""); + apiKeyList.innerHTML = tokens.map(generateTokenRow).join(""); }); } From f585a71744d4b539202a8d140f216e81a57c5355 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Wed, 1 Nov 2023 01:59:36 -0700 Subject: [PATCH 013/194] Put logout, settings under dropdown menu with logged in user's profile picture - Create dropdown menu. Put settings page, logout action under it - Make user's profile picture the dropdown menu heading - Create khoj.js to store shared js across web client It currently stores the dropdown menu open, close functionality - Put shared styling for khoj dropdown menu under khoj.css --- src/khoj/interface/web/assets/khoj.css | 59 ++++++++++++++++++++++ src/khoj/interface/web/assets/khoj.js | 15 ++++++ src/khoj/interface/web/base_config.html | 11 ++++- src/khoj/interface/web/chat.html | 10 +++- src/khoj/interface/web/config.html | 10 ---- src/khoj/interface/web/index.html | 11 ++++- src/khoj/routers/web_client.py | 65 +++++++++++++++++++++++-- 7 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 src/khoj/interface/web/assets/khoj.js diff --git a/src/khoj/interface/web/assets/khoj.css b/src/khoj/interface/web/assets/khoj.css index a84d562f..616c0543 100644 --- a/src/khoj/interface/web/assets/khoj.css +++ b/src/khoj/interface/web/assets/khoj.css @@ -100,6 +100,65 @@ p#khoj-banner { display: inline; } +/* Dropdown in navigation menu*/ +.khoj-nav-dropdown-content { + display: block; + grid-auto-flow: row; + position: absolute; + background-color: var(--background-color); + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + right: 15vw; + z-index: 1; + opacity: 0; + transition: opacity 0.1s ease-in-out; + pointer-events: none; + text-align: left; +} +.khoj-nav-dropdown-content.show { + opacity: 1; + pointer-events: auto; +} +.khoj-nav-dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} +.khoj-nav-dropdown-content a:hover { + background-color: var(--primary-hover); +} +.khoj-nav-username { + padding: 12px 16px; + text-decoration: none; + display: block; + font-weight: bold; +} +img.circle { + border-radius: 50%; + border: 2px solid var(--primary-hover); + width: 40px; + height: 40px; + vertical-align: text-top; +} + + +@media screen and (max-width: 700px) { + .khoj-nav-dropdown-content { + display: block; + grid-auto-flow: row; + position: absolute; + background-color: var(--background-color); + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + right: 10px; + z-index: 1; + opacity: 0; + transition: opacity 0.1s ease-in-out; + pointer-events: none; + } +} + @media only screen and (max-width: 600px) { div.khoj-header { display: grid; diff --git a/src/khoj/interface/web/assets/khoj.js b/src/khoj/interface/web/assets/khoj.js new file mode 100644 index 00000000..b4d29a1f --- /dev/null +++ b/src/khoj/interface/web/assets/khoj.js @@ -0,0 +1,15 @@ +// Toggle the navigation menu +function toggleMenu() { + var menu = document.getElementById("khoj-nav-menu"); + menu.classList.toggle("show"); +} + +// Close the dropdown menu if the user clicks outside of it +document.addEventListener('click', function(event) { + let menu = document.getElementById("khoj-nav-menu"); + let menuContainer = document.getElementById("khoj-nav-menu-container"); + let isClickOnMenu = menuContainer.contains(event.target) || menuContainer === event.target; + if (isClickOnMenu === false && menu.classList.contains("show")) { + menu.classList.remove("show"); + } +}); diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index b2748b6d..41347141 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -8,6 +8,7 @@ +
@@ -18,7 +19,15 @@
diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 5e3f23b4..d9cd80d4 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -8,6 +8,7 @@ + + - @@ -38,11 +36,11 @@
Khoj Access Key

- Access Key + API Key

- +
@@ -131,7 +129,7 @@ body, input { padding: 0px; margin: 0px; - background: #fff; + background: var(--background-color); color: #475569; font-family: roboto, karma, segoe ui, sans-serif; font-size: small; @@ -191,7 +189,7 @@ gap: 8px; padding: 24px 16px; width: 450px; - background: white; + background: var(--background-color); 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); @@ -259,7 +257,7 @@ } .primary-button { border: none; - color: white; + color: var(--background-color); padding: 15px 32px; text-align: center; text-decoration: none; @@ -268,7 +266,7 @@ } button.card-button.disabled { - color: rgb(255, 136, 136); + color: var(--flower); background: transparent; font-size: small; cursor: pointer; @@ -280,11 +278,7 @@ } button.card-button.happy { - color: rgb(0, 146, 0); - } - - button.card-button.happy { - color: rgb(0, 146, 0); + color: var(--leaf); } img.configured-icon { @@ -308,7 +302,9 @@ div.folder-element { display: grid; grid-template-columns: auto 1fr; - box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2); + 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.8); padding: 4px; margin-bottom: 8px; } @@ -326,7 +322,7 @@ background-color: rgb(253 214 214); border-radius: 3px; border: none; - color: rgb(207, 67, 59); + color: var(--flower); padding: 4px; } @@ -335,14 +331,14 @@ background-color: rgb(255 235 235); border-radius: 3px; border: none; - color: rgb(207, 67, 59); + color: var(--flower); padding: 4px; cursor: pointer; } #sync-data { - background-color: #ffb300; + background-color: var(--primary); border: none; - color: white; + color: var(--main-text-color); padding: 12px; text-align: center; text-decoration: none; @@ -351,12 +347,12 @@ border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; - box-shadow: 0px 5px 0px #f9f5de; + box-shadow: 0px 5px 0px var(--background-color); } #sync-data:hover { - background-color: #ffcc00; - box-shadow: 0px 3px 0px #f9f5de; + background-color: var(--primary-hover); + box-shadow: 0px 3px 0px var(--background-color); } .sync-force-toggle { align-content: center; diff --git a/src/interface/desktop/search.html b/src/interface/desktop/search.html index 9ad0ea87..dbb18f5f 100644 --- a/src/interface/desktop/search.html +++ b/src/interface/desktop/search.html @@ -302,7 +302,7 @@ body { padding: 0px; margin: 0px; - background: #fff; + background: var(--background-color); color: #475569; font-family: roboto, karma, segoe ui, sans-serif; font-size: small; diff --git a/src/interface/obsidian/styles.css b/src/interface/obsidian/styles.css index 3e3808f7..d322804d 100644 --- a/src/interface/obsidian/styles.css +++ b/src/interface/obsidian/styles.css @@ -8,7 +8,7 @@ If your plugin does not need CSS, delete this file. */ :root { - --khoj-chat-primary: #ffb300; + --khoj-chat-primary: #fee285; --khoj-chat-dark-grey: #475569; } diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index cba8c20c..5b98ecc2 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -63,7 +63,7 @@ background: var(--background-color); 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); + box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.1),0px 1px 2px -1px rgba(0,0,0,0.8); } #api-settings-card-description { margin: 8px 0 0 0; From 126d3f45634e1653d4ce4e70cfc2f1c4e4088441 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 3 Nov 2023 00:16:11 -0700 Subject: [PATCH 035/194] Render each file, folder to index row with icon in desktop app Make the file, folders to index look less like an editable field --- src/interface/desktop/config.html | 4 ++-- src/interface/desktop/renderer.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/interface/desktop/config.html b/src/interface/desktop/config.html index af6171bf..3f901f3e 100644 --- a/src/interface/desktop/config.html +++ b/src/interface/desktop/config.html @@ -179,7 +179,7 @@ .card-input { padding: 4px; - box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.2); + box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.3); border: none; width: 450px; } @@ -301,7 +301,7 @@ div.file-element, div.folder-element { display: grid; - grid-template-columns: auto 1fr; + grid-template-columns: 1fr auto 1fr; 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.8); diff --git a/src/interface/desktop/renderer.js b/src/interface/desktop/renderer.js index b365ceff..3d8a1d4e 100644 --- a/src/interface/desktop/renderer.js +++ b/src/interface/desktop/renderer.js @@ -61,6 +61,13 @@ toggleFoldersButton.addEventListener('click', () => { function makeFileElement(file) { let fileElement = document.createElement("div"); fileElement.classList.add("file-element"); + + let fileIconElement = document.createElement("img"); + fileIconElement.classList.add("card-icon"); + fileIconElement.src = "./assets/icons/plaintext.svg"; + fileIconElement.alt = "File"; + fileElement.appendChild(fileIconElement); + let fileNameElement = document.createElement("div"); fileNameElement.classList.add("content-name"); fileNameElement.innerHTML = file.path; @@ -82,6 +89,13 @@ function makeFileElement(file) { function makeFolderElement(folder) { let folderElement = document.createElement("div"); folderElement.classList.add("folder-element"); + + let folderIconElement = document.createElement("img"); + folderIconElement.classList.add("card-icon"); + folderIconElement.src = "./assets/icons/folder.svg"; + folderIconElement.alt = "File"; + folderElement.appendChild(folderIconElement); + let folderNameElement = document.createElement("div"); folderNameElement.classList.add("content-name"); folderNameElement.innerHTML = folder.path; From 34661c33a25720a36d6215bc7f68d9b6277f18d7 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 3 Nov 2023 02:46:39 -0700 Subject: [PATCH 036/194] Show splash screen on starting desktop app --- src/interface/desktop/assets/three.min.js | 991 +++++++++++++++++++++ src/interface/desktop/loading-animation.js | 129 +++ src/interface/desktop/main.js | 22 +- src/interface/desktop/splash.html | 15 + 4 files changed, 1156 insertions(+), 1 deletion(-) create mode 100644 src/interface/desktop/assets/three.min.js create mode 100644 src/interface/desktop/loading-animation.js create mode 100644 src/interface/desktop/splash.html diff --git a/src/interface/desktop/assets/three.min.js b/src/interface/desktop/assets/three.min.js new file mode 100644 index 00000000..57018496 --- /dev/null +++ b/src/interface/desktop/assets/three.min.js @@ -0,0 +1,991 @@ +// threejs.org/license +'use strict';var THREE={REVISION:"77"};"function"===typeof define&&define.amd?define("three",THREE):"undefined"!==typeof exports&&"undefined"!==typeof module&&(module.exports=THREE);void 0===Number.EPSILON&&(Number.EPSILON=Math.pow(2,-52));void 0===Math.sign&&(Math.sign=function(a){return 0>a?-1:0>16&255)/255;this.g=(a>>8&255)/255;this.b=(a&255)/255;return this},setRGB:function(a,b,c){this.r=a;this.g=b;this.b=c;return this},setHSL:function(){function a(a,c,d){0>d&&(d+=1);1d?c:d<2/3?a+6*(c-a)*(2/3-d):a}return function(b,c,d){b=THREE.Math.euclideanModulo(b,1);c=THREE.Math.clamp(c,0,1);d=THREE.Math.clamp(d,0,1);0===c?this.r=this.g=this.b=d:(c=.5>=d?d*(1+c):d+c-d*c,d=2*d-c,this.r=a(d,c,b+1/3),this.g=a(d,c,b),this.b=a(d,c,b-1/3));return this}}(),setStyle:function(a){function b(b){void 0!==b&&1>parseFloat(b)&&console.warn("THREE.Color: Alpha component of "+a+" will be ignored.")}var c;if(c=/^((?:rgb|hsl)a?)\(\s*([^\)]*)\)/.exec(a)){var d=c[2];switch(c[1]){case "rgb":case "rgba":if(c= +/^(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(,\s*([0-9]*\.?[0-9]+)\s*)?$/.exec(d))return this.r=Math.min(255,parseInt(c[1],10))/255,this.g=Math.min(255,parseInt(c[2],10))/255,this.b=Math.min(255,parseInt(c[3],10))/255,b(c[5]),this;if(c=/^(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(,\s*([0-9]*\.?[0-9]+)\s*)?$/.exec(d))return this.r=Math.min(100,parseInt(c[1],10))/100,this.g=Math.min(100,parseInt(c[2],10))/100,this.b=Math.min(100,parseInt(c[3],10))/100,b(c[5]),this;break;case "hsl":case "hsla":if(c=/^([0-9]*\.?[0-9]+)\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(,\s*([0-9]*\.?[0-9]+)\s*)?$/.exec(d)){var d= +parseFloat(c[1])/360,e=parseInt(c[2],10)/100,f=parseInt(c[3],10)/100;b(c[5]);return this.setHSL(d,e,f)}}}else if(c=/^\#([A-Fa-f0-9]+)$/.exec(a)){c=c[1];d=c.length;if(3===d)return this.r=parseInt(c.charAt(0)+c.charAt(0),16)/255,this.g=parseInt(c.charAt(1)+c.charAt(1),16)/255,this.b=parseInt(c.charAt(2)+c.charAt(2),16)/255,this;if(6===d)return this.r=parseInt(c.charAt(0)+c.charAt(1),16)/255,this.g=parseInt(c.charAt(2)+c.charAt(3),16)/255,this.b=parseInt(c.charAt(4)+c.charAt(5),16)/255,this}a&&0=h?k/(e+f):k/(2-e-f);switch(e){case b:g=(c-d)/k+(cf&&c>b?(c=2*Math.sqrt(1+c-f-b),this._w=(k-g)/c,this._x=.25*c,this._y=(a+e)/c,this._z=(d+h)/c):f>b?(c=2*Math.sqrt(1+f-c-b),this._w=(d-h)/c,this._x=(a+e)/c,this._y= +.25*c,this._z=(g+k)/c):(c=2*Math.sqrt(1+b-c-f),this._w=(e-a)/c,this._x=(d+h)/c,this._y=(g+k)/c,this._z=.25*c);this.onChangeCallback();return this},setFromUnitVectors:function(){var a,b;return function(c,d){void 0===a&&(a=new THREE.Vector3);b=c.dot(d)+1;1E-6>b?(b=0,Math.abs(c.x)>Math.abs(c.z)?a.set(-c.y,c.x,0):a.set(0,-c.z,c.y)):a.crossVectors(c,d);this._x=a.x;this._y=a.y;this._z=a.z;this._w=b;return this.normalize()}}(),inverse:function(){return this.conjugate().normalize()},conjugate:function(){this._x*= +-1;this._y*=-1;this._z*=-1;this.onChangeCallback();return this},dot:function(a){return this._x*a._x+this._y*a._y+this._z*a._z+this._w*a._w},lengthSq:function(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w},length:function(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)},normalize:function(){var a=this.length();0===a?(this._z=this._y=this._x=0,this._w=1):(a=1/a,this._x*=a,this._y*=a,this._z*=a,this._w*=a);this.onChangeCallback();return this}, +multiply:function(a,b){return void 0!==b?(console.warn("THREE.Quaternion: .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead."),this.multiplyQuaternions(a,b)):this.multiplyQuaternions(this,a)},premultiply:function(a){return this.multiplyQuaternions(a,this)},multiplyQuaternions:function(a,b){var c=a._x,d=a._y,e=a._z,f=a._w,g=b._x,h=b._y,k=b._z,l=b._w;this._x=c*l+f*g+d*k-e*h;this._y=d*l+f*h+e*g-c*k;this._z=e*l+f*k+c*h-d*g;this._w=f*l-c*g-d*h-e*k;this.onChangeCallback(); +return this},slerp:function(a,b){if(0===b)return this;if(1===b)return this.copy(a);var c=this._x,d=this._y,e=this._z,f=this._w,g=f*a._w+c*a._x+d*a._y+e*a._z;0>g?(this._w=-a._w,this._x=-a._x,this._y=-a._y,this._z=-a._z,g=-g):this.copy(a);if(1<=g)return this._w=f,this._x=c,this._y=d,this._z=e,this;var h=Math.sqrt(1-g*g);if(.001>Math.abs(h))return this._w=.5*(f+this._w),this._x=.5*(c+this._x),this._y=.5*(d+this._y),this._z=.5*(e+this._z),this;var k=Math.atan2(h,g),g=Math.sin((1-b)*k)/h,h=Math.sin(b* +k)/h;this._w=f*g+this._w*h;this._x=c*g+this._x*h;this._y=d*g+this._y*h;this._z=e*g+this._z*h;this.onChangeCallback();return this},equals:function(a){return a._x===this._x&&a._y===this._y&&a._z===this._z&&a._w===this._w},fromArray:function(a,b){void 0===b&&(b=0);this._x=a[b];this._y=a[b+1];this._z=a[b+2];this._w=a[b+3];this.onChangeCallback();return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this._x;a[b+1]=this._y;a[b+2]=this._z;a[b+3]=this._w;return a},onChange:function(a){this.onChangeCallback= +a;return this},onChangeCallback:function(){}}; +Object.assign(THREE.Quaternion,{slerp:function(a,b,c,d){return c.copy(a).slerp(b,d)},slerpFlat:function(a,b,c,d,e,f,g){var h=c[d+0],k=c[d+1],l=c[d+2];c=c[d+3];d=e[f+0];var n=e[f+1],p=e[f+2];e=e[f+3];if(c!==e||h!==d||k!==n||l!==p){f=1-g;var m=h*d+k*n+l*p+c*e,q=0<=m?1:-1,r=1-m*m;r>Number.EPSILON&&(r=Math.sqrt(r),m=Math.atan2(r,m*q),f=Math.sin(f*m)/r,g=Math.sin(g*m)/r);q*=g;h=h*f+d*q;k=k*f+n*q;l=l*f+p*q;c=c*f+e*q;f===1-g&&(g=1/Math.sqrt(h*h+k*k+l*l+c*c),h*=g,k*=g,l*=g,c*=g)}a[b]=h;a[b+1]=k;a[b+2]=l; +a[b+3]=c}});THREE.Vector2=function(a,b){this.x=a||0;this.y=b||0}; +THREE.Vector2.prototype={constructor:THREE.Vector2,get width(){return this.x},set width(a){this.x=a},get height(){return this.y},set height(a){this.y=a},set:function(a,b){this.x=a;this.y=b;return this},setScalar:function(a){this.y=this.x=a;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x; +case 1:return this.y;default:throw Error("index is out of range: "+a);}},clone:function(){return new this.constructor(this.x,this.y)},copy:function(a){this.x=a.x;this.y=a.y;return this},add:function(a,b){if(void 0!==b)return console.warn("THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;return this},addScalar:function(a){this.x+=a;this.y+=a;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;return this}, +addScaledVector:function(a,b){this.x+=a.x*b;this.y+=a.y*b;return this},sub:function(a,b){if(void 0!==b)return console.warn("THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-=a.x;this.y-=a.y;return this},subScalar:function(a){this.x-=a;this.y-=a;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;return this},multiply:function(a){this.x*=a.x;this.y*=a.y;return this},multiplyScalar:function(a){isFinite(a)?(this.x*=a, +this.y*=a):this.y=this.x=0;return this},divide:function(a){this.x/=a.x;this.y/=a.y;return this},divideScalar:function(a){return this.multiplyScalar(1/a)},min:function(a){this.x=Math.min(this.x,a.x);this.y=Math.min(this.y,a.y);return this},max:function(a){this.x=Math.max(this.x,a.x);this.y=Math.max(this.y,a.y);return this},clamp:function(a,b){this.x=Math.max(a.x,Math.min(b.x,this.x));this.y=Math.max(a.y,Math.min(b.y,this.y));return this},clampScalar:function(){var a,b;return function(c,d){void 0=== +a&&(a=new THREE.Vector2,b=new THREE.Vector2);a.set(c,c);b.set(d,d);return this.clamp(a,b)}}(),clampLength:function(a,b){var c=this.length();return this.multiplyScalar(Math.max(a,Math.min(b,c))/c)},floor:function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);return this},ceil:function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);return this},round:function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this},roundToZero:function(){this.x=0>this.x?Math.ceil(this.x):Math.floor(this.x); +this.y=0>this.y?Math.ceil(this.y):Math.floor(this.y);return this},negate:function(){this.x=-this.x;this.y=-this.y;return this},dot:function(a){return this.x*a.x+this.y*a.y},lengthSq:function(){return this.x*this.x+this.y*this.y},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)},normalize:function(){return this.divideScalar(this.length())},angle:function(){var a=Math.atan2(this.y,this.x);0>a&&(a+=2*Math.PI);return a}, +distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x;a=this.y-a.y;return b*b+a*a},setLength:function(a){return this.multiplyScalar(a/this.length())},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;return this},lerpVectors:function(a,b,c){return this.subVectors(b,a).multiplyScalar(c).add(a)},equals:function(a){return a.x===this.x&&a.y===this.y},fromArray:function(a,b){void 0===b&&(b=0);this.x=a[b];this.y=a[b+1];return this}, +toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this.x;a[b+1]=this.y;return a},fromAttribute:function(a,b,c){void 0===c&&(c=0);b=b*a.itemSize+c;this.x=a.array[b];this.y=a.array[b+1];return this},rotateAround:function(a,b){var c=Math.cos(b),d=Math.sin(b),e=this.x-a.x,f=this.y-a.y;this.x=e*c-f*d+a.x;this.y=e*d+f*c+a.y;return this}};THREE.Vector3=function(a,b,c){this.x=a||0;this.y=b||0;this.z=c||0}; +THREE.Vector3.prototype={constructor:THREE.Vector3,set:function(a,b,c){this.x=a;this.y=b;this.z=c;return this},setScalar:function(a){this.z=this.y=this.x=a;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setZ:function(a){this.z=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y; +case 2:return this.z;default:throw Error("index is out of range: "+a);}},clone:function(){return new this.constructor(this.x,this.y,this.z)},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;return this},add:function(a,b){if(void 0!==b)return console.warn("THREE.Vector3: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;this.z+=a.z;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;return this},addVectors:function(a, +b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;return this},addScaledVector:function(a,b){this.x+=a.x*b;this.y+=a.y*b;this.z+=a.z*b;return this},sub:function(a,b){if(void 0!==b)return console.warn("THREE.Vector3: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-=a.x;this.y-=a.y;this.z-=a.z;return this},subScalar:function(a){this.x-=a;this.y-=a;this.z-=a;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;return this}, +multiply:function(a,b){if(void 0!==b)return console.warn("THREE.Vector3: .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead."),this.multiplyVectors(a,b);this.x*=a.x;this.y*=a.y;this.z*=a.z;return this},multiplyScalar:function(a){isFinite(a)?(this.x*=a,this.y*=a,this.z*=a):this.z=this.y=this.x=0;return this},multiplyVectors:function(a,b){this.x=a.x*b.x;this.y=a.y*b.y;this.z=a.z*b.z;return this},applyEuler:function(){var a;return function(b){!1===b instanceof THREE.Euler&& +console.error("THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.");void 0===a&&(a=new THREE.Quaternion);return this.applyQuaternion(a.setFromEuler(b))}}(),applyAxisAngle:function(){var a;return function(b,c){void 0===a&&(a=new THREE.Quaternion);return this.applyQuaternion(a.setFromAxisAngle(b,c))}}(),applyMatrix3:function(a){var b=this.x,c=this.y,d=this.z;a=a.elements;this.x=a[0]*b+a[3]*c+a[6]*d;this.y=a[1]*b+a[4]*c+a[7]*d;this.z=a[2]*b+a[5]*c+a[8]*d;return this}, +applyMatrix4:function(a){var b=this.x,c=this.y,d=this.z;a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d+a[12];this.y=a[1]*b+a[5]*c+a[9]*d+a[13];this.z=a[2]*b+a[6]*c+a[10]*d+a[14];return this},applyProjection:function(a){var b=this.x,c=this.y,d=this.z;a=a.elements;var e=1/(a[3]*b+a[7]*c+a[11]*d+a[15]);this.x=(a[0]*b+a[4]*c+a[8]*d+a[12])*e;this.y=(a[1]*b+a[5]*c+a[9]*d+a[13])*e;this.z=(a[2]*b+a[6]*c+a[10]*d+a[14])*e;return this},applyQuaternion:function(a){var b=this.x,c=this.y,d=this.z,e=a.x,f=a.y,g=a.z;a= +a.w;var h=a*b+f*d-g*c,k=a*c+g*b-e*d,l=a*d+e*c-f*b,b=-e*b-f*c-g*d;this.x=h*a+b*-e+k*-g-l*-f;this.y=k*a+b*-f+l*-e-h*-g;this.z=l*a+b*-g+h*-f-k*-e;return this},project:function(){var a;return function(b){void 0===a&&(a=new THREE.Matrix4);a.multiplyMatrices(b.projectionMatrix,a.getInverse(b.matrixWorld));return this.applyProjection(a)}}(),unproject:function(){var a;return function(b){void 0===a&&(a=new THREE.Matrix4);a.multiplyMatrices(b.matrixWorld,a.getInverse(b.projectionMatrix));return this.applyProjection(a)}}(), +transformDirection:function(a){var b=this.x,c=this.y,d=this.z;a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d;this.y=a[1]*b+a[5]*c+a[9]*d;this.z=a[2]*b+a[6]*c+a[10]*d;return this.normalize()},divide:function(a){this.x/=a.x;this.y/=a.y;this.z/=a.z;return this},divideScalar:function(a){return this.multiplyScalar(1/a)},min:function(a){this.x=Math.min(this.x,a.x);this.y=Math.min(this.y,a.y);this.z=Math.min(this.z,a.z);return this},max:function(a){this.x=Math.max(this.x,a.x);this.y=Math.max(this.y,a.y);this.z= +Math.max(this.z,a.z);return this},clamp:function(a,b){this.x=Math.max(a.x,Math.min(b.x,this.x));this.y=Math.max(a.y,Math.min(b.y,this.y));this.z=Math.max(a.z,Math.min(b.z,this.z));return this},clampScalar:function(){var a,b;return function(c,d){void 0===a&&(a=new THREE.Vector3,b=new THREE.Vector3);a.set(c,c,c);b.set(d,d,d);return this.clamp(a,b)}}(),clampLength:function(a,b){var c=this.length();return this.multiplyScalar(Math.max(a,Math.min(b,c))/c)},floor:function(){this.x=Math.floor(this.x);this.y= +Math.floor(this.y);this.z=Math.floor(this.z);return this},ceil:function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);this.z=Math.ceil(this.z);return this},round:function(){this.x=Math.round(this.x);this.y=Math.round(this.y);this.z=Math.round(this.z);return this},roundToZero:function(){this.x=0>this.x?Math.ceil(this.x):Math.floor(this.x);this.y=0>this.y?Math.ceil(this.y):Math.floor(this.y);this.z=0>this.z?Math.ceil(this.z):Math.floor(this.z);return this},negate:function(){this.x=-this.x;this.y= +-this.y;this.z=-this.z;return this},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)},normalize:function(){return this.divideScalar(this.length())},setLength:function(a){return this.multiplyScalar(a/this.length())},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+= +(a.y-this.y)*b;this.z+=(a.z-this.z)*b;return this},lerpVectors:function(a,b,c){return this.subVectors(b,a).multiplyScalar(c).add(a)},cross:function(a,b){if(void 0!==b)return console.warn("THREE.Vector3: .cross() now only accepts one argument. Use .crossVectors( a, b ) instead."),this.crossVectors(a,b);var c=this.x,d=this.y,e=this.z;this.x=d*a.z-e*a.y;this.y=e*a.x-c*a.z;this.z=c*a.y-d*a.x;return this},crossVectors:function(a,b){var c=a.x,d=a.y,e=a.z,f=b.x,g=b.y,h=b.z;this.x=d*h-e*g;this.y=e*f-c*h; +this.z=c*g-d*f;return this},projectOnVector:function(){var a,b;return function(c){void 0===a&&(a=new THREE.Vector3);a.copy(c).normalize();b=this.dot(a);return this.copy(a).multiplyScalar(b)}}(),projectOnPlane:function(){var a;return function(b){void 0===a&&(a=new THREE.Vector3);a.copy(this).projectOnVector(b);return this.sub(a)}}(),reflect:function(){var a;return function(b){void 0===a&&(a=new THREE.Vector3);return this.sub(a.copy(b).multiplyScalar(2*this.dot(b)))}}(),angleTo:function(a){a=this.dot(a)/ +Math.sqrt(this.lengthSq()*a.lengthSq());return Math.acos(THREE.Math.clamp(a,-1,1))},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x,c=this.y-a.y;a=this.z-a.z;return b*b+c*c+a*a},setFromSpherical:function(a){var b=Math.sin(a.phi)*a.radius;this.x=b*Math.sin(a.theta);this.y=Math.cos(a.phi)*a.radius;this.z=b*Math.cos(a.theta);return this},setFromMatrixPosition:function(a){return this.setFromMatrixColumn(a,3)},setFromMatrixScale:function(a){var b= +this.setFromMatrixColumn(a,0).length(),c=this.setFromMatrixColumn(a,1).length();a=this.setFromMatrixColumn(a,2).length();this.x=b;this.y=c;this.z=a;return this},setFromMatrixColumn:function(a,b){if("number"===typeof a){console.warn("THREE.Vector3: setFromMatrixColumn now expects ( matrix, index ).");var c=a;a=b;b=c}return this.fromArray(a.elements,4*b)},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z},fromArray:function(a,b){void 0===b&&(b=0);this.x=a[b];this.y=a[b+1];this.z=a[b+ +2];return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this.x;a[b+1]=this.y;a[b+2]=this.z;return a},fromAttribute:function(a,b,c){void 0===c&&(c=0);b=b*a.itemSize+c;this.x=a.array[b];this.y=a.array[b+1];this.z=a.array[b+2];return this}};THREE.Vector4=function(a,b,c,d){this.x=a||0;this.y=b||0;this.z=c||0;this.w=void 0!==d?d:1}; +THREE.Vector4.prototype={constructor:THREE.Vector4,set:function(a,b,c,d){this.x=a;this.y=b;this.z=c;this.w=d;return this},setScalar:function(a){this.w=this.z=this.y=this.x=a;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setZ:function(a){this.z=a;return this},setW:function(a){this.w=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;case 3:this.w=b;break;default:throw Error("index is out of range: "+ +a);}},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw Error("index is out of range: "+a);}},clone:function(){return new this.constructor(this.x,this.y,this.z,this.w)},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;this.w=void 0!==a.w?a.w:1;return this},add:function(a,b){if(void 0!==b)return console.warn("THREE.Vector4: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b); +this.x+=a.x;this.y+=a.y;this.z+=a.z;this.w+=a.w;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;this.w+=a;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;this.w=a.w+b.w;return this},addScaledVector:function(a,b){this.x+=a.x*b;this.y+=a.y*b;this.z+=a.z*b;this.w+=a.w*b;return this},sub:function(a,b){if(void 0!==b)return console.warn("THREE.Vector4: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-= +a.x;this.y-=a.y;this.z-=a.z;this.w-=a.w;return this},subScalar:function(a){this.x-=a;this.y-=a;this.z-=a;this.w-=a;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;this.w=a.w-b.w;return this},multiplyScalar:function(a){isFinite(a)?(this.x*=a,this.y*=a,this.z*=a,this.w*=a):this.w=this.z=this.y=this.x=0;return this},applyMatrix4:function(a){var b=this.x,c=this.y,d=this.z,e=this.w;a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d+a[12]*e;this.y=a[1]*b+a[5]*c+a[9]*d+a[13]*e;this.z= +a[2]*b+a[6]*c+a[10]*d+a[14]*e;this.w=a[3]*b+a[7]*c+a[11]*d+a[15]*e;return this},divideScalar:function(a){return this.multiplyScalar(1/a)},setAxisAngleFromQuaternion:function(a){this.w=2*Math.acos(a.w);var b=Math.sqrt(1-a.w*a.w);1E-4>b?(this.x=1,this.z=this.y=0):(this.x=a.x/b,this.y=a.y/b,this.z=a.z/b);return this},setAxisAngleFromRotationMatrix:function(a){var b,c,d;a=a.elements;var e=a[0];d=a[4];var f=a[8],g=a[1],h=a[5],k=a[9];c=a[2];b=a[6];var l=a[10];if(.01>Math.abs(d-g)&&.01>Math.abs(f-c)&&.01> +Math.abs(k-b)){if(.1>Math.abs(d+g)&&.1>Math.abs(f+c)&&.1>Math.abs(k+b)&&.1>Math.abs(e+h+l-3))return this.set(1,0,0,0),this;a=Math.PI;e=(e+1)/2;h=(h+1)/2;l=(l+1)/2;d=(d+g)/4;f=(f+c)/4;k=(k+b)/4;e>h&&e>l?.01>e?(b=0,d=c=.707106781):(b=Math.sqrt(e),c=d/b,d=f/b):h>l?.01>h?(b=.707106781,c=0,d=.707106781):(c=Math.sqrt(h),b=d/c,d=k/c):.01>l?(c=b=.707106781,d=0):(d=Math.sqrt(l),b=f/d,c=k/d);this.set(b,c,d,a);return this}a=Math.sqrt((b-k)*(b-k)+(f-c)*(f-c)+(g-d)*(g-d));.001>Math.abs(a)&&(a=1);this.x=(b-k)/ +a;this.y=(f-c)/a;this.z=(g-d)/a;this.w=Math.acos((e+h+l-1)/2);return this},min:function(a){this.x=Math.min(this.x,a.x);this.y=Math.min(this.y,a.y);this.z=Math.min(this.z,a.z);this.w=Math.min(this.w,a.w);return this},max:function(a){this.x=Math.max(this.x,a.x);this.y=Math.max(this.y,a.y);this.z=Math.max(this.z,a.z);this.w=Math.max(this.w,a.w);return this},clamp:function(a,b){this.x=Math.max(a.x,Math.min(b.x,this.x));this.y=Math.max(a.y,Math.min(b.y,this.y));this.z=Math.max(a.z,Math.min(b.z,this.z)); +this.w=Math.max(a.w,Math.min(b.w,this.w));return this},clampScalar:function(){var a,b;return function(c,d){void 0===a&&(a=new THREE.Vector4,b=new THREE.Vector4);a.set(c,c,c,c);b.set(d,d,d,d);return this.clamp(a,b)}}(),floor:function(){this.x=Math.floor(this.x);this.y=Math.floor(this.y);this.z=Math.floor(this.z);this.w=Math.floor(this.w);return this},ceil:function(){this.x=Math.ceil(this.x);this.y=Math.ceil(this.y);this.z=Math.ceil(this.z);this.w=Math.ceil(this.w);return this},round:function(){this.x= +Math.round(this.x);this.y=Math.round(this.y);this.z=Math.round(this.z);this.w=Math.round(this.w);return this},roundToZero:function(){this.x=0>this.x?Math.ceil(this.x):Math.floor(this.x);this.y=0>this.y?Math.ceil(this.y):Math.floor(this.y);this.z=0>this.z?Math.ceil(this.z):Math.floor(this.z);this.w=0>this.w?Math.ceil(this.w):Math.floor(this.w);return this},negate:function(){this.x=-this.x;this.y=-this.y;this.z=-this.z;this.w=-this.w;return this},dot:function(a){return this.x*a.x+this.y*a.y+this.z* +a.z+this.w*a.w},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)},normalize:function(){return this.divideScalar(this.length())},setLength:function(a){return this.multiplyScalar(a/this.length())},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;this.z+=(a.z- +this.z)*b;this.w+=(a.w-this.w)*b;return this},lerpVectors:function(a,b,c){return this.subVectors(b,a).multiplyScalar(c).add(a)},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z&&a.w===this.w},fromArray:function(a,b){void 0===b&&(b=0);this.x=a[b];this.y=a[b+1];this.z=a[b+2];this.w=a[b+3];return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this.x;a[b+1]=this.y;a[b+2]=this.z;a[b+3]=this.w;return a},fromAttribute:function(a,b,c){void 0===c&&(c=0);b=b*a.itemSize+ +c;this.x=a.array[b];this.y=a.array[b+1];this.z=a.array[b+2];this.w=a.array[b+3];return this}};THREE.Euler=function(a,b,c,d){this._x=a||0;this._y=b||0;this._z=c||0;this._order=d||THREE.Euler.DefaultOrder};THREE.Euler.RotationOrders="XYZ YZX ZXY XZY YXZ ZYX".split(" ");THREE.Euler.DefaultOrder="XYZ"; +THREE.Euler.prototype={constructor:THREE.Euler,get x(){return this._x},set x(a){this._x=a;this.onChangeCallback()},get y(){return this._y},set y(a){this._y=a;this.onChangeCallback()},get z(){return this._z},set z(a){this._z=a;this.onChangeCallback()},get order(){return this._order},set order(a){this._order=a;this.onChangeCallback()},set:function(a,b,c,d){this._x=a;this._y=b;this._z=c;this._order=d||this._order;this.onChangeCallback();return this},clone:function(){return new this.constructor(this._x, +this._y,this._z,this._order)},copy:function(a){this._x=a._x;this._y=a._y;this._z=a._z;this._order=a._order;this.onChangeCallback();return this},setFromRotationMatrix:function(a,b,c){var d=THREE.Math.clamp,e=a.elements;a=e[0];var f=e[4],g=e[8],h=e[1],k=e[5],l=e[9],n=e[2],p=e[6],e=e[10];b=b||this._order;"XYZ"===b?(this._y=Math.asin(d(g,-1,1)),.99999>Math.abs(g)?(this._x=Math.atan2(-l,e),this._z=Math.atan2(-f,a)):(this._x=Math.atan2(p,k),this._z=0)):"YXZ"===b?(this._x=Math.asin(-d(l,-1,1)),.99999>Math.abs(l)? +(this._y=Math.atan2(g,e),this._z=Math.atan2(h,k)):(this._y=Math.atan2(-n,a),this._z=0)):"ZXY"===b?(this._x=Math.asin(d(p,-1,1)),.99999>Math.abs(p)?(this._y=Math.atan2(-n,e),this._z=Math.atan2(-f,k)):(this._y=0,this._z=Math.atan2(h,a))):"ZYX"===b?(this._y=Math.asin(-d(n,-1,1)),.99999>Math.abs(n)?(this._x=Math.atan2(p,e),this._z=Math.atan2(h,a)):(this._x=0,this._z=Math.atan2(-f,k))):"YZX"===b?(this._z=Math.asin(d(h,-1,1)),.99999>Math.abs(h)?(this._x=Math.atan2(-l,k),this._y=Math.atan2(-n,a)):(this._x= +0,this._y=Math.atan2(g,e))):"XZY"===b?(this._z=Math.asin(-d(f,-1,1)),.99999>Math.abs(f)?(this._x=Math.atan2(p,k),this._y=Math.atan2(g,a)):(this._x=Math.atan2(-l,e),this._y=0)):console.warn("THREE.Euler: .setFromRotationMatrix() given unsupported order: "+b);this._order=b;if(!1!==c)this.onChangeCallback();return this},setFromQuaternion:function(){var a;return function(b,c,d){void 0===a&&(a=new THREE.Matrix4);a.makeRotationFromQuaternion(b);return this.setFromRotationMatrix(a,c,d)}}(),setFromVector3:function(a, +b){return this.set(a.x,a.y,a.z,b||this._order)},reorder:function(){var a=new THREE.Quaternion;return function(b){a.setFromEuler(this);return this.setFromQuaternion(a,b)}}(),equals:function(a){return a._x===this._x&&a._y===this._y&&a._z===this._z&&a._order===this._order},fromArray:function(a){this._x=a[0];this._y=a[1];this._z=a[2];void 0!==a[3]&&(this._order=a[3]);this.onChangeCallback();return this},toArray:function(a,b){void 0===a&&(a=[]);void 0===b&&(b=0);a[b]=this._x;a[b+1]=this._y;a[b+2]=this._z; +a[b+3]=this._order;return a},toVector3:function(a){return a?a.set(this._x,this._y,this._z):new THREE.Vector3(this._x,this._y,this._z)},onChange:function(a){this.onChangeCallback=a;return this},onChangeCallback:function(){}};THREE.Line3=function(a,b){this.start=void 0!==a?a:new THREE.Vector3;this.end=void 0!==b?b:new THREE.Vector3}; +THREE.Line3.prototype={constructor:THREE.Line3,set:function(a,b){this.start.copy(a);this.end.copy(b);return this},clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.start.copy(a.start);this.end.copy(a.end);return this},center:function(a){return(a||new THREE.Vector3).addVectors(this.start,this.end).multiplyScalar(.5)},delta:function(a){return(a||new THREE.Vector3).subVectors(this.end,this.start)},distanceSq:function(){return this.start.distanceToSquared(this.end)},distance:function(){return this.start.distanceTo(this.end)}, +at:function(a,b){var c=b||new THREE.Vector3;return this.delta(c).multiplyScalar(a).add(this.start)},closestPointToPointParameter:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(c,d){a.subVectors(c,this.start);b.subVectors(this.end,this.start);var e=b.dot(b),e=b.dot(a)/e;d&&(e=THREE.Math.clamp(e,0,1));return e}}(),closestPointToPoint:function(a,b,c){a=this.closestPointToPointParameter(a,b);c=c||new THREE.Vector3;return this.delta(c).multiplyScalar(a).add(this.start)},applyMatrix4:function(a){this.start.applyMatrix4(a); +this.end.applyMatrix4(a);return this},equals:function(a){return a.start.equals(this.start)&&a.end.equals(this.end)}};THREE.Box2=function(a,b){this.min=void 0!==a?a:new THREE.Vector2(Infinity,Infinity);this.max=void 0!==b?b:new THREE.Vector2(-Infinity,-Infinity)}; +THREE.Box2.prototype={constructor:THREE.Box2,set:function(a,b){this.min.copy(a);this.max.copy(b);return this},setFromPoints:function(a){this.makeEmpty();for(var b=0,c=a.length;bthis.max.x||a.ythis.max.y?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<=this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y?!0:!1},getParameter:function(a,b){return(b||new THREE.Vector2).set((a.x-this.min.x)/(this.max.x-this.min.x),(a.y-this.min.y)/(this.max.y-this.min.y))},intersectsBox:function(a){return a.max.xthis.max.x||a.max.y +this.max.y?!1:!0},clampPoint:function(a,b){return(b||new THREE.Vector2).copy(a).clamp(this.min,this.max)},distanceToPoint:function(){var a=new THREE.Vector2;return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),intersect:function(a){this.min.max(a.min);this.max.min(a.max);return this},union:function(a){this.min.min(a.min);this.max.max(a.max);return this},translate:function(a){this.min.add(a);this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&& +a.max.equals(this.max)}};THREE.Box3=function(a,b){this.min=void 0!==a?a:new THREE.Vector3(Infinity,Infinity,Infinity);this.max=void 0!==b?b:new THREE.Vector3(-Infinity,-Infinity,-Infinity)}; +THREE.Box3.prototype={constructor:THREE.Box3,set:function(a,b){this.min.copy(a);this.max.copy(b);return this},setFromArray:function(a){for(var b=Infinity,c=Infinity,d=Infinity,e=-Infinity,f=-Infinity,g=-Infinity,h=0,k=a.length;he&&(e=l);n>f&&(f=n);p>g&&(g=p)}this.min.set(b,c,d);this.max.set(e,f,g)},setFromPoints:function(a){this.makeEmpty();for(var b=0,c=a.length;bthis.max.x||a.ythis.max.y||a.z< +this.min.z||a.z>this.max.z?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<=this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y&&this.min.z<=a.min.z&&a.max.z<=this.max.z?!0:!1},getParameter:function(a,b){return(b||new THREE.Vector3).set((a.x-this.min.x)/(this.max.x-this.min.x),(a.y-this.min.y)/(this.max.y-this.min.y),(a.z-this.min.z)/(this.max.z-this.min.z))},intersectsBox:function(a){return a.max.xthis.max.x||a.max.ythis.max.y||a.max.z< +this.min.z||a.min.z>this.max.z?!1:!0},intersectsSphere:function(){var a;return function(b){void 0===a&&(a=new THREE.Vector3);this.clampPoint(b.center,a);return a.distanceToSquared(b.center)<=b.radius*b.radius}}(),intersectsPlane:function(a){var b,c;0=a.constant},clampPoint:function(a,b){return(b||new THREE.Vector3).copy(a).clamp(this.min,this.max)},distanceToPoint:function(){var a=new THREE.Vector3;return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),getBoundingSphere:function(){var a=new THREE.Vector3;return function(b){b=b||new THREE.Sphere;b.center=this.center();b.radius=.5*this.size(a).length();return b}}(), +intersect:function(a){this.min.max(a.min);this.max.min(a.max);this.isEmpty()&&this.makeEmpty();return this},union:function(a){this.min.min(a.min);this.max.max(a.max);return this},applyMatrix4:function(){var a=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];return function(b){if(this.isEmpty())return this;a[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(b);a[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(b); +a[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(b);a[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(b);a[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(b);a[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(b);a[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(b);a[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(b);this.setFromPoints(a);return this}}(),translate:function(a){this.min.add(a);this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&& +a.max.equals(this.max)}};THREE.Matrix3=function(){this.elements=new Float32Array([1,0,0,0,1,0,0,0,1]);0this.determinant()&&(g=-g);c.x=f[12];c.y=f[13];c.z=f[14];b.elements.set(this.elements);c=1/g;var f=1/h,l=1/k;b.elements[0]*=c;b.elements[1]*=c; +b.elements[2]*=c;b.elements[4]*=f;b.elements[5]*=f;b.elements[6]*=f;b.elements[8]*=l;b.elements[9]*=l;b.elements[10]*=l;d.setFromRotationMatrix(b);e.x=g;e.y=h;e.z=k;return this}}(),makeFrustum:function(a,b,c,d,e,f){var g=this.elements;g[0]=2*e/(b-a);g[4]=0;g[8]=(b+a)/(b-a);g[12]=0;g[1]=0;g[5]=2*e/(d-c);g[9]=(d+c)/(d-c);g[13]=0;g[2]=0;g[6]=0;g[10]=-(f+e)/(f-e);g[14]=-2*f*e/(f-e);g[3]=0;g[7]=0;g[11]=-1;g[15]=0;return this},makePerspective:function(a,b,c,d){a=c*Math.tan(THREE.Math.DEG2RAD*a*.5);var e= +-a;return this.makeFrustum(e*b,a*b,e,a,c,d)},makeOrthographic:function(a,b,c,d,e,f){var g=this.elements,h=1/(b-a),k=1/(c-d),l=1/(f-e);g[0]=2*h;g[4]=0;g[8]=0;g[12]=-((b+a)*h);g[1]=0;g[5]=2*k;g[9]=0;g[13]=-((c+d)*k);g[2]=0;g[6]=0;g[10]=-2*l;g[14]=-((f+e)*l);g[3]=0;g[7]=0;g[11]=0;g[15]=1;return this},equals:function(a){var b=this.elements;a=a.elements;for(var c=0;16>c;c++)if(b[c]!==a[c])return!1;return!0},fromArray:function(a){this.elements.set(a);return this},toArray:function(a,b){void 0===a&&(a=[]); +void 0===b&&(b=0);var c=this.elements;a[b]=c[0];a[b+1]=c[1];a[b+2]=c[2];a[b+3]=c[3];a[b+4]=c[4];a[b+5]=c[5];a[b+6]=c[6];a[b+7]=c[7];a[b+8]=c[8];a[b+9]=c[9];a[b+10]=c[10];a[b+11]=c[11];a[b+12]=c[12];a[b+13]=c[13];a[b+14]=c[14];a[b+15]=c[15];return a}};THREE.Ray=function(a,b){this.origin=void 0!==a?a:new THREE.Vector3;this.direction=void 0!==b?b:new THREE.Vector3}; +THREE.Ray.prototype={constructor:THREE.Ray,set:function(a,b){this.origin.copy(a);this.direction.copy(b);return this},clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.origin.copy(a.origin);this.direction.copy(a.direction);return this},at:function(a,b){return(b||new THREE.Vector3).copy(this.direction).multiplyScalar(a).add(this.origin)},lookAt:function(a){this.direction.copy(a).sub(this.origin).normalize();return this},recast:function(){var a=new THREE.Vector3;return function(b){this.origin.copy(this.at(b, +a));return this}}(),closestPointToPoint:function(a,b){var c=b||new THREE.Vector3;c.subVectors(a,this.origin);var d=c.dot(this.direction);return 0>d?c.copy(this.origin):c.copy(this.direction).multiplyScalar(d).add(this.origin)},distanceToPoint:function(a){return Math.sqrt(this.distanceSqToPoint(a))},distanceSqToPoint:function(){var a=new THREE.Vector3;return function(b){var c=a.subVectors(b,this.origin).dot(this.direction);if(0>c)return this.origin.distanceToSquared(b);a.copy(this.direction).multiplyScalar(c).add(this.origin); +return a.distanceToSquared(b)}}(),distanceSqToSegment:function(){var a=new THREE.Vector3,b=new THREE.Vector3,c=new THREE.Vector3;return function(d,e,f,g){a.copy(d).add(e).multiplyScalar(.5);b.copy(e).sub(d).normalize();c.copy(this.origin).sub(a);var h=.5*d.distanceTo(e),k=-this.direction.dot(b),l=c.dot(this.direction),n=-c.dot(b),p=c.lengthSq(),m=Math.abs(1-k*k),q;0=-q?e<=q?(h=1/m,d*=h,e*=h,k=d*(d+k*e+2*l)+e*(k*d+e+2*n)+p):(e=h,d=Math.max(0,-(k*e+l)),k=-d*d+e*(e+2* +n)+p):(e=-h,d=Math.max(0,-(k*e+l)),k=-d*d+e*(e+2*n)+p):e<=-q?(d=Math.max(0,-(-k*h+l)),e=0f)return null;f=Math.sqrt(f-e);e=d-f;d+=f;return 0>e&&0>d?null:0>e?this.at(d,c):this.at(e,c)}}(),intersectsSphere:function(a){return this.distanceToPoint(a.center)<=a.radius},distanceToPlane:function(a){var b=a.normal.dot(this.direction);if(0===b)return 0===a.distanceToPoint(this.origin)?0:null;a=-(this.origin.dot(a.normal)+a.constant)/b;return 0<=a?a:null},intersectPlane:function(a,b){var c= +this.distanceToPlane(a);return null===c?null:this.at(c,b)},intersectsPlane:function(a){var b=a.distanceToPoint(this.origin);return 0===b||0>a.normal.dot(this.direction)*b?!0:!1},intersectBox:function(a,b){var c,d,e,f,g;d=1/this.direction.x;f=1/this.direction.y;g=1/this.direction.z;var h=this.origin;0<=d?(c=(a.min.x-h.x)*d,d*=a.max.x-h.x):(c=(a.max.x-h.x)*d,d*=a.min.x-h.x);0<=f?(e=(a.min.y-h.y)*f,f*=a.max.y-h.y):(e=(a.max.y-h.y)*f,f*=a.min.y-h.y);if(c>f||e>d)return null;if(e>c||c!==c)c=e;if(fg||e>d)return null;if(e>c||c!==c)c=e;if(gd?null:this.at(0<=c?c:d,b)},intersectsBox:function(){var a=new THREE.Vector3;return function(b){return null!==this.intersectBox(b,a)}}(),intersectTriangle:function(){var a=new THREE.Vector3,b=new THREE.Vector3,c=new THREE.Vector3,d=new THREE.Vector3;return function(e,f,g,h,k){b.subVectors(f,e);c.subVectors(g,e);d.crossVectors(b,c);f=this.direction.dot(d); +if(0f)h=-1,f=-f;else return null;a.subVectors(this.origin,e);e=h*this.direction.dot(c.crossVectors(a,c));if(0>e)return null;g=h*this.direction.dot(b.cross(a));if(0>g||e+g>f)return null;e=-h*a.dot(d);return 0>e?null:this.at(e/f,k)}}(),applyMatrix4:function(a){this.direction.add(this.origin).applyMatrix4(a);this.origin.applyMatrix4(a);this.direction.sub(this.origin);this.direction.normalize();return this},equals:function(a){return a.origin.equals(this.origin)&&a.direction.equals(this.direction)}}; +THREE.Sphere=function(a,b){this.center=void 0!==a?a:new THREE.Vector3;this.radius=void 0!==b?b:0}; +THREE.Sphere.prototype={constructor:THREE.Sphere,set:function(a,b){this.center.copy(a);this.radius=b;return this},setFromPoints:function(){var a=new THREE.Box3;return function(b,c){var d=this.center;void 0!==c?d.copy(c):a.setFromPoints(b).center(d);for(var e=0,f=0,g=b.length;f=this.radius},containsPoint:function(a){return a.distanceToSquared(this.center)<=this.radius*this.radius},distanceToPoint:function(a){return a.distanceTo(this.center)-this.radius},intersectsSphere:function(a){var b=this.radius+a.radius;return a.center.distanceToSquared(this.center)<=b*b},intersectsBox:function(a){return a.intersectsSphere(this)},intersectsPlane:function(a){return Math.abs(this.center.dot(a.normal)-a.constant)<=this.radius},clampPoint:function(a,b){var c= +this.center.distanceToSquared(a),d=b||new THREE.Vector3;d.copy(a);c>this.radius*this.radius&&(d.sub(this.center).normalize(),d.multiplyScalar(this.radius).add(this.center));return d},getBoundingBox:function(a){a=a||new THREE.Box3;a.set(this.center,this.center);a.expandByScalar(this.radius);return a},applyMatrix4:function(a){this.center.applyMatrix4(a);this.radius*=a.getMaxScaleOnAxis();return this},translate:function(a){this.center.add(a);return this},equals:function(a){return a.center.equals(this.center)&& +a.radius===this.radius}};THREE.Frustum=function(a,b,c,d,e,f){this.planes=[void 0!==a?a:new THREE.Plane,void 0!==b?b:new THREE.Plane,void 0!==c?c:new THREE.Plane,void 0!==d?d:new THREE.Plane,void 0!==e?e:new THREE.Plane,void 0!==f?f:new THREE.Plane]}; +THREE.Frustum.prototype={constructor:THREE.Frustum,set:function(a,b,c,d,e,f){var g=this.planes;g[0].copy(a);g[1].copy(b);g[2].copy(c);g[3].copy(d);g[4].copy(e);g[5].copy(f);return this},clone:function(){return(new this.constructor).copy(this)},copy:function(a){for(var b=this.planes,c=0;6>c;c++)b[c].copy(a.planes[c]);return this},setFromMatrix:function(a){var b=this.planes,c=a.elements;a=c[0];var d=c[1],e=c[2],f=c[3],g=c[4],h=c[5],k=c[6],l=c[7],n=c[8],p=c[9],m=c[10],q=c[11],r=c[12],s=c[13],u=c[14], +c=c[15];b[0].setComponents(f-a,l-g,q-n,c-r).normalize();b[1].setComponents(f+a,l+g,q+n,c+r).normalize();b[2].setComponents(f+d,l+h,q+p,c+s).normalize();b[3].setComponents(f-d,l-h,q-p,c-s).normalize();b[4].setComponents(f-e,l-k,q-m,c-u).normalize();b[5].setComponents(f+e,l+k,q+m,c+u).normalize();return this},intersectsObject:function(){var a=new THREE.Sphere;return function(b){var c=b.geometry;null===c.boundingSphere&&c.computeBoundingSphere();a.copy(c.boundingSphere).applyMatrix4(b.matrixWorld);return this.intersectsSphere(a)}}(), +intersectsSprite:function(){var a=new THREE.Sphere;return function(b){a.center.set(0,0,0);a.radius=.7071067811865476;a.applyMatrix4(b.matrixWorld);return this.intersectsSphere(a)}}(),intersectsSphere:function(a){var b=this.planes,c=a.center;a=-a.radius;for(var d=0;6>d;d++)if(b[d].distanceToPoint(c)e;e++){var f=d[e];a.x=0g&&0>f)return!1}return!0}}(),containsPoint:function(a){for(var b=this.planes,c=0;6>c;c++)if(0>b[c].distanceToPoint(a))return!1;return!0}};THREE.Plane=function(a,b){this.normal=void 0!==a?a:new THREE.Vector3(1,0,0);this.constant=void 0!==b?b:0}; +THREE.Plane.prototype={constructor:THREE.Plane,set:function(a,b){this.normal.copy(a);this.constant=b;return this},setComponents:function(a,b,c,d){this.normal.set(a,b,c);this.constant=d;return this},setFromNormalAndCoplanarPoint:function(a,b){this.normal.copy(a);this.constant=-b.dot(this.normal);return this},setFromCoplanarPoints:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(c,d,e){d=a.subVectors(e,d).cross(b.subVectors(c,d)).normalize();this.setFromNormalAndCoplanarPoint(d, +c);return this}}(),clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.normal.copy(a.normal);this.constant=a.constant;return this},normalize:function(){var a=1/this.normal.length();this.normal.multiplyScalar(a);this.constant*=a;return this},negate:function(){this.constant*=-1;this.normal.negate();return this},distanceToPoint:function(a){return this.normal.dot(a)+this.constant},distanceToSphere:function(a){return this.distanceToPoint(a.center)-a.radius},projectPoint:function(a, +b){return this.orthoPoint(a,b).sub(a).negate()},orthoPoint:function(a,b){var c=this.distanceToPoint(a);return(b||new THREE.Vector3).copy(this.normal).multiplyScalar(c)},intersectLine:function(){var a=new THREE.Vector3;return function(b,c){var d=c||new THREE.Vector3,e=b.delta(a),f=this.normal.dot(e);if(0===f){if(0===this.distanceToPoint(b.start))return d.copy(b.start)}else return f=-(b.start.dot(this.normal)+this.constant)/f,0>f||1b&&0a&&0e;e++)8===e||13===e||18===e||23===e?b[e]="-":14===e?b[e]="4":(2>=c&&(c=33554432+16777216*Math.random()|0),d=c&15,c>>=4,b[e]=a[19===e?d&3|8:d]);return b.join("")}}(),clamp:function(a,b,c){return Math.max(b,Math.min(c,a))},euclideanModulo:function(a,b){return(a%b+b)%b},mapLinear:function(a,b,c, +d,e){return d+(a-b)*(e-d)/(c-b)},smoothstep:function(a,b,c){if(a<=b)return 0;if(a>=c)return 1;a=(a-b)/(c-b);return a*a*(3-2*a)},smootherstep:function(a,b,c){if(a<=b)return 0;if(a>=c)return 1;a=(a-b)/(c-b);return a*a*a*(a*(6*a-15)+10)},random16:function(){console.warn("THREE.Math.random16() has been deprecated. Use Math.random() instead.");return Math.random()},randInt:function(a,b){return a+Math.floor(Math.random()*(b-a+1))},randFloat:function(a,b){return a+Math.random()*(b-a)},randFloatSpread:function(a){return a* +(.5-Math.random())},degToRad:function(a){return a*THREE.Math.DEG2RAD},radToDeg:function(a){return a*THREE.Math.RAD2DEG},isPowerOfTwo:function(a){return 0===(a&a-1)&&0!==a},nearestPowerOfTwo:function(a){return Math.pow(2,Math.round(Math.log(a)/Math.LN2))},nextPowerOfTwo:function(a){a--;a|=a>>1;a|=a>>2;a|=a>>4;a|=a>>8;a|=a>>16;a++;return a}}; +THREE.Spline=function(a){function b(a,b,c,d,e,f,g){a=.5*(c-a);d=.5*(d-b);return(2*(b-c)+a+d)*g+(-3*(b-c)-2*a-d)*f+a*e+b}this.points=a;var c=[],d={x:0,y:0,z:0},e,f,g,h,k,l,n,p,m;this.initFromArray=function(a){this.points=[];for(var b=0;bthis.points.length-2?this.points.length-1:f+1;c[3]=f>this.points.length-3?this.points.length-1:f+ +2;l=this.points[c[0]];n=this.points[c[1]];p=this.points[c[2]];m=this.points[c[3]];h=g*g;k=g*h;d.x=b(l.x,n.x,p.x,m.x,g,h,k);d.y=b(l.y,n.y,p.y,m.y,g,h,k);d.z=b(l.z,n.z,p.z,m.z,g,h,k);return d};this.getControlPointsArray=function(){var a,b,c=this.points.length,d=[];for(a=0;a=b.x+b.y}}(); +THREE.Triangle.prototype={constructor:THREE.Triangle,set:function(a,b,c){this.a.copy(a);this.b.copy(b);this.c.copy(c);return this},setFromPointsAndIndices:function(a,b,c,d){this.a.copy(a[b]);this.b.copy(a[c]);this.c.copy(a[d]);return this},clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.a.copy(a.a);this.b.copy(a.b);this.c.copy(a.c);return this},area:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(){a.subVectors(this.c,this.b);b.subVectors(this.a, +this.b);return.5*a.cross(b).length()}}(),midpoint:function(a){return(a||new THREE.Vector3).addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)},normal:function(a){return THREE.Triangle.normal(this.a,this.b,this.c,a)},plane:function(a){return(a||new THREE.Plane).setFromCoplanarPoints(this.a,this.b,this.c)},barycoordFromPoint:function(a,b){return THREE.Triangle.barycoordFromPoint(a,this.a,this.b,this.c,b)},containsPoint:function(a){return THREE.Triangle.containsPoint(a,this.a,this.b,this.c)}, +closestPointToPoint:function(){var a,b,c,d;return function(e,f){void 0===a&&(a=new THREE.Plane,b=[new THREE.Line3,new THREE.Line3,new THREE.Line3],c=new THREE.Vector3,d=new THREE.Vector3);var g=f||new THREE.Vector3,h=Infinity;a.setFromCoplanarPoints(this.a,this.b,this.c);a.projectPoint(e,c);if(!0===this.containsPoint(c))g.copy(c);else{b[0].set(this.a,this.b);b[1].set(this.b,this.c);b[2].set(this.c,this.a);for(var k=0;k=e)break a;else{f=b[1];a=e)break b}d= +c;c=0}}for(;c>>1,ad;d++)if(e[d]===e[(d+1)%3]){a.push(f);break}for(f=a.length-1;0<=f;f--)for(e=a[f],this.faces.splice(e, +1),c=0,g=this.faceVertexUvs.length;cb||0===c)return;this._startTime=null;b*=c}b*=this._updateTimeScale(a);c=this._updateTime(b);a=this._updateWeight(a);if(0c.parameterPositions[1]&&(this.stopFading(),0===d&&(this.enabled=!1))}}return this._effectiveWeight=b},_updateTimeScale:function(a){var b=0;if(!this.paused){var b=this.timeScale,c=this._timeScaleInterpolant;if(null!==c){var d=c.evaluate(a)[0],b=b*d;a>c.parameterPositions[1]&&(this.stopWarping(),0===b?this.pause=!0: +this.timeScale=b)}}return this._effectiveTimeScale=b},_updateTime:function(a){var b=this.time+a;if(0===a)return b;var c=this._clip.duration,d=this.loop,e=this._loopCount;if(d===THREE.LoopOnce)a:{if(-1===e&&(this.loopCount=0,this._setEndings(!0,!0,!1)),b>=c)b=c;else if(0>b)b=0;else break a;this.clampWhenFinished?this.pause=!0:this.enabled=!1;this._mixer.dispatchEvent({type:"finished",action:this,direction:0>a?-1:1})}else{d=d===THREE.LoopPingPong;-1===e&&(0<=a?(e=0,this._setEndings(!0,0===this.repetitions, +d)):this._setEndings(0===this.repetitions,!0,d));if(b>=c||0>b){var f=Math.floor(b/c),b=b-c*f,e=e+Math.abs(f),g=this.repetitions-e;0>g?(this.clampWhenFinished?this.paused=!0:this.enabled=!1,b=0a,this._setEndings(a,!a,d)):this._setEndings(!1,!1,d),this._loopCount=e,this._mixer.dispatchEvent({type:"loop",action:this,loopDelta:f}))}if(d&&1===(e&1))return this.time=b,c-b}return this.time=b},_setEndings:function(a, +b,c){var d=this._interpolantSettings;c?(d.endingStart=THREE.ZeroSlopeEnding,d.endingEnd=THREE.ZeroSlopeEnding):(d.endingStart=a?this.zeroSlopeAtStart?THREE.ZeroSlopeEnding:THREE.ZeroCurvatureEnding:THREE.WrapAroundEnding,d.endingEnd=b?this.zeroSlopeAtEnd?THREE.ZeroSlopeEnding:THREE.ZeroCurvatureEnding:THREE.WrapAroundEnding)},_scheduleFading:function(a,b,c){var d=this._mixer,e=d.time,f=this._weightInterpolant;null===f&&(this._weightInterpolant=f=d._lendControlInterpolant());d=f.parameterPositions; +f=f.sampleValues;d[0]=e;f[0]=b;d[1]=e+a;f[1]=c;return this}};THREE.AnimationClip=function(a,b,c){this.name=a;this.tracks=c;this.duration=void 0!==b?b:-1;this.uuid=THREE.Math.generateUUID();0>this.duration&&this.resetDuration();this.trim();this.optimize()}; +THREE.AnimationClip.prototype={constructor:THREE.AnimationClip,resetDuration:function(){for(var a=0,b=0,c=this.tracks.length;b!==c;++b)var d=this.tracks[b],a=Math.max(a,d.times[d.times.length-1]);this.duration=a},trim:function(){for(var a=0;a=c){var p=c++,m=b[p];d[m.uuid]= +n;b[n]=m;d[l]=p;b[p]=k;k=0;for(l=f;k!==l;++k){var m=e[k],q=m[n];m[n]=m[p];m[p]=q}}}this.nCachedObjects_=c},uncache:function(a){for(var b=this._objects,c=b.length,d=this.nCachedObjects_,e=this._indicesByUUID,f=this._bindings,g=f.length,h=0,k=arguments.length;h!==k;++h){var l=arguments[h].uuid,n=e[l];if(void 0!==n)if(delete e[l],nb;)--f;++f;if(0!==e||f!==d)e>=f&&(f=Math.max(f,1),e=f-1),d=this.getValueSize(),this.times=THREE.AnimationUtils.arraySlice(c,e,f),this.values=THREE.AnimationUtils.arraySlice(this.values,e*d,f*d);return this},validate:function(){var a=!0,b=this.getValueSize();0!==b-Math.floor(b)&&(console.error("invalid value size in track", +this),a=!1);var c=this.times,b=this.values,d=c.length;0===d&&(console.error("track is empty",this),a=!1);for(var e=null,f=0;f!==d;f++){var g=c[f];if("number"===typeof g&&isNaN(g)){console.error("time is not a valid number",this,f,g);a=!1;break}if(null!==e&&e>g){console.error("out of order keys",this,f,g,e);a=!1;break}e=g}if(void 0!==b&&THREE.AnimationUtils.isTypedArray(b))for(f=0,c=b.length;f!==c;++f)if(d=b[f],isNaN(d)){console.error("value is not a valid number",this,f,d);a=!1;break}return a},optimize:function(){for(var a= +this.times,b=this.values,c=this.getValueSize(),d=1,e=1,f=a.length-1;e<=f;++e){var g=!1,h=a[e];if(h!==a[e+1]&&(1!==e||h!==h[0]))for(var k=e*c,l=k-c,n=k+c,h=0;h!==c;++h){var p=b[k+h];if(p!==b[l+h]||p!==b[n+h]){g=!0;break}}if(g){if(e!==d)for(a[d]=a[e],g=e*c,k=d*c,h=0;h!==c;++h)b[k+h]=b[g+h];++d}}d!==a.length&&(this.times=THREE.AnimationUtils.arraySlice(a,0,d),this.values=THREE.AnimationUtils.arraySlice(b,0,d*c));return this}}; +Object.assign(THREE.KeyframeTrack,{parse:function(a){if(void 0===a.type)throw Error("track type undefined, can not parse");var b=THREE.KeyframeTrack._getTrackTypeForValueTypeName(a.type);if(void 0===a.times){var c=[],d=[];THREE.AnimationUtils.flattenJSON(a.keys,c,d,"value");a.times=c;a.values=d}return void 0!==b.parse?b.parse(a):new b(a.name,a.times,a.values,a.interpolation)},toJSON:function(a){var b=a.constructor;if(void 0!==b.toJSON)b=b.toJSON(a);else{var b={name:a.name,times:THREE.AnimationUtils.convertArray(a.times, +Array),values:THREE.AnimationUtils.convertArray(a.values,Array)},c=a.getInterpolation();c!==a.DefaultInterpolation&&(b.interpolation=c)}b.type=a.ValueTypeName;return b},_getTrackTypeForValueTypeName:function(a){switch(a.toLowerCase()){case "scalar":case "double":case "float":case "number":case "integer":return THREE.NumberKeyframeTrack;case "vector":case "vector2":case "vector3":case "vector4":return THREE.VectorKeyframeTrack;case "color":return THREE.ColorKeyframeTrack;case "quaternion":return THREE.QuaternionKeyframeTrack; +case "bool":case "boolean":return THREE.BooleanKeyframeTrack;case "string":return THREE.StringKeyframeTrack}throw Error("Unsupported typeName: "+a);}});THREE.PropertyBinding=function(a,b,c){this.path=b;this.parsedPath=c||THREE.PropertyBinding.parseTrackName(b);this.node=THREE.PropertyBinding.findNode(a,this.parsedPath.nodeName)||a;this.rootNode=a}; +THREE.PropertyBinding.prototype={constructor:THREE.PropertyBinding,getValue:function(a,b){this.bind();this.getValue(a,b)},setValue:function(a,b){this.bind();this.setValue(a,b)},bind:function(){var a=this.node,b=this.parsedPath,c=b.objectName,d=b.propertyName,e=b.propertyIndex;a||(this.node=a=THREE.PropertyBinding.findNode(this.rootNode,b.nodeName)||this.rootNode);this.getValue=this._getValue_unavailable;this.setValue=this._setValue_unavailable;if(a){if(c){var f=b.objectIndex;switch(c){case "materials":if(!a.material){console.error(" can not bind to material as node does not have a material", +this);return}if(!a.material.materials){console.error(" can not bind to material.materials as node.material does not have a materials array",this);return}a=a.material.materials;break;case "bones":if(!a.skeleton){console.error(" can not bind to bones as node does not have a skeleton",this);return}a=a.skeleton.bones;for(c=0;cd&&this._mixBufferRegion(c,a,3*b,1-d,b);for(var d=b,f=b+b;d!==f;++d)if(c[d]!==c[d+b]){e.setValue(c,a); +break}},saveOriginalState:function(){var a=this.buffer,b=this.valueSize,c=3*b;this.binding.getValue(a,c);for(var d=b;d!==c;++d)a[d]=a[c+d%b];this.cumulativeWeight=0},restoreOriginalState:function(){this.binding.setValue(this.buffer,3*this.valueSize)},_select:function(a,b,c,d,e){if(.5<=d)for(d=0;d!==e;++d)a[b+d]=a[c+d]},_slerp:function(a,b,c,d,e){THREE.Quaternion.slerpFlat(a,b,a,b,a,c,d)},_lerp:function(a,b,c,d,e){for(var f=1-d,g=0;g!==e;++g){var h=b+g;a[h]=a[h]*f+a[c+g]*d}}}; +THREE.BooleanKeyframeTrack=function(a,b,c){THREE.KeyframeTrack.call(this,a,b,c)};THREE.BooleanKeyframeTrack.prototype=Object.assign(Object.create(THREE.KeyframeTrack.prototype),{constructor:THREE.BooleanKeyframeTrack,ValueTypeName:"bool",ValueBufferType:Array,DefaultInterpolation:THREE.InterpolateDiscrete,InterpolantFactoryMethodLinear:void 0,InterpolantFactoryMethodSmooth:void 0});THREE.ColorKeyframeTrack=function(a,b,c,d){THREE.KeyframeTrack.call(this,a,b,c,d)}; +THREE.ColorKeyframeTrack.prototype=Object.assign(Object.create(THREE.KeyframeTrack.prototype),{constructor:THREE.ColorKeyframeTrack,ValueTypeName:"color"});THREE.NumberKeyframeTrack=function(a,b,c,d){THREE.KeyframeTrack.call(this,a,b,c,d)};THREE.NumberKeyframeTrack.prototype=Object.assign(Object.create(THREE.KeyframeTrack.prototype),{constructor:THREE.NumberKeyframeTrack,ValueTypeName:"number"});THREE.QuaternionKeyframeTrack=function(a,b,c,d){THREE.KeyframeTrack.call(this,a,b,c,d)}; +THREE.QuaternionKeyframeTrack.prototype=Object.assign(Object.create(THREE.KeyframeTrack.prototype),{constructor:THREE.QuaternionKeyframeTrack,ValueTypeName:"quaternion",DefaultInterpolation:THREE.InterpolateLinear,InterpolantFactoryMethodLinear:function(a){return new THREE.QuaternionLinearInterpolant(this.times,this.values,this.getValueSize(),a)},InterpolantFactoryMethodSmooth:void 0});THREE.StringKeyframeTrack=function(a,b,c,d){THREE.KeyframeTrack.call(this,a,b,c,d)}; +THREE.StringKeyframeTrack.prototype=Object.assign(Object.create(THREE.KeyframeTrack.prototype),{constructor:THREE.StringKeyframeTrack,ValueTypeName:"string",ValueBufferType:Array,DefaultInterpolation:THREE.InterpolateDiscrete,InterpolantFactoryMethodLinear:void 0,InterpolantFactoryMethodSmooth:void 0});THREE.VectorKeyframeTrack=function(a,b,c,d){THREE.KeyframeTrack.call(this,a,b,c,d)}; +THREE.VectorKeyframeTrack.prototype=Object.assign(Object.create(THREE.KeyframeTrack.prototype),{constructor:THREE.VectorKeyframeTrack,ValueTypeName:"vector"}); +THREE.Audio=function(a){THREE.Object3D.call(this);this.type="Audio";this.context=a.context;this.source=this.context.createBufferSource();this.source.onended=this.onEnded.bind(this);this.gain=this.context.createGain();this.gain.connect(a.getInput());this.autoplay=!1;this.startTime=0;this.playbackRate=1;this.isPlaying=!1;this.hasPlaybackControl=!0;this.sourceType="empty";this.filters=[]}; +THREE.Audio.prototype=Object.assign(Object.create(THREE.Object3D.prototype),{constructor:THREE.Audio,getOutput:function(){return this.gain},setNodeSource:function(a){this.hasPlaybackControl=!1;this.sourceType="audioNode";this.source=a;this.connect();return this},setBuffer:function(a){this.source.buffer=a;this.sourceType="buffer";this.autoplay&&this.play();return this},play:function(){if(!0===this.isPlaying)console.warn("THREE.Audio: Audio is already playing.");else if(!1===this.hasPlaybackControl)console.warn("THREE.Audio: this Audio has no playback control."); +else{var a=this.context.createBufferSource();a.buffer=this.source.buffer;a.loop=this.source.loop;a.onended=this.source.onended;a.start(0,this.startTime);a.playbackRate.value=this.playbackRate;this.isPlaying=!0;this.source=a;return this.connect()}},pause:function(){if(!1===this.hasPlaybackControl)console.warn("THREE.Audio: this Audio has no playback control.");else return this.source.stop(),this.startTime=this.context.currentTime,this},stop:function(){if(!1===this.hasPlaybackControl)console.warn("THREE.Audio: this Audio has no playback control."); +else return this.source.stop(),this.startTime=0,this},connect:function(){if(0k.opacity&&(k.transparent=!0);c.setTextures(h);return c.parse(k)}}()}; +THREE.Loader.Handlers={handlers:[],add:function(a,b){this.handlers.push(a,b)},get:function(a){for(var b=this.handlers,c=0,d=b.length;cg;g++)m=v[k++],x=u[2*m],m=u[2*m+1],x=new THREE.Vector2(x,m),2!==g&&c.faceVertexUvs[d][h].push(x),0!==g&&c.faceVertexUvs[d][h+1].push(x);p&&(p=3*v[k++],q.normal.set(C[p++],C[p++],C[p]),s.normal.copy(q.normal));if(r)for(d=0;4>d;d++)p=3*v[k++],r=new THREE.Vector3(C[p++],C[p++],C[p]),2!==d&&q.vertexNormals.push(r),0!==d&&s.vertexNormals.push(r); +n&&(n=v[k++],n=w[n],q.color.setHex(n),s.color.setHex(n));if(b)for(d=0;4>d;d++)n=v[k++],n=w[n],2!==d&&q.vertexColors.push(new THREE.Color(n)),0!==d&&s.vertexColors.push(new THREE.Color(n));c.faces.push(q);c.faces.push(s)}else{q=new THREE.Face3;q.a=v[k++];q.b=v[k++];q.c=v[k++];h&&(h=v[k++],q.materialIndex=h);h=c.faces.length;if(d)for(d=0;dg;g++)m=v[k++],x=u[2*m],m=u[2*m+1],x=new THREE.Vector2(x,m),c.faceVertexUvs[d][h].push(x);p&&(p=3*v[k++],q.normal.set(C[p++], +C[p++],C[p]));if(r)for(d=0;3>d;d++)p=3*v[k++],r=new THREE.Vector3(C[p++],C[p++],C[p]),q.vertexNormals.push(r);n&&(n=v[k++],q.color.setHex(w[n]));if(b)for(d=0;3>d;d++)n=v[k++],q.vertexColors.push(new THREE.Color(w[n]));c.faces.push(q)}})(d);(function(){var b=void 0!==a.influencesPerVertex?a.influencesPerVertex:2;if(a.skinWeights)for(var d=0,g=a.skinWeights.length;dthis.opacity&&(d.opacity=this.opacity);!0===this.transparent&&(d.transparent=this.transparent);0a.x||1a.x?0:1;break;case THREE.MirroredRepeatWrapping:1===Math.abs(Math.floor(a.x)%2)?a.x=Math.ceil(a.x)-a.x:a.x-=Math.floor(a.x)}if(0>a.y||1a.y?0:1;break;case THREE.MirroredRepeatWrapping:1=== +Math.abs(Math.floor(a.y)%2)?a.y=Math.ceil(a.y)-a.y:a.y-=Math.floor(a.y)}this.flipY&&(a.y=1-a.y)}}};Object.assign(THREE.Texture.prototype,THREE.EventDispatcher.prototype);THREE.TextureIdCount=0; +THREE.DepthTexture=function(a,b,c,d,e,f,g,h,k){THREE.Texture.call(this,null,d,e,f,g,h,THREE.DepthFormat,c,k);this.image={width:a,height:b};this.type=void 0!==c?c:THREE.UnsignedShortType;this.magFilter=void 0!==g?g:THREE.NearestFilter;this.minFilter=void 0!==h?h:THREE.NearestFilter;this.generateMipmaps=this.flipY=!1};THREE.DepthTexture.prototype=Object.create(THREE.Texture.prototype);THREE.DepthTexture.prototype.constructor=THREE.DepthTexture; +THREE.CanvasTexture=function(a,b,c,d,e,f,g,h,k){THREE.Texture.call(this,a,b,c,d,e,f,g,h,k);this.needsUpdate=!0};THREE.CanvasTexture.prototype=Object.create(THREE.Texture.prototype);THREE.CanvasTexture.prototype.constructor=THREE.CanvasTexture;THREE.CubeTexture=function(a,b,c,d,e,f,g,h,k,l){a=void 0!==a?a:[];b=void 0!==b?b:THREE.CubeReflectionMapping;THREE.Texture.call(this,a,b,c,d,e,f,g,h,k,l);this.flipY=!1};THREE.CubeTexture.prototype=Object.create(THREE.Texture.prototype); +THREE.CubeTexture.prototype.constructor=THREE.CubeTexture;Object.defineProperty(THREE.CubeTexture.prototype,"images",{get:function(){return this.image},set:function(a){this.image=a}});THREE.CompressedTexture=function(a,b,c,d,e,f,g,h,k,l,n,p){THREE.Texture.call(this,null,f,g,h,k,l,d,e,n,p);this.image={width:b,height:c};this.mipmaps=a;this.generateMipmaps=this.flipY=!1};THREE.CompressedTexture.prototype=Object.create(THREE.Texture.prototype);THREE.CompressedTexture.prototype.constructor=THREE.CompressedTexture; +THREE.DataTexture=function(a,b,c,d,e,f,g,h,k,l,n,p){THREE.Texture.call(this,null,f,g,h,k,l,d,e,n,p);this.image={data:a,width:b,height:c};this.magFilter=void 0!==k?k:THREE.NearestFilter;this.minFilter=void 0!==l?l:THREE.NearestFilter;this.generateMipmaps=this.flipY=!1};THREE.DataTexture.prototype=Object.create(THREE.Texture.prototype);THREE.DataTexture.prototype.constructor=THREE.DataTexture; +THREE.VideoTexture=function(a,b,c,d,e,f,g,h,k){function l(){requestAnimationFrame(l);a.readyState>=a.HAVE_CURRENT_DATA&&(n.needsUpdate=!0)}THREE.Texture.call(this,a,b,c,d,e,f,g,h,k);this.generateMipmaps=!1;var n=this;l()};THREE.VideoTexture.prototype=Object.create(THREE.Texture.prototype);THREE.VideoTexture.prototype.constructor=THREE.VideoTexture;THREE.Group=function(){THREE.Object3D.call(this);this.type="Group"};THREE.Group.prototype=Object.assign(Object.create(THREE.Object3D.prototype),{constructor:THREE.Group}); +THREE.Points=function(a,b){THREE.Object3D.call(this);this.type="Points";this.geometry=void 0!==a?a:new THREE.BufferGeometry;this.material=void 0!==b?b:new THREE.PointsMaterial({color:16777215*Math.random()})}; +THREE.Points.prototype=Object.assign(Object.create(THREE.Object3D.prototype),{constructor:THREE.Points,raycast:function(){var a=new THREE.Matrix4,b=new THREE.Ray,c=new THREE.Sphere;return function(d,e){function f(a,c){var f=b.distanceSqToPoint(a);if(fd.far||e.push({distance:m,distanceToRay:Math.sqrt(f),point:h.clone(),index:c,face:null,object:g})}}var g=this,h=this.geometry,k=this.matrixWorld,l=d.params.Points.threshold; +null===h.boundingSphere&&h.computeBoundingSphere();c.copy(h.boundingSphere);c.applyMatrix4(k);if(!1!==d.ray.intersectsSphere(c)){a.getInverse(k);b.copy(d.ray).applyMatrix4(a);var l=l/((this.scale.x+this.scale.y+this.scale.z)/3),n=l*l,l=new THREE.Vector3;if(h instanceof THREE.BufferGeometry){var p=h.index,h=h.attributes.position.array;if(null!==p)for(var m=p.array,p=0,q=m.length;pf||(n.applyMatrix4(this.matrixWorld),s=d.ray.origin.distanceTo(n),sd.far||e.push({distance:s,point:h.clone().applyMatrix4(this.matrixWorld),index:g,face:null,faceIndex:null,object:this}))}else for(g=0,r= +q.length/3-1;gf||(n.applyMatrix4(this.matrixWorld),s=d.ray.origin.distanceTo(n),sd.far||e.push({distance:s,point:h.clone().applyMatrix4(this.matrixWorld),index:g,face:null,faceIndex:null,object:this}))}else if(g instanceof THREE.Geometry)for(k=g.vertices,l=k.length,g=0;gf||(n.applyMatrix4(this.matrixWorld),s=d.ray.origin.distanceTo(n),sd.far|| +e.push({distance:s,point:h.clone().applyMatrix4(this.matrixWorld),index:g,face:null,faceIndex:null,object:this}))}}}(),clone:function(){return(new this.constructor(this.geometry,this.material)).copy(this)}});THREE.LineSegments=function(a,b){THREE.Line.call(this,a,b);this.type="LineSegments"};THREE.LineSegments.prototype=Object.assign(Object.create(THREE.Line.prototype),{constructor:THREE.LineSegments}); +THREE.Mesh=function(a,b){THREE.Object3D.call(this);this.type="Mesh";this.geometry=void 0!==a?a:new THREE.BufferGeometry;this.material=void 0!==b?b:new THREE.MeshBasicMaterial({color:16777215*Math.random()});this.drawMode=THREE.TrianglesDrawMode;this.updateMorphTargets()}; +THREE.Mesh.prototype=Object.assign(Object.create(THREE.Object3D.prototype),{constructor:THREE.Mesh,setDrawMode:function(a){this.drawMode=a},updateMorphTargets:function(){if(void 0!==this.geometry.morphTargets&&0b.far?null:{distance:c,point:x.clone(),object:a}}function c(c,d,e,f,l,p,n,s){g.fromArray(f,3*p);h.fromArray(f,3*n);k.fromArray(f,3*s);if(c=b(c,d,e,g,h,k,u))l&&(m.fromArray(l,2*p),q.fromArray(l,2*n),r.fromArray(l,2*s),c.uv=a(u,g,h,k,m,q,r)),c.face=new THREE.Face3(p,n,s,THREE.Triangle.normal(g,h,k)),c.faceIndex=p;return c}var d=new THREE.Matrix4,e=new THREE.Ray,f=new THREE.Sphere, +g=new THREE.Vector3,h=new THREE.Vector3,k=new THREE.Vector3,l=new THREE.Vector3,n=new THREE.Vector3,p=new THREE.Vector3,m=new THREE.Vector2,q=new THREE.Vector2,r=new THREE.Vector2,s=new THREE.Vector3,u=new THREE.Vector3,x=new THREE.Vector3;return function(s,x){var w=this.geometry,D=this.material,A=this.matrixWorld;if(void 0!==D&&(null===w.boundingSphere&&w.computeBoundingSphere(),f.copy(w.boundingSphere),f.applyMatrix4(A),!1!==s.ray.intersectsSphere(f)&&(d.getInverse(A),e.copy(s.ray).applyMatrix4(d), +null===w.boundingBox||!1!==e.intersectsBox(w.boundingBox)))){var y,B;if(w instanceof THREE.BufferGeometry){var G,z,D=w.index,A=w.attributes,w=A.position.array;void 0!==A.uv&&(y=A.uv.array);if(null!==D)for(var A=D.array,H=0,M=A.length;H= +d[e].distance)d[e-1].object.visible=!1,d[e].object.visible=!0;else break;for(;ethis.scale.x*this.scale.y/4||c.push({distance:Math.sqrt(d),point:this.position,face:null,object:this})}}(),clone:function(){return(new this.constructor(this.material)).copy(this)}}); +THREE.LensFlare=function(a,b,c,d,e){THREE.Object3D.call(this);this.lensFlares=[];this.positionScreen=new THREE.Vector3;this.customUpdateCallback=void 0;void 0!==a&&this.add(a,b,c,d,e)}; +THREE.LensFlare.prototype=Object.assign(Object.create(THREE.Object3D.prototype),{constructor:THREE.LensFlare,copy:function(a){THREE.Object3D.prototype.copy.call(this,a);this.positionScreen.copy(a.positionScreen);this.customUpdateCallback=a.customUpdateCallback;for(var b=0,c=a.lensFlares.length;bc;c++)t.deleteFramebuffer(b.__webglFramebuffer[c]),b.__webglDepthbuffer&&t.deleteRenderbuffer(b.__webglDepthbuffer[c]);else t.deleteFramebuffer(b.__webglFramebuffer), +b.__webglDepthbuffer&&t.deleteRenderbuffer(b.__webglDepthbuffer);T.delete(a.texture);T.delete(a)}ja.textures--}function h(a){a=a.target;a.removeEventListener("dispose",h);k(a);T.delete(a)}function k(a){var b=T.get(a).program;a.program=void 0;void 0!==b&&pa.releaseProgram(b)}function l(a,b){return Math.abs(b[0])-Math.abs(a[0])}function n(a,b){return a.object.renderOrder!==b.object.renderOrder?a.object.renderOrder-b.object.renderOrder:a.material.id!==b.material.id?a.material.id-b.material.id:a.z!== +b.z?a.z-b.z:a.id-b.id}function p(a,b){return a.object.renderOrder!==b.object.renderOrder?a.object.renderOrder-b.object.renderOrder:a.z!==b.z?b.z-a.z:a.id-b.id}function m(a,b,c,d,e){var g;c.transparent?(d=R,g=++F):(d=P,g=++Q);g=d[g];void 0!==g?(g.id=a.id,g.object=a,g.geometry=b,g.material=c,g.z=X.z,g.group=e):(g={id:a.id,object:a,geometry:b,material:c,z:X.z,group:e},d.push(g))}function q(a){if(!Ba.intersectsSphere(a))return!1;var b=ba.numPlanes;if(0===b)return!0;var c=W.clippingPlanes,d=a.center;a= +-a.radius;var e=0;do if(c[e].distanceToPoint(d)b||a.height>b){var c=b/Math.max(a.width,a.height),d=document.createElement("canvas");d.width=Math.floor(a.width*c);d.height=Math.floor(a.height*c);d.getContext("2d").drawImage(a,0,0,a.width,a.height,0,0,d.width,d.height);console.warn("THREE.WebGLRenderer: image is too big ("+a.width+"x"+a.height+ +"). Resized to "+d.width+"x"+d.height,a);return d}return a}function D(a){return THREE.Math.isPowerOfTwo(a.width)&&THREE.Math.isPowerOfTwo(a.height)}function A(a,b,c,d){var e=G(b.texture.format),f=G(b.texture.type);J.texImage2D(d,0,e,b.width,b.height,0,e,f,null);t.bindFramebuffer(t.FRAMEBUFFER,a);t.framebufferTexture2D(t.FRAMEBUFFER,c,d,T.get(b.texture).__webglTexture,0);t.bindFramebuffer(t.FRAMEBUFFER,null)}function y(a,b){t.bindRenderbuffer(t.RENDERBUFFER,a);b.depthBuffer&&!b.stencilBuffer?(t.renderbufferStorage(t.RENDERBUFFER, +t.DEPTH_COMPONENT16,b.width,b.height),t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_ATTACHMENT,t.RENDERBUFFER,a)):b.depthBuffer&&b.stencilBuffer?(t.renderbufferStorage(t.RENDERBUFFER,t.DEPTH_STENCIL,b.width,b.height),t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_STENCIL_ATTACHMENT,t.RENDERBUFFER,a)):t.renderbufferStorage(t.RENDERBUFFER,t.RGBA4,b.width,b.height);t.bindRenderbuffer(t.RENDERBUFFER,null)}function B(a){return a===THREE.NearestFilter||a===THREE.NearestMipMapNearestFilter||a===THREE.NearestMipMapLinearFilter? +t.NEAREST:t.LINEAR}function G(a){var b;if(a===THREE.RepeatWrapping)return t.REPEAT;if(a===THREE.ClampToEdgeWrapping)return t.CLAMP_TO_EDGE;if(a===THREE.MirroredRepeatWrapping)return t.MIRRORED_REPEAT;if(a===THREE.NearestFilter)return t.NEAREST;if(a===THREE.NearestMipMapNearestFilter)return t.NEAREST_MIPMAP_NEAREST;if(a===THREE.NearestMipMapLinearFilter)return t.NEAREST_MIPMAP_LINEAR;if(a===THREE.LinearFilter)return t.LINEAR;if(a===THREE.LinearMipMapNearestFilter)return t.LINEAR_MIPMAP_NEAREST;if(a=== +THREE.LinearMipMapLinearFilter)return t.LINEAR_MIPMAP_LINEAR;if(a===THREE.UnsignedByteType)return t.UNSIGNED_BYTE;if(a===THREE.UnsignedShort4444Type)return t.UNSIGNED_SHORT_4_4_4_4;if(a===THREE.UnsignedShort5551Type)return t.UNSIGNED_SHORT_5_5_5_1;if(a===THREE.UnsignedShort565Type)return t.UNSIGNED_SHORT_5_6_5;if(a===THREE.ByteType)return t.BYTE;if(a===THREE.ShortType)return t.SHORT;if(a===THREE.UnsignedShortType)return t.UNSIGNED_SHORT;if(a===THREE.IntType)return t.INT;if(a===THREE.UnsignedIntType)return t.UNSIGNED_INT; +if(a===THREE.FloatType)return t.FLOAT;b=V.get("OES_texture_half_float");if(null!==b&&a===THREE.HalfFloatType)return b.HALF_FLOAT_OES;if(a===THREE.AlphaFormat)return t.ALPHA;if(a===THREE.RGBFormat)return t.RGB;if(a===THREE.RGBAFormat)return t.RGBA;if(a===THREE.LuminanceFormat)return t.LUMINANCE;if(a===THREE.LuminanceAlphaFormat)return t.LUMINANCE_ALPHA;if(a===THREE.DepthFormat)return t.DEPTH_COMPONENT;if(a===THREE.AddEquation)return t.FUNC_ADD;if(a===THREE.SubtractEquation)return t.FUNC_SUBTRACT;if(a=== +THREE.ReverseSubtractEquation)return t.FUNC_REVERSE_SUBTRACT;if(a===THREE.ZeroFactor)return t.ZERO;if(a===THREE.OneFactor)return t.ONE;if(a===THREE.SrcColorFactor)return t.SRC_COLOR;if(a===THREE.OneMinusSrcColorFactor)return t.ONE_MINUS_SRC_COLOR;if(a===THREE.SrcAlphaFactor)return t.SRC_ALPHA;if(a===THREE.OneMinusSrcAlphaFactor)return t.ONE_MINUS_SRC_ALPHA;if(a===THREE.DstAlphaFactor)return t.DST_ALPHA;if(a===THREE.OneMinusDstAlphaFactor)return t.ONE_MINUS_DST_ALPHA;if(a===THREE.DstColorFactor)return t.DST_COLOR; +if(a===THREE.OneMinusDstColorFactor)return t.ONE_MINUS_DST_COLOR;if(a===THREE.SrcAlphaSaturateFactor)return t.SRC_ALPHA_SATURATE;b=V.get("WEBGL_compressed_texture_s3tc");if(null!==b){if(a===THREE.RGB_S3TC_DXT1_Format)return b.COMPRESSED_RGB_S3TC_DXT1_EXT;if(a===THREE.RGBA_S3TC_DXT1_Format)return b.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(a===THREE.RGBA_S3TC_DXT3_Format)return b.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(a===THREE.RGBA_S3TC_DXT5_Format)return b.COMPRESSED_RGBA_S3TC_DXT5_EXT}b=V.get("WEBGL_compressed_texture_pvrtc"); +if(null!==b){if(a===THREE.RGB_PVRTC_4BPPV1_Format)return b.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(a===THREE.RGB_PVRTC_2BPPV1_Format)return b.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(a===THREE.RGBA_PVRTC_4BPPV1_Format)return b.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(a===THREE.RGBA_PVRTC_2BPPV1_Format)return b.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}b=V.get("WEBGL_compressed_texture_etc1");if(null!==b&&a===THREE.RGB_ETC1_Format)return b.COMPRESSED_RGB_ETC1_WEBGL;b=V.get("EXT_blend_minmax");if(null!==b){if(a===THREE.MinEquation)return b.MIN_EXT; +if(a===THREE.MaxEquation)return b.MAX_EXT}return 0}console.log("THREE.WebGLRenderer",THREE.REVISION);a=a||{};var z=void 0!==a.canvas?a.canvas:document.createElement("canvas"),H=void 0!==a.context?a.context:null,M=void 0!==a.alpha?a.alpha:!1,O=void 0!==a.depth?a.depth:!0,N=void 0!==a.stencil?a.stencil:!0,E=void 0!==a.antialias?a.antialias:!1,K=void 0!==a.premultipliedAlpha?a.premultipliedAlpha:!0,I=void 0!==a.preserveDrawingBuffer?a.preserveDrawingBuffer:!1,L=[],P=[],Q=-1,R=[],F=-1,da=new Float32Array(8), +U=[],Y=[];this.domElement=z;this.context=null;this.sortObjects=this.autoClearStencil=this.autoClearDepth=this.autoClearColor=this.autoClear=!0;this.clippingPlanes=[];this.localClippingEnabled=!1;this.gammaFactor=2;this.physicallyCorrectLights=this.gammaOutput=this.gammaInput=!1;this.toneMapping=THREE.LinearToneMapping;this.toneMappingWhitePoint=this.toneMappingExposure=1;this.maxMorphTargets=8;this.maxMorphNormals=4;this.autoScaleCubemaps=!0;var W=this,fa=null,la=null,ga=null,Z=-1,oa="",ea=null,ra= +new THREE.Vector4,Aa=null,ma=new THREE.Vector4,ta=0,aa=new THREE.Color(0),ia=0,va=z.width,wa=z.height,$=1,ya=new THREE.Vector4(0,0,va,wa),Ca=!1,na=new THREE.Vector4(0,0,va,wa),Ba=new THREE.Frustum,ba=new THREE.WebGLClipping,ua=!1,za=!1,ka=new THREE.Sphere,sa=new THREE.Matrix4,X=new THREE.Vector3,S={hash:"",ambient:[0,0,0],directional:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotShadowMap:[],spotShadowMatrix:[],point:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],shadows:[]}, +ja={geometries:0,textures:0},ha={calls:0,vertices:0,faces:0,points:0};this.info={render:ha,memory:ja,programs:null};var t;try{M={alpha:M,depth:O,stencil:N,antialias:E,premultipliedAlpha:K,preserveDrawingBuffer:I};t=H||z.getContext("webgl",M)||z.getContext("experimental-webgl",M);if(null===t){if(null!==z.getContext("webgl"))throw"Error creating WebGL context with your selected attributes.";throw"Error creating WebGL context.";}void 0===t.getShaderPrecisionFormat&&(t.getShaderPrecisionFormat=function(){return{rangeMin:1, +rangeMax:1,precision:1}});z.addEventListener("webglcontextlost",e,!1)}catch(Fa){console.error("THREE.WebGLRenderer: "+Fa)}var Da="undefined"!==typeof WebGL2RenderingContext&&t instanceof WebGL2RenderingContext,V=new THREE.WebGLExtensions(t);V.get("WEBGL_depth_texture");V.get("OES_texture_float");V.get("OES_texture_float_linear");V.get("OES_texture_half_float");V.get("OES_texture_half_float_linear");V.get("OES_standard_derivatives");V.get("ANGLE_instanced_arrays");V.get("OES_element_index_uint")&& +(THREE.BufferGeometry.MaxIndex=4294967296);var ca=new THREE.WebGLCapabilities(t,V,a),J=new THREE.WebGLState(t,V,G),T=new THREE.WebGLProperties,qa=new THREE.WebGLObjects(t,T,this.info),pa=new THREE.WebGLPrograms(this,ca),xa=new THREE.WebGLLights;this.info.programs=pa.programs;var Ga=new THREE.WebGLBufferRenderer(t,V,ha),Ha=new THREE.WebGLIndexedBufferRenderer(t,V,ha);c();this.context=t;this.capabilities=ca;this.extensions=V;this.properties=T;this.state=J;var Ea=new THREE.WebGLShadowMap(this,S,qa); +this.shadowMap=Ea;var Ia=new THREE.SpritePlugin(this,U),Ja=new THREE.LensFlarePlugin(this,Y);this.getContext=function(){return t};this.getContextAttributes=function(){return t.getContextAttributes()};this.forceContextLoss=function(){V.get("WEBGL_lose_context").loseContext()};this.getMaxAnisotropy=function(){var a;return function(){if(void 0!==a)return a;var b=V.get("EXT_texture_filter_anisotropic");return a=null!==b?t.getParameter(b.MAX_TEXTURE_MAX_ANISOTROPY_EXT):0}}();this.getPrecision=function(){return ca.precision}; +this.getPixelRatio=function(){return $};this.setPixelRatio=function(a){void 0!==a&&($=a,this.setSize(na.z,na.w,!1))};this.getSize=function(){return{width:va,height:wa}};this.setSize=function(a,b,c){va=a;wa=b;z.width=a*$;z.height=b*$;!1!==c&&(z.style.width=a+"px",z.style.height=b+"px");this.setViewport(0,0,a,b)};this.setViewport=function(a,b,c,d){J.viewport(na.set(a,b,c,d))};this.setScissor=function(a,b,c,d){J.scissor(ya.set(a,b,c,d))};this.setScissorTest=function(a){J.setScissorTest(Ca=a)};this.getClearColor= +function(){return aa};this.setClearColor=function(a,c){aa.set(a);ia=void 0!==c?c:1;b(aa.r,aa.g,aa.b,ia)};this.getClearAlpha=function(){return ia};this.setClearAlpha=function(a){ia=a;b(aa.r,aa.g,aa.b,ia)};this.clear=function(a,b,c){var d=0;if(void 0===a||a)d|=t.COLOR_BUFFER_BIT;if(void 0===b||b)d|=t.DEPTH_BUFFER_BIT;if(void 0===c||c)d|=t.STENCIL_BUFFER_BIT;t.clear(d)};this.clearColor=function(){this.clear(!0,!1,!1)};this.clearDepth=function(){this.clear(!1,!0,!1)};this.clearStencil=function(){this.clear(!1, +!1,!0)};this.clearTarget=function(a,b,c,d){this.setRenderTarget(a);this.clear(b,c,d)};this.resetGLState=d;this.dispose=function(){z.removeEventListener("webglcontextlost",e,!1)};this.renderBufferImmediate=function(a,b,c){J.initAttributes();var d=T.get(a);a.hasPositions&&!d.position&&(d.position=t.createBuffer());a.hasNormals&&!d.normal&&(d.normal=t.createBuffer());a.hasUvs&&!d.uv&&(d.uv=t.createBuffer());a.hasColors&&!d.color&&(d.color=t.createBuffer());b=b.getAttributes();a.hasPositions&&(t.bindBuffer(t.ARRAY_BUFFER, +d.position),t.bufferData(t.ARRAY_BUFFER,a.positionArray,t.DYNAMIC_DRAW),J.enableAttribute(b.position),t.vertexAttribPointer(b.position,3,t.FLOAT,!1,0,0));if(a.hasNormals){t.bindBuffer(t.ARRAY_BUFFER,d.normal);if("MeshPhongMaterial"!==c.type&&"MeshStandardMaterial"!==c.type&&"MeshPhysicalMaterial"!==c.type&&c.shading===THREE.FlatShading)for(var e=0,f=3*a.count;e=ca.maxTextures&&console.warn("WebGLRenderer: trying to use "+ +a+" texture units while this GPU supports only "+ca.maxTextures);ta+=1;return a};this.setTexture2D=function(){var a=!1;return function(b,c){b instanceof THREE.WebGLRenderTarget&&(a||(console.warn("THREE.WebGLRenderer.setTexture2D: don't use render targets as textures. Use their .texture property instead."),a=!0),b=b.texture);var d=b,e=T.get(d);if(0m;m++)k[m]=!W.autoScaleCubemaps||g||h?h?d.image[m].image:d.image[m]:w(d.image[m],ca.maxCubemapSize);var l=D(k[0]),n=G(d.format),p=G(d.type);C(t.TEXTURE_CUBE_MAP,d,l);for(m=0;6>m;m++)if(g)for(var q,r=k[m].mipmaps, +s=0,x=r.length;sf;f++)b.__webglFramebuffer[f]=t.createFramebuffer()}else b.__webglFramebuffer=t.createFramebuffer();if(d){J.bindTexture(t.TEXTURE_CUBE_MAP,c.__webglTexture);C(t.TEXTURE_CUBE_MAP,a.texture,e);for(f=0;6>f;f++)A(b.__webglFramebuffer[f], +a,t.COLOR_ATTACHMENT0,t.TEXTURE_CUBE_MAP_POSITIVE_X+f);a.texture.generateMipmaps&&e&&t.generateMipmap(t.TEXTURE_CUBE_MAP);J.bindTexture(t.TEXTURE_CUBE_MAP,null)}else J.bindTexture(t.TEXTURE_2D,c.__webglTexture),C(t.TEXTURE_2D,a.texture,e),A(b.__webglFramebuffer,a,t.COLOR_ATTACHMENT0,t.TEXTURE_2D),a.texture.generateMipmaps&&e&&t.generateMipmap(t.TEXTURE_2D),J.bindTexture(t.TEXTURE_2D,null);if(a.depthBuffer){b=T.get(a);c=a instanceof THREE.WebGLRenderTargetCube;if(a.depthTexture){if(c)throw Error("target.depthTexture not supported in Cube render targets"); +if(a instanceof THREE.WebGLRenderTargetCube)throw Error("Depth Texture with cube render targets is not supported!");t.bindFramebuffer(t.FRAMEBUFFER,b.__webglFramebuffer);if(!(a.depthTexture instanceof THREE.DepthTexture))throw Error("renderTarget.depthTexture must be an instance of THREE.DepthTexture");T.get(a.depthTexture).__webglTexture&&a.depthTexture.image.width===a.width&&a.depthTexture.image.height===a.height||(a.depthTexture.image.width=a.width,a.depthTexture.image.height=a.height,a.depthTexture.needsUpdate= +!0);W.setTexture2D(a.depthTexture,0);b=T.get(a.depthTexture).__webglTexture;t.framebufferTexture2D(t.FRAMEBUFFER,t.DEPTH_ATTACHMENT,t.TEXTURE_2D,b,0)}else if(c)for(b.__webglDepthbuffer=[],c=0;6>c;c++)t.bindFramebuffer(t.FRAMEBUFFER,b.__webglFramebuffer[c]),b.__webglDepthbuffer[c]=t.createRenderbuffer(),y(b.__webglDepthbuffer[c],a);else t.bindFramebuffer(t.FRAMEBUFFER,b.__webglFramebuffer),b.__webglDepthbuffer=t.createRenderbuffer(),y(b.__webglDepthbuffer,a);t.bindFramebuffer(t.FRAMEBUFFER,null)}}b= +a instanceof THREE.WebGLRenderTargetCube;a?(c=T.get(a),c=b?c.__webglFramebuffer[a.activeCubeFace]:c.__webglFramebuffer,ra.copy(a.scissor),Aa=a.scissorTest,ma.copy(a.viewport)):(c=null,ra.copy(ya).multiplyScalar($),Aa=Ca,ma.copy(na).multiplyScalar($));ga!==c&&(t.bindFramebuffer(t.FRAMEBUFFER,c),ga=c);J.scissor(ra);J.setScissorTest(Aa);J.viewport(ma);b&&(b=T.get(a.texture),t.framebufferTexture2D(t.FRAMEBUFFER,t.COLOR_ATTACHMENT0,t.TEXTURE_CUBE_MAP_POSITIVE_X+a.activeCubeFace,b.__webglTexture,a.activeMipMapLevel))}; +this.readRenderTargetPixels=function(a,b,c,d,e,f){if(!1===a instanceof THREE.WebGLRenderTarget)console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");else{var g=T.get(a).__webglFramebuffer;if(g){var h=!1;g!==ga&&(t.bindFramebuffer(t.FRAMEBUFFER,g),h=!0);try{var k=a.texture;k.format!==THREE.RGBAFormat&&G(k.format)!==t.getParameter(t.IMPLEMENTATION_COLOR_READ_FORMAT)?console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format."): +k.type===THREE.UnsignedByteType||G(k.type)===t.getParameter(t.IMPLEMENTATION_COLOR_READ_TYPE)||k.type===THREE.FloatType&&V.get("WEBGL_color_buffer_float")||k.type===THREE.HalfFloatType&&V.get("EXT_color_buffer_half_float")?t.checkFramebufferStatus(t.FRAMEBUFFER)===t.FRAMEBUFFER_COMPLETE?0<=b&&b<=a.width-d&&0<=c&&c<=a.height-e&&t.readPixels(b,c,d,e,G(k.format),G(k.type),f):console.error("THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete."):console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.")}finally{h&& +t.bindFramebuffer(t.FRAMEBUFFER,ga)}}}}}; +THREE.WebGLRenderTarget=function(a,b,c){this.uuid=THREE.Math.generateUUID();this.width=a;this.height=b;this.scissor=new THREE.Vector4(0,0,a,b);this.scissorTest=!1;this.viewport=new THREE.Vector4(0,0,a,b);c=c||{};void 0===c.minFilter&&(c.minFilter=THREE.LinearFilter);this.texture=new THREE.Texture(void 0,void 0,c.wrapS,c.wrapT,c.magFilter,c.minFilter,c.format,c.type,c.anisotropy,c.encoding);this.depthBuffer=void 0!==c.depthBuffer?c.depthBuffer:!0;this.stencilBuffer=void 0!==c.stencilBuffer?c.stencilBuffer: +!0;this.depthTexture=null}; +Object.assign(THREE.WebGLRenderTarget.prototype,THREE.EventDispatcher.prototype,{setSize:function(a,b){if(this.width!==a||this.height!==b)this.width=a,this.height=b,this.dispose();this.viewport.set(0,0,a,b);this.scissor.set(0,0,a,b)},clone:function(){return(new this.constructor).copy(this)},copy:function(a){this.width=a.width;this.height=a.height;this.viewport.copy(a.viewport);this.texture=a.texture.clone();this.depthBuffer=a.depthBuffer;this.stencilBuffer=a.stencilBuffer;this.depthTexture=a.depthTexture; +return this},dispose:function(){this.dispatchEvent({type:"dispose"})}});THREE.WebGLRenderTargetCube=function(a,b,c){THREE.WebGLRenderTarget.call(this,a,b,c);this.activeMipMapLevel=this.activeCubeFace=0};THREE.WebGLRenderTargetCube.prototype=Object.create(THREE.WebGLRenderTarget.prototype);THREE.WebGLRenderTargetCube.prototype.constructor=THREE.WebGLRenderTargetCube; +THREE.WebGLBufferRenderer=function(a,b,c){var d;this.setMode=function(a){d=a};this.render=function(b,f){a.drawArrays(d,b,f);c.calls++;c.vertices+=f;d===a.TRIANGLES&&(c.faces+=f/3)};this.renderInstances=function(e){var f=b.get("ANGLE_instanced_arrays");if(null===f)console.error("THREE.WebGLBufferRenderer: using THREE.InstancedBufferGeometry but hardware does not support extension ANGLE_instanced_arrays.");else{var g=e.attributes.position,h=0,h=g instanceof THREE.InterleavedBufferAttribute?g.data.count: +g.count;f.drawArraysInstancedANGLE(d,0,h,e.maxInstancedCount);c.calls++;c.vertices+=h*e.maxInstancedCount;d===a.TRIANGLES&&(c.faces+=e.maxInstancedCount*h/3)}}}; +THREE.WebGLClipping=function(){function a(){l.value!==d&&(l.value=d,l.needsUpdate=0c){var d=b;b=c;c=d}d=a[b];return void 0===d?(a[b]=[c],!0):-1===d.indexOf(c)?(d.push(c),!0):!1}var f=new THREE.WebGLGeometries(a,b,c);this.getAttributeBuffer=function(a){return a instanceof THREE.InterleavedBufferAttribute?b.get(a.data).__webglBuffer:b.get(a).__webglBuffer};this.getWireframeAttribute= +function(c){var f=b.get(c);if(void 0!==f.wireframe)return f.wireframe;var k=[],l=c.index,n=c.attributes;c=n.position;if(null!==l)for(var n={},l=l.array,p=0,m=l.length;p/g,function(a,b){var c=THREE.ShaderChunk[b];if(void 0===c)throw Error("Can not resolve #include <"+ +b+">");return k(c)})}function l(a){return a.replace(/for \( int i \= (\d+)\; i < (\d+)\; i \+\+ \) \{([\s\S]+?)(?=\})\}/g,function(a,b,c,d){a="";for(b=parseInt(b);b=e||0 0 ) {\nfloat depth = gl_FragCoord.z / gl_FragCoord.w;\nfloat fogFactor = 0.0;\nif ( fogType == 1 ) {\nfogFactor = smoothstep( fogNear, fogFar, depth );\n} else {\nconst float LOG2 = 1.442695;\nfogFactor = exp2( - fogDensity * fogDensity * depth * depth * LOG2 );\nfogFactor = 1.0 - clamp( fogFactor, 0.0, 1.0 );\n}\ngl_FragColor = mix( gl_FragColor, vec4( fogColor, gl_FragColor.w ), fogFactor );\n}\n}"].join("\n")); +w.compileShader(K);w.compileShader(I);w.attachShader(E,K);w.attachShader(E,I);w.linkProgram(E);B=E;x=w.getAttribLocation(B,"position");v=w.getAttribLocation(B,"uv");c=w.getUniformLocation(B,"uvOffset");d=w.getUniformLocation(B,"uvScale");e=w.getUniformLocation(B,"rotation");f=w.getUniformLocation(B,"scale");g=w.getUniformLocation(B,"color");h=w.getUniformLocation(B,"map");k=w.getUniformLocation(B,"opacity");l=w.getUniformLocation(B,"modelViewMatrix");n=w.getUniformLocation(B,"projectionMatrix");p= +w.getUniformLocation(B,"fogType");m=w.getUniformLocation(B,"fogDensity");q=w.getUniformLocation(B,"fogNear");r=w.getUniformLocation(B,"fogFar");s=w.getUniformLocation(B,"fogColor");u=w.getUniformLocation(B,"alphaTest");E=document.createElement("canvas");E.width=8;E.height=8;K=E.getContext("2d");K.fillStyle="white";K.fillRect(0,0,8,8);G=new THREE.Texture(E);G.needsUpdate=!0}w.useProgram(B);D.initAttributes();D.enableAttribute(x);D.enableAttribute(v);D.disableUnusedAttributes();D.disable(w.CULL_FACE); +D.enable(w.BLEND);w.bindBuffer(w.ARRAY_BUFFER,A);w.vertexAttribPointer(x,2,w.FLOAT,!1,16,0);w.vertexAttribPointer(v,2,w.FLOAT,!1,16,8);w.bindBuffer(w.ELEMENT_ARRAY_BUFFER,y);w.uniformMatrix4fv(n,!1,N.projectionMatrix.elements);D.activeTexture(w.TEXTURE0);w.uniform1i(h,0);K=E=0;(I=O.fog)?(w.uniform3f(s,I.color.r,I.color.g,I.color.b),I instanceof THREE.Fog?(w.uniform1f(q,I.near),w.uniform1f(r,I.far),w.uniform1i(p,1),K=E=1):I instanceof THREE.FogExp2&&(w.uniform1f(m,I.density),w.uniform1i(p,2),K=E=2)): +(w.uniform1i(p,0),K=E=0);for(var I=0,L=b.length;Ic)return null;var d=[],e=[],f=[],g,h,k;if(0=l--){console.warn("THREE.ShapeUtils: Unable to triangulate polygon! in triangulate()");break}g=h;c<=g&&(g=0);h=g+1;c<=h&&(h=0);k=h+1;c<=k&&(k=0);var n;a:{var p= +n=void 0,m=void 0,q=void 0,r=void 0,s=void 0,u=void 0,x=void 0,v=void 0,p=a[e[g]].x,m=a[e[g]].y,q=a[e[h]].x,r=a[e[h]].y,s=a[e[k]].x,u=a[e[k]].y;if(Number.EPSILON>(q-p)*(u-m)-(r-m)*(s-p))n=!1;else{var C=void 0,w=void 0,D=void 0,A=void 0,y=void 0,B=void 0,G=void 0,z=void 0,H=void 0,M=void 0,H=z=G=v=x=void 0,C=s-q,w=u-r,D=p-s,A=m-u,y=q-p,B=r-m;for(n=0;n=-Number.EPSILON&& +z>=-Number.EPSILON&&G>=-Number.EPSILON)){n=!1;break a}n=!0}}if(n){d.push([a[e[g]],a[e[h]],a[e[k]]]);f.push([e[g],e[h],e[k]]);g=h;for(k=h+1;kNumber.EPSILON){if(0B||B> +y)return[];k=l*n-k*p;if(0>k||k>y)return[]}else{if(0d?[]:k===d?f?[]:[g]:a<=d?[g,h]:[g,l]}function e(a,b,c,d){var e=b.x-a.x,f=b.y-a.y;b=c.x-a.x;c=c.y-a.y;var g=d.x-a.x;d=d.y-a.y;a=e*c-f*b;e=e*d-f*g;return Math.abs(a)>Number.EPSILON?(b=g*c-d*b,0f&&(f=d);var g=a+1;g>d&&(g=0);d=e(h[a],h[f],h[g],k[b]);if(!d)return!1;d=k.length-1;f=b-1;0>f&&(f=d);g=b+1;g>d&&(g=0);return(d=e(k[b],k[f],k[g],h[a]))?!0:!1}function f(a,b){var c,e;for(c=0;cN){console.log("Infinite Loop! Holes left:"+l.length+", Probably Hole outside Shape!");break}for(p=z;ph;h++)l=k[h].x+":"+k[h].y,l=n[l],void 0!==l&&(k[h]=l);return p.concat()},isClockWise:function(a){return 0>THREE.ShapeUtils.area(a)},b2:function(){return function(a,b,c,d){var e=1-a;return e*e*b+2*(1-a)*a*c+a*a*d}}(),b3:function(){return function(a,b,c,d,e){var f= +1-a,g=1-a;return f*f*f*b+3*g*g*a*c+3*(1-a)*a*a*d+a*a*a*e}}()};THREE.Curve=function(){}; +THREE.Curve.prototype={constructor:THREE.Curve,getPoint:function(a){console.warn("THREE.Curve: Warning, getPoint() not implemented!");return null},getPointAt:function(a){a=this.getUtoTmapping(a);return this.getPoint(a)},getPoints:function(a){a||(a=5);var b,c=[];for(b=0;b<=a;b++)c.push(this.getPoint(b/a));return c},getSpacedPoints:function(a){a||(a=5);var b,c=[];for(b=0;b<=a;b++)c.push(this.getPointAt(b/a));return c},getLength:function(){var a=this.getLengths();return a[a.length-1]},getLengths:function(a){a|| +(a=this.__arcLengthDivisions?this.__arcLengthDivisions:200);if(this.cacheArcLengths&&this.cacheArcLengths.length===a+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;var b=[],c,d=this.getPoint(0),e,f=0;b.push(0);for(e=1;e<=a;e++)c=this.getPoint(e/a),f+=c.distanceTo(d),b.push(f),d=c;return this.cacheArcLengths=b},updateArcLengths:function(){this.needsUpdate=!0;this.getLengths()},getUtoTmapping:function(a,b){var c=this.getLengths(),d=0,e=c.length,f;f=b?b:a*c[e-1];for(var g=0,h=e- +1,k;g<=h;)if(d=Math.floor(g+(h-g)/2),k=c[d]-f,0>k)g=d+1;else if(0b&&(b=0);1=b)return a=this.curves[d],b=1-(c[d]-b)/a.getLength(),a.getPointAt(b);d++}return null},getLength:function(){var a= +this.getCurveLengths();return a[a.length-1]},getCurveLengths:function(){if(this.cacheLengths&&this.cacheLengths.length===this.curves.length)return this.cacheLengths;for(var a=[],b=0,c=0,d=this.curves.length;cNumber.EPSILON){if(0>l&&(g=b[f],k=-k,h=b[e],l=-l),!(a.yh.y))if(a.y===g.y){if(a.x=== +g.x)return!0}else{e=l*(a.x-g.x)-k*(a.y-g.y);if(0===e)return!0;0>e||(d=!d)}}else if(a.y===g.y&&(h.x<=a.x&&a.x<=g.x||g.x<=a.x&&a.x<=h.x))return!0}return d}var e=THREE.ShapeUtils.isClockWise,f=function(a){for(var b=[],c=new THREE.Path,d=0,e=a.length;db.length-2?b.length-1:c+1],b=b[c>b.length-3?b.length-1:c+2],c=THREE.CurveUtils.interpolate;return new THREE.Vector2(c(d.x,e.x,f.x,b.x,a),c(d.y,e.y,f.y,b.y,a))}; +THREE.EllipseCurve=function(a,b,c,d,e,f,g,h){this.aX=a;this.aY=b;this.xRadius=c;this.yRadius=d;this.aStartAngle=e;this.aEndAngle=f;this.aClockwise=g;this.aRotation=h||0};THREE.EllipseCurve.prototype=Object.create(THREE.Curve.prototype);THREE.EllipseCurve.prototype.constructor=THREE.EllipseCurve; +THREE.EllipseCurve.prototype.getPoint=function(a){var b=this.aEndAngle-this.aStartAngle;0>b&&(b+=2*Math.PI);b>2*Math.PI&&(b-=2*Math.PI);b=!0===this.aClockwise?this.aEndAngle+(1-a)*(2*Math.PI-b):this.aStartAngle+a*b;a=this.aX+this.xRadius*Math.cos(b);var c=this.aY+this.yRadius*Math.sin(b);if(0!==this.aRotation){var b=Math.cos(this.aRotation),d=Math.sin(this.aRotation),e=a;a=(e-this.aX)*b-(c-this.aY)*d+this.aX;c=(e-this.aX)*d+(c-this.aY)*b+this.aY}return new THREE.Vector2(a,c)}; +THREE.ArcCurve=function(a,b,c,d,e,f){THREE.EllipseCurve.call(this,a,b,c,c,d,e,f)};THREE.ArcCurve.prototype=Object.create(THREE.EllipseCurve.prototype);THREE.ArcCurve.prototype.constructor=THREE.ArcCurve;THREE.LineCurve3=THREE.Curve.create(function(a,b){this.v1=a;this.v2=b},function(a){var b=new THREE.Vector3;b.subVectors(this.v2,this.v1);b.multiplyScalar(a);b.add(this.v1);return b}); +THREE.QuadraticBezierCurve3=THREE.Curve.create(function(a,b,c){this.v0=a;this.v1=b;this.v2=c},function(a){var b=THREE.ShapeUtils.b2;return new THREE.Vector3(b(a,this.v0.x,this.v1.x,this.v2.x),b(a,this.v0.y,this.v1.y,this.v2.y),b(a,this.v0.z,this.v1.z,this.v2.z))}); +THREE.CubicBezierCurve3=THREE.Curve.create(function(a,b,c,d){this.v0=a;this.v1=b;this.v2=c;this.v3=d},function(a){var b=THREE.ShapeUtils.b3;return new THREE.Vector3(b(a,this.v0.x,this.v1.x,this.v2.x,this.v3.x),b(a,this.v0.y,this.v1.y,this.v2.y,this.v3.y),b(a,this.v0.z,this.v1.z,this.v2.z,this.v3.z))}); +THREE.SplineCurve3=THREE.Curve.create(function(a){console.warn("THREE.SplineCurve3 will be deprecated. Please use THREE.CatmullRomCurve3");this.points=void 0==a?[]:a},function(a){var b=this.points;a*=b.length-1;var c=Math.floor(a);a-=c;var d=b[0==c?c:c-1],e=b[c],f=b[c>b.length-2?b.length-1:c+1],b=b[c>b.length-3?b.length-1:c+2],c=THREE.CurveUtils.interpolate;return new THREE.Vector3(c(d.x,e.x,f.x,b.x,a),c(d.y,e.y,f.y,b.y,a),c(d.z,e.z,f.z,b.z,a))}); +THREE.CatmullRomCurve3=function(){function a(){}var b=new THREE.Vector3,c=new a,d=new a,e=new a;a.prototype.init=function(a,b,c,d){this.c0=a;this.c1=c;this.c2=-3*a+3*b-2*c-d;this.c3=2*a-2*b+c+d};a.prototype.initNonuniformCatmullRom=function(a,b,c,d,e,n,p){a=((b-a)/e-(c-a)/(e+n)+(c-b)/n)*n;d=((c-b)/n-(d-b)/(n+p)+(d-c)/p)*n;this.init(b,c,a,d)};a.prototype.initCatmullRom=function(a,b,c,d,e){this.init(b,c,e*(c-a),e*(d-b))};a.prototype.calc=function(a){var b=a*a;return this.c0+this.c1*a+this.c2*b+this.c3* +b*a};return THREE.Curve.create(function(a){this.points=a||[];this.closed=!1},function(a){var g=this.points,h,k;k=g.length;2>k&&console.log("duh, you need at least 2 points");a*=k-(this.closed?0:1);h=Math.floor(a);a-=h;this.closed?h+=0h&&(h=1);1E-4>k&&(k=h);1E-4>m&&(m=h);c.initNonuniformCatmullRom(l.x,n.x,p.x,g.x,k,h,m);d.initNonuniformCatmullRom(l.y,n.y,p.y,g.y,k,h,m);e.initNonuniformCatmullRom(l.z,n.z,p.z,g.z,k,h,m)}else"catmullrom"===this.type&&(k=void 0!==this.tension?this.tension:.5,c.initCatmullRom(l.x,n.x,p.x,g.x, +k),d.initCatmullRom(l.y,n.y,p.y,g.y,k),e.initCatmullRom(l.z,n.z,p.z,g.z,k));return new THREE.Vector3(c.calc(a),d.calc(a),e.calc(a))})}();THREE.ClosedSplineCurve3=function(a){console.warn("THREE.ClosedSplineCurve3 has been deprecated. Please use THREE.CatmullRomCurve3.");THREE.CatmullRomCurve3.call(this,a);this.type="catmullrom";this.closed=!0};THREE.ClosedSplineCurve3.prototype=Object.create(THREE.CatmullRomCurve3.prototype); +THREE.BoxGeometry=function(a,b,c,d,e,f){THREE.Geometry.call(this);this.type="BoxGeometry";this.parameters={width:a,height:b,depth:c,widthSegments:d,heightSegments:e,depthSegments:f};this.fromBufferGeometry(new THREE.BoxBufferGeometry(a,b,c,d,e,f));this.mergeVertices()};THREE.BoxGeometry.prototype=Object.create(THREE.Geometry.prototype);THREE.BoxGeometry.prototype.constructor=THREE.BoxGeometry;THREE.CubeGeometry=THREE.BoxGeometry; +THREE.BoxBufferGeometry=function(a,b,c,d,e,f){function g(a,b,c,d,e,f,g,k,l,M,O){var N=f/l,E=g/M,K=f/2,I=g/2,L=k/2;g=l+1;for(var P=M+1,Q=f=0,R=new THREE.Vector3,F=0;Fm;m++){e[0]=p[g[m]];e[1]=p[g[(m+1)%3]];e.sort(c);var q=e.toString();void 0===f[q]?f[q]={vert1:e[0],vert2:e[1],face1:l, +face2:void 0}:f[q].face2=l}e=[];for(q in f)if(g=f[q],void 0===g.face2||h[g.face1].normal.dot(h[g.face2].normal)<=d)l=k[g.vert1],e.push(l.x),e.push(l.y),e.push(l.z),l=k[g.vert2],e.push(l.x),e.push(l.y),e.push(l.z);this.addAttribute("position",new THREE.BufferAttribute(new Float32Array(e),3))};THREE.EdgesGeometry.prototype=Object.create(THREE.BufferGeometry.prototype);THREE.EdgesGeometry.prototype.constructor=THREE.EdgesGeometry; +THREE.ExtrudeGeometry=function(a,b){"undefined"!==typeof a&&(THREE.Geometry.call(this),this.type="ExtrudeGeometry",a=Array.isArray(a)?a:[a],this.addShapeList(a,b),this.computeFaceNormals())};THREE.ExtrudeGeometry.prototype=Object.create(THREE.Geometry.prototype);THREE.ExtrudeGeometry.prototype.constructor=THREE.ExtrudeGeometry;THREE.ExtrudeGeometry.prototype.addShapeList=function(a,b){for(var c=a.length,d=0;dNumber.EPSILON){var k=Math.sqrt(h),l=Math.sqrt(f*f+g*g),h=b.x-e/k;b=b.y+d/k;f=((c.x-g/l-h)*g-(c.y+f/l-b)*f)/(d*g-e*f);c=h+d*f-a.x;a=b+e*f-a.y;d=c*c+a*a;if(2>=d)return new THREE.Vector2(c,a);d=Math.sqrt(d/2)}else a=!1,d>Number.EPSILON? +f>Number.EPSILON&&(a=!0):d<-Number.EPSILON?f<-Number.EPSILON&&(a=!0):Math.sign(e)===Math.sign(g)&&(a=!0),a?(c=-e,a=d,d=Math.sqrt(h)):(c=d,a=e,d=Math.sqrt(h/2));return new THREE.Vector2(c/d,a/d)}function e(a,b){var c,d;for(F=a.length;0<=--F;){c=F;d=F-1;0>d&&(d=a.length-1);for(var e=0,f=q+2*n,e=0;eMath.abs(b.y-c.y)?[new THREE.Vector2(b.x,1-b.z),new THREE.Vector2(c.x,1-c.z),new THREE.Vector2(d.x,1-d.z),new THREE.Vector2(e.x,1-e.z)]:[new THREE.Vector2(b.y,1-b.z),new THREE.Vector2(c.y,1-c.z),new THREE.Vector2(d.y, +1-d.z),new THREE.Vector2(e.y,1-e.z)]}};THREE.ShapeGeometry=function(a,b){THREE.Geometry.call(this);this.type="ShapeGeometry";!1===Array.isArray(a)&&(a=[a]);this.addShapeList(a,b);this.computeFaceNormals()};THREE.ShapeGeometry.prototype=Object.create(THREE.Geometry.prototype);THREE.ShapeGeometry.prototype.constructor=THREE.ShapeGeometry;THREE.ShapeGeometry.prototype.addShapeList=function(a,b){for(var c=0,d=a.length;cNumber.EPSILON&&(h.normalize(),d=Math.acos(THREE.Math.clamp(e[l-1].dot(e[l]),-1,1)),f[l].applyMatrix4(k.makeRotationAxis(h,d))),g[l].crossVectors(e[l],f[l]);if(c)for(d=Math.acos(THREE.Math.clamp(f[0].dot(f[b-1]),-1,1)),d/=b-1,0c&&1===a.x&&(a=new THREE.Vector2(a.x-1,a.y));0===b.x&&0===b.z&&(a=new THREE.Vector2(c/ +2/Math.PI+.5,a.y));return a.clone()}THREE.Geometry.call(this);this.type="PolyhedronGeometry";this.parameters={vertices:a,indices:b,radius:c,detail:d};c=c||1;d=d||0;for(var k=this,l=0,n=a.length;lq&&(.2>d&&(b[0].x+=1),.2>a&&(b[1].x+=1),.2>p&&(b[2].x+=1));l=0;for(n=this.vertices.length;lp;p++){c[0]=n[e[p]];c[1]=n[e[(p+1)%3]];c.sort(b);var m=c.toString();void 0===d[m]&&(k[2*h]=c[0],k[2*h+1]=c[1],d[m]=!0,h++)}c=new Float32Array(6*h);a=0;for(l=h;ap;p++)d=f[k[2*a+p]],h=6*a+3*p,c[h+0]=d.x,c[h+1]=d.y, +c[h+2]=d.z;this.addAttribute("position",new THREE.BufferAttribute(c,3))}else if(a instanceof THREE.BufferGeometry){if(null!==a.index){l=a.index.array;f=a.attributes.position;e=a.groups;h=0;0===e.length&&a.addGroup(0,l.length);k=new Uint32Array(2*l.length);g=0;for(n=e.length;gp;p++)c[0]=l[a+p],c[1]=l[a+(p+1)%3],c.sort(b),m=c.toString(),void 0===d[m]&&(k[2*h]=c[0],k[2*h+1]=c[1],d[m]=!0,h++)}c=new Float32Array(6*h);a=0;for(l=h;a< +l;a++)for(p=0;2>p;p++)h=6*a+3*p,d=k[2*a+p],c[h+0]=f.getX(d),c[h+1]=f.getY(d),c[h+2]=f.getZ(d)}else for(f=a.attributes.position.array,h=f.length/3,k=h/3,c=new Float32Array(6*h),a=0,l=k;ap;p++)h=18*a+6*p,k=9*a+3*p,c[h+0]=f[k],c[h+1]=f[k+1],c[h+2]=f[k+2],d=9*a+(p+1)%3*3,c[h+3]=f[d],c[h+4]=f[d+1],c[h+5]=f[d+2];this.addAttribute("position",new THREE.BufferAttribute(c,3))}};THREE.WireframeGeometry.prototype=Object.create(THREE.BufferGeometry.prototype); +THREE.WireframeGeometry.prototype.constructor=THREE.WireframeGeometry;THREE.AxisHelper=function(a){a=a||1;var b=new Float32Array([0,0,0,a,0,0,0,0,0,0,a,0,0,0,0,0,0,a]),c=new Float32Array([1,0,0,1,.6,0,0,1,0,.6,1,0,0,0,1,0,.6,1]);a=new THREE.BufferGeometry;a.addAttribute("position",new THREE.BufferAttribute(b,3));a.addAttribute("color",new THREE.BufferAttribute(c,3));b=new THREE.LineBasicMaterial({vertexColors:THREE.VertexColors});THREE.LineSegments.call(this,a,b)};THREE.AxisHelper.prototype=Object.create(THREE.LineSegments.prototype); +THREE.AxisHelper.prototype.constructor=THREE.AxisHelper; +THREE.ArrowHelper=function(){var a=new THREE.BufferGeometry;a.addAttribute("position",new THREE.Float32Attribute([0,0,0,0,1,0],3));var b=new THREE.CylinderBufferGeometry(0,.5,1,5,1);b.translate(0,-.5,0);return function(c,d,e,f,g,h){THREE.Object3D.call(this);void 0===f&&(f=16776960);void 0===e&&(e=1);void 0===g&&(g=.2*e);void 0===h&&(h=.2*g);this.position.copy(d);this.line=new THREE.Line(a,new THREE.LineBasicMaterial({color:f}));this.line.matrixAutoUpdate=!1;this.add(this.line);this.cone=new THREE.Mesh(b, +new THREE.MeshBasicMaterial({color:f}));this.cone.matrixAutoUpdate=!1;this.add(this.cone);this.setDirection(c);this.setLength(e,g,h)}}();THREE.ArrowHelper.prototype=Object.create(THREE.Object3D.prototype);THREE.ArrowHelper.prototype.constructor=THREE.ArrowHelper; +THREE.ArrowHelper.prototype.setDirection=function(){var a=new THREE.Vector3,b;return function(c){.99999c.y?this.quaternion.set(1,0,0,0):(a.set(c.z,0,-c.x).normalize(),b=Math.acos(c.y),this.quaternion.setFromAxisAngle(a,b))}}();THREE.ArrowHelper.prototype.setLength=function(a,b,c){void 0===b&&(b=.2*a);void 0===c&&(c=.2*b);this.line.scale.set(1,Math.max(0,a-b),1);this.line.updateMatrix();this.cone.scale.set(c,b,c);this.cone.position.y=a;this.cone.updateMatrix()}; +THREE.ArrowHelper.prototype.setColor=function(a){this.line.material.color.copy(a);this.cone.material.color.copy(a)};THREE.BoxHelper=function(a){var b=new Uint16Array([0,1,1,2,2,3,3,0,4,5,5,6,6,7,7,4,0,4,1,5,2,6,3,7]),c=new Float32Array(24),d=new THREE.BufferGeometry;d.setIndex(new THREE.BufferAttribute(b,1));d.addAttribute("position",new THREE.BufferAttribute(c,3));THREE.LineSegments.call(this,d,new THREE.LineBasicMaterial({color:16776960}));void 0!==a&&this.update(a)};THREE.BoxHelper.prototype=Object.create(THREE.LineSegments.prototype); +THREE.BoxHelper.prototype.constructor=THREE.BoxHelper; +THREE.BoxHelper.prototype.update=function(){var a=new THREE.Box3;return function(b){b instanceof THREE.Box3?a.copy(b):a.setFromObject(b);if(!a.isEmpty()){b=a.min;var c=a.max,d=this.geometry.attributes.position,e=d.array;e[0]=c.x;e[1]=c.y;e[2]=c.z;e[3]=b.x;e[4]=c.y;e[5]=c.z;e[6]=b.x;e[7]=b.y;e[8]=c.z;e[9]=c.x;e[10]=b.y;e[11]=c.z;e[12]=c.x;e[13]=c.y;e[14]=b.z;e[15]=b.x;e[16]=c.y;e[17]=b.z;e[18]=b.x;e[19]=b.y;e[20]=b.z;e[21]=c.x;e[22]=b.y;e[23]=b.z;d.needsUpdate=!0;this.geometry.computeBoundingSphere()}}}(); +THREE.BoundingBoxHelper=function(a,b){var c=void 0!==b?b:8947848;this.object=a;this.box=new THREE.Box3;THREE.Mesh.call(this,new THREE.BoxGeometry(1,1,1),new THREE.MeshBasicMaterial({color:c,wireframe:!0}))};THREE.BoundingBoxHelper.prototype=Object.create(THREE.Mesh.prototype);THREE.BoundingBoxHelper.prototype.constructor=THREE.BoundingBoxHelper;THREE.BoundingBoxHelper.prototype.update=function(){this.box.setFromObject(this.object);this.box.size(this.scale);this.box.center(this.position)}; +THREE.CameraHelper=function(a){function b(a,b,d){c(a,d);c(b,d)}function c(a,b){d.vertices.push(new THREE.Vector3);d.colors.push(new THREE.Color(b));void 0===f[a]&&(f[a]=[]);f[a].push(d.vertices.length-1)}var d=new THREE.Geometry,e=new THREE.LineBasicMaterial({color:16777215,vertexColors:THREE.FaceColors}),f={};b("n1","n2",16755200);b("n2","n4",16755200);b("n4","n3",16755200);b("n3","n1",16755200);b("f1","f2",16755200);b("f2","f4",16755200);b("f4","f3",16755200);b("f3","f1",16755200);b("n1","f1",16755200); +b("n2","f2",16755200);b("n3","f3",16755200);b("n4","f4",16755200);b("p","n1",16711680);b("p","n2",16711680);b("p","n3",16711680);b("p","n4",16711680);b("u1","u2",43775);b("u2","u3",43775);b("u3","u1",43775);b("c","t",16777215);b("p","c",3355443);b("cn1","cn2",3355443);b("cn3","cn4",3355443);b("cf1","cf2",3355443);b("cf3","cf4",3355443);THREE.LineSegments.call(this,d,e);this.camera=a;this.camera.updateProjectionMatrix();this.matrix=a.matrixWorld;this.matrixAutoUpdate=!1;this.pointMap=f;this.update()}; +THREE.CameraHelper.prototype=Object.create(THREE.LineSegments.prototype);THREE.CameraHelper.prototype.constructor=THREE.CameraHelper; +THREE.CameraHelper.prototype.update=function(){function a(a,g,h,k){d.set(g,h,k).unproject(e);a=c[a];if(void 0!==a)for(g=0,h=a.length;gd;d++)c.faces[d].color=this.colors[4>d?0:1];d=new THREE.MeshBasicMaterial({vertexColors:THREE.FaceColors,wireframe:!0});this.lightSphere=new THREE.Mesh(c,d);this.add(this.lightSphere);this.update()}; +THREE.HemisphereLightHelper.prototype=Object.create(THREE.Object3D.prototype);THREE.HemisphereLightHelper.prototype.constructor=THREE.HemisphereLightHelper;THREE.HemisphereLightHelper.prototype.dispose=function(){this.lightSphere.geometry.dispose();this.lightSphere.material.dispose()}; +THREE.HemisphereLightHelper.prototype.update=function(){var a=new THREE.Vector3;return function(){this.colors[0].copy(this.light.color).multiplyScalar(this.light.intensity);this.colors[1].copy(this.light.groundColor).multiplyScalar(this.light.intensity);this.lightSphere.lookAt(a.setFromMatrixPosition(this.light.matrixWorld).negate());this.lightSphere.geometry.colorsNeedUpdate=!0}}(); +THREE.PointLightHelper=function(a,b){this.light=a;this.light.updateMatrixWorld();var c=new THREE.SphereBufferGeometry(b,4,2),d=new THREE.MeshBasicMaterial({wireframe:!0,fog:!1});d.color.copy(this.light.color).multiplyScalar(this.light.intensity);THREE.Mesh.call(this,c,d);this.matrix=this.light.matrixWorld;this.matrixAutoUpdate=!1};THREE.PointLightHelper.prototype=Object.create(THREE.Mesh.prototype);THREE.PointLightHelper.prototype.constructor=THREE.PointLightHelper; +THREE.PointLightHelper.prototype.dispose=function(){this.geometry.dispose();this.material.dispose()};THREE.PointLightHelper.prototype.update=function(){this.material.color.copy(this.light.color).multiplyScalar(this.light.intensity)}; +THREE.SkeletonHelper=function(a){this.bones=this.getBoneList(a);for(var b=new THREE.Geometry,c=0;cc;c++,d++){var e=c/32*Math.PI*2,f=d/32*Math.PI*2;b.push(Math.cos(e),Math.sin(e),1,Math.cos(f),Math.sin(f),1)}a.addAttribute("position",new THREE.Float32Attribute(b,3));b=new THREE.LineBasicMaterial({fog:!1});this.cone=new THREE.LineSegments(a, +b);this.add(this.cone);this.update()};THREE.SpotLightHelper.prototype=Object.create(THREE.Object3D.prototype);THREE.SpotLightHelper.prototype.constructor=THREE.SpotLightHelper;THREE.SpotLightHelper.prototype.dispose=function(){this.cone.geometry.dispose();this.cone.material.dispose()}; +THREE.SpotLightHelper.prototype.update=function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(){var c=this.light.distance?this.light.distance:1E3,d=c*Math.tan(this.light.angle);this.cone.scale.set(d,d,c);a.setFromMatrixPosition(this.light.matrixWorld);b.setFromMatrixPosition(this.light.target.matrixWorld);this.cone.lookAt(b.sub(a));this.cone.material.color.copy(this.light.color).multiplyScalar(this.light.intensity)}}(); +THREE.VertexNormalsHelper=function(a,b,c,d){this.object=a;this.size=void 0!==b?b:1;a=void 0!==c?c:16711680;d=void 0!==d?d:1;b=0;c=this.object.geometry;c instanceof THREE.Geometry?b=3*c.faces.length:c instanceof THREE.BufferGeometry&&(b=c.attributes.normal.count);c=new THREE.BufferGeometry;b=new THREE.Float32Attribute(6*b,3);c.addAttribute("position",b);THREE.LineSegments.call(this,c,new THREE.LineBasicMaterial({color:a,linewidth:d}));this.matrixAutoUpdate=!1;this.update()}; +THREE.VertexNormalsHelper.prototype=Object.create(THREE.LineSegments.prototype);THREE.VertexNormalsHelper.prototype.constructor=THREE.VertexNormalsHelper; +THREE.VertexNormalsHelper.prototype.update=function(){var a=new THREE.Vector3,b=new THREE.Vector3,c=new THREE.Matrix3;return function(){var d=["a","b","c"];this.object.updateMatrixWorld(!0);c.getNormalMatrix(this.object.matrixWorld);var e=this.object.matrixWorld,f=this.geometry.attributes.position,g=this.object.geometry;if(g instanceof THREE.Geometry)for(var h=g.vertices,k=g.faces,l=g=0,n=k.length;lh.end&&(h.end=f);c||(c=k)}}for(k in d)h=d[k],this.createAnimation(k,h.start,h.end,a);this.firstAnimation=c}; +THREE.MorphBlendMesh.prototype.setAnimationDirectionForward=function(a){if(a=this.animationsMap[a])a.direction=1,a.directionBackwards=!1};THREE.MorphBlendMesh.prototype.setAnimationDirectionBackward=function(a){if(a=this.animationsMap[a])a.direction=-1,a.directionBackwards=!0};THREE.MorphBlendMesh.prototype.setAnimationFPS=function(a,b){var c=this.animationsMap[a];c&&(c.fps=b,c.duration=(c.end-c.start)/c.fps)}; +THREE.MorphBlendMesh.prototype.setAnimationDuration=function(a,b){var c=this.animationsMap[a];c&&(c.duration=b,c.fps=(c.end-c.start)/c.duration)};THREE.MorphBlendMesh.prototype.setAnimationWeight=function(a,b){var c=this.animationsMap[a];c&&(c.weight=b)};THREE.MorphBlendMesh.prototype.setAnimationTime=function(a,b){var c=this.animationsMap[a];c&&(c.time=b)};THREE.MorphBlendMesh.prototype.getAnimationTime=function(a){var b=0;if(a=this.animationsMap[a])b=a.time;return b}; +THREE.MorphBlendMesh.prototype.getAnimationDuration=function(a){var b=-1;if(a=this.animationsMap[a])b=a.duration;return b};THREE.MorphBlendMesh.prototype.playAnimation=function(a){var b=this.animationsMap[a];b?(b.time=0,b.active=!0):console.warn("THREE.MorphBlendMesh: animation["+a+"] undefined in .playAnimation()")};THREE.MorphBlendMesh.prototype.stopAnimation=function(a){if(a=this.animationsMap[a])a.active=!1}; +THREE.MorphBlendMesh.prototype.update=function(a){for(var b=0,c=this.animationsList.length;bd.duration||0>d.time)d.direction*=-1,d.time>d.duration&&(d.time=d.duration,d.directionBackwards=!0),0>d.time&&(d.time=0,d.directionBackwards=!1)}else d.time%=d.duration,0>d.time&&(d.time+=d.duration);var f=d.start+THREE.Math.clamp(Math.floor(d.time/e),0,d.length-1),g=d.weight;f!==d.currentFrame&& +(this.morphTargetInfluences[d.lastFrame]=0,this.morphTargetInfluences[d.currentFrame]=1*g,this.morphTargetInfluences[f]=0,d.lastFrame=d.currentFrame,d.currentFrame=f);e=d.time%e/e;d.directionBackwards&&(e=1-e);d.currentFrame!==d.lastFrame?(this.morphTargetInfluences[d.currentFrame]=e*g,this.morphTargetInfluences[d.lastFrame]=(1-e)*g):this.morphTargetInfluences[d.currentFrame]=g}}}; diff --git a/src/interface/desktop/loading-animation.js b/src/interface/desktop/loading-animation.js new file mode 100644 index 00000000..9f1a91f8 --- /dev/null +++ b/src/interface/desktop/loading-animation.js @@ -0,0 +1,129 @@ +var $wrap = document.getElementById('loading-animation'), + +canvassize = 280, + +length = 30, +radius = 5.6, + +rotatevalue = 0.035, +acceleration = 0, +animatestep = 0, +toend = false, + +pi2 = Math.PI*2, + +group = new THREE.Group(), +mesh, ringcover, ring, + +camera, scene, renderer; + + +camera = new THREE.PerspectiveCamera(65, 1, 1, 10000); +camera.position.z = 150; + +scene = new THREE.Scene(); +// scene.add(new THREE.AxisHelper(30)); +scene.add(group); + +mesh = new THREE.Mesh( + new THREE.TubeGeometry(new (THREE.Curve.create(function() {}, + function(percent) { + + var x = length*Math.sin(pi2*percent), + y = radius*Math.cos(pi2*3*percent), + z, t; + + t = percent%0.25/0.25; + t = percent%0.25-(2*(1-t)*t* -0.0185 +t*t*0.25); + if (Math.floor(percent/0.25) == 0 || Math.floor(percent/0.25) == 2) { + t *= -1; + } + z = radius*Math.sin(pi2*2* (percent-t)); + + return new THREE.Vector3(x, y, z); + + } + ))(), 200, 1.1, 2, true), + new THREE.MeshBasicMaterial({ + color: 0xfcc50b + // , wireframe: true + }) +); +group.add(mesh); + +ringcover = new THREE.Mesh(new THREE.PlaneGeometry(50, 15, 1), new THREE.MeshBasicMaterial({color: 0xd1684e, opacity: 0, transparent: true})); +ringcover.position.x = length+1; +ringcover.rotation.y = Math.PI/2; +group.add(ringcover); + +ring = new THREE.Mesh(new THREE.RingGeometry(4.3, 5.55, 32), new THREE.MeshBasicMaterial({color: 0xfcc50b, opacity: 0, transparent: true})); +ring.position.x = length+1.1; +ring.rotation.y = Math.PI/2; +group.add(ring); + +// fake shadow +(function() { + var plain, i; + for (i = 0; i < 10; i++) { + plain = new THREE.Mesh(new THREE.PlaneGeometry(length*2+1, radius*3, 1), new THREE.MeshBasicMaterial({color: 0xd1684e, transparent: true, opacity: 0.15})); + plain.position.z = -2.5+i*0.5; + group.add(plain); + } +})(); + +renderer = new THREE.WebGLRenderer({ + antialias: true +}); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(canvassize, canvassize); +renderer.setClearColor('#d1684e'); + + +$wrap.appendChild(renderer.domElement); + +function start() { + toend = true; +} + +function back() { + toend = false; +} + +function tilt(percent) { + group.rotation.y = percent*0.5; +} + +function render() { + var progress; + + animatestep = Math.max(0, Math.min(240, toend ? animatestep+1 : animatestep-4)); + acceleration = easing(animatestep, 0, 1, 240); + + if (acceleration > 0.35) { + progress = (acceleration-0.35)/0.65; + group.rotation.y = -Math.PI/2 *progress; + group.position.z = 50*progress; + progress = Math.max(0, (acceleration-0.97)/0.03); + mesh.material.opacity = 1-progress; + ringcover.material.opacity = ring.material.opacity = progress; + ring.scale.x = ring.scale.y = 0.9 + 0.1*progress; + } + + renderer.render(scene, camera); + +} + +function animate() { + mesh.rotation.x += rotatevalue + acceleration; + render(); + requestAnimationFrame(animate); +} + +function easing(t, b, c, d) { + if ((t /= d/2) < 1) + return c/2*t*t+b; + return c/2*((t-=2)*t*t+2)+b; +} + +animate(); +setTimeout(start, 300); diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index 2464286c..c7466edc 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -305,11 +305,13 @@ async function syncData (regenerate = false) { } } +let firstRun = true; let win = null; const createWindow = (tab = 'chat.html') => { win = new BrowserWindow({ width: 800, height: 800, + show: false, // titleBarStyle: 'hidden', webPreferences: { preload: path.join(__dirname, 'preload.js'), @@ -330,12 +332,30 @@ const createWindow = (tab = 'chat.html') => { win.setResizable(true); win.setOpacity(0.95); - win.setBackgroundColor('#FFFFFF'); + win.setBackgroundColor('#f5f4f3'); win.setHasShadow(true); job.start(); win.loadFile(tab) + + if (firstRun === true) { + firstRun = false; + + // Create splash screen + var splash = new BrowserWindow({width: 300, height: 300, transparent: true, frame: false, alwaysOnTop: true}); + splash.setOpacity(0.85); + splash.setBackgroundColor('#d16b4e'); + splash.loadFile('splash.html'); + + // Show splash screen on app load + win.once('ready-to-show', () => { + setTimeout(function(){ splash.close(); win.show(); }, 4500); + }); + } else { + // Show main window directly if not first run + win.once('ready-to-show', () => { win.show(); }); + } } app.whenReady().then(() => { diff --git a/src/interface/desktop/splash.html b/src/interface/desktop/splash.html new file mode 100644 index 00000000..5cc32ab6 --- /dev/null +++ b/src/interface/desktop/splash.html @@ -0,0 +1,15 @@ + + + + + Khoj + + + + + + +
+ + + From 4cd76311adb3610cd388e2fe6b7d05d670467c4e Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 3 Nov 2023 04:26:07 -0700 Subject: [PATCH 037/194] Slow down spinning at end of splash sequence. Make animation bigger --- src/interface/desktop/loading-animation.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/interface/desktop/loading-animation.js b/src/interface/desktop/loading-animation.js index 9f1a91f8..860b5d07 100644 --- a/src/interface/desktop/loading-animation.js +++ b/src/interface/desktop/loading-animation.js @@ -2,8 +2,8 @@ var $wrap = document.getElementById('loading-animation'), canvassize = 280, -length = 30, -radius = 5.6, +length = 40, +radius = 7.3, rotatevalue = 0.035, acceleration = 0, @@ -19,7 +19,7 @@ camera, scene, renderer; camera = new THREE.PerspectiveCamera(65, 1, 1, 10000); -camera.position.z = 150; +camera.position.z = 120; scene = new THREE.Scene(); // scene.add(new THREE.AxisHelper(30)); @@ -102,8 +102,8 @@ function render() { if (acceleration > 0.35) { progress = (acceleration-0.35)/0.65; group.rotation.y = -Math.PI/2 *progress; - group.position.z = 50*progress; - progress = Math.max(0, (acceleration-0.97)/0.03); + group.position.z = 20*progress; + progress = Math.max(0, (acceleration-0.99)/0.01); mesh.material.opacity = 1-progress; ringcover.material.opacity = ring.material.opacity = progress; ring.scale.x = ring.scale.y = 0.9 + 0.1*progress; @@ -114,7 +114,7 @@ function render() { } function animate() { - mesh.rotation.x += rotatevalue + acceleration; + mesh.rotation.x += rotatevalue + acceleration*Math.sin(Math.PI*acceleration); render(); requestAnimationFrame(animate); } @@ -126,4 +126,4 @@ function easing(t, b, c, d) { } animate(); -setTimeout(start, 300); +setTimeout(start, 30); From db57eeaefef3f93f07f05ee8b06ae270564a6be2 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 3 Nov 2023 05:11:41 -0700 Subject: [PATCH 038/194] Console log a welcome message on loading Desktop client --- src/interface/desktop/chat.html | 2 ++ src/interface/desktop/config.html | 1 + src/interface/desktop/khoj.js | 14 ++++++++++++++ src/interface/desktop/search.html | 1 + 4 files changed, 18 insertions(+) create mode 100644 src/interface/desktop/khoj.js diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 72a7a515..2c07600f 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -8,6 +8,8 @@ + + diff --git a/src/interface/desktop/khoj.js b/src/interface/desktop/khoj.js new file mode 100644 index 00000000..87552106 --- /dev/null +++ b/src/interface/desktop/khoj.js @@ -0,0 +1,14 @@ +console.log(`%c %s`, "font-family:monospace", ` + __ __ __ __ ______ __ _____ __ +/\\ \\/ / /\\ \\_\\ \\ /\\ __ \\ /\\ \\ /\\ __ \\ /\\ \\ +\\ \\ _"-. \\ \\ __ \\ \\ \\ \\/\\ \\ _\\_\\ \\ \\ \\ __ \\ \\ \\ \\ + \\ \\_\\ \\_\\ \\ \\_\\ \\_\\ \\ \\_____\\ /\\_____\\ \\ \\_\\ \\_\\ \\ \\_\\ + \\/_/\\/_/ \\/_/\\/_/ \\/_____/ \\/_____/ \\/_/\\/_/ \\/_/ + +Greetings traveller, + +I am ✨Khoj✨, your open-source, personal AI copilot. + +See my source code at https://github.com/khoj-ai/khoj +Read my operating manual at https://docs.khoj.dev +`); diff --git a/src/interface/desktop/search.html b/src/interface/desktop/search.html index dbb18f5f..edde6906 100644 --- a/src/interface/desktop/search.html +++ b/src/interface/desktop/search.html @@ -10,6 +10,7 @@ + + + diff --git a/src/interface/desktop/search.html b/src/interface/desktop/search.html index edde6906..62410db1 100644 --- a/src/interface/desktop/search.html +++ b/src/interface/desktop/search.html @@ -10,7 +10,7 @@ - + +
diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index d36e8062..13ba9875 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -8,7 +8,7 @@ - + - + + + +
+ +

Khoj for Desktop +

+
+
+ + +
+
+ © 2023 Khoj Inc. All rights reserved. +
+ + diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index c7466edc..82e54c42 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -1,5 +1,6 @@ -const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } = require('electron'); +const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage, shell } = require('electron'); const todesktop = require("@todesktop/runtime"); +const khojPackage = require('./package.json'); todesktop.init(); @@ -390,11 +391,12 @@ app.whenReady().then(() => { app.setAboutPanelOptions({ applicationName: "Khoj", - applicationVersion: "0.0.1", - version: "0.0.1", - authors: "Khoj Team", + applicationVersion: khojPackage.version, + version: khojPackage.version, + authors: "Saba Imran, Debanjum Singh Solanky and contributors", website: "https://khoj.dev", - iconPath: path.join(__dirname, 'assets', 'khoj.png') + copyright: "GPL v3", + iconPath: path.join(__dirname, 'assets', 'icons', 'favicon-128x128.png') }); app.on('ready', async() => { @@ -418,6 +420,43 @@ app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) +/* +** About Page +*/ + +let aboutWindow; + +function openAboutWindow() { + if (aboutWindow) { aboutWindow.focus(); return; } + + aboutWindow = new BrowserWindow({ + width: 400, + height: 400, + titleBarStyle: 'hidden', + show: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: true, + }, + }); + + aboutWindow.loadFile('about.html'); + + // Pass OS, Khoj version to About page + aboutWindow.webContents.on('did-finish-load', () => { + aboutWindow.webContents.send('appInfo', { version: khojPackage.version, platform: process.platform }); + }); + + // Open links in external browser + aboutWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); + + aboutWindow.once('ready-to-show', () => { aboutWindow.show(); }); + aboutWindow.on('closed', () => { aboutWindow = null; }); +} + /* ** System Tray Icon */ @@ -441,6 +480,7 @@ app.whenReady().then(() => { { label: 'Search', type: 'normal', click: () => { openWindow('search.html') }}, { label: 'Configure', type: 'normal', click: () => { openWindow('config.html') }}, { type: 'separator' }, + { label: 'About Khoj', type: 'normal', click: () => { openAboutWindow(); } }, { label: 'Quit', type: 'normal', click: () => { app.quit() } } ]) diff --git a/src/interface/desktop/preload.js b/src/interface/desktop/preload.js index 89c56062..3228fdb0 100644 --- a/src/interface/desktop/preload.js +++ b/src/interface/desktop/preload.js @@ -52,3 +52,7 @@ contextBridge.exposeInMainWorld('tokenAPI', { setToken: (token) => ipcRenderer.invoke('setToken', token), getToken: () => ipcRenderer.invoke('getToken') }) + +contextBridge.exposeInMainWorld('appInfoAPI', { + getInfo: (callback) => ipcRenderer.on('appInfo', callback) +}) diff --git a/src/interface/desktop/utils.js b/src/interface/desktop/utils.js index 87552106..8f9c0aeb 100644 --- a/src/interface/desktop/utils.js +++ b/src/interface/desktop/utils.js @@ -12,3 +12,15 @@ I am ✨Khoj✨, your open-source, personal AI copilot. See my source code at https://github.com/khoj-ai/khoj Read my operating manual at https://docs.khoj.dev `); + + +window.appInfoAPI.getInfo((_, info) => { + let khojVersionElement = document.getElementById("about-page-version"); + if (khojVersionElement) { + khojVersionElement.innerHTML = `${info.version}`; + } + let khojTitleElement = document.getElementById("about-page-title"); + if (khojTitleElement) { + khojTitleElement.innerHTML = 'Khoj for ' + (info.platform === 'win32' ? 'Windows' : info.platform === 'darwin' ? 'macOS' : 'Linux') + ''; + } +}); From 3ef05f4803024c6b4270b0c18a94e92f45b5d119 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 3 Nov 2023 22:14:00 -0700 Subject: [PATCH 041/194] Use css var for main font color in search, chat page of desktop app --- src/interface/desktop/assets/khoj.css | 3 ++- src/interface/desktop/chat.html | 2 +- src/interface/desktop/search.html | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/interface/desktop/assets/khoj.css b/src/interface/desktop/assets/khoj.css index 68acd15e..649f79bb 100644 --- a/src/interface/desktop/assets/khoj.css +++ b/src/interface/desktop/assets/khoj.css @@ -92,8 +92,9 @@ a.khoj-logo { } .khoj-nav a:hover { background-color: var(--primary-hover); + color: var(--main-text-color); } -.khoj-nav-selected { +a.khoj-nav-selected { background-color: var(--primary); } img.khoj-logo { diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 031c53f4..11f6f17d 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -306,7 +306,7 @@ body { display: grid; background: var(--background-color); - color: #475569; + color: var(--main-text-color); text-align: center; font-family: roboto, karma, segoe ui, sans-serif; font-size: small; diff --git a/src/interface/desktop/search.html b/src/interface/desktop/search.html index 62410db1..d02dc5a5 100644 --- a/src/interface/desktop/search.html +++ b/src/interface/desktop/search.html @@ -304,7 +304,7 @@ padding: 0px; margin: 0px; background: var(--background-color); - color: #475569; + color: var(--main-text-color); font-family: roboto, karma, segoe ui, sans-serif; font-size: small; font-weight: 300; From e8f568d79c3598d0f6942e1e85aac5e58247b0c2 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Fri, 3 Nov 2023 22:20:11 -0700 Subject: [PATCH 042/194] Make splash screen wider, opaque and fix it's spinner radius Radius should be such that final spin doesn't extend out of the circle Opaque background improves contrast for better visual --- src/interface/desktop/loading-animation.js | 6 +++--- src/interface/desktop/main.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/interface/desktop/loading-animation.js b/src/interface/desktop/loading-animation.js index 860b5d07..d9bffb57 100644 --- a/src/interface/desktop/loading-animation.js +++ b/src/interface/desktop/loading-animation.js @@ -1,11 +1,11 @@ var $wrap = document.getElementById('loading-animation'), -canvassize = 280, +canvassize = 380, length = 40, -radius = 7.3, +radius = 6.8, -rotatevalue = 0.035, +rotatevalue = 0.02, acceleration = 0, animatestep = 0, toend = false, diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index 82e54c42..1d5c4be2 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -344,8 +344,8 @@ const createWindow = (tab = 'chat.html') => { firstRun = false; // Create splash screen - var splash = new BrowserWindow({width: 300, height: 300, transparent: true, frame: false, alwaysOnTop: true}); - splash.setOpacity(0.85); + var splash = new BrowserWindow({width: 400, height: 400, transparent: true, frame: false, alwaysOnTop: true}); + splash.setOpacity(1.0); splash.setBackgroundColor('#d16b4e'); splash.loadFile('splash.html'); From 2f1756cc15625b95ed795ce68906fc0530665c13 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 00:13:10 -0700 Subject: [PATCH 043/194] Do not use icon for each file, folder to index in desktop app. Other minor fixes based on PR feedback --- src/interface/desktop/config.html | 2 +- src/interface/desktop/renderer.js | 12 ------------ src/khoj/interface/web/login.html | 2 +- tests/conftest.py | 2 +- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/interface/desktop/config.html b/src/interface/desktop/config.html index 9b89368b..afea6a80 100644 --- a/src/interface/desktop/config.html +++ b/src/interface/desktop/config.html @@ -302,7 +302,7 @@ div.file-element, div.folder-element { display: grid; - grid-template-columns: 1fr auto 1fr; + grid-template-columns: auto 1fr; 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.8); diff --git a/src/interface/desktop/renderer.js b/src/interface/desktop/renderer.js index 3d8a1d4e..1e1fae32 100644 --- a/src/interface/desktop/renderer.js +++ b/src/interface/desktop/renderer.js @@ -62,12 +62,6 @@ function makeFileElement(file) { let fileElement = document.createElement("div"); fileElement.classList.add("file-element"); - let fileIconElement = document.createElement("img"); - fileIconElement.classList.add("card-icon"); - fileIconElement.src = "./assets/icons/plaintext.svg"; - fileIconElement.alt = "File"; - fileElement.appendChild(fileIconElement); - let fileNameElement = document.createElement("div"); fileNameElement.classList.add("content-name"); fileNameElement.innerHTML = file.path; @@ -90,12 +84,6 @@ function makeFolderElement(folder) { let folderElement = document.createElement("div"); folderElement.classList.add("folder-element"); - let folderIconElement = document.createElement("img"); - folderIconElement.classList.add("card-icon"); - folderIconElement.src = "./assets/icons/folder.svg"; - folderIconElement.alt = "File"; - folderElement.appendChild(folderIconElement); - let folderNameElement = document.createElement("div"); folderNameElement.classList.add("content-name"); folderNameElement.innerHTML = folder.path; diff --git a/src/khoj/interface/web/login.html b/src/khoj/interface/web/login.html index 5d40397c..2aace820 100644 --- a/src/khoj/interface/web/login.html +++ b/src/khoj/interface/web/login.html @@ -135,7 +135,7 @@ display: block; } - .login-modal-title { + div.login-modal-title { text-align: center; line-height: 28px; font-size: 24px; diff --git a/tests/conftest.py b/tests/conftest.py index 80feaaac..b20e5a7f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -169,7 +169,7 @@ def md_content_config(): return markdown_config -@pytest.fixture(scope="session") +@pytest.fixture(scope="function") def chat_client(search_config: SearchConfig, default_user2: KhojUser): # Initialize app state state.config.search_type = search_config From 8273bf26b78dfacd7accf04f9367579cf6e16d45 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 01:09:35 -0700 Subject: [PATCH 044/194] Fix multi-line chat input and output render on web, desktop clients - Remove spurious whitespace in chat input box on page load being added because text area element was ending on newline - Do not insert newline in message when send message by hitting enter key This would be more evident when send message with cursor in the middle of the sentence, as a newline would be inserted at the cursor point - Remove chat message separator tokens from model output. Model sometimes starts to output text in it's chat format --- src/interface/desktop/chat.html | 6 ++++-- src/khoj/interface/web/chat.html | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index 11f6f17d..b27bbc90 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -68,6 +68,8 @@ // Replace any ** with and __ with newHTML = newHTML.replace(/\*\*([\s\S]*?)\*\*/g, '$1'); newHTML = newHTML.replace(/__([\s\S]*?)__/g, '$1'); + // Remove any text between [INST] and tags. These are spurious instructions for the AI chat model. + newHTML = newHTML.replace(/\[INST\].+(<\/s>)?/g, ''); return newHTML; } @@ -168,6 +170,7 @@ function incrementalChat(event) { if (!event.shiftKey && event.key === 'Enter') { + event.preventDefault(); chat(); } } @@ -291,8 +294,7 @@ diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 13ba9875..30304caf 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -67,6 +67,8 @@ // Replace any ** with and __ with newHTML = newHTML.replace(/\*\*([\s\S]*?)\*\*/g, '$1'); newHTML = newHTML.replace(/__([\s\S]*?)__/g, '$1'); + // Remove any text between [INST] and tags. These are spurious instructions for the AI chat model. + newHTML = newHTML.replace(/\[INST\].+(<\/s>)?/g, ''); return newHTML; } @@ -163,6 +165,7 @@ function incrementalChat(event) { if (!event.shiftKey && event.key === 'Enter') { + e.preventDefault(); chat(); } } @@ -281,8 +284,7 @@ From 3678aa5614858ac75189a9e0f5518c3138072f56 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 14:29:30 -0700 Subject: [PATCH 045/194] Add tests to validate expected behaviors in the multi-user scenario --- tests/conftest.py | 13 ++++ tests/test_client.py | 33 +++++------ tests/test_multiple_users.py | 111 +++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 tests/test_multiple_users.py diff --git a/tests/conftest.py b/tests/conftest.py index a5f23dd2..3c579834 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,6 +98,19 @@ def api_user(default_user): ) +@pytest.mark.django_db +@pytest.fixture +def api_user2(default_user2): + if KhojApiUser.objects.filter(user=default_user2).exists(): + return KhojApiUser.objects.get(user=default_user2) + + return KhojApiUser.objects.create( + user=default_user2, + name="api-key", + token="kk-diff-secret", + ) + + @pytest.fixture(scope="session") def search_models(search_config: SearchConfig): search_models = SearchModels() diff --git a/tests/test_client.py b/tests/test_client.py index 5cf438c7..c105c605 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -352,21 +352,20 @@ def test_different_user_data_not_accessed(client, sample_org_data, default_user: def get_sample_files_data(): - return { - "files": ("path/to/filename.org", "* practicing piano", "text/org"), - "files": ("path/to/filename1.org", "** top 3 reasons why I moved to SF", "text/org"), - "files": ("path/to/filename2.org", "* how to build a search engine", "text/org"), - "files": ("path/to/filename.pdf", "Moore's law does not apply to consumer hardware", "application/pdf"), - "files": ("path/to/filename1.pdf", "The sun is a ball of helium", "application/pdf"), - "files": ("path/to/filename2.pdf", "Effect of sunshine on baseline human happiness", "application/pdf"), - "files": ("path/to/filename.txt", "data,column,value", "text/plain"), - "files": ("path/to/filename1.txt", "my first web page", "text/plain"), - "files": ("path/to/filename2.txt", "2021-02-02 Journal Entry", "text/plain"), - "files": ("path/to/filename.md", "# Notes from client call", "text/markdown"), - "files": ( - "path/to/filename1.md", - "## Studying anthropological records from the Fatimid caliphate", - "text/markdown", + return [ + ("files", ("path/to/filename.org", "* practicing piano", "text/org")), + ("files", ("path/to/filename1.org", "** top 3 reasons why I moved to SF", "text/org")), + ("files", ("path/to/filename2.org", "* how to build a search engine", "text/org")), + ("files", ("path/to/filename.pdf", "Moore's law does not apply to consumer hardware", "application/pdf")), + ("files", ("path/to/filename1.pdf", "The sun is a ball of helium", "application/pdf")), + ("files", ("path/to/filename2.pdf", "Effect of sunshine on baseline human happiness", "application/pdf")), + ("files", ("path/to/filename.txt", "data,column,value", "text/plain")), + ("files", ("path/to/filename1.txt", "my first web page", "text/plain")), + ("files", ("path/to/filename2.txt", "2021-02-02 Journal Entry", "text/plain")), + ("files", ("path/to/filename.md", "# Notes from client call", "text/markdown")), + ( + "files", + ("path/to/filename1.md", "## Studying anthropological records from the Fatimid caliphate", "text/markdown"), ), - "files": ("path/to/filename2.md", "**Understanding science through the lens of art**", "text/markdown"), - } + ("files", ("path/to/filename2.md", "**Understanding science through the lens of art**", "text/markdown")), + ] diff --git a/tests/test_multiple_users.py b/tests/test_multiple_users.py new file mode 100644 index 00000000..95a2535f --- /dev/null +++ b/tests/test_multiple_users.py @@ -0,0 +1,111 @@ +# Standard Modules +from io import BytesIO +from PIL import Image +from urllib.parse import quote +import pytest + +# External Packages +from fastapi.testclient import TestClient +from fastapi import FastAPI, UploadFile +from io import BytesIO +import pytest + +# Internal Packages +from khoj.configure import configure_routes, configure_search_types +from khoj.utils import state +from khoj.utils.state import search_models, content_index, config +from khoj.search_type import text_search, image_search +from khoj.utils.rawconfig import ContentConfig, SearchConfig +from khoj.processor.org_mode.org_to_entries import OrgToEntries +from database.models import KhojUser, KhojApiUser +from database.adapters import EntryAdapters + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) +def test_search_for_user2_returns_empty(client, api_user2: KhojApiUser): + token = api_user2.token + headers = {"Authorization": f"Bearer {token}"} + for content_type in ["all", "org", "markdown", "pdf", "github", "notion", "plaintext"]: + # Act + response = client.get(f"/api/search?q=random&t={content_type}", headers=headers) + # Assert + assert response.text == "[]" + assert response.status_code == 200, f"Returned status: {response.status_code} for content type: {content_type}" + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) +def test_index_update_with_user2(client, api_user2: KhojApiUser): + # Arrange + files = get_sample_files_data() + source_file_symbol = set([f[1][0] for f in files]) + + headers = {"Authorization": f"Bearer {api_user2.token}"} + update_response = client.post("/api/v1/index/update", files=files, headers=headers) + search_response = client.get("/api/search?q=hardware&t=all", headers=headers) + results = search_response.json() + + # Assert + assert update_response.status_code == 200 + assert len(results) == 5 + for result in results: + assert result["additional"]["file"] in source_file_symbol + + +@pytest.mark.django_db(transaction=True) +def test_index_update_with_user2_inaccessible_user1(client, api_user2: KhojApiUser, api_user: KhojApiUser): + # Arrange + files = get_sample_files_data() + source_file_symbol = set([f[1][0] for f in files]) + + headers = {"Authorization": f"Bearer {api_user2.token}"} + update_response = client.post("/api/v1/index/update", files=files, headers=headers) + + # Act + headers = {"Authorization": f"Bearer {api_user.token}"} + search_response = client.get("/api/search?q=hardware&t=all", headers=headers) + results = search_response.json() + + # Assert + assert update_response.status_code == 200 + assert len(results) == 4 + for result in results: + assert result["additional"]["file"] not in source_file_symbol + + +# ---------------------------------------------------------------------------------------------------- +@pytest.mark.django_db(transaction=True) +def test_different_user_data_not_accessed(client, sample_org_data, default_user: KhojUser): + # Arrange + headers = {"Authorization": "Bearer kk-token"} # Token for default_user2 + text_search.setup(OrgToEntries, sample_org_data, regenerate=False, user=default_user) + user_query = quote("How to git install application?") + + # Act + response = client.get(f"/api/search?q={user_query}&n=1&t=org", headers=headers) + + # Assert + assert response.status_code == 403 + # assert actual response has no data as the default_user is different from the user making the query (anonymous) + assert len(response.json()) == 1 and response.json()["detail"] == "Forbidden" + + +def get_sample_files_data(): + return [ + ("files", ("path/to/filename.org", "* practicing piano", "text/org")), + ("files", ("path/to/filename1.org", "** top 3 reasons why I moved to SF", "text/org")), + ("files", ("path/to/filename2.org", "* how to build a search engine", "text/org")), + ("files", ("path/to/filename.pdf", "Moore's law does not apply to consumer hardware", "application/pdf")), + ("files", ("path/to/filename1.pdf", "The sun is a ball of helium", "application/pdf")), + ("files", ("path/to/filename2.pdf", "Effect of sunshine on baseline human happiness", "application/pdf")), + ("files", ("path/to/filename.txt", "data,column,value", "text/plain")), + ("files", ("path/to/filename1.txt", "my first web page", "text/plain")), + ("files", ("path/to/filename2.txt", "2021-02-02 Journal Entry", "text/plain")), + ("files", ("path/to/filename.md", "# Notes from client call", "text/markdown")), + ( + "files", + ("path/to/filename1.md", "## Studying anthropological records from the Fatimid caliphate", "text/markdown"), + ), + ("files", ("path/to/filename2.md", "**Understanding science through the lens of art**", "text/markdown")), + ] From b5972e93111fb122db193b85f262b26464d127bf Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 17:15:28 -0700 Subject: [PATCH 046/194] Use OCR to extract image text in PDFs --- pyproject.toml | 1 + src/khoj/processor/pdf/pdf_to_entries.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c816f4d2..25a78ab9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ dependencies = [ "gunicorn == 21.2.0", "lxml == 4.9.3", "tzdata == 2023.3", + "rapidocr-onnxruntime == 1.3.8" ] dynamic = ["version"] diff --git a/src/khoj/processor/pdf/pdf_to_entries.py b/src/khoj/processor/pdf/pdf_to_entries.py index 24dcdc5a..19d463eb 100644 --- a/src/khoj/processor/pdf/pdf_to_entries.py +++ b/src/khoj/processor/pdf/pdf_to_entries.py @@ -68,7 +68,7 @@ class PdfToEntries(TextToEntries): with open(f"{tmp_file}", "wb") as f: bytes = pdf_files[pdf_file] f.write(bytes) - loader = PyMuPDFLoader(f"{tmp_file}") + loader = PyMuPDFLoader(f"{tmp_file}", extract_images=True) pdf_entries_per_file = [page.page_content for page in loader.load()] entry_to_location_map += zip(pdf_entries_per_file, [pdf_file] * len(pdf_entries_per_file)) entries.extend(pdf_entries_per_file) From 800bb4f458e995391f66e4445897943a67a3ac8f Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 17:17:04 -0700 Subject: [PATCH 047/194] Remove references to demo - The demo setting is no longer necessary for the time being, as we won't have anymore demo instances --- src/interface/desktop/assets/khoj.css | 15 -- src/interface/desktop/chat.html | 36 +-- src/interface/desktop/config.html | 29 --- src/interface/desktop/search.html | 60 ----- src/khoj/configure.py | 30 ++- src/khoj/interface/web/assets/khoj.css | 15 -- src/khoj/interface/web/chat.html | 75 +------ src/khoj/interface/web/login.html | 60 ----- src/khoj/interface/web/search.html | 72 ------ src/khoj/main.py | 1 - src/khoj/routers/api.py | 291 +++++++++++++------------ src/khoj/routers/web_client.py | 260 +++++++++++----------- src/khoj/utils/cli.py | 1 - src/khoj/utils/state.py | 1 - 14 files changed, 297 insertions(+), 649 deletions(-) diff --git a/src/interface/desktop/assets/khoj.css b/src/interface/desktop/assets/khoj.css index 649f79bb..b2e048c5 100644 --- a/src/interface/desktop/assets/khoj.css +++ b/src/interface/desktop/assets/khoj.css @@ -103,21 +103,6 @@ img.khoj-logo { justify-self: center; } -a.khoj-banner { - color: black; - text-decoration: none; -} - -p.khoj-banner { - font-size: small; - margin: 0; - padding: 10px; -} - -p#khoj-banner { - display: inline; -} - @media only screen and (max-width: 600px) { div.khoj-header { display: grid; diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index b27bbc90..b908b747 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -274,8 +274,9 @@ } -
+
+
-

- Enroll in Khoj cloud to get your own assistant -

-
- - - {% endif %} +
@@ -480,12 +470,6 @@ margin: 4px; grid-template-columns: auto; } - a.khoj-banner { - display: block; - } - p.khoj-banner { - padding: 0; - } } @media only screen and (min-width: 700px) { body { @@ -497,14 +481,6 @@ } } - div.khoj-banner-container { - background: linear-gradient(-45deg, #FFC107, #FF9800, #FF5722, #FF9800, #FFC107); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - text-align: center; - padding: 10px; - } - div#chat-tooltip { text-align: left; font-size: medium; @@ -526,19 +502,7 @@ text-align: center; } - button#khoj-banner-submit, - input#khoj-banner-email { - padding: 10px; - border-radius: 5px; - border: 1px solid var(--main-text-color); - background: #f9fafc; - } - - button#khoj-banner-submit:hover, - input#khoj-banner-email:hover { - box-shadow: 0 0 11px #aaa; - } - div.khoj-banner-container-hidden { + div.khoj-empty-container { margin: 0px; padding: 0px; } @@ -558,39 +522,4 @@ white-space: pre-wrap; } - diff --git a/src/khoj/interface/web/login.html b/src/khoj/interface/web/login.html index 2aace820..a191de7b 100644 --- a/src/khoj/interface/web/login.html +++ b/src/khoj/interface/web/login.html @@ -10,18 +10,6 @@ - {% if demo %} - - - {% endif %}
@@ -106,19 +94,6 @@ justify-self: center; } - button#khoj-banner-submit, - input#khoj-banner-email { - padding: 10px; - border-radius: 5px; - border: 1px solid #475569; - background: #f9fafc; - } - - button#khoj-banner-submit:hover, - input#khoj-banner-email:hover { - box-shadow: 0 0 11px #aaa; - } - div#login-modal { display: grid; grid-template-columns: 1fr; @@ -143,12 +118,6 @@ } @media only screen and (max-width: 700px) { - a.khoj-banner { - display: block; - } - p.khoj-banner { - padding: 0; - } div#login-modal { margin-left: 10%; margin-right: 10%; @@ -156,34 +125,5 @@ } - diff --git a/src/khoj/interface/web/search.html b/src/khoj/interface/web/search.html index 1a014a93..8f98d0f3 100644 --- a/src/khoj/interface/web/search.html +++ b/src/khoj/interface/web/search.html @@ -270,19 +270,6 @@ - {% if demo %} - - - {% endif %} - {% import 'utils.html' as utils %} {{ utils.heading_pane(user_photo, username) }} @@ -458,14 +445,6 @@ max-width: 90%; } - div.khoj-banner-container { - background: linear-gradient(-45deg, #FFC107, #FF9800, #FF5722, #FF9800, #FFC107); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - text-align: center; - padding: 10px; - } - @keyframes gradient { 0% { background-position: 0% 50%; @@ -482,57 +461,6 @@ text-align: center; } - button#khoj-banner-submit, - input#khoj-banner-email { - padding: 10px; - border-radius: 5px; - border: 1px solid var(--main-text-color); - background: #f9fafc; - } - - button#khoj-banner-submit:hover, - input#khoj-banner-email:hover { - box-shadow: 0 0 11px #aaa; - } - - @media only screen and (max-width: 700px) { - a.khoj-banner { - display: block; - } - p.khoj-banner { - padding: 0; - } - } - - diff --git a/src/khoj/main.py b/src/khoj/main.py index 33029b94..f92e8cbe 100644 --- a/src/khoj/main.py +++ b/src/khoj/main.py @@ -119,7 +119,6 @@ def set_state(args): state.verbose = args.verbose state.host = args.host state.port = args.port - state.demo = args.demo state.anonymous_mode = args.anonymous_mode state.khoj_version = version("khoj-assistant") state.chat_on_gpu = args.chat_on_gpu diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index b8a5350b..5c1ec912 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -111,183 +111,187 @@ async def map_config_to_db(config: FullConfig, user: KhojUser): ) -# If it's a demo instance, prevent updating any of the configuration. -if not state.demo: +def _initialize_config(): + if state.config is None: + state.config = FullConfig() + state.config.search_type = SearchConfig.parse_obj(constants.default_config["search-type"]) - def _initialize_config(): - if state.config is None: - state.config = FullConfig() - state.config.search_type = SearchConfig.parse_obj(constants.default_config["search-type"]) - @api.get("/config/data", response_model=FullConfig) - @requires(["authenticated"]) - def get_config_data(request: Request): - user = request.user.object - EntryAdapters.get_unique_file_types(user) +@api.get("/config/data", response_model=FullConfig) +@requires(["authenticated"]) +def get_config_data(request: Request): + user = request.user.object + EntryAdapters.get_unique_file_types(user) - return state.config + return state.config - @api.post("/config/data") - @requires(["authenticated"]) - async def set_config_data( - request: Request, - updated_config: FullConfig, - client: Optional[str] = None, - ): - user = request.user.object - await map_config_to_db(updated_config, user) - configuration_update_metadata = {} +@api.post("/config/data") +@requires(["authenticated"]) +async def set_config_data( + request: Request, + updated_config: FullConfig, + client: Optional[str] = None, +): + user = request.user.object + await map_config_to_db(updated_config, user) - enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user) + configuration_update_metadata = {} - if state.config.content_type is not None: - configuration_update_metadata["github"] = "github" in enabled_content - configuration_update_metadata["notion"] = "notion" in enabled_content - configuration_update_metadata["org"] = "org" in enabled_content - configuration_update_metadata["pdf"] = "pdf" in enabled_content - configuration_update_metadata["markdown"] = "markdown" in enabled_content + enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user) - if state.config.processor is not None: - configuration_update_metadata["conversation_processor"] = state.config.processor.conversation is not None + if state.config.content_type is not None: + configuration_update_metadata["github"] = "github" in enabled_content + configuration_update_metadata["notion"] = "notion" in enabled_content + configuration_update_metadata["org"] = "org" in enabled_content + configuration_update_metadata["pdf"] = "pdf" in enabled_content + configuration_update_metadata["markdown"] = "markdown" in enabled_content - update_telemetry_state( - request=request, - telemetry_type="api", - api="set_config", - client=client, - metadata=configuration_update_metadata, - ) - return state.config + if state.config.processor is not None: + configuration_update_metadata["conversation_processor"] = state.config.processor.conversation is not None - @api.post("/config/data/content_type/github", status_code=200) - @requires(["authenticated"]) - async def set_content_config_github_data( - request: Request, - updated_config: Union[GithubContentConfig, None], - client: Optional[str] = None, - ): - _initialize_config() + update_telemetry_state( + request=request, + telemetry_type="api", + api="set_config", + client=client, + metadata=configuration_update_metadata, + ) + return state.config - user = request.user.object - await adapters.set_user_github_config( - user=user, - pat_token=updated_config.pat_token, - repos=updated_config.repos, - ) +@api.post("/config/data/content_type/github", status_code=200) +@requires(["authenticated"]) +async def set_content_config_github_data( + request: Request, + updated_config: Union[GithubContentConfig, None], + client: Optional[str] = None, +): + _initialize_config() - update_telemetry_state( - request=request, - telemetry_type="api", - api="set_content_config", - client=client, - metadata={"content_type": "github"}, - ) + user = request.user.object - return {"status": "ok"} + await adapters.set_user_github_config( + user=user, + pat_token=updated_config.pat_token, + repos=updated_config.repos, + ) - @api.post("/config/data/content_type/notion", status_code=200) - @requires(["authenticated"]) - async def set_content_config_notion_data( - request: Request, - updated_config: Union[NotionContentConfig, None], - client: Optional[str] = None, - ): - _initialize_config() + update_telemetry_state( + request=request, + telemetry_type="api", + api="set_content_config", + client=client, + metadata={"content_type": "github"}, + ) - user = request.user.object + return {"status": "ok"} - await adapters.set_notion_config( - user=user, - token=updated_config.token, - ) - update_telemetry_state( - request=request, - telemetry_type="api", - api="set_content_config", - client=client, - metadata={"content_type": "notion"}, - ) +@api.post("/config/data/content_type/notion", status_code=200) +@requires(["authenticated"]) +async def set_content_config_notion_data( + request: Request, + updated_config: Union[NotionContentConfig, None], + client: Optional[str] = None, +): + _initialize_config() - return {"status": "ok"} + user = request.user.object - @api.post("/delete/config/data/content_type/{content_type}", status_code=200) - @requires(["authenticated"]) - async def remove_content_config_data( - request: Request, - content_type: str, - client: Optional[str] = None, - ): - user = request.user.object + await adapters.set_notion_config( + user=user, + token=updated_config.token, + ) - update_telemetry_state( - request=request, - telemetry_type="api", - api="delete_content_config", - client=client, - metadata={"content_type": content_type}, - ) + update_telemetry_state( + request=request, + telemetry_type="api", + api="set_content_config", + client=client, + metadata={"content_type": "notion"}, + ) - content_object = map_config_to_object(content_type) - if content_object is None: - raise ValueError(f"Invalid content type: {content_type}") + return {"status": "ok"} - await content_object.objects.filter(user=user).adelete() - await sync_to_async(EntryAdapters.delete_all_entries)(user, content_type) - enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user) - return {"status": "ok"} +@api.post("/delete/config/data/content_type/{content_type}", status_code=200) +@requires(["authenticated"]) +async def remove_content_config_data( + request: Request, + content_type: str, + client: Optional[str] = None, +): + user = request.user.object - @api.post("/config/data/content_type/{content_type}", status_code=200) - @requires(["authenticated"]) - async def set_content_config_data( - request: Request, - content_type: str, - updated_config: Union[TextContentConfig, None], - client: Optional[str] = None, - ): - _initialize_config() + update_telemetry_state( + request=request, + telemetry_type="api", + api="delete_content_config", + client=client, + metadata={"content_type": content_type}, + ) - user = request.user.object + content_object = map_config_to_object(content_type) + if content_object is None: + raise ValueError(f"Invalid content type: {content_type}") - content_object = map_config_to_object(content_type) - await adapters.set_text_content_config(user, content_object, updated_config) + await content_object.objects.filter(user=user).adelete() + await sync_to_async(EntryAdapters.delete_all_entries)(user, content_type) - update_telemetry_state( - request=request, - telemetry_type="api", - api="set_content_config", - client=client, - metadata={"content_type": content_type}, - ) + enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user) + return {"status": "ok"} - return {"status": "ok"} - @api.post("/config/data/conversation/model", status_code=200) - @requires(["authenticated"]) - async def update_chat_model( - request: Request, - id: str, - client: Optional[str] = None, - ): - user = request.user.object +@api.post("/config/data/content_type/{content_type}", status_code=200) +@requires(["authenticated"]) +async def set_content_config_data( + request: Request, + content_type: str, + updated_config: Union[TextContentConfig, None], + client: Optional[str] = None, +): + _initialize_config() - new_config = await ConversationAdapters.aset_user_conversation_processor(user, int(id)) + user = request.user.object - update_telemetry_state( - request=request, - telemetry_type="api", - api="set_conversation_chat_model", - client=client, - metadata={"processor_conversation_type": "conversation"}, - ) + content_object = map_config_to_object(content_type) + await adapters.set_text_content_config(user, content_object, updated_config) - if new_config is None: - return {"status": "error", "message": "Model not found"} + update_telemetry_state( + request=request, + telemetry_type="api", + api="set_content_config", + client=client, + metadata={"content_type": content_type}, + ) - return {"status": "ok"} + return {"status": "ok"} + + +@api.post("/config/data/conversation/model", status_code=200) +@requires(["authenticated"]) +async def update_chat_model( + request: Request, + id: str, + client: Optional[str] = None, +): + user = request.user.object + + new_config = await ConversationAdapters.aset_user_conversation_processor(user, int(id)) + + update_telemetry_state( + request=request, + telemetry_type="api", + api="set_conversation_chat_model", + client=client, + metadata={"processor_conversation_type": "conversation"}, + ) + + if new_config is None: + return {"status": "error", "message": "Model not found"} + + return {"status": "ok"} # Create Routes @@ -377,6 +381,7 @@ async def search( SearchType.Github, SearchType.Notion, SearchType.Plaintext, + SearchType.Pdf, ]: # query markdown notes search_futures += [ diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 1ab6beb7..35603e18 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -38,7 +38,6 @@ def index(request: Request): "chat.html", context={ "request": request, - "demo": state.demo, "username": user.username, "user_photo": user_picture, }, @@ -55,7 +54,6 @@ def index_post(request: Request): "chat.html", context={ "request": request, - "demo": state.demo, "username": user.username, "user_photo": user_picture, }, @@ -72,7 +70,6 @@ def search_page(request: Request): "search.html", context={ "request": request, - "demo": state.demo, "username": user.username, "user_photo": user_picture, }, @@ -89,7 +86,6 @@ def chat_page(request: Request): "chat.html", context={ "request": request, - "demo": state.demo, "username": user.username, "user_photo": user_picture, }, @@ -107,7 +103,6 @@ def login_page(request: Request): "login.html", context={ "request": request, - "demo": state.demo, "google_client_id": google_client_id, "redirect_uri": redirect_uri, }, @@ -125,142 +120,139 @@ def map_config_to_object(content_type: str): return LocalPlaintextConfig -if not state.demo: +@web_client.get("/config", response_class=HTMLResponse) +@requires(["authenticated"], redirect="login_page") +def config_page(request: Request): + user = request.user.object + user_picture = request.session.get("user", {}).get("picture") + enabled_content = set(EntryAdapters.get_unique_file_types(user).all()) - @web_client.get("/config", response_class=HTMLResponse) - @requires(["authenticated"], redirect="login_page") - def config_page(request: Request): - user = request.user.object - user_picture = request.session.get("user", {}).get("picture") - enabled_content = set(EntryAdapters.get_unique_file_types(user).all()) + successfully_configured = { + "pdf": ("pdf" in enabled_content), + "markdown": ("markdown" in enabled_content), + "org": ("org" in enabled_content), + "image": False, + "github": ("github" in enabled_content), + "notion": ("notion" in enabled_content), + "plaintext": ("plaintext" in enabled_content), + } - successfully_configured = { - "pdf": ("pdf" in enabled_content), - "markdown": ("markdown" in enabled_content), - "org": ("org" in enabled_content), - "image": False, - "github": ("github" in enabled_content), - "notion": ("notion" in enabled_content), - "plaintext": ("plaintext" in enabled_content), - } - - if state.content_index: - successfully_configured.update( - { - "image": state.content_index.image is not None, - } - ) - - 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} - ) - - selected_conversation_config = ConversationAdapters.get_conversation_config(user) - - return templates.TemplateResponse( - "config.html", - context={ - "request": request, - "current_model_state": successfully_configured, - "anonymous_mode": state.anonymous_mode, - "username": user.username if user else None, - "conversation_options": all_conversation_options, - "selected_conversation_config": selected_conversation_config.id - if selected_conversation_config - else None, - "user_photo": user_picture, - }, + if state.content_index: + successfully_configured.update( + { + "image": state.content_index.image is not None, + } ) - @web_client.get("/config/content_type/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") - current_github_config = get_user_github_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}) - if current_github_config: - raw_repos = current_github_config.githubrepoconfig.all() - repos = [] - for repo in raw_repos: - repos.append( - GithubRepoConfig( - name=repo.name, - owner=repo.owner, - branch=repo.branch, - ) + selected_conversation_config = ConversationAdapters.get_conversation_config(user) + + return templates.TemplateResponse( + "config.html", + context={ + "request": request, + "current_model_state": successfully_configured, + "anonymous_mode": state.anonymous_mode, + "username": user.username if user else None, + "conversation_options": all_conversation_options, + "selected_conversation_config": selected_conversation_config.id if selected_conversation_config else None, + "user_photo": user_picture, + }, + ) + + +@web_client.get("/config/content_type/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") + current_github_config = get_user_github_config(user) + + if current_github_config: + raw_repos = current_github_config.githubrepoconfig.all() + repos = [] + for repo in raw_repos: + repos.append( + GithubRepoConfig( + name=repo.name, + owner=repo.owner, + branch=repo.branch, ) - current_config = GithubContentConfig( - pat_token=current_github_config.pat_token, - repos=repos, ) - current_config = json.loads(current_config.json()) - else: - current_config = {} # type: ignore - - return templates.TemplateResponse( - "content_type_github_input.html", - context={ - "request": request, - "current_config": current_config, - "username": user.username, - "user_photo": user_picture, - }, - ) - - @web_client.get("/config/content_type/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") - current_notion_config = get_user_notion_config(user) - - current_config = NotionContentConfig( - token=current_notion_config.token if current_notion_config else "", - ) - - current_config = json.loads(current_config.json()) - - return templates.TemplateResponse( - "content_type_notion_input.html", - context={ - "request": request, - "current_config": current_config, - "username": user.username, - "user_photo": user_picture, - }, - ) - - @web_client.get("/config/content_type/{content_type}", response_class=HTMLResponse) - @requires(["authenticated"], redirect="login_page") - def content_config_page(request: Request, content_type: str): - if content_type not in VALID_TEXT_CONTENT_TYPES: - return templates.TemplateResponse("config.html", context={"request": request}) - - object = map_config_to_object(content_type) - user = request.user.object - user_picture = request.session.get("user", {}).get("picture") - config = object.objects.filter(user=user).first() - if config == None: - config = object.objects.create(user=user) - - current_config = TextContentConfig( - input_files=config.input_files, - input_filter=config.input_filter, - index_heading_entries=config.index_heading_entries, + current_config = GithubContentConfig( + pat_token=current_github_config.pat_token, + repos=repos, ) current_config = json.loads(current_config.json()) + else: + current_config = {} # type: ignore - return templates.TemplateResponse( - "content_type_input.html", - context={ - "request": request, - "current_config": current_config, - "content_type": content_type, - "username": user.username, - "user_photo": user_picture, - }, - ) + return templates.TemplateResponse( + "content_type_github_input.html", + context={ + "request": request, + "current_config": current_config, + "username": user.username, + "user_photo": user_picture, + }, + ) + + +@web_client.get("/config/content_type/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") + current_notion_config = get_user_notion_config(user) + + current_config = NotionContentConfig( + token=current_notion_config.token if current_notion_config else "", + ) + + current_config = json.loads(current_config.json()) + + return templates.TemplateResponse( + "content_type_notion_input.html", + context={ + "request": request, + "current_config": current_config, + "username": user.username, + "user_photo": user_picture, + }, + ) + + +@web_client.get("/config/content_type/{content_type}", response_class=HTMLResponse) +@requires(["authenticated"], redirect="login_page") +def content_config_page(request: Request, content_type: str): + if content_type not in VALID_TEXT_CONTENT_TYPES: + return templates.TemplateResponse("config.html", context={"request": request}) + + object = map_config_to_object(content_type) + user = request.user.object + user_picture = request.session.get("user", {}).get("picture") + config = object.objects.filter(user=user).first() + if config == None: + config = object.objects.create(user=user) + + current_config = TextContentConfig( + input_files=config.input_files, + input_filter=config.input_filter, + index_heading_entries=config.index_heading_entries, + ) + current_config = json.loads(current_config.json()) + + return templates.TemplateResponse( + "content_type_input.html", + context={ + "request": request, + "current_config": current_config, + "content_type": content_type, + "username": user.username, + "user_photo": user_picture, + }, + ) diff --git a/src/khoj/utils/cli.py b/src/khoj/utils/cli.py index c0928d5e..ddf45658 100644 --- a/src/khoj/utils/cli.py +++ b/src/khoj/utils/cli.py @@ -42,7 +42,6 @@ def cli(args=None): parser.add_argument( "--disable-chat-on-gpu", action="store_true", default=False, help="Disable using GPU for the offline chat model" ) - parser.add_argument("--demo", action="store_true", default=False, help="Run Khoj in demo mode") parser.add_argument( "--anonymous-mode", action="store_true", diff --git a/src/khoj/utils/state.py b/src/khoj/utils/state.py index 1b830245..748ca15a 100644 --- a/src/khoj/utils/state.py +++ b/src/khoj/utils/state.py @@ -31,7 +31,6 @@ config_lock = threading.Lock() chat_lock = threading.Lock() SearchType = utils_config.SearchType telemetry: List[Dict[str, str]] = [] -demo: bool = False khoj_version: str = None device = get_device() chat_on_gpu: bool = True From fdfab399424cfe7d3931c814c6ceecd9a11aa8ea Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 19:03:34 -0700 Subject: [PATCH 048/194] Update the config UI to show all files indexed with option to delete - Given the separation of the client and server now, the web UI will no longer support configuration of local file paths of data to index - Expose a way to show all the files that are currently set for indexing, along with an option to delete all or specific files --- src/database/adapters/__init__.py | 19 ++- src/khoj/interface/web/base_config.html | 69 +++++++- src/khoj/interface/web/config.html | 217 ++++++++++-------------- src/khoj/routers/api.py | 79 ++++++--- 4 files changed, 226 insertions(+), 158 deletions(-) diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index 7fbc5287..a54a640a 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -291,8 +291,11 @@ class EntryAdapters: return deleted_count @staticmethod - def delete_all_entries(user: KhojUser, file_type: str): - deleted_count, _ = Entry.objects.filter(user=user, file_type=file_type).delete() + def delete_all_entries(user: KhojUser, file_type: str = None): + if file_type is None: + deleted_count, _ = Entry.objects.filter(user=user).delete() + else: + deleted_count, _ = Entry.objects.filter(user=user, file_type=file_type).delete() return deleted_count @staticmethod @@ -314,6 +317,18 @@ class EntryAdapters: async def user_has_entries(user: KhojUser): return await Entry.objects.filter(user=user).aexists() + @staticmethod + async def adelete_entry_by_file(user: KhojUser, file_path: str): + return await Entry.objects.filter(user=user, file_path=file_path).adelete() + + @staticmethod + def aget_all_filenames(user: KhojUser): + return Entry.objects.filter(user=user).distinct("file_path").values_list("file_path", flat=True) + + @staticmethod + async def adelete_all_entries(user: KhojUser): + return await Entry.objects.filter(user=user).adelete() + @staticmethod def apply_filters(user: KhojUser, query: str, file_type_filter: str = None): q_filter_terms = Q() diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index e3c9a7dc..2c35e465 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -53,10 +53,10 @@ justify-self: center; } - .api-settings { + div.section-manage-files, + div.api-settings { display: grid; grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr auto; justify-items: start; gap: 8px; padding: 24px 24px; @@ -64,13 +64,23 @@ 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.8); - } - #api-settings-card-description { + } + + div.section-manage-files { + width: 640px; + } + + div.api-settings { + grid-template-rows: 1fr 1fr auto; + } + + #api-settings-card-description { margin: 8px 0 0 0; - } - #api-settings-keys-table { - margin-bottom: 16px; - } + } + + #api-settings-keys-table { + margin-bottom: 16px; + } div.instructions { font-size: large; @@ -184,6 +194,37 @@ text-align: left; } + button.remove-file-button:hover { + background-color: rgb(255 235 235); + border-radius: 3px; + border: none; + color: var(--flower); + padding: 4px; + cursor: pointer; + } + + button.remove-file-button { + background-color: rgb(253 214 214); + border-radius: 3px; + border: none; + color: var(--flower); + padding: 4px; + } + + div.file-element { + display: grid; + grid-template-columns: 1fr auto; + 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.8); + padding: 4px; + margin-bottom: 8px; + } + + div.remove-button-container { + text-align: right; + } + button.card-button.happy { color: var(--leaf); } @@ -246,6 +287,11 @@ cursor: pointer; } + a { + color: #3b82f6; + text-decoration: none; + } + @media screen and (max-width: 700px) { .section-cards { grid-template-columns: 1fr; @@ -255,7 +301,7 @@ body { display: grid; grid-template-columns: 1fr; - grid-template-rows: 1fr auto auto auto minmax(80px, 100%); + grid-template-rows: 1fr auto auto auto auto; } body > * { grid-column: 1; @@ -281,9 +327,14 @@ grid-template-columns: auto; } + div.section-manage-files, div.api-settings { width: auto; } + + div.finalize-buttons { + padding: 0; + } } diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index 851a18d0..b19bbff6 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -67,130 +67,6 @@
{% endif %}
-
-
- markdown -

- Markdown - {% if current_model_state.markdown == True%} - Configured - {% endif %} -

-
-
-

Set markdown files to index

-
- - {% if current_model_state.markdown %} -
- -
- {% endif %} -
-
-
- org -

- Org - {% if current_model_state.org == True %} - Configured - {% endif %} -

-
-
-

Set org files to index

-
- - {% if current_model_state.org %} -
- -
- {% endif %} -
-
-
- PDF -

- PDF - {% if current_model_state.pdf == True %} - Configured - {% endif %} -

-
-
-

Set PDF files to index

-
- - {% if current_model_state.pdf %} -
- -
- {% endif %} -
-
-
- Plaintext -

- Plaintext - {% if current_model_state.plaintext == True %} - Configured - {% endif %} -

-
-
-

Set Plaintext files to index

-
- - {% if current_model_state.plaintext %} -
- -
- {% endif %} -
@@ -246,6 +122,16 @@
+
+

Manage Data

+
+
+ +
+
+
+
+
@@ -291,8 +177,8 @@ }; function clearContentType(content_type) { - fetch('/api/delete/config/data/content_type/' + content_type, { - method: 'POST', + fetch('/api/config/data/content_type/' + content_type, { + method: 'DELETE', headers: { 'Content-Type': 'application/json', } @@ -462,5 +348,84 @@ // List user's API keys on page load listApiKeys(); + function removeFile(path) { + fetch('/api/config/data/file?filename=' + path, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.status == "ok") { + getAllFilenames(); + } + }) + } + + // Get all currently indexed files + function getAllFilenames() { + fetch('/api/config/data/all') + .then(response => response.json()) + .then(data => { + var indexedFiles = document.getElementsByClassName("indexed-files")[0]; + indexedFiles.innerHTML = ""; + + if (data.length == 0) { + document.getElementById("delete-all-files").style.display = "none"; + indexedFiles.innerHTML = "
Use the Khoj Desktop client to index files.
"; + } else { + document.getElementById("delete-all-files").style.display = "block"; + } + + for (var filename of data) { + let fileElement = document.createElement("div"); + fileElement.classList.add("file-element"); + + let fileNameElement = document.createElement("div"); + fileNameElement.classList.add("content-name"); + fileNameElement.innerHTML = filename; + fileElement.appendChild(fileNameElement); + + let buttonContainer = document.createElement("div"); + buttonContainer.classList.add("remove-button-container"); + let removeFileButton = document.createElement("button"); + removeFileButton.classList.add("remove-file-button"); + removeFileButton.innerHTML = "🗑️"; + removeFileButton.addEventListener("click", ((filename) => { + return () => { + removeFile(filename); + }; + })(filename)); + buttonContainer.appendChild(removeFileButton); + fileElement.appendChild(buttonContainer); + indexedFiles.appendChild(fileElement); + } + }) + .catch((error) => { + console.error('Error:', error); + }); + } + + // Get all currently indexed files on page load + getAllFilenames(); + + let deleteAllFilesButton = document.getElementById("delete-all-files"); + deleteAllFilesButton.addEventListener("click", function(event) { + event.preventDefault(); + fetch('/api/config/data/all', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.status == "ok") { + getAllFilenames(); + } + }) + }); + {% endblock %} diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 5c1ec912..84e63b09 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -45,7 +45,15 @@ from fastapi.requests import Request from database import adapters from database.adapters import EntryAdapters, ConversationAdapters -from database.models import LocalMarkdownConfig, LocalOrgConfig, LocalPdfConfig, LocalPlaintextConfig, KhojUser +from database.models import ( + LocalMarkdownConfig, + LocalOrgConfig, + LocalPdfConfig, + LocalPlaintextConfig, + KhojUser, + GithubConfig, + NotionConfig, +) # Initialize Router @@ -54,14 +62,10 @@ logger = logging.getLogger(__name__) def map_config_to_object(content_type: str): - if content_type == "org": - return LocalOrgConfig - if content_type == "markdown": - return LocalMarkdownConfig - if content_type == "pdf": - return LocalPdfConfig - if content_type == "plaintext": - return LocalPlaintextConfig + if content_type == "github": + return GithubConfig + if content_type == "notion": + return NotionConfig async def map_config_to_db(config: FullConfig, user: KhojUser): @@ -215,7 +219,7 @@ async def set_content_config_notion_data( return {"status": "ok"} -@api.post("/delete/config/data/content_type/{content_type}", status_code=200) +@api.delete("/config/data/content_type/{content_type}", status_code=200) @requires(["authenticated"]) async def remove_content_config_data( request: Request, @@ -243,29 +247,62 @@ async def remove_content_config_data( return {"status": "ok"} -@api.post("/config/data/content_type/{content_type}", status_code=200) +@api.delete("/config/data/file", status_code=200) @requires(["authenticated"]) -async def set_content_config_data( +async def remove_file_data( request: Request, - content_type: str, - updated_config: Union[TextContentConfig, None], + filename: str, client: Optional[str] = None, ): - _initialize_config() - user = request.user.object - content_object = map_config_to_object(content_type) - await adapters.set_text_content_config(user, content_object, updated_config) - update_telemetry_state( request=request, telemetry_type="api", - api="set_content_config", + api="delete_file", client=client, - metadata={"content_type": content_type}, ) + await EntryAdapters.adelete_entry_by_file(user, filename) + + return {"status": "ok"} + + +@api.get("/config/data/all", response_model=List[str]) +@requires(["authenticated"]) +async def get_all_filenames( + request: Request, + client: Optional[str] = None, +): + user = request.user.object + + update_telemetry_state( + request=request, + telemetry_type="api", + api="get_all_filenames", + client=client, + ) + + return await sync_to_async(list)(EntryAdapters.aget_all_filenames(user)) + + +@api.delete("/config/data/all", status_code=200) +@requires(["authenticated"]) +async def remove_all_config_data( + request: Request, + client: Optional[str] = None, +): + user = request.user.object + + update_telemetry_state( + request=request, + telemetry_type="api", + api="delete_all_config", + client=client, + ) + + await EntryAdapters.adelete_all_entries(user) + return {"status": "ok"} From 8c3d5a49da58f0c631cfa80083cfd9289e2958a3 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 19:27:18 -0700 Subject: [PATCH 049/194] Add try/except around image extraction step --- src/khoj/processor/pdf/pdf_to_entries.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/khoj/processor/pdf/pdf_to_entries.py b/src/khoj/processor/pdf/pdf_to_entries.py index 19d463eb..64e13031 100644 --- a/src/khoj/processor/pdf/pdf_to_entries.py +++ b/src/khoj/processor/pdf/pdf_to_entries.py @@ -68,13 +68,16 @@ class PdfToEntries(TextToEntries): with open(f"{tmp_file}", "wb") as f: bytes = pdf_files[pdf_file] f.write(bytes) - loader = PyMuPDFLoader(f"{tmp_file}", extract_images=True) + try: + loader = PyMuPDFLoader(f"{tmp_file}", extract_images=True) + except ModuleNotFoundError: + loader = PyMuPDFLoader(f"{tmp_file}") pdf_entries_per_file = [page.page_content for page in loader.load()] entry_to_location_map += zip(pdf_entries_per_file, [pdf_file] * len(pdf_entries_per_file)) entries.extend(pdf_entries_per_file) except Exception as e: logger.warning(f"Unable to process file: {pdf_file}. This file will not be indexed.") - logger.warning(e) + logger.warning(e, exc_info=True) finally: if os.path.exists(f"{tmp_file}"): os.remove(f"{tmp_file}") From dbaa892665c880c7d15699ac31dcbb635292c2b4 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 19:34:10 -0700 Subject: [PATCH 050/194] Flip catching modulenotfound to import error exception --- src/khoj/processor/pdf/pdf_to_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/processor/pdf/pdf_to_entries.py b/src/khoj/processor/pdf/pdf_to_entries.py index 64e13031..78ba034e 100644 --- a/src/khoj/processor/pdf/pdf_to_entries.py +++ b/src/khoj/processor/pdf/pdf_to_entries.py @@ -70,7 +70,7 @@ class PdfToEntries(TextToEntries): f.write(bytes) try: loader = PyMuPDFLoader(f"{tmp_file}", extract_images=True) - except ModuleNotFoundError: + except ImportError: loader = PyMuPDFLoader(f"{tmp_file}") pdf_entries_per_file = [page.page_content for page in loader.load()] entry_to_location_map += zip(pdf_entries_per_file, [pdf_file] * len(pdf_entries_per_file)) From 88eeee3f4b0687c1cac25725d17e5ead1009cd7f Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 19:46:47 -0700 Subject: [PATCH 051/194] Move try/catch for import one line later --- src/khoj/processor/pdf/pdf_to_entries.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/khoj/processor/pdf/pdf_to_entries.py b/src/khoj/processor/pdf/pdf_to_entries.py index 78ba034e..81c2250f 100644 --- a/src/khoj/processor/pdf/pdf_to_entries.py +++ b/src/khoj/processor/pdf/pdf_to_entries.py @@ -70,9 +70,10 @@ class PdfToEntries(TextToEntries): f.write(bytes) try: loader = PyMuPDFLoader(f"{tmp_file}", extract_images=True) + pdf_entries_per_file = [page.page_content for page in loader.load()] except ImportError: loader = PyMuPDFLoader(f"{tmp_file}") - pdf_entries_per_file = [page.page_content for page in loader.load()] + pdf_entries_per_file = [page.page_content for page in loader.load()] entry_to_location_map += zip(pdf_entries_per_file, [pdf_file] * len(pdf_entries_per_file)) entries.extend(pdf_entries_per_file) except Exception as e: From dc9946fc03d8ea2cb8b5fc11758a966c96cf2863 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 04:55:51 -0700 Subject: [PATCH 052/194] Flatten nested loops, improve progress reporting in text_to_jsonl indexer Flatten the nested loops to improve visibilty into indexing progress Reduce spurious logs, report the logs at aggregated level and update the logging description text to improve indexing progress reporting --- src/khoj/processor/text_to_entries.py | 119 +++++++++++++------------- src/khoj/routers/indexer.py | 1 - src/khoj/search_type/text_search.py | 2 +- src/khoj/utils/cli.py | 3 +- 4 files changed, 61 insertions(+), 64 deletions(-) diff --git a/src/khoj/processor/text_to_entries.py b/src/khoj/processor/text_to_entries.py index b121f1c4..501ef5d3 100644 --- a/src/khoj/processor/text_to_entries.py +++ b/src/khoj/processor/text_to_entries.py @@ -1,11 +1,12 @@ # Standard Packages from abc import ABC, abstractmethod import hashlib +from itertools import repeat import logging import uuid from tqdm import tqdm from typing import Callable, List, Tuple, Set, Any -from khoj.utils.helpers import timer, batcher +from khoj.utils.helpers import is_none_or_empty, timer, batcher # Internal Packages @@ -83,92 +84,88 @@ class TextToEntries(ABC): user: KhojUser = None, regenerate: bool = False, ): - with timer("Construct current entry hashes", logger): + with timer("Constructed current entry hashes in", logger): hashes_by_file = dict[str, set[str]]() current_entry_hashes = list(map(TextToEntries.hash_func(key), current_entries)) hash_to_current_entries = dict(zip(current_entry_hashes, current_entries)) for entry in tqdm(current_entries, desc="Hashing Entries"): hashes_by_file.setdefault(entry.file, set()).add(TextToEntries.hash_func(key)(entry)) - num_deleted_embeddings = 0 - with timer("Preparing dataset for regeneration", logger): - if regenerate: - logger.debug(f"Deleting all embeddings for file type {file_type}") - num_deleted_embeddings = EntryAdapters.delete_all_entries(user, file_type) + num_deleted_entries = 0 + if regenerate: + with timer("Prepared dataset for regeneration in", logger): + logger.debug(f"Deleting all entries for file type {file_type}") + num_deleted_entries = EntryAdapters.delete_all_entries(user, file_type) - num_new_embeddings = 0 - with timer("Identify hashes for adding new entries", logger): - for file in tqdm(hashes_by_file, desc="Processing file with hashed values"): + hashes_to_process = set() + with timer("Identified entries to add to database in", logger): + for file in tqdm(hashes_by_file, desc="Identify new entries"): hashes_for_file = hashes_by_file[file] - hashes_to_process = set() existing_entries = DbEntry.objects.filter( user=user, hashed_value__in=hashes_for_file, file_type=file_type ) existing_entry_hashes = set([entry.hashed_value for entry in existing_entries]) - hashes_to_process = hashes_for_file - existing_entry_hashes + hashes_to_process |= hashes_for_file - existing_entry_hashes - entries_to_process = [hash_to_current_entries[hashed_val] for hashed_val in hashes_to_process] - data_to_embed = [getattr(entry, key) for entry in entries_to_process] - embeddings = self.embeddings_model.embed_documents(data_to_embed) + embeddings = [] + with timer("Generated embeddings for entries to add to database in", logger): + entries_to_process = [hash_to_current_entries[hashed_val] for hashed_val in hashes_to_process] + data_to_embed = [getattr(entry, key) for entry in entries_to_process] + embeddings += self.embeddings_model.embed_documents(data_to_embed) - with timer("Update the database with new vector embeddings", logger): - num_items = len(hashes_to_process) - assert num_items == len(embeddings) - batch_size = min(200, num_items) - entry_batches = zip(hashes_to_process, embeddings) + added_entries: list[DbEntry] = [] + with timer("Added entries to database in", logger): + num_items = len(hashes_to_process) + assert num_items == len(embeddings) + batch_size = min(200, num_items) + entry_batches = zip(hashes_to_process, embeddings) - for entry_batch in tqdm( - batcher(entry_batches, batch_size), desc="Processing embeddings in batches" - ): - batch_embeddings_to_create = [] - for entry_hash, new_entry in entry_batch: - entry = hash_to_current_entries[entry_hash] - batch_embeddings_to_create.append( - DbEntry( - user=user, - embeddings=new_entry, - raw=entry.raw, - compiled=entry.compiled, - heading=entry.heading[:1000], # Truncate to max chars of field allowed - file_path=entry.file, - file_type=file_type, - hashed_value=entry_hash, - corpus_id=entry.corpus_id, - ) - ) - new_entries = DbEntry.objects.bulk_create(batch_embeddings_to_create) - logger.debug(f"Created {len(new_entries)} new embeddings") - num_new_embeddings += len(new_entries) + for entry_batch in tqdm(batcher(entry_batches, batch_size), desc="Add entries to database"): + batch_embeddings_to_create = [] + for entry_hash, new_entry in entry_batch: + entry = hash_to_current_entries[entry_hash] + batch_embeddings_to_create.append( + DbEntry( + user=user, + embeddings=new_entry, + raw=entry.raw, + compiled=entry.compiled, + heading=entry.heading[:1000], # Truncate to max chars of field allowed + file_path=entry.file, + file_type=file_type, + hashed_value=entry_hash, + corpus_id=entry.corpus_id, + ) + ) + added_entries += DbEntry.objects.bulk_create(batch_embeddings_to_create) + logger.debug(f"Added {len(added_entries)} {file_type} entries to database") - dates_to_create = [] - with timer("Create new date associations for new embeddings", logger): - for new_entry in new_entries: - dates = self.date_filter.extract_dates(new_entry.raw) - for date in dates: - dates_to_create.append( - EntryDates( - date=date, - entry=new_entry, - ) - ) - new_dates = EntryDates.objects.bulk_create(dates_to_create) - if len(new_dates) > 0: - logger.debug(f"Created {len(new_dates)} new date entries") + new_dates = [] + with timer("Indexed dates from added entries in", logger): + for added_entry in added_entries: + dates_in_entries = zip(self.date_filter.extract_dates(added_entry.raw), repeat(added_entry)) + dates_to_create = [ + EntryDates(date=date, entry=added_entry) + for date, added_entry in dates_in_entries + if not is_none_or_empty(date) + ] + new_dates += EntryDates.objects.bulk_create(dates_to_create) + logger.debug(f"Indexed {len(new_dates)} dates from added {file_type} entries") - with timer("Identify hashes for removed entries", logger): + with timer("Deleted entries identified by server from database in", logger): for file in hashes_by_file: existing_entry_hashes = EntryAdapters.get_existing_entry_hashes_by_file(user, file) to_delete_entry_hashes = set(existing_entry_hashes) - hashes_by_file[file] - num_deleted_embeddings += len(to_delete_entry_hashes) + num_deleted_entries += len(to_delete_entry_hashes) EntryAdapters.delete_entry_by_hash(user, hashed_values=list(to_delete_entry_hashes)) - with timer("Identify hashes for deleting entries", logger): + with timer("Deleted entries requested by clients from database in", logger): if deletion_filenames is not None: for file_path in deletion_filenames: deleted_count = EntryAdapters.delete_entry_by_file(user, file_path) - num_deleted_embeddings += deleted_count + num_deleted_entries += deleted_count - return num_new_embeddings, num_deleted_embeddings + return len(added_entries), num_deleted_entries @staticmethod def mark_entries_for_update( diff --git a/src/khoj/routers/indexer.py b/src/khoj/routers/indexer.py index 06275dbe..1bbf53c2 100644 --- a/src/khoj/routers/indexer.py +++ b/src/khoj/routers/indexer.py @@ -321,7 +321,6 @@ def load_content( content_index: Optional[ContentIndex], search_models: SearchModels, ): - logger.info(f"Loading content from existing embeddings...") if content_config is None: logger.warning("🚨 No Content configuration available.") return None diff --git a/src/khoj/search_type/text_search.py b/src/khoj/search_type/text_search.py index cacf5c77..14f5b770 100644 --- a/src/khoj/search_type/text_search.py +++ b/src/khoj/search_type/text_search.py @@ -207,7 +207,7 @@ def setup( file_names = [file_name for file_name in files] logger.info( - f"Created {num_new_embeddings} new embeddings. Deleted {num_deleted_embeddings} embeddings for user {user} and files {file_names}" + f"Deleted {num_deleted_embeddings} entries. Created {num_new_embeddings} new entries for user {user} from files {file_names}" ) diff --git a/src/khoj/utils/cli.py b/src/khoj/utils/cli.py index c0928d5e..a1efd59f 100644 --- a/src/khoj/utils/cli.py +++ b/src/khoj/utils/cli.py @@ -52,7 +52,8 @@ def cli(args=None): args, remaining_args = parser.parse_known_args(args) - logger.debug(f"Ignoring unknown commandline args: {remaining_args}") + if len(remaining_args) > 0: + logger.info(f"⚠️ Ignoring unknown commandline args: {remaining_args}") # Set default values for arguments args.chat_on_gpu = not args.disable_chat_on_gpu From 34b5a86d1d5b8daa228945603fe41e3c537dccb4 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 05:19:50 -0700 Subject: [PATCH 053/194] Use SentenceTransformer to disable progress bar when encoding query The Langchain HuggingFaceEmbeddings wrapper doesn't support disabling progressbar, not especially for only query but not documents. This makes the logs noisy with encoding progressbar for each incremental queries No features of the Langchain wrapper for SentenceTransformer was currently being used anyway for now, and we can always switch back to it if required --- src/khoj/processor/embeddings.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/khoj/processor/embeddings.py b/src/khoj/processor/embeddings.py index fbcddb67..fcd88d80 100644 --- a/src/khoj/processor/embeddings.py +++ b/src/khoj/processor/embeddings.py @@ -1,7 +1,6 @@ from typing import List -from langchain.embeddings import HuggingFaceEmbeddings -from sentence_transformers import CrossEncoder +from sentence_transformers import SentenceTransformer, CrossEncoder from khoj.utils.helpers import get_device from khoj.utils.rawconfig import SearchResponse @@ -10,17 +9,15 @@ from khoj.utils.rawconfig import SearchResponse class EmbeddingsModel: def __init__(self): self.model_name = "thenlper/gte-small" - encode_kwargs = {"normalize_embeddings": True, "show_progress_bar": True} + self.encode_kwargs = {"normalize_embeddings": True} model_kwargs = {"device": get_device()} - self.embeddings_model = HuggingFaceEmbeddings( - model_name=self.model_name, encode_kwargs=encode_kwargs, model_kwargs=model_kwargs - ) + self.embeddings_model = SentenceTransformer(self.model_name, **model_kwargs) def embed_query(self, query): - return self.embeddings_model.embed_query(query) + return self.embeddings_model.encode([query], show_progress_bar=False, **self.encode_kwargs)[0] def embed_documents(self, docs): - return self.embeddings_model.embed_documents(docs) + return self.embeddings_model.encode(docs, show_progress_bar=True, **self.encode_kwargs).tolist() class CrossEncoderModel: From 5489e98b9c568f388d710c7d5e8b0c3b5658b3db Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 05:58:27 -0700 Subject: [PATCH 054/194] Do not index org heading entries by default This is to maintain the previous default behavior --- src/khoj/processor/org_mode/org_to_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/processor/org_mode/org_to_entries.py b/src/khoj/processor/org_mode/org_to_entries.py index 387f8572..bf6df6dc 100644 --- a/src/khoj/processor/org_mode/org_to_entries.py +++ b/src/khoj/processor/org_mode/org_to_entries.py @@ -24,7 +24,7 @@ class OrgToEntries(TextToEntries): self, files: dict[str, str] = None, full_corpus: bool = True, user: KhojUser = None, regenerate: bool = False ) -> Tuple[int, int]: # Extract required fields from config - index_heading_entries = True + index_heading_entries = False if not full_corpus: deletion_file_names = set([file for file in files if files[file] == ""]) From 084a8becc595b597b055a0f2ecff7f1b0dc5ecae Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sat, 4 Nov 2023 20:13:33 -0700 Subject: [PATCH 055/194] Fix but to prevent default in chat trigger --- src/khoj/interface/web/chat.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 20d73f59..838155a5 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -165,7 +165,7 @@ function incrementalChat(event) { if (!event.shiftKey && event.key === 'Enter') { - e.preventDefault(); + event.preventDefault(); chat(); } } From 022017dd0f684b710d846baf853008e3af12bcc4 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 15:26:04 -0700 Subject: [PATCH 056/194] Fix text search tests to test updated indexing log messages --- tests/test_text_search.py | 41 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/tests/test_text_search.py b/tests/test_text_search.py index b5b78646..17eb5643 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -67,7 +67,7 @@ def test_text_search_setup_with_empty_file_raises_error( with caplog.at_level(logging.INFO): text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) - assert "Created 0 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message + assert "Deleted 3 entries. Created 0 new entries for user " in caplog.records[-1].message verify_embeddings(0, default_user) @@ -83,8 +83,8 @@ def test_text_indexer_deletes_embedding_before_regenerate( text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) # Assert - assert "Deleting all embeddings for file type org" in caplog.text - assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message + assert "Deleting all entries for file type org" in caplog.text + assert "Deleted 3 entries. Created 10 new entries for user " in caplog.records[-1].message # ---------------------------------------------------------------------------------------------------- @@ -97,9 +97,7 @@ def test_text_search_setup_batch_processes(content_config: ContentConfig, defaul text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) # Assert - assert "Created 4 new embeddings" in caplog.text - assert "Created 6 new embeddings" in caplog.text - assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message + assert "Deleted 3 entries. Created 10 new entries for user " in caplog.records[-1].message # ---------------------------------------------------------------------------------------------------- @@ -122,8 +120,8 @@ def test_text_index_same_if_content_unchanged(content_config: ContentConfig, def final_logs = caplog.text # Assert - assert "Deleting all embeddings for file type org" in initial_logs - assert "Deleting all embeddings for file type org" not in final_logs + assert "Deleting all entries for file type org" in initial_logs + assert "Deleting all entries for file type org" not in final_logs # ---------------------------------------------------------------------------------------------------- @@ -188,8 +186,9 @@ def test_entry_chunking_by_max_tokens(org_config_with_only_new_file: LocalOrgCon text_search.setup(OrgToEntries, data, regenerate=False, user=default_user) # Assert - # verify newly added org-mode entry is split by max tokens - assert "Created 2 new embeddings. Deleted 0 embeddings for user " in caplog.records[-1].message + assert ( + "Deleted 0 entries. Created 2 new entries for user " in caplog.records[-1].message + ), "new entry not split by max tokens" # ---------------------------------------------------------------------------------------------------- @@ -245,8 +244,9 @@ conda activate khoj ) # Assert - # verify newly added org-mode entry is split by max tokens - assert "Created 2 new embeddings. Deleted 0 embeddings for user " in caplog.records[-1].message + assert ( + "Deleted 0 entries. Created 2 new entries for user " in caplog.records[-1].message + ), "new entry not split by max tokens" # ---------------------------------------------------------------------------------------------------- @@ -261,7 +261,7 @@ def test_regenerate_index_with_new_entry( with caplog.at_level(logging.INFO): text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) - assert "Created 10 new embeddings. Deleted 3 embeddings for user " in caplog.records[-1].message + assert "Deleted 3 entries. Created 10 new entries for user " in caplog.records[-1].message # append org-mode entry to first org input file in config org_config.input_files = [f"{new_org_file}"] @@ -276,7 +276,7 @@ def test_regenerate_index_with_new_entry( text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) # Assert - assert "Created 11 new embeddings. Deleted 10 embeddings for user " in caplog.records[-1].message + assert "Deleted 10 entries. Created 11 new entries for user " in caplog.records[-1].message verify_embeddings(11, default_user) @@ -311,8 +311,8 @@ def test_update_index_with_duplicate_entries_in_stable_order( # Assert # verify only 1 entry added even if there are multiple duplicate entries - assert "Created 1 new embeddings. Deleted 3 embeddings for user " in initial_logs - assert "Created 0 new embeddings. Deleted 0 embeddings for user " in final_logs + assert "Deleted 3 entries. Created 1 new entries for user " in initial_logs + assert "Deleted 0 entries. Created 0 new entries for user " in final_logs verify_embeddings(1, default_user) @@ -348,8 +348,8 @@ def test_update_index_with_deleted_entry(org_config_with_only_new_file: LocalOrg # Assert # verify only 1 entry added even if there are multiple duplicate entries - assert "Created 2 new embeddings. Deleted 3 embeddings for user " in initial_logs - assert "Created 0 new embeddings. Deleted 1 embeddings for user " in final_logs + assert "Deleted 3 entries. Created 2 new entries for user " in initial_logs + assert "Deleted 1 entries. Created 0 new entries for user " in final_logs verify_embeddings(1, default_user) @@ -379,9 +379,8 @@ def test_update_index_with_new_entry(content_config: ContentConfig, new_org_file final_logs = caplog.text # Assert - assert "Created 10 new embeddings. Deleted 3 embeddings for user " in initial_logs - assert "Created 1 new embeddings. Deleted 0 embeddings for user " in final_logs - + assert "Deleted 3 entries. Created 10 new entries for user " in initial_logs + assert "Deleted 0 entries. Created 1 new entries for user " in final_logs verify_embeddings(11, default_user) From f212cc717464bc431fedf5f8bbea8aaed601477e Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 15:36:10 -0700 Subject: [PATCH 057/194] Arrange remaining text search tests in arrange, act, assert order --- tests/test_text_search.py | 60 +++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/tests/test_text_search.py b/tests/test_text_search.py index 17eb5643..7d8c30fb 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -48,10 +48,11 @@ def test_get_org_files_with_org_suffixed_dir_doesnt_raise_error(tmp_path, defaul user=default_user, ) + # Act org_files = collect_files(user=default_user)["org"] - # Act - # should not raise IsADirectoryError and return orgfile + # Assert + # should return orgfile and not raise IsADirectoryError assert org_files == {f"{orgfile}": "* Heading\n- List item\n"} @@ -62,11 +63,13 @@ def test_text_search_setup_with_empty_file_raises_error( ): # Arrange data = get_org_files(org_config_with_only_new_file) + # Act # Generate notes embeddings during asymmetric setup with caplog.at_level(logging.INFO): text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) + # Assert assert "Deleted 3 entries. Created 0 new entries for user " in caplog.records[-1].message verify_embeddings(0, default_user) @@ -79,6 +82,9 @@ def test_text_indexer_deletes_embedding_before_regenerate( # Arrange org_config = LocalOrgConfig.objects.filter(user=default_user).first() data = get_org_files(org_config) + + # Act + # Generate notes embeddings during asymmetric setup with caplog.at_level(logging.DEBUG): text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) @@ -93,6 +99,9 @@ def test_text_search_setup_batch_processes(content_config: ContentConfig, defaul # Arrange org_config = LocalOrgConfig.objects.filter(user=default_user).first() data = get_org_files(org_config) + + # Act + # Generate notes embeddings during asymmetric setup with caplog.at_level(logging.DEBUG): text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) @@ -133,7 +142,6 @@ async def test_text_search(search_config: SearchConfig): default_user = await KhojUser.objects.acreate( username="test_user", password="test_password", email="test@example.com" ) - # Arrange org_config = await LocalOrgConfig.objects.acreate( input_files=None, input_filter=["tests/data/org/*.org"], @@ -157,13 +165,12 @@ async def test_text_search(search_config: SearchConfig): # Act hits = await text_search.query(default_user, query) - - # Assert results = text_search.collate_results(hits) results = sorted(results, key=lambda x: float(x.score))[:1] - # search results should contain "git clone" entry + + # Assert search_result = results[0].entry - assert "git clone" in search_result + assert "git clone" in search_result, 'search result did not contain "git clone" entry' # ---------------------------------------------------------------------------------------------------- @@ -256,27 +263,29 @@ def test_regenerate_index_with_new_entry( ): # Arrange org_config = LocalOrgConfig.objects.filter(user=default_user).first() - data = get_org_files(org_config) - - with caplog.at_level(logging.INFO): - text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) - - assert "Deleted 3 entries. Created 10 new entries for user " in caplog.records[-1].message + initial_data = get_org_files(org_config) # append org-mode entry to first org input file in config org_config.input_files = [f"{new_org_file}"] with open(new_org_file, "w") as f: f.write("\n* A Chihuahua doing Tango\n- Saw a super cute video of a chihuahua doing the Tango on Youtube\n") - data = get_org_files(org_config) + final_data = get_org_files(org_config) # Act + with caplog.at_level(logging.INFO): + text_search.setup(OrgToEntries, initial_data, regenerate=True, user=default_user) + initial_logs = caplog.text + caplog.clear() # Clear logs + # regenerate notes jsonl, model embeddings and model to include entry from new file with caplog.at_level(logging.INFO): - text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) + text_search.setup(OrgToEntries, final_data, regenerate=True, user=default_user) + final_logs = caplog.text # Assert - assert "Deleted 10 entries. Created 11 new entries for user " in caplog.records[-1].message + assert "Deleted 3 entries. Created 10 new entries for user " in initial_logs + assert "Deleted 10 entries. Created 11 new entries for user " in final_logs verify_embeddings(11, default_user) @@ -327,23 +336,23 @@ def test_update_index_with_deleted_entry(org_config_with_only_new_file: LocalOrg new_entry = "* TODO A Chihuahua doing Tango\n- Saw a super cute video of a chihuahua doing the Tango on Youtube\n" with open(new_file_to_index, "w") as f: f.write(f"{new_entry}{new_entry} -- Tatooine") - data = get_org_files(org_config_with_only_new_file) - - # load embeddings, entries, notes model after adding new org file with 2 entries - with caplog.at_level(logging.INFO): - text_search.setup(OrgToEntries, data, regenerate=True, user=default_user) - initial_logs = caplog.text - caplog.clear() # Clear logs + initial_data = get_org_files(org_config_with_only_new_file) # update embeddings, entries, notes model after removing an entry from the org file with open(new_file_to_index, "w") as f: f.write(f"{new_entry}") - data = get_org_files(org_config_with_only_new_file) + final_data = get_org_files(org_config_with_only_new_file) # Act + # load embeddings, entries, notes model after adding new org file with 2 entries with caplog.at_level(logging.INFO): - text_search.setup(OrgToEntries, data, regenerate=False, user=default_user) + text_search.setup(OrgToEntries, initial_data, regenerate=True, user=default_user) + initial_logs = caplog.text + caplog.clear() # Clear logs + + with caplog.at_level(logging.INFO): + text_search.setup(OrgToEntries, final_data, regenerate=False, user=default_user) final_logs = caplog.text # Assert @@ -389,6 +398,7 @@ def test_update_index_with_new_entry(content_config: ContentConfig, new_org_file def test_text_search_setup_github(content_config: ContentConfig, default_user: KhojUser): # Arrange github_config = GithubConfig.objects.filter(user=default_user).first() + # Act # Regenerate github embeddings to test asymmetric setup without caching text_search.setup( From ef24485ada93840f779077d0976e68c4badf7998 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sat, 4 Nov 2023 20:06:24 -0700 Subject: [PATCH 058/194] Improve Khoj with DB setup instructions in the Django app readme (for now) --- src/app/README.md | 52 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/src/app/README.md b/src/app/README.md index cbfe5356..14fc8501 100644 --- a/src/app/README.md +++ b/src/app/README.md @@ -17,16 +17,26 @@ docker-compose up ## Setup (Local) -### Install dependencies +### Install Postgres (with PgVector) + +#### MacOS +- Install the [Postgres.app](https://postgresapp.com/). + +#### Debian, Ubuntu +From [official instructions](https://wiki.postgresql.org/wiki/Apt) ```bash -pip install -e '.[dev]' +sudo apt install -y postgresql-common +sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh +sudo apt install postgres-16 postgresql-16-pgvector ``` -### Setup the database +#### Windows +- Use the [recommended installer](https://www.postgresql.org/download/windows/) -1. Ensure you have Postgres installed. For MacOS, you can use [Postgres.app](https://postgresapp.com/). -2. If you're not using Postgres.app, you may have to install the pgvector extension manually. You can find the instructions [here](https://github.com/pgvector/pgvector#installation). If you're using Postgres.app, you can skip this step. Reproduced instructions below for convenience. +#### From Source +1. Follow instructions to [Install Postgres](https://www.postgresql.org/download/) +2. Follow instructions to [Install PgVector](https://github.com/pgvector/pgvector#installation) in case you need to manually install it. Reproduced instructions below for convenience. ```bash cd /tmp @@ -35,32 +45,50 @@ cd pgvector make make install # may need sudo ``` -3. Create a database -### Create the khoj database +### Create the Khoj database +#### MacOS ```bash createdb khoj -U postgres ``` -### Make migrations +#### Debian, Ubuntu +```bash +sudo -u postgres createdb khoj +``` -This command will create the migrations for the database app. This command should be run whenever a new model is added to the database app or an existing model is modified (updated or deleted). +- [Optional] To set default postgres user's password + - Execute `ALTER USER postgres PASSWORD 'my_secure_password';` using `psql` + - Run `export $POSTGRES_PASSWORD=my_secure_password` in your terminal for Khoj to use it later + +### Install Khoj + +```bash +pip install -e '.[dev]' +``` + +### Make Khoj DB migrations + +This command will create the migrations for the database app. This command should be run whenever a new db model is added to the database app or an existing db model is modified (updated or deleted). ```bash python3 src/manage.py makemigrations ``` -### Run migrations +### Run Khoj DB migrations This command will run any pending migrations in your application. ```bash python3 src/manage.py migrate ``` -### Run the server +### Start Khoj Server While we're using Django for the ORM, we're still using the FastAPI server for the API. This command automatically scaffolds the Django application in the backend. + +*Note: Anonymous mode bypasses authentication for local, single-user usage.* + ```bash -python3 src/khoj/main.py +python3 src/khoj/main.py --anonymous-mode ``` From a4f407f595bdf9d95aaf2f3d9063698c70e2992a Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Sun, 5 Nov 2023 03:32:29 -0800 Subject: [PATCH 059/194] Test memory leak on MPS device when generating vector embeddings Slope threshold of 2.0 determined qualitatively on local Mac device Minor unused import and clean-up --- pyproject.toml | 1 + src/database/adapters/__init__.py | 4 ---- src/khoj/processor/embeddings.py | 6 ++--- tests/test_helpers.py | 37 +++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c816f4d2..6d1ef0a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,7 @@ test = [ "factory-boy >= 3.2.1", "trio >= 0.22.0", "pytest-xdist", + "psutil >= 5.8.0", ] dev = [ "khoj-assistant[test]", diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index 7fbc5287..5669414d 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -1,4 +1,3 @@ -import secrets from typing import Type, TypeVar, List from datetime import date import secrets @@ -36,9 +35,6 @@ from database.models import ( OfflineChatProcessorConversationConfig, ) from khoj.utils.helpers import generate_random_name -from khoj.utils.rawconfig import ( - ConversationProcessorConfig as UserConversationProcessorConfig, -) from khoj.search_filter.word_filter import WordFilter from khoj.search_filter.file_filter import FileFilter from khoj.search_filter.date_filter import DateFilter diff --git a/src/khoj/processor/embeddings.py b/src/khoj/processor/embeddings.py index fcd88d80..1e92f27d 100644 --- a/src/khoj/processor/embeddings.py +++ b/src/khoj/processor/embeddings.py @@ -8,10 +8,10 @@ from khoj.utils.rawconfig import SearchResponse class EmbeddingsModel: def __init__(self): - self.model_name = "thenlper/gte-small" self.encode_kwargs = {"normalize_embeddings": True} - model_kwargs = {"device": get_device()} - self.embeddings_model = SentenceTransformer(self.model_name, **model_kwargs) + self.model_kwargs = {"device": get_device()} + self.model_name = "thenlper/gte-small" + self.embeddings_model = SentenceTransformer(self.model_name, **self.model_kwargs) def embed_query(self, query): return self.embeddings_model.encode([query], show_progress_bar=False, **self.encode_kwargs)[0] diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 622592b1..30499049 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,3 +1,14 @@ +# Standard Packages +import numpy as np +import psutil +from scipy.stats import linregress +import secrets + +# External Packages +import pytest + +# Internal Packages +from khoj.processor.embeddings import EmbeddingsModel from khoj.utils import helpers @@ -44,3 +55,29 @@ def test_lru_cache(): cache["b"] # accessing 'b' makes it the most recently used item cache["d"] = 4 # so 'c' is deleted from the cache instead of 'b' assert cache == {"b": 2, "d": 4} + + +@pytest.mark.skip(reason="Memory leak exists on GPU, MPS devices") +def test_encode_docs_memory_leak(): + # Arrange + iterations = 50 + batch_size = 20 + embeddings_model = EmbeddingsModel() + memory_usage_trend = [] + + # Act + # Encode random strings repeatedly and record memory usage trend + for iteration in range(iterations): + random_docs = [" ".join(secrets.token_hex(5) for _ in range(10)) for _ in range(batch_size)] + a = [embeddings_model.embed_documents(random_docs)] + memory_usage_trend += [psutil.Process().memory_info().rss / (1024 * 1024)] + print(f"{iteration:02d}, {memory_usage_trend[-1]:.2f}", flush=True) + + # Calculate slope of line fitting memory usage history + memory_usage_trend = np.array(memory_usage_trend) + slope, _, _, _, _ = linregress(np.arange(len(memory_usage_trend)), memory_usage_trend) + + # Assert + # If slope is positive memory utilization is increasing + # Positive threshold of 2, from observing memory usage trend on MPS vs CPU device + assert slope < 2, f"Memory usage increasing at ~{slope:.2f} MB per iteration" From fdd727712f9a5a1cfbd4bff3049e638d5c765b0e Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 5 Nov 2023 14:33:07 -0800 Subject: [PATCH 060/194] Rename test files from x_to_jsonl to x_to_entries --- tests/{test_markdown_to_jsonl.py => test_markdown_to_entries.py} | 0 tests/{test_org_to_jsonl.py => test_org_to_entries.py} | 0 tests/{test_pdf_to_jsonl.py => test_pdf_to_entries.py} | 0 .../{test_plaintext_to_jsonl.py => test_plaintext_to_entries.py} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/{test_markdown_to_jsonl.py => test_markdown_to_entries.py} (100%) rename tests/{test_org_to_jsonl.py => test_org_to_entries.py} (100%) rename tests/{test_pdf_to_jsonl.py => test_pdf_to_entries.py} (100%) rename tests/{test_plaintext_to_jsonl.py => test_plaintext_to_entries.py} (100%) diff --git a/tests/test_markdown_to_jsonl.py b/tests/test_markdown_to_entries.py similarity index 100% rename from tests/test_markdown_to_jsonl.py rename to tests/test_markdown_to_entries.py diff --git a/tests/test_org_to_jsonl.py b/tests/test_org_to_entries.py similarity index 100% rename from tests/test_org_to_jsonl.py rename to tests/test_org_to_entries.py diff --git a/tests/test_pdf_to_jsonl.py b/tests/test_pdf_to_entries.py similarity index 100% rename from tests/test_pdf_to_jsonl.py rename to tests/test_pdf_to_entries.py diff --git a/tests/test_plaintext_to_jsonl.py b/tests/test_plaintext_to_entries.py similarity index 100% rename from tests/test_plaintext_to_jsonl.py rename to tests/test_plaintext_to_entries.py From 5f1e37fff0baa594a7edae29d785d7e31d5f8c4d Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 5 Nov 2023 14:33:23 -0800 Subject: [PATCH 061/194] Adjust indentation for css property --- src/khoj/interface/web/base_config.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index 2c35e465..8137dd76 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -79,7 +79,7 @@ } #api-settings-keys-table { - margin-bottom: 16px; + margin-bottom: 16px; } div.instructions { From 3d6e8d53fee1fe56b55a53972dfa7fcf01055b8f Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 5 Nov 2023 15:09:40 -0800 Subject: [PATCH 062/194] Try adding dependencies for libgl in order to run OCR in github action unit tests --- .github/workflows/test.yml | 2 +- tests/data/pdf/ocr_samples.pdf | Bin 0 -> 730678 bytes tests/test_pdf_to_entries.py | 17 +++++++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/data/pdf/ocr_samples.pdf diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 84fbb1aa..697579da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,7 +61,7 @@ jobs: env: DEBIAN_FRONTEND: noninteractive run: | - apt update && apt install -y libegl1 sqlite3 libsqlite3-dev libsqlite3-0 + apt update && apt install -y libegl1 sqlite3 libsqlite3-dev libsqlite3-0 ffmpeg libsm6 libxext6 - name: ⬇️ Install Postgres env: diff --git a/tests/data/pdf/ocr_samples.pdf b/tests/data/pdf/ocr_samples.pdf new file mode 100644 index 0000000000000000000000000000000000000000..100f60e02f494759d01a37cba52e9c82c4380efb GIT binary patch literal 730678 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%0A?*^d;l1#Oz9 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%0DnH)_O<<^ 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 z z3rEbJ(hJ4F&EX|&m(|7X8>Uk@ZG6|U!`eV|$dVlEf})a_8-zTyw9uiK+qbJt&h34B z7t%Er=J%-Q?p+$dB3!<-WBcZrbmGtfzrKBO9_tNK49nsBciPO9`7`^B9@4yfhdQrx zPciB^kZb6H<%J-@fuqV|Hqp-J^~)z$UOv0lx!n)jHZRD}iYQXWCMvxyoZWr>(!u9X zu5@VK04&#vq8OAA+J*9bZ2!_VOUJBTHg?O}DZ2NLGel_!5@*4z0nDU7ig9ZesniEH(Y<%%^^?i zDY>=;6saW9$VO6{?(k=F4Pp2Hi?;tyh}$H?Nr|@+3pYu$p=>zWKJL-6UdzS`e|L?mx@JmMtQrK+gZH?m=dVUM>@ypK5~x`I)Yr2@*BjF#1#ZjL>s!!1cU}b<^wzGa>YIGZfuWg%Vv5w#+nBB

2kWPg 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 W{6KkIG`gDXFL&5 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%0Dh*M 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 z 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 e{r`|Zr&19@ Date: Sun, 5 Nov 2023 15:23:42 -0800 Subject: [PATCH 063/194] Use repeat rather than manually specify auto in grid-template-rows Co-authored-by: Debanjum --- src/khoj/interface/web/base_config.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index 8137dd76..8e33677c 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -301,7 +301,7 @@ body { display: grid; grid-template-columns: 1fr; - grid-template-rows: 1fr auto auto auto auto; + grid-template-rows: 1fr repeat(4, auto); } body > * { grid-column: 1; From 8ebb12820c3aa61c5b9ce556e638b825197984b6 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 5 Nov 2023 15:40:05 -0800 Subject: [PATCH 064/194] Add OCR runtime dependencies to prod Dockerfile as well --- prod.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prod.Dockerfile b/prod.Dockerfile index 3cf6a600..693a3a8b 100644 --- a/prod.Dockerfile +++ b/prod.Dockerfile @@ -4,7 +4,7 @@ FROM nvidia/cuda:12.2.0-devel-ubuntu22.04 LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj # Install System Dependencies -RUN apt update -y && apt -y install python3-pip git +RUN apt update -y && apt -y install python3-pip git libsqlite3-0 ffmpeg libsm6 libxext6 WORKDIR /app From 270f7b3eb30a65e5093a00cbbab0dd863e322ca2 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Sun, 5 Nov 2023 15:46:43 -0800 Subject: [PATCH 065/194] Update the chat UI to have richer representation of the references --- src/khoj/interface/web/chat.html | 214 ++++++++++++++++++++++++++++--- 1 file changed, 194 insertions(+), 20 deletions(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index 838155a5..dc3b70ee 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -33,32 +33,101 @@ let escaped_ref = reference.replaceAll('"', '"'); // Generate HTML for Chat Reference - return `${index}`; + let short_ref = escaped_ref.slice(0, 100); + short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref; + let referenceButton = document.createElement('button'); + referenceButton.innerHTML = short_ref; + referenceButton.id = `ref-${index}`; + referenceButton.classList.add("reference-button"); + referenceButton.classList.add("collapsed"); + referenceButton.tabIndex = 0; + + // Add event listener to toggle full reference on click + referenceButton.addEventListener('click', function() { + console.log(`Toggling ref-${index}`) + if (this.classList.contains("collapsed")) { + this.classList.remove("collapsed"); + this.classList.add("expanded"); + this.innerHTML = escaped_ref; + } else { + this.classList.add("collapsed"); + this.classList.remove("expanded"); + this.innerHTML = short_ref; + } + }); + + return referenceButton; } - function renderMessage(message, by, dt=null) { + function renderMessage(message, by, dt=null, annotations=null) { let message_time = formatDate(dt ?? new Date()); let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let formattedMessage = formatHTMLMessage(message); - // Generate HTML for Chat Message and Append to Chat Body - document.getElementById("chat-body").innerHTML += ` -

- `; + let chatBody = document.getElementById("chat-body"); + + // Create a new div for the chat message + let chatMessage = document.createElement('div'); + chatMessage.className = `chat-message ${by}`; + chatMessage.dataset.meta = `${by_name} at ${message_time}`; + + // Create a new div for the chat message text and append it to the chat message + let chatMessageText = document.createElement('div'); + chatMessageText.className = `chat-message-text ${by}`; + chatMessageText.innerHTML = formattedMessage; + chatMessage.appendChild(chatMessageText); + + // Append annotations div to the chat message + if (annotations) { + chatMessageText.appendChild(annotations); + } + + // Append chat message div to chat body + chatBody.appendChild(chatMessage); + // Scroll to bottom of chat-body element - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + chatBody.scrollTop = chatBody.scrollHeight; } function renderMessageWithReference(message, by, context=null, dt=null) { - let references = ''; - if (context) { - references = context - .map((reference, index) => generateReference(reference, index)) - .join(","); + if (context == null || context.length == 0) { + renderMessage(message, by, dt); + return; } - renderMessage(message+references, by, dt); + let references = document.createElement('div'); + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = context.length == 1 ? "1 reference" : `${context.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + references.classList.add("references"); + if (context) { + for (let index in context) { + let reference = context[index]; + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + } + } + references.appendChild(referenceSection); + + renderMessage(message, by, dt, references); } function formatHTMLMessage(htmlMessage) { @@ -120,12 +189,14 @@ const decoder = new TextDecoder(); function readStream() { + let reference = null; reader.read().then(({ done, value }) => { if (done) { // Evaluate the contents of new_response_text.innerHTML after all the data has been streamed const currentHTML = newResponseText.innerHTML; newResponseText.innerHTML = formatHTMLMessage(currentHTML); - + newResponseText.appendChild(references); + document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; return; } @@ -138,11 +209,36 @@ const rawReference = chunk.split("### compiled references:")[1]; const rawReferenceAsJson = JSON.parse(rawReference); - let polishedReference = rawReferenceAsJson.map((reference, index) => generateReference(reference, index)) - .join(","); + references = document.createElement('div'); + references.classList.add("references"); - newResponseText.innerHTML += polishedReference; - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = rawReferenceAsJson.length == 1 ? "1 reference" : `${rawReferenceAsJson.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + rawReferenceAsJson.forEach((reference, index) => { + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + }); + references.appendChild(referenceSection); readStream(); } else { // Display response from Khoj @@ -237,6 +333,7 @@ }); }) .catch(err => { + console.log(err); return; }); @@ -299,6 +396,83 @@ padding: 10px; margin: 10px; } + + div.collapsed { + display: none; + } + + div.expanded { + display: block; + } + + div.reference { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + div.expanded.reference-section { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + button.reference-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px solid var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.2s ease-in-out; + text-align: left; + max-height: 50px; + transition: max-height 0.3s ease-in-out; + overflow: hidden; + } + button.reference-button.expanded { + max-height: 200px; + } + + button.reference-button::before { + content: "▶"; + margin-right: 5px; + display: inline-block; + transition: transform 0.3s ease-in-out; + } + + button.reference-button:active:before, + button.reference-button[aria-expanded="true"]::before { + transform: rotate(90deg); + } + + button.reference-expand-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px dotted var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.4s ease-in-out; + text-align: left; + } + + button.reference-expand-button:hover { + background: var(--primary-hover); + } + #chat-body { font-size: medium; margin: 0px; From e01ecf141962452939d12dd771288ab31564aeb6 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Mon, 6 Nov 2023 16:12:25 -0800 Subject: [PATCH 066/194] /s/references/reference to fix bug of jumping references --- src/khoj/interface/web/chat.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index dc3b70ee..61f176c7 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -189,7 +189,7 @@ const decoder = new TextDecoder(); function readStream() { - let reference = null; + let references = null; reader.read().then(({ done, value }) => { if (done) { // Evaluate the contents of new_response_text.innerHTML after all the data has been streamed From 6c8689e4aed558358ce7230a02d6c76b557c3091 Mon Sep 17 00:00:00 2001 From: sabaimran Date: Mon, 6 Nov 2023 16:18:41 -0800 Subject: [PATCH 067/194] Update corresponding chat UX in the desktop client as well --- src/interface/desktop/chat.html | 215 ++++++++++++++++++++++++++++---- 1 file changed, 194 insertions(+), 21 deletions(-) diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index b908b747..8666b340 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -34,32 +34,101 @@ let escaped_ref = reference.replaceAll('"', '"'); // Generate HTML for Chat Reference - return `${index}`; + let short_ref = escaped_ref.slice(0, 100); + short_ref = short_ref.length < escaped_ref.length ? short_ref + "..." : short_ref; + let referenceButton = document.createElement('button'); + referenceButton.innerHTML = short_ref; + referenceButton.id = `ref-${index}`; + referenceButton.classList.add("reference-button"); + referenceButton.classList.add("collapsed"); + referenceButton.tabIndex = 0; + + // Add event listener to toggle full reference on click + referenceButton.addEventListener('click', function() { + console.log(`Toggling ref-${index}`) + if (this.classList.contains("collapsed")) { + this.classList.remove("collapsed"); + this.classList.add("expanded"); + this.innerHTML = escaped_ref; + } else { + this.classList.add("collapsed"); + this.classList.remove("expanded"); + this.innerHTML = short_ref; + } + }); + + return referenceButton; } - function renderMessage(message, by, dt=null) { + function renderMessage(message, by, dt=null, annotations=null) { let message_time = formatDate(dt ?? new Date()); let by_name = by == "khoj" ? "🏮 Khoj" : "🤔 You"; let formattedMessage = formatHTMLMessage(message); - // Generate HTML for Chat Message and Append to Chat Body - document.getElementById("chat-body").innerHTML += ` -
-
${formattedMessage}
-
- `; + let chatBody = document.getElementById("chat-body"); + + // Create a new div for the chat message + let chatMessage = document.createElement('div'); + chatMessage.className = `chat-message ${by}`; + chatMessage.dataset.meta = `${by_name} at ${message_time}`; + + // Create a new div for the chat message text and append it to the chat message + let chatMessageText = document.createElement('div'); + chatMessageText.className = `chat-message-text ${by}`; + chatMessageText.innerHTML = formattedMessage; + chatMessage.appendChild(chatMessageText); + + // Append annotations div to the chat message + if (annotations) { + chatMessageText.appendChild(annotations); + } + + // Append chat message div to chat body + chatBody.appendChild(chatMessage); + // Scroll to bottom of chat-body element - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + chatBody.scrollTop = chatBody.scrollHeight; } function renderMessageWithReference(message, by, context=null, dt=null) { - let references = ''; - if (context) { - references = context - .map((reference, index) => generateReference(reference, index)) - .join(","); + if (context == null || context.length == 0) { + renderMessage(message, by, dt); + return; } - renderMessage(message+references, by, dt); + let references = document.createElement('div'); + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = context.length == 1 ? "1 reference" : `${context.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + references.classList.add("references"); + if (context) { + for (let index in context) { + let reference = context[index]; + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + } + } + references.appendChild(referenceSection); + + renderMessage(message, by, dt, references); } function formatHTMLMessage(htmlMessage) { @@ -120,17 +189,19 @@ // Call specified Khoj API which returns a streamed response of type text/plain fetch(url, { headers }) - .then(response => { + .then(response => { const reader = response.body.getReader(); const decoder = new TextDecoder(); function readStream() { + let references = null; reader.read().then(({ done, value }) => { if (done) { // Evaluate the contents of new_response_text.innerHTML after all the data has been streamed const currentHTML = newResponseText.innerHTML; newResponseText.innerHTML = formatHTMLMessage(currentHTML); - + newResponseText.appendChild(references); + document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; return; } @@ -143,11 +214,36 @@ const rawReference = chunk.split("### compiled references:")[1]; const rawReferenceAsJson = JSON.parse(rawReference); - let polishedReference = rawReferenceAsJson.map((reference, index) => generateReference(reference, index)) - .join(","); + references = document.createElement('div'); + references.classList.add("references"); - newResponseText.innerHTML += polishedReference; - document.getElementById("chat-body").scrollTop = document.getElementById("chat-body").scrollHeight; + + let referenceExpandButton = document.createElement('button'); + referenceExpandButton.classList.add("reference-expand-button"); + let expandButtonText = rawReferenceAsJson.length == 1 ? "1 reference" : `${rawReferenceAsJson.length} references`; + referenceExpandButton.innerHTML = expandButtonText; + + references.appendChild(referenceExpandButton); + + let referenceSection = document.createElement('div'); + referenceSection.classList.add("reference-section"); + referenceSection.classList.add("collapsed"); + + referenceExpandButton.addEventListener('click', function() { + if (referenceSection.classList.contains("collapsed")) { + referenceSection.classList.remove("collapsed"); + referenceSection.classList.add("expanded"); + } else { + referenceSection.classList.add("collapsed"); + referenceSection.classList.remove("expanded"); + } + }); + + rawReferenceAsJson.forEach((reference, index) => { + let polishedReference = generateReference(reference, index); + referenceSection.appendChild(polishedReference); + }); + references.appendChild(referenceSection); readStream(); } else { // Display response from Khoj @@ -443,6 +539,83 @@ box-shadow: 0 0 12px rgb(119, 156, 46); } + div.collapsed { + display: none; + } + + div.expanded { + display: block; + } + + div.reference { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + div.expanded.reference-section { + display: grid; + grid-template-rows: auto; + grid-auto-flow: row; + grid-column-gap: 10px; + grid-row-gap: 10px; + margin: 10px; + } + + button.reference-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px solid var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.2s ease-in-out; + text-align: left; + max-height: 50px; + transition: max-height 0.3s ease-in-out; + overflow: hidden; + } + button.reference-button.expanded { + max-height: 200px; + } + + button.reference-button::before { + content: "▶"; + margin-right: 5px; + display: inline-block; + transition: transform 0.3s ease-in-out; + } + + button.reference-button:active:before, + button.reference-button[aria-expanded="true"]::before { + transform: rotate(90deg); + } + + button.reference-expand-button { + background: var(--background-color); + color: var(--main-text-color); + border: 1px dotted var(--main-text-color); + border-radius: 5px; + padding: 5px; + font-size: 14px; + font-weight: 300; + line-height: 1.5em; + cursor: pointer; + transition: background 0.4s ease-in-out; + text-align: left; + } + + button.reference-expand-button:hover { + background: var(--primary-hover); + } + + .option-enabled:focus { outline: none !important; border:1px solid #475569; From a08b15235851432fbf1aa0f6e31f18b7a8ac7538 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Mon, 6 Nov 2023 19:26:54 -0800 Subject: [PATCH 068/194] Improve log messages in text_entries and memory leak unit test --- src/khoj/processor/text_to_entries.py | 2 +- tests/test_helpers.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/khoj/processor/text_to_entries.py b/src/khoj/processor/text_to_entries.py index 501ef5d3..4661fd9b 100644 --- a/src/khoj/processor/text_to_entries.py +++ b/src/khoj/processor/text_to_entries.py @@ -93,7 +93,7 @@ class TextToEntries(ABC): num_deleted_entries = 0 if regenerate: - with timer("Prepared dataset for regeneration in", logger): + with timer("Cleared existing dataset for regeneration in", logger): logger.debug(f"Deleting all entries for file type {file_type}") num_deleted_entries = EntryAdapters.delete_all_entries(user, file_type) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 30499049..fdd29b02 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -64,6 +64,7 @@ def test_encode_docs_memory_leak(): batch_size = 20 embeddings_model = EmbeddingsModel() memory_usage_trend = [] + device = f"{helpers.get_device()}".upper() # Act # Encode random strings repeatedly and record memory usage trend @@ -76,8 +77,9 @@ def test_encode_docs_memory_leak(): # Calculate slope of line fitting memory usage history memory_usage_trend = np.array(memory_usage_trend) slope, _, _, _, _ = linregress(np.arange(len(memory_usage_trend)), memory_usage_trend) + print(f"Memory usage increased at ~{slope:.2f} MB per iteration on {device}") # Assert # If slope is positive memory utilization is increasing # Positive threshold of 2, from observing memory usage trend on MPS vs CPU device - assert slope < 2, f"Memory usage increasing at ~{slope:.2f} MB per iteration" + assert slope < 2, f"Memory leak suspected on {device}. Memory usage increased at ~{slope:.2f} MB per iteration" From 97cf8339aa7c61d3b57801f999ae11742367ab50 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Mon, 6 Nov 2023 21:57:37 -0800 Subject: [PATCH 069/194] Rename Sync button, Force Sync toggle to Save, Save All buttons --- src/interface/desktop/config.html | 11 +++++------ src/interface/desktop/renderer.js | 10 +++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/interface/desktop/config.html b/src/interface/desktop/config.html index 0629a5f7..3f8e19d9 100644 --- a/src/interface/desktop/config.html +++ b/src/interface/desktop/config.html @@ -91,11 +91,10 @@
- +
-
- - +
+
@@ -336,7 +335,7 @@ padding: 4px; cursor: pointer; } - #sync-data { + button.sync-data { background-color: var(--primary); border: none; color: var(--main-text-color); @@ -351,7 +350,7 @@ box-shadow: 0px 5px 0px var(--background-color); } - #sync-data:hover { + button.sync-data:hover { background-color: var(--primary-hover); box-shadow: 0px 3px 0px var(--background-color); } diff --git a/src/interface/desktop/renderer.js b/src/interface/desktop/renderer.js index 1e1fae32..26765bf0 100644 --- a/src/interface/desktop/renderer.js +++ b/src/interface/desktop/renderer.js @@ -196,9 +196,13 @@ khojKeyInput.addEventListener('blur', async () => { }); const syncButton = document.getElementById('sync-data'); -const syncForceToggle = document.getElementById('sync-force'); syncButton.addEventListener('click', async () => { loadingBar.style.display = 'block'; - const regenerate = syncForceToggle.checked; - await window.syncDataAPI.syncData(regenerate); + await window.syncDataAPI.syncData(false); +}); + +const syncForceButton = document.getElementById('sync-force'); +syncForceButton.addEventListener('click', async () => { + loadingBar.style.display = 'block'; + await window.syncDataAPI.syncData(true); }); From 9f47fc8e34d2bac840f6677d45b5856d38df3b80 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Mon, 6 Nov 2023 21:58:33 -0800 Subject: [PATCH 070/194] Upgrade langchain version since adding support for OCR-ing PDFs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e87d205e..f6080ce6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "torch == 2.0.1", "uvicorn == 0.17.6", "aiohttp == 3.8.5", - "langchain >= 0.0.187", + "langchain >= 0.0.331", "requests >= 2.26.0", "bs4 >= 0.0.1", "anyio == 3.7.1", From c82cd0862aa79edf92a19c708bef8589f9ab1a18 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Mon, 6 Nov 2023 23:11:22 -0800 Subject: [PATCH 071/194] Delete deprecated content config pages for local files from web client The desktop app now manages syncing local computer files to index The server only manages "cloud" data source like github and notion. --- .../interface/web/content_type_input.html | 159 ------------------ src/khoj/routers/web_client.py | 47 ------ 2 files changed, 206 deletions(-) delete mode 100644 src/khoj/interface/web/content_type_input.html diff --git a/src/khoj/interface/web/content_type_input.html b/src/khoj/interface/web/content_type_input.html deleted file mode 100644 index f8751ddc..00000000 --- a/src/khoj/interface/web/content_type_input.html +++ /dev/null @@ -1,159 +0,0 @@ -{% extends "base_config.html" %} -{% block content %} -
-
-

- {{ content_type|capitalize }} - {{ content_type|capitalize }} -

-
- - - - - - - - - - - -
- - - {% if current_config['input_files'] is none %} - - {% else %} - {% for input_file in current_config['input_files'] %} - - {% endfor %} - {% endif %} - - -
- - - {% if current_config['input_filter'] is none %} - - {% else %} - {% for input_filter in current_config['input_filter'] %} - - {% endfor %} - {% endif %} - - -
-
- - -
-
-
-
- -{% endblock %} diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 35603e18..65292ccf 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -9,7 +9,6 @@ from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse from fastapi.templating import Jinja2Templates from starlette.authentication import requires from khoj.utils.rawconfig import ( - TextContentConfig, GithubContentConfig, GithubRepoConfig, NotionContentConfig, @@ -18,14 +17,11 @@ from khoj.utils.rawconfig import ( # Internal Packages from khoj.utils import constants, state from database.adapters import EntryAdapters, get_user_github_config, get_user_notion_config, ConversationAdapters -from database.models import LocalOrgConfig, LocalMarkdownConfig, LocalPdfConfig, LocalPlaintextConfig # Initialize Router web_client = APIRouter() templates = Jinja2Templates(directory=constants.web_directory) -VALID_TEXT_CONTENT_TYPES = ["org", "markdown", "pdf", "plaintext"] - # Create Routes @web_client.get("/", response_class=FileResponse) @@ -109,17 +105,6 @@ def login_page(request: Request): ) -def map_config_to_object(content_type: str): - if content_type == "org": - return LocalOrgConfig - if content_type == "markdown": - return LocalMarkdownConfig - if content_type == "pdf": - return LocalPdfConfig - if content_type == "plaintext": - return LocalPlaintextConfig - - @web_client.get("/config", response_class=HTMLResponse) @requires(["authenticated"], redirect="login_page") def config_page(request: Request): @@ -224,35 +209,3 @@ def notion_config_page(request: Request): "user_photo": user_picture, }, ) - - -@web_client.get("/config/content_type/{content_type}", response_class=HTMLResponse) -@requires(["authenticated"], redirect="login_page") -def content_config_page(request: Request, content_type: str): - if content_type not in VALID_TEXT_CONTENT_TYPES: - return templates.TemplateResponse("config.html", context={"request": request}) - - object = map_config_to_object(content_type) - user = request.user.object - user_picture = request.session.get("user", {}).get("picture") - config = object.objects.filter(user=user).first() - if config == None: - config = object.objects.create(user=user) - - current_config = TextContentConfig( - input_files=config.input_files, - input_filter=config.input_filter, - index_heading_entries=config.index_heading_entries, - ) - current_config = json.loads(current_config.json()) - - return templates.TemplateResponse( - "content_type_input.html", - context={ - "request": request, - "current_config": current_config, - "content_type": content_type, - "username": user.username, - "user_photo": user_picture, - }, - ) From 9ab327a2b6b30891896786c6f53408479dfa243c Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Mon, 6 Nov 2023 23:49:08 -0800 Subject: [PATCH 072/194] Store the data source of each entry in database This will be useful for updating, deleting entries by their data source. Data source can be one of Computer, Github or Notion for now Store each file/entries source in database --- src/database/adapters/__init__.py | 22 ++++++++++++++++--- .../migrations/0012_entry_file_source.py | 21 ++++++++++++++++++ src/database/models/__init__.py | 6 +++++ .../processor/github/github_to_entries.py | 7 +++++- .../processor/markdown/markdown_to_entries.py | 1 + .../processor/notion/notion_to_entries.py | 7 +++++- src/khoj/processor/org_mode/org_to_entries.py | 1 + src/khoj/processor/pdf/pdf_to_entries.py | 1 + .../plaintext/plaintext_to_entries.py | 1 + src/khoj/processor/text_to_entries.py | 4 +++- src/khoj/search_type/text_search.py | 9 ++++---- tests/test_text_search.py | 2 +- 12 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 src/database/migrations/0012_entry_file_source.py diff --git a/src/database/adapters/__init__.py b/src/database/adapters/__init__.py index fa37aa99..69a3c1f4 100644 --- a/src/database/adapters/__init__.py +++ b/src/database/adapters/__init__.py @@ -287,13 +287,21 @@ class EntryAdapters: return deleted_count @staticmethod - def delete_all_entries(user: KhojUser, file_type: str = None): + def delete_all_entries_by_type(user: KhojUser, file_type: str = None): if file_type is None: deleted_count, _ = Entry.objects.filter(user=user).delete() else: deleted_count, _ = Entry.objects.filter(user=user, file_type=file_type).delete() return deleted_count + @staticmethod + def delete_all_entries_by_source(user: KhojUser, file_source: str = None): + if file_source is None: + deleted_count, _ = Entry.objects.filter(user=user).delete() + else: + deleted_count, _ = Entry.objects.filter(user=user, file_source=file_source).delete() + return deleted_count + @staticmethod def get_existing_entry_hashes_by_file(user: KhojUser, file_path: str): return Entry.objects.filter(user=user, file_path=file_path).values_list("hashed_value", flat=True) @@ -318,8 +326,12 @@ class EntryAdapters: return await Entry.objects.filter(user=user, file_path=file_path).adelete() @staticmethod - def aget_all_filenames(user: KhojUser): - return Entry.objects.filter(user=user).distinct("file_path").values_list("file_path", flat=True) + def aget_all_filenames_by_source(user: KhojUser, file_source: str): + return ( + Entry.objects.filter(user=user, file_source=file_source) + .distinct("file_path") + .values_list("file_path", flat=True) + ) @staticmethod async def adelete_all_entries(user: KhojUser): @@ -384,3 +396,7 @@ class EntryAdapters: @staticmethod def get_unique_file_types(user: KhojUser): return Entry.objects.filter(user=user).values_list("file_type", flat=True).distinct() + + @staticmethod + def get_unique_file_source(user: KhojUser): + return Entry.objects.filter(user=user).values_list("file_source", flat=True).distinct() diff --git a/src/database/migrations/0012_entry_file_source.py b/src/database/migrations/0012_entry_file_source.py new file mode 100644 index 00000000..187136ae --- /dev/null +++ b/src/database/migrations/0012_entry_file_source.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.5 on 2023-11-07 07:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("database", "0011_merge_20231102_0138"), + ] + + operations = [ + migrations.AddField( + model_name="entry", + name="file_source", + field=models.CharField( + choices=[("computer", "Computer"), ("notion", "Notion"), ("github", "Github")], + default="computer", + max_length=30, + ), + ), + ] diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py index 5dd9622b..b1be9ded 100644 --- a/src/database/models/__init__.py +++ b/src/database/models/__init__.py @@ -131,11 +131,17 @@ class Entry(BaseModel): GITHUB = "github" CONVERSATION = "conversation" + class EntrySource(models.TextChoices): + COMPUTER = "computer" + NOTION = "notion" + GITHUB = "github" + user = models.ForeignKey(KhojUser, on_delete=models.CASCADE, default=None, null=True, blank=True) embeddings = VectorField(dimensions=384) raw = models.TextField() compiled = models.TextField() heading = models.CharField(max_length=1000, default=None, null=True, blank=True) + file_source = models.CharField(max_length=30, choices=EntrySource.choices, default=EntrySource.COMPUTER) file_type = models.CharField(max_length=30, choices=EntryType.choices, default=EntryType.PLAINTEXT) file_path = models.CharField(max_length=400, default=None, null=True, blank=True) file_name = models.CharField(max_length=400, default=None, null=True, blank=True) diff --git a/src/khoj/processor/github/github_to_entries.py b/src/khoj/processor/github/github_to_entries.py index 14e9b696..56279453 100644 --- a/src/khoj/processor/github/github_to_entries.py +++ b/src/khoj/processor/github/github_to_entries.py @@ -104,7 +104,12 @@ class GithubToEntries(TextToEntries): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( - current_entries, DbEntry.EntryType.GITHUB, key="compiled", logger=logger, user=user + current_entries, + DbEntry.EntryType.GITHUB, + DbEntry.EntrySource.GITHUB, + key="compiled", + logger=logger, + user=user, ) return num_new_embeddings, num_deleted_embeddings diff --git a/src/khoj/processor/markdown/markdown_to_entries.py b/src/khoj/processor/markdown/markdown_to_entries.py index e0b76368..0dd71740 100644 --- a/src/khoj/processor/markdown/markdown_to_entries.py +++ b/src/khoj/processor/markdown/markdown_to_entries.py @@ -47,6 +47,7 @@ class MarkdownToEntries(TextToEntries): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, DbEntry.EntryType.MARKDOWN, + DbEntry.EntrySource.COMPUTER, "compiled", logger, deletion_file_names, diff --git a/src/khoj/processor/notion/notion_to_entries.py b/src/khoj/processor/notion/notion_to_entries.py index a4b15d4e..7a88e2a1 100644 --- a/src/khoj/processor/notion/notion_to_entries.py +++ b/src/khoj/processor/notion/notion_to_entries.py @@ -250,7 +250,12 @@ class NotionToEntries(TextToEntries): # Identify, mark and merge any new entries with previous entries with timer("Identify new or updated entries", logger): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( - current_entries, DbEntry.EntryType.NOTION, key="compiled", logger=logger, user=user + current_entries, + DbEntry.EntryType.NOTION, + DbEntry.EntrySource.NOTION, + key="compiled", + logger=logger, + user=user, ) return num_new_embeddings, num_deleted_embeddings diff --git a/src/khoj/processor/org_mode/org_to_entries.py b/src/khoj/processor/org_mode/org_to_entries.py index bf6df6dc..04ce97e4 100644 --- a/src/khoj/processor/org_mode/org_to_entries.py +++ b/src/khoj/processor/org_mode/org_to_entries.py @@ -48,6 +48,7 @@ class OrgToEntries(TextToEntries): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, DbEntry.EntryType.ORG, + DbEntry.EntrySource.COMPUTER, "compiled", logger, deletion_file_names, diff --git a/src/khoj/processor/pdf/pdf_to_entries.py b/src/khoj/processor/pdf/pdf_to_entries.py index 81c2250f..3a47096a 100644 --- a/src/khoj/processor/pdf/pdf_to_entries.py +++ b/src/khoj/processor/pdf/pdf_to_entries.py @@ -46,6 +46,7 @@ class PdfToEntries(TextToEntries): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, DbEntry.EntryType.PDF, + DbEntry.EntrySource.COMPUTER, "compiled", logger, deletion_file_names, diff --git a/src/khoj/processor/plaintext/plaintext_to_entries.py b/src/khoj/processor/plaintext/plaintext_to_entries.py index fd5e1de2..d42dae30 100644 --- a/src/khoj/processor/plaintext/plaintext_to_entries.py +++ b/src/khoj/processor/plaintext/plaintext_to_entries.py @@ -56,6 +56,7 @@ class PlaintextToEntries(TextToEntries): num_new_embeddings, num_deleted_embeddings = self.update_embeddings( current_entries, DbEntry.EntryType.PLAINTEXT, + DbEntry.EntrySource.COMPUTER, key="compiled", logger=logger, deletion_filenames=deletion_file_names, diff --git a/src/khoj/processor/text_to_entries.py b/src/khoj/processor/text_to_entries.py index 4661fd9b..3d79e02e 100644 --- a/src/khoj/processor/text_to_entries.py +++ b/src/khoj/processor/text_to_entries.py @@ -78,6 +78,7 @@ class TextToEntries(ABC): self, current_entries: List[Entry], file_type: str, + file_source: str, key="compiled", logger: logging.Logger = None, deletion_filenames: Set[str] = None, @@ -95,7 +96,7 @@ class TextToEntries(ABC): if regenerate: with timer("Cleared existing dataset for regeneration in", logger): logger.debug(f"Deleting all entries for file type {file_type}") - num_deleted_entries = EntryAdapters.delete_all_entries(user, file_type) + num_deleted_entries = EntryAdapters.delete_all_entries_by_type(user, file_type) hashes_to_process = set() with timer("Identified entries to add to database in", logger): @@ -132,6 +133,7 @@ class TextToEntries(ABC): compiled=entry.compiled, heading=entry.heading[:1000], # Truncate to max chars of field allowed file_path=entry.file, + file_source=file_source, file_type=file_type, hashed_value=entry_hash, corpus_id=entry.corpus_id, diff --git a/src/khoj/search_type/text_search.py b/src/khoj/search_type/text_search.py index 14f5b770..ba2fc9ec 100644 --- a/src/khoj/search_type/text_search.py +++ b/src/khoj/search_type/text_search.py @@ -204,11 +204,12 @@ def setup( files=files, full_corpus=full_corpus, user=user, regenerate=regenerate ) - file_names = [file_name for file_name in files] + if files: + file_names = [file_name for file_name in files] - logger.info( - f"Deleted {num_deleted_embeddings} entries. Created {num_new_embeddings} new entries for user {user} from files {file_names}" - ) + logger.info( + f"Deleted {num_deleted_embeddings} entries. Created {num_new_embeddings} new entries for user {user} from files {file_names}" + ) def cross_encoder_score(query: str, hits: List[SearchResponse]) -> List[SearchResponse]: diff --git a/tests/test_text_search.py b/tests/test_text_search.py index 7d8c30fb..3d729ab5 100644 --- a/tests/test_text_search.py +++ b/tests/test_text_search.py @@ -58,7 +58,7 @@ def test_get_org_files_with_org_suffixed_dir_doesnt_raise_error(tmp_path, defaul # ---------------------------------------------------------------------------------------------------- @pytest.mark.django_db -def test_text_search_setup_with_empty_file_raises_error( +def test_text_search_setup_with_empty_file_creates_no_entries( org_config_with_only_new_file: LocalOrgConfig, default_user: KhojUser, caplog ): # Arrange From d527b644f4a36f607f46b7ef56c68ffc2a3903db Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Mon, 6 Nov 2023 23:51:43 -0800 Subject: [PATCH 073/194] Update content by source via API. Make web client use this API for config --- docs/github_integration.md | 2 +- docs/notion_integration.md | 2 +- src/khoj/interface/web/config.html | 15 +++++----- ....html => content_source_github_input.html} | 2 +- ....html => content_source_notion_input.html} | 2 +- src/khoj/routers/api.py | 30 ++++++++++--------- src/khoj/routers/web_client.py | 8 ++--- 7 files changed, 32 insertions(+), 29 deletions(-) rename src/khoj/interface/web/{content_type_github_input.html => content_source_github_input.html} (99%) rename src/khoj/interface/web/{content_type_notion_input.html => content_source_notion_input.html} (97%) diff --git a/docs/github_integration.md b/docs/github_integration.md index 6b8dce48..413dd41e 100644 --- a/docs/github_integration.md +++ b/docs/github_integration.md @@ -9,6 +9,6 @@ The Github integration allows you to index as many repositories as you want. It' ## 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 [http://localhost:42110/config/content_type/github](http://localhost:42110/config/content_type/github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index. +2. Navigate to [http://localhost:42110/config/content-source/github](http://localhost:42110/config/content-source/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 [http://localhost:42110/](http://localhost:42110/) and start searching! diff --git a/docs/notion_integration.md b/docs/notion_integration.md index 5fee7ff6..d3b645ca 100644 --- a/docs/notion_integration.md +++ b/docs/notion_integration.md @@ -8,7 +8,7 @@ We haven't setup a fancy integration with OAuth yet, so this integration still r ![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_type/notion. Click `Save`. +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). That's it! You should be ready to start searching and chatting. Make sure you've configured your OpenAI API Key for chat. diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index b19bbff6..4e77a8ef 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -19,7 +19,7 @@

Set repositories to index

- + {% if current_model_state.content %} Update {% else %} @@ -176,8 +176,9 @@ }) }; - function clearContentType(content_type) { - fetch('/api/config/data/content_type/' + content_type, { + function clearContentType(content_source) { + + fetch('/api/config/data/content-source/' + content_source, { method: 'DELETE', headers: { 'Content-Type': 'application/json', @@ -186,15 +187,15 @@ .then(response => response.json()) .then(data => { if (data.status == "ok") { - var contentTypeClearButton = document.getElementById("clear-" + content_type); + var contentTypeClearButton = document.getElementById("clear-" + content_source); contentTypeClearButton.style.display = "none"; - var configuredIcon = document.getElementById("configured-icon-" + content_type); + var configuredIcon = document.getElementById("configured-icon-" + content_source); if (configuredIcon) { configuredIcon.style.display = "none"; } - var misconfiguredIcon = document.getElementById("misconfigured-icon-" + content_type); + var misconfiguredIcon = document.getElementById("misconfigured-icon-" + content_source); if (misconfiguredIcon) { misconfiguredIcon.style.display = "none"; } diff --git a/src/khoj/interface/web/content_type_github_input.html b/src/khoj/interface/web/content_source_github_input.html similarity index 99% rename from src/khoj/interface/web/content_type_github_input.html rename to src/khoj/interface/web/content_source_github_input.html index 0e41645a..ff82b1f2 100644 --- a/src/khoj/interface/web/content_type_github_input.html +++ b/src/khoj/interface/web/content_source_github_input.html @@ -125,7 +125,7 @@ } const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1]; - fetch('/api/config/data/content_type/github', { + fetch('/api/config/data/content-source/github', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/khoj/interface/web/content_type_notion_input.html b/src/khoj/interface/web/content_source_notion_input.html similarity index 97% rename from src/khoj/interface/web/content_type_notion_input.html rename to src/khoj/interface/web/content_source_notion_input.html index 965c1ef5..18eb5a7f 100644 --- a/src/khoj/interface/web/content_type_notion_input.html +++ b/src/khoj/interface/web/content_source_notion_input.html @@ -42,7 +42,7 @@ } const csrfToken = document.cookie.split('; ').find(row => row.startsWith('csrftoken'))?.split('=')[1]; - fetch('/api/config/data/content_type/notion', { + fetch('/api/config/data/content-source/notion', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index 84e63b09..c2002048 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -61,11 +61,13 @@ api = APIRouter() logger = logging.getLogger(__name__) -def map_config_to_object(content_type: str): - if content_type == "github": +def map_config_to_object(content_source: str): + if content_source == "github": return GithubConfig - if content_type == "notion": + if content_source == "notion": return NotionConfig + if content_source == "computer": + return "Computer" async def map_config_to_db(config: FullConfig, user: KhojUser): @@ -164,7 +166,7 @@ async def set_config_data( return state.config -@api.post("/config/data/content_type/github", status_code=200) +@api.post("/config/data/content-source/github", status_code=200) @requires(["authenticated"]) async def set_content_config_github_data( request: Request, @@ -192,7 +194,7 @@ async def set_content_config_github_data( return {"status": "ok"} -@api.post("/config/data/content_type/notion", status_code=200) +@api.post("/config/data/content-source/notion", status_code=200) @requires(["authenticated"]) async def set_content_config_notion_data( request: Request, @@ -219,11 +221,11 @@ async def set_content_config_notion_data( return {"status": "ok"} -@api.delete("/config/data/content_type/{content_type}", status_code=200) +@api.delete("/config/data/content-source/{content_source}", status_code=200) @requires(["authenticated"]) -async def remove_content_config_data( +async def remove_content_source_data( request: Request, - content_type: str, + content_source: str, client: Optional[str] = None, ): user = request.user.object @@ -233,15 +235,15 @@ async def remove_content_config_data( telemetry_type="api", api="delete_content_config", client=client, - metadata={"content_type": content_type}, + metadata={"content_source": content_source}, ) - content_object = map_config_to_object(content_type) + content_object = map_config_to_object(content_source) if content_object is None: - raise ValueError(f"Invalid content type: {content_type}") - - await content_object.objects.filter(user=user).adelete() - await sync_to_async(EntryAdapters.delete_all_entries)(user, content_type) + raise ValueError(f"Invalid content source: {content_source}") + elif content_object != "Computer": + await content_object.objects.filter(user=user).adelete() + await sync_to_async(EntryAdapters.delete_all_entries_by_source)(user, content_source) enabled_content = await sync_to_async(EntryAdapters.get_unique_file_types)(user) return {"status": "ok"} diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 65292ccf..8016cfce 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -150,7 +150,7 @@ def config_page(request: Request): ) -@web_client.get("/config/content_type/github", response_class=HTMLResponse) +@web_client.get("/config/content-source/github", response_class=HTMLResponse) @requires(["authenticated"], redirect="login_page") def github_config_page(request: Request): user = request.user.object @@ -177,7 +177,7 @@ def github_config_page(request: Request): current_config = {} # type: ignore return templates.TemplateResponse( - "content_type_github_input.html", + "content_source_github_input.html", context={ "request": request, "current_config": current_config, @@ -187,7 +187,7 @@ def github_config_page(request: Request): ) -@web_client.get("/config/content_type/notion", response_class=HTMLResponse) +@web_client.get("/config/content-source/notion", response_class=HTMLResponse) @requires(["authenticated"], redirect="login_page") def notion_config_page(request: Request): user = request.user.object @@ -201,7 +201,7 @@ def notion_config_page(request: Request): current_config = json.loads(current_config.json()) return templates.TemplateResponse( - "content_type_notion_input.html", + "content_source_notion_input.html", context={ "request": request, "current_config": current_config, From 6e957584acd7d1df87b73a70fefe94fc6ab217ec Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 7 Nov 2023 02:08:06 -0800 Subject: [PATCH 074/194] Create config page on web app to manage computer files indexed by Khoj Remove the table of all files indexed by Khoj. This seems overkill and doesn't match the UI semantics of the other data sources like Github, Notion. Create instead a data source card for computer files with the same update, disable semantics of the Github and Notion data source cards Users can disable each data source from its card on the main config page. They can see/delete individual files indexed from the computer data source once they click into the computer files data source card on the config page --- .../interface/web/assets/icons/computer.png | Bin 0 -> 10517 bytes src/khoj/interface/web/config.html | 114 ++++++------------ .../web/content_source_computer_input.html | 107 ++++++++++++++++ src/khoj/routers/api.py | 25 +--- src/khoj/routers/web_client.py | 35 +++--- 5 files changed, 165 insertions(+), 116 deletions(-) create mode 100644 src/khoj/interface/web/assets/icons/computer.png create mode 100644 src/khoj/interface/web/content_source_computer_input.html diff --git a/src/khoj/interface/web/assets/icons/computer.png b/src/khoj/interface/web/assets/icons/computer.png new file mode 100644 index 0000000000000000000000000000000000000000..12473485486c7dbf1b90dbc3d19ae9cc0e340171 GIT binary patch literal 10517 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

1k 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

  • aXw=eA6-b=){p6n7dkwC+xwEV?BzlJ(cF5Y= z;X-)OyCJw1^penX108^iYX_Rp;vuG|fwW9SPxnYSqV^h6fsPXoHwct~QHc8)`ZMUq zpg+voAVlx7%PuOz(gm~w9SH)!15AsZAFPx5loRwPm;j6i;>=bIwOSVXYt;lPq>z6A XUvqKATJtxd00000NkvXXu0mjfHN?UZ literal 0 HcmV?d00001 diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index 4e77a8ef..1410f20b 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -3,9 +3,40 @@
    -

    Plugins

    +

    Content

    + -
    -

    Manage Data

    -
    -
    - -
    -
    -
    -
    -
    @@ -363,70 +384,5 @@ } }) } - - // Get all currently indexed files - function getAllFilenames() { - fetch('/api/config/data/all') - .then(response => response.json()) - .then(data => { - var indexedFiles = document.getElementsByClassName("indexed-files")[0]; - indexedFiles.innerHTML = ""; - - if (data.length == 0) { - document.getElementById("delete-all-files").style.display = "none"; - indexedFiles.innerHTML = ""; - } else { - document.getElementById("delete-all-files").style.display = "block"; - } - - for (var filename of data) { - let fileElement = document.createElement("div"); - fileElement.classList.add("file-element"); - - let fileNameElement = document.createElement("div"); - fileNameElement.classList.add("content-name"); - fileNameElement.innerHTML = filename; - fileElement.appendChild(fileNameElement); - - let buttonContainer = document.createElement("div"); - buttonContainer.classList.add("remove-button-container"); - let removeFileButton = document.createElement("button"); - removeFileButton.classList.add("remove-file-button"); - removeFileButton.innerHTML = "🗑️"; - removeFileButton.addEventListener("click", ((filename) => { - return () => { - removeFile(filename); - }; - })(filename)); - buttonContainer.appendChild(removeFileButton); - fileElement.appendChild(buttonContainer); - indexedFiles.appendChild(fileElement); - } - }) - .catch((error) => { - console.error('Error:', error); - }); - } - - // Get all currently indexed files on page load - getAllFilenames(); - - let deleteAllFilesButton = document.getElementById("delete-all-files"); - deleteAllFilesButton.addEventListener("click", function(event) { - event.preventDefault(); - fetch('/api/config/data/all', { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - } - }) - .then(response => response.json()) - .then(data => { - if (data.status == "ok") { - getAllFilenames(); - } - }) - }); - {% endblock %} diff --git a/src/khoj/interface/web/content_source_computer_input.html b/src/khoj/interface/web/content_source_computer_input.html new file mode 100644 index 00000000..01992d5e --- /dev/null +++ b/src/khoj/interface/web/content_source_computer_input.html @@ -0,0 +1,107 @@ +{% extends "base_config.html" %} +{% block content %} +
    +
    +

    + files + Files +
    +

    Manage files from your computer

    +

    Download the Khoj Desktop app to sync files from your computer

    +
    +

    +
    +
    + +
    +
    +
    +
    +
    +
    + + +{% endblock %} diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index c2002048..fabfebe1 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -270,10 +270,11 @@ async def remove_file_data( return {"status": "ok"} -@api.get("/config/data/all", response_model=List[str]) +@api.get("/config/data/{content_source}", response_model=List[str]) @requires(["authenticated"]) async def get_all_filenames( request: Request, + content_source: str, client: Optional[str] = None, ): user = request.user.object @@ -285,27 +286,7 @@ async def get_all_filenames( client=client, ) - return await sync_to_async(list)(EntryAdapters.aget_all_filenames(user)) - - -@api.delete("/config/data/all", status_code=200) -@requires(["authenticated"]) -async def remove_all_config_data( - request: Request, - client: Optional[str] = None, -): - user = request.user.object - - update_telemetry_state( - request=request, - telemetry_type="api", - api="delete_all_config", - client=client, - ) - - await EntryAdapters.adelete_all_entries(user) - - return {"status": "ok"} + return await sync_to_async(list)(EntryAdapters.aget_all_filenames_by_source(user, content_source)) @api.post("/config/data/conversation/model", status_code=200) diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index 8016cfce..3e568bc7 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -110,25 +110,14 @@ def login_page(request: Request): def config_page(request: Request): user = request.user.object user_picture = request.session.get("user", {}).get("picture") - enabled_content = set(EntryAdapters.get_unique_file_types(user).all()) + enabled_content_source = set(EntryAdapters.get_unique_file_source(user).all()) successfully_configured = { - "pdf": ("pdf" in enabled_content), - "markdown": ("markdown" in enabled_content), - "org": ("org" in enabled_content), - "image": False, - "github": ("github" in enabled_content), - "notion": ("notion" in enabled_content), - "plaintext": ("plaintext" in enabled_content), + "computer": ("computer" in enabled_content_source), + "github": ("github" in enabled_content_source), + "notion": ("notion" in enabled_content_source), } - if state.content_index: - successfully_configured.update( - { - "image": state.content_index.image is not None, - } - ) - conversation_options = ConversationAdapters.get_conversation_processor_options().all() all_conversation_options = list() for conversation_option in conversation_options: @@ -209,3 +198,19 @@ def notion_config_page(request: Request): "user_photo": user_picture, }, ) + + +@web_client.get("/config/content-source/computer", response_class=HTMLResponse) +@requires(["authenticated"], redirect="login_page") +def computer_config_page(request: Request): + user = request.user.object + user_picture = request.session.get("user", {}).get("picture") + + return templates.TemplateResponse( + "content_source_computer_input.html", + context={ + "request": request, + "username": user.username, + "user_photo": user_picture, + }, + ) From 404d47f1a1fbf1ea37c539f393695f92b6f94aec Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 7 Nov 2023 02:20:11 -0800 Subject: [PATCH 075/194] Bubble up content indexing errors to notify user on client apps --- src/khoj/configure.py | 13 +++++++------ src/khoj/routers/indexer.py | 21 ++++++++++++++++----- tests/conftest.py | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/khoj/configure.py b/src/khoj/configure.py index bc9e9bf8..ecd35cf9 100644 --- a/src/khoj/configure.py +++ b/src/khoj/configure.py @@ -1,5 +1,4 @@ # Standard Packages -import sys import logging import json from enum import Enum @@ -109,7 +108,6 @@ def configure_server( state.search_models = configure_search(state.search_models, state.config.search_type) initialize_content(regenerate, search_type, init, user) except Exception as e: - logger.error(f"🚨 Failed to configure search models", exc_info=True) raise e finally: state.config_lock.release() @@ -125,7 +123,7 @@ def initialize_content(regenerate: bool, search_type: Optional[SearchType] = Non else: logger.info("📬 Updating content index...") all_files = collect_files(user=user) - state.content_index = configure_content( + state.content_index, status = configure_content( state.content_index, state.config.content_type, all_files, @@ -134,8 +132,9 @@ def initialize_content(regenerate: bool, search_type: Optional[SearchType] = Non search_type, user=user, ) + if not status: + raise RuntimeError("Failed to update content index") except Exception as e: - logger.error(f"🚨 Failed to index content", exc_info=True) raise e @@ -165,13 +164,15 @@ def update_search_index(): logger.info("📬 Updating content index via Scheduler") for user in get_all_users(): all_files = collect_files(user=user) - state.content_index = configure_content( + state.content_index, success = configure_content( state.content_index, state.config.content_type, all_files, state.search_models, user=user ) all_files = collect_files(user=None) - state.content_index = configure_content( + state.content_index, success = configure_content( state.content_index, state.config.content_type, all_files, state.search_models, user=None ) + if not success: + raise RuntimeError("Failed to update content index") logger.info("📪 Content index updated via Scheduler") except Exception as e: logger.error(f"🚨 Error updating content index via Scheduler: {e}", exc_info=True) diff --git a/src/khoj/routers/indexer.py b/src/khoj/routers/indexer.py index 1bbf53c2..a7a1249d 100644 --- a/src/khoj/routers/indexer.py +++ b/src/khoj/routers/indexer.py @@ -126,7 +126,7 @@ async def update( # Extract required fields from config loop = asyncio.get_event_loop() - state.content_index = await loop.run_in_executor( + state.content_index, success = await loop.run_in_executor( None, configure_content, state.content_index, @@ -138,6 +138,8 @@ async def update( False, user, ) + if not success: + raise RuntimeError("Failed to update content index") logger.info(f"Finished processing batch indexing request") except Exception as e: logger.error(f"Failed to process batch indexing request: {e}", exc_info=True) @@ -145,6 +147,7 @@ async def update( f"🚨 Failed to {force} update {t} content index triggered via API call by {client} client: {e}", exc_info=True, ) + return Response(content="Failed", status_code=500) update_telemetry_state( request=request, @@ -182,18 +185,19 @@ def configure_content( t: Optional[state.SearchType] = None, full_corpus: bool = True, user: KhojUser = None, -) -> Optional[ContentIndex]: +) -> tuple[Optional[ContentIndex], bool]: content_index = ContentIndex() + success = True 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 None + return None, False search_type = t.value if t else None if files is None: logger.warning(f"🚨 No files to process for {search_type} search.") - return None + return None, True try: # Initialize Org Notes Search @@ -209,6 +213,7 @@ def configure_content( ) except Exception as e: logger.error(f"🚨 Failed to setup org: {e}", exc_info=True) + success = False try: # Initialize Markdown Search @@ -225,6 +230,7 @@ def configure_content( except Exception as e: logger.error(f"🚨 Failed to setup markdown: {e}", exc_info=True) + success = False try: # Initialize PDF Search @@ -241,6 +247,7 @@ def configure_content( except Exception as e: logger.error(f"🚨 Failed to setup PDF: {e}", exc_info=True) + success = False try: # Initialize Plaintext Search @@ -257,6 +264,7 @@ def configure_content( except Exception as e: logger.error(f"🚨 Failed to setup plaintext: {e}", exc_info=True) + success = False try: # Initialize Image Search @@ -274,6 +282,7 @@ def configure_content( except Exception as e: logger.error(f"🚨 Failed to setup images: {e}", exc_info=True) + success = False try: github_config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first() @@ -291,6 +300,7 @@ def configure_content( except Exception as e: logger.error(f"🚨 Failed to setup GitHub: {e}", exc_info=True) + success = False try: # Initialize Notion Search @@ -308,12 +318,13 @@ def configure_content( except Exception as e: logger.error(f"🚨 Failed to setup GitHub: {e}", exc_info=True) + success = False # Invalidate Query Cache if user: state.query_cache[user.uuid] = LRU() - return content_index + return content_index, success def load_content( diff --git a/tests/conftest.py b/tests/conftest.py index fbb98476..59104123 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -196,7 +196,7 @@ def chat_client(search_config: SearchConfig, default_user2: KhojUser): # Index Markdown Content for Search all_files = fs_syncer.collect_files(user=default_user2) - state.content_index = configure_content( + state.content_index, _ = configure_content( state.content_index, state.config.content_type, all_files, state.search_models, user=default_user2 ) From 779fa531a5ff806766bc25cda501aaff0ad23afe Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 7 Nov 2023 03:17:42 -0800 Subject: [PATCH 076/194] Prevent Desktop app triggering multiple simultaneous syncs to server Lock syncing to server if a sync is already in progress. While the sync save button gets disabled while sync is in progress, the background sync job can still trigger a sync in parallel. This sync lock prevents that --- src/interface/desktop/main.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index 1d5c4be2..045144cc 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -110,6 +110,15 @@ function filenameToMimeType (filename) { } function pushDataToKhoj (regenerate = false) { + // Don't sync if token or hostURL is not set or if already syncing + if (store.get('khojToken') === '' || store.get('hostURL') === '' || store.get('syncing') === true) { + const win = BrowserWindow.getAllWindows()[0]; + if (win) win.webContents.send('update-state', state); + return; + } else { + store.set('syncing', true); + } + let filesToPush = []; const files = store.get('files') || []; const folders = store.get('folders') || []; @@ -192,11 +201,13 @@ function pushDataToKhoj (regenerate = false) { }) .finally(() => { // Syncing complete + store.set('syncing', false); const win = BrowserWindow.getAllWindows()[0]; if (win) win.webContents.send('update-state', state); }); } else { // Syncing complete + store.set('syncing', false); const win = BrowserWindow.getAllWindows()[0]; if (win) win.webContents.send('update-state', state); } From 7c424e0d5f15dea82cac9932d6425aa2c7b7428c Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 7 Nov 2023 03:37:16 -0800 Subject: [PATCH 077/194] Enable deleting all indexed desktop files from Khoj via Desktop app --- src/interface/desktop/config.html | 3 +++ src/interface/desktop/main.js | 14 ++++++++++++++ src/interface/desktop/preload.js | 3 ++- src/interface/desktop/renderer.js | 6 ++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/interface/desktop/config.html b/src/interface/desktop/config.html index 3f8e19d9..c63a2a5c 100644 --- a/src/interface/desktop/config.html +++ b/src/interface/desktop/config.html @@ -96,6 +96,9 @@
    +
    + +
    diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index 045144cc..e6524d73 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -317,6 +317,19 @@ async function syncData (regenerate = false) { } } +async function deleteAllFiles () { + try { + store.set('files', []); + store.set('folders', []); + pushDataToKhoj(true); + const date = new Date(); + console.log('Pushing data to Khoj at: ', date); + } catch (err) { + console.error(err); + } +} + + let firstRun = true; let win = null; const createWindow = (tab = 'chat.html') => { @@ -397,6 +410,7 @@ app.whenReady().then(() => { ipcMain.handle('syncData', (event, regenerate) => { syncData(regenerate); }); + ipcMain.handle('deleteAllFiles', deleteAllFiles); createWindow() diff --git a/src/interface/desktop/preload.js b/src/interface/desktop/preload.js index 3228fdb0..eb5a6cc2 100644 --- a/src/interface/desktop/preload.js +++ b/src/interface/desktop/preload.js @@ -45,7 +45,8 @@ contextBridge.exposeInMainWorld('hostURLAPI', { }) contextBridge.exposeInMainWorld('syncDataAPI', { - syncData: (regenerate) => ipcRenderer.invoke('syncData', regenerate) + syncData: (regenerate) => ipcRenderer.invoke('syncData', regenerate), + deleteAllFiles: () => ipcRenderer.invoke('deleteAllFiles') }) contextBridge.exposeInMainWorld('tokenAPI', { diff --git a/src/interface/desktop/renderer.js b/src/interface/desktop/renderer.js index 26765bf0..849a8293 100644 --- a/src/interface/desktop/renderer.js +++ b/src/interface/desktop/renderer.js @@ -206,3 +206,9 @@ syncForceButton.addEventListener('click', async () => { loadingBar.style.display = 'block'; await window.syncDataAPI.syncData(true); }); + +const deleteAllButton = document.getElementById('delete-all'); +deleteAllButton.addEventListener('click', async () => { + loadingBar.style.display = 'block'; + await window.syncDataAPI.deleteAllFiles(); +}); From 045c2252d6e25143e7b5648b2d310446af7d7013 Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 7 Nov 2023 04:45:25 -0800 Subject: [PATCH 078/194] Set content enabled status on update via config buttons on web app Previously hitting configure or disable wouldn't update the state of the content cards. It needed page refresh to see if the content was synced correctly. Now cards automatically get set to new state on hitting disable button on card or global configure buttons --- src/khoj/interface/web/config.html | 105 +++++++++++++++++------------ 1 file changed, 62 insertions(+), 43 deletions(-) diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index 1410f20b..5a2089ca 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -8,11 +8,13 @@
    Computer -

    +

    Files - {% if current_model_state.computer == True %} - Configured - {% endif %} + Configured

    @@ -28,22 +30,23 @@
    - {% if current_model_state.computer %} -
    - -
    - {% endif %} +
    + +
    Github

    Github - {% if current_model_state.github == True %} - Configured - {% endif %} + Configured

    @@ -59,22 +62,24 @@
    - {% if current_model_state.github %} -
    - -
    - {% endif %} +
    + +
    Notion

    Notion - {% if current_model_state.notion == True %} - Configured - {% endif %} + Configured

    @@ -90,13 +95,13 @@
    - {% if current_model_state.notion %} -
    - -
    - {% endif %} +
    + +
    @@ -208,18 +213,11 @@ .then(response => response.json()) .then(data => { if (data.status == "ok") { - var contentTypeClearButton = document.getElementById("clear-" + content_source); - contentTypeClearButton.style.display = "none"; - - var configuredIcon = document.getElementById("configured-icon-" + content_source); - if (configuredIcon) { - configuredIcon.style.display = "none"; - } - - var misconfiguredIcon = document.getElementById("misconfigured-icon-" + content_source); - if (misconfiguredIcon) { - misconfiguredIcon.style.display = "none"; - } + document.getElementById("configured-icon-" + content_source).style.display = "none"; + document.getElementById("clear-" + content_source).style.display = "none"; + } else { + document.getElementById("configured-icon-" + content_source).style.display = ""; + document.getElementById("clear-" + content_source).style.display = ""; } }) }; @@ -265,6 +263,7 @@ if (data.detail != null) { throw new Error(data.detail); } + document.getElementById("status").innerHTML = emoji + " " + successText; document.getElementById("status").style.display = "block"; button.disabled = false; @@ -277,6 +276,26 @@ button.disabled = false; button.innerHTML = '⚠️ Unsuccessful'; }); + + content_sources = ["computer", "github", "notion"]; + content_sources.forEach(content_source => { + fetch(`/api/config/data/${content_source}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + } + }) + .then(response => response.json()) + .then(data => { + if (data.length > 0) { + document.getElementById("configured-icon-" + content_source).style.display = ""; + document.getElementById("clear-" + content_source).style.display = ""; + } else { + document.getElementById("configured-icon-" + content_source).style.display = "none"; + document.getElementById("clear-" + content_source).style.display = "none"; + } + }); + }); } // Setup the results count slider From 156421d30a6a1ee4217644b8757c71726cf12c8f Mon Sep 17 00:00:00 2001 From: Debanjum Singh Solanky Date: Tue, 7 Nov 2023 05:16:41 -0800 Subject: [PATCH 079/194] Show file type icons for each indexed file in config card of web app --- src/khoj/interface/web/base_config.html | 8 +++++-- .../web/content_source_computer_input.html | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html index 8e33677c..619c34c0 100644 --- a/src/khoj/interface/web/base_config.html +++ b/src/khoj/interface/web/base_config.html @@ -209,16 +209,20 @@ border: none; color: var(--flower); padding: 4px; + width: 32px; + margin-bottom: 0px } div.file-element { display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: 1fr 5fr 1fr; 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.8); - padding: 4px; + padding: 4px 0; margin-bottom: 8px; + justify-items: center; + align-items: center; } div.remove-button-container { diff --git a/src/khoj/interface/web/content_source_computer_input.html b/src/khoj/interface/web/content_source_computer_input.html index 01992d5e..aba3d8ee 100644 --- a/src/khoj/interface/web/content_source_computer_input.html +++ b/src/khoj/interface/web/content_source_computer_input.html @@ -23,6 +23,12 @@ #desktop-client { font-weight: normal; } + .indexed-files { + width: 100%; + } + .content-name { + font-size: smaller; + }