Improve Khoj Chat and Settings UI (#630)

* Fix license in pyproject.toml. Remove unused utils.state import

* Use single debug mode check function. Disable telemetry in debug mode

- Use single logic to check if khoj is running in debug mode.
  Previously there were 3 different variants of the check

- Do not log telemetry if KHOJ_DEBUG is set to true. Previously didn't
log telemetry even if KHOJ_DEBUG set to false

* Respect line breaks in user, khoj chat messages to improve formatting

* Disable Whatsapp config section on web client if Twilio not configured

Simplify Whatsapp configuration status checking js by standardizing
external input to lower case

* Disable Phone API when Twilio not setup and rate limit calls to it

- Move phone api to separate router and only enable it if Twilio enabled
- Add rate-limiting to OTP and verification calls

* Add slugs for phone rate limiting

---------

Co-authored-by: sabaimran <narmiabas@gmail.com>
This commit is contained in:
Debanjum 2024-01-29 18:03:43 +05:30 committed by GitHub
parent 9ad44f0e77
commit d1bfb245df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 134 additions and 90 deletions

View file

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "khoj-assistant"
description = "An AI copilot for your Second Brain"
readme = "README.md"
license = "GPL-3.0-or-later"
license = "AGPL-3.0-or-later"
requires-python = ">=3.8"
authors = [
{ name = "Debanjum Singh Solanky, Saba Imran" },

View file

@ -892,6 +892,7 @@
display: inline-block;
max-width: 80%;
text-align: left;
white-space: pre-line;
}
/* color chat bubble by khoj blue */
.chat-message-text.khoj {

View file

@ -13,6 +13,8 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
import os
from pathlib import Path
from khoj.utils.helpers import in_debug_mode
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@ -24,7 +26,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.getenv("KHOJ_DJANGO_SECRET_KEY", "!secret")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv("KHOJ_DEBUG") == "True"
DEBUG = in_debug_mode()
# All Subdomains of KHOJ_DOMAIN are trusted
KHOJ_DOMAIN = os.getenv("KHOJ_DOMAIN", "khoj.dev")

View file

@ -34,6 +34,7 @@ from khoj.database.adapters import (
from khoj.database.models import ClientApplication, KhojUser, Subscription
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
from khoj.routers.indexer import configure_content, configure_search, load_content
from khoj.routers.twilio import is_twilio_enabled
from khoj.utils import constants, state
from khoj.utils.config import SearchType
from khoj.utils.fs_syncer import collect_files
@ -258,18 +259,26 @@ def configure_routes(app):
from khoj.routers.api_config import api_config
from khoj.routers.auth import auth_router
from khoj.routers.indexer import indexer
from khoj.routers.subscription import subscription_router
from khoj.routers.web_client import web_client
app.include_router(api, prefix="/api")
app.include_router(api_config, prefix="/api/config")
app.include_router(indexer, prefix="/api/v1/index")
if state.billing_enabled:
logger.info("💳 Enabled Billing")
app.include_router(subscription_router, prefix="/api/subscription")
app.include_router(web_client)
app.include_router(auth_router, prefix="/auth")
if state.billing_enabled:
from khoj.routers.subscription import subscription_router
logger.info("💳 Enabled Billing")
app.include_router(subscription_router, prefix="/api/subscription")
if is_twilio_enabled():
logger.info("📞 Enabled Twilio")
from khoj.routers.api_phone import api_phone
app.include_router(api_phone, prefix="/api/config/phone")
def configure_middleware(app):
app.add_middleware(AuthenticationMiddleware, backend=UserAuthenticationBackend())

View file

@ -965,6 +965,7 @@ To get started, just start typing below. You can also type / to see a list of co
display: inline-block;
max-width: 80%;
text-align: left;
white-space: pre-line;
}
/* color chat bubble by khoj blue */
.chat-message-text.khoj {

View file

@ -194,7 +194,9 @@
</button>
</div>
</div>
<div id="phone-number-input-id" class="api-settings">
</div>
<div id="whatsapp" class="section">
<div id="phone-number-input-card" class="api-settings">
<div class="card-title-row">
<img class="card-icon" src="/static/assets/icons/whatsapp.svg" alt="WhatsApp icon">
<h3 class="card-title">WhatsApp</h3>
@ -610,15 +612,21 @@
const phonenumberVerifiedText = document.getElementById("api-settings-card-description-verified");
const phonenumberUnverifiedText = document.getElementById("api-settings-card-description-unverified");
let preExistingPhoneNumber = "{{ phone_number }}";
const preExistingPhoneNumber = "{{ phone_number }}";
let isPhoneNumberVerified = "{{ is_phone_number_verified }}";
let isTwilioEnabled = "{{ is_twilio_enabled }}";
isPhoneNumberVerified = isPhoneNumberVerified.toLowerCase();
isTwilioEnabled = isTwilioEnabled.toLowerCase();
if (preExistingPhoneNumber != "None" && (isPhoneNumberVerified == "True" || isPhoneNumberVerified == "true")) {
if (isTwilioEnabled !== "true" ) {
const phoneNumberVerificationCard = document.getElementById("phone-number-input-card");
phoneNumberVerificationCard.style.display = "none";
}
if (preExistingPhoneNumber != "None" && isPhoneNumberVerified === "true") {
phonenumberVerifyButton.style.display = "none";
phonenumberRemoveButton.style.display = "";
} else if (preExistingPhoneNumber != "None" && (isPhoneNumberVerified == "False" || isPhoneNumberVerified == "false")) {
if (isTwilioEnabled == "True" || isTwilioEnabled == "true") {
} else if (preExistingPhoneNumber != "None" && isPhoneNumberVerified === "false") {
if (isTwilioEnabled == "true") {
phonenumberVerifyButton.style.display = "";
phonenumberRemoveButton.style.display = "none";
} else {
@ -759,7 +767,5 @@
}, 5000);
});
})
</script>
{% endblock %}

View file

@ -14,6 +14,8 @@ import threading
import warnings
from importlib.metadata import version
from khoj.utils.helpers import in_debug_mode
# Ignore non-actionable warnings
warnings.filterwarnings("ignore", message=r"snapshot_download.py has been made private", category=FutureWarning)
warnings.filterwarnings("ignore", message=r"legacy way to download files from the HF hub,", category=FutureWarning)
@ -45,7 +47,7 @@ with redirect_stdout(collectstatic_output):
call_command("collectstatic", "--noinput")
# Initialize the Application Server
if os.getenv("KHOJ_DEBUG", "false").lower() == "true":
if in_debug_mode():
app = FastAPI(debug=True)
else:
app = FastAPI(docs_url=None) # Disable Swagger UI in production

View file

@ -22,7 +22,6 @@ from khoj.database.models import (
NotionConfig,
)
from khoj.routers.helpers import CommonQueryParams, update_telemetry_state
from khoj.routers.twilio import create_otp, is_twilio_enabled, verify_otp
from khoj.utils import constants, state
from khoj.utils.rawconfig import (
FullConfig,
@ -279,77 +278,6 @@ async def update_search_model(
return {"status": "ok"}
@api_config.post("/phone", status_code=200)
@requires(["authenticated"])
async def update_phone_number(
request: Request,
phone_number: str,
client: Optional[str] = None,
):
user = request.user.object
await adapters.aset_user_phone_number(user, phone_number)
if is_twilio_enabled():
create_otp(user)
else:
logger.warning("Phone verification is not enabled")
update_telemetry_state(
request=request,
telemetry_type="api",
api="set_phone_number",
client=client,
metadata={"phone_number": phone_number},
)
return {"status": "ok"}
@api_config.delete("/phone", status_code=200)
@requires(["authenticated"])
async def delete_phone_number(
request: Request,
client: Optional[str] = None,
):
user = request.user.object
await adapters.aremove_phone_number(user)
update_telemetry_state(
request=request,
telemetry_type="api",
api="delete_phone_number",
client=client,
)
return {"status": "ok"}
@api_config.post("/phone/verify", status_code=200)
@requires(["authenticated"])
async def verify_mobile_otp(
request: Request,
code: str,
client: Optional[str] = None,
):
user: KhojUser = request.user.object
update_telemetry_state(
request=request,
telemetry_type="api",
api="verify_phone_number",
client=client,
)
if is_twilio_enabled() and not verify_otp(user, code):
raise HTTPException(status_code=400, detail="Invalid OTP")
user.verified_phone_number = True
await user.asave()
return {"status": "ok"}
@api_config.get("/index/size", response_model=Dict[str, int])
@requires(["authenticated"])
async def get_indexed_data_size(request: Request, common: CommonQueryParams):

View file

@ -0,0 +1,86 @@
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from starlette.authentication import requires
from khoj.database import adapters
from khoj.database.models import KhojUser
from khoj.routers.helpers import ApiUserRateLimiter, update_telemetry_state
from khoj.routers.twilio import create_otp, verify_otp
api_phone = APIRouter()
logger = logging.getLogger(__name__)
@api_phone.post("", status_code=200)
@requires(["authenticated"])
async def update_phone_number(
request: Request,
phone_number: str,
client: Optional[str] = None,
rate_limiter_per_day=Depends(
ApiUserRateLimiter(requests=5, subscribed_requests=5, window=60 * 60 * 24, slug="update_phone")
),
):
user = request.user.object
await adapters.aset_user_phone_number(user, phone_number)
create_otp(user)
update_telemetry_state(
request=request,
telemetry_type="api",
api="set_phone_number",
client=client,
metadata={"phone_number": phone_number},
)
return {"status": "ok"}
@api_phone.delete("", status_code=200)
@requires(["authenticated"])
async def delete_phone_number(
request: Request,
client: Optional[str] = None,
):
user = request.user.object
await adapters.aremove_phone_number(user)
update_telemetry_state(
request=request,
telemetry_type="api",
api="delete_phone_number",
client=client,
)
return {"status": "ok"}
@api_phone.post("/verify", status_code=200)
@requires(["authenticated"])
async def verify_mobile_otp(
request: Request,
code: str,
client: Optional[str] = None,
rate_limiter_per_day=Depends(
ApiUserRateLimiter(requests=5, subscribed_requests=5, window=60 * 60 * 24, slug="verify_phone")
),
):
user: KhojUser = request.user.object
update_telemetry_state(
request=request,
telemetry_type="api",
api="verify_phone_number",
client=client,
)
if not verify_otp(user, code):
raise HTTPException(status_code=400, detail="Invalid OTP")
user.verified_phone_number = True
await user.asave()
return {"status": "ok"}

View file

@ -360,6 +360,11 @@ class ApiUserRateLimiter:
if subscribed and count_requests >= self.subscribed_requests:
raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
if not subscribed and count_requests >= self.requests:
if self.subscribed_requests == self.requests:
raise HTTPException(
status_code=429,
detail="Slow down! Too Many Requests",
)
raise HTTPException(
status_code=429,
detail="We're glad you're enjoying Khoj! You've exceeded your usage limit for today. Come back tomorrow or subscribe to increase your rate limit via [your settings](https://app.khoj.dev/config).",

View file

@ -16,7 +16,7 @@ from khoj.migrations.migrate_processor_config_openai import (
)
from khoj.migrations.migrate_server_pg import migrate_server_pg
from khoj.migrations.migrate_version import migrate_config_to_version
from khoj.utils.helpers import resolve_absolute_path
from khoj.utils.helpers import in_debug_mode, resolve_absolute_path
from khoj.utils.yaml import parse_config_from_file
@ -73,7 +73,7 @@ def cli(args=None):
else:
args = run_migrations(args)
args.config = parse_config_from_file(args.config_file)
if os.environ.get("KHOJ_DEBUG"):
if in_debug_mode():
args.config.app.should_log_telemetry = False
return args

View file

@ -317,3 +317,9 @@ def batcher(iterable, max_n):
if not chunk:
return
yield (x for x in chunk if x is not None)
def in_debug_mode():
"""Check if Khoj is running in debug mode.
Set KHOJ_DEBUG environment variable to true to enable debug mode."""
return os.getenv("KHOJ_DEBUG", "false").lower() == "true"

View file

@ -5,8 +5,6 @@ import openai
import torch
from tqdm import trange
from khoj.utils import state
class BaseEncoder(ABC):
@abstractmethod