Skip to content

Commit

Permalink
Improve Khoj Chat and Settings UI (#630)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
debanjum and sabaimran authored Jan 29, 2024
1 parent 9ad44f0 commit d1bfb24
Show file tree
Hide file tree
Showing 13 changed files with 133 additions and 89 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
1 change: 1 addition & 0 deletions src/interface/desktop/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion src/khoj/app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
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

Expand All @@ -24,7 +26,7 @@
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")
Expand Down
15 changes: 12 additions & 3 deletions src/khoj/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
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
Expand Down Expand Up @@ -258,17 +259,25 @@ 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")
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")
app.include_router(web_client)
app.include_router(auth_router, prefix="/auth")

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):
Expand Down
1 change: 1 addition & 0 deletions src/khoj/interface/web/chat.html
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,7 @@
display: inline-block;
max-width: 80%;
text-align: left;
white-space: pre-line;
}
/* color chat bubble by khoj blue */
.chat-message-text.khoj {
Expand Down
20 changes: 13 additions & 7 deletions src/khoj/interface/web/config.html
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ <h3 class="card-title">API Keys</h3>
</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>
Expand Down Expand Up @@ -610,15 +612,21 @@ <h3 class="card-title">
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 {
Expand Down Expand Up @@ -759,7 +767,5 @@ <h3 class="card-title">
}, 5000);
});
})


</script>
{% endblock %}
4 changes: 3 additions & 1 deletion src/khoj/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
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)
Expand Down Expand Up @@ -45,7 +47,7 @@
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
Expand Down
72 changes: 0 additions & 72 deletions src/khoj/routers/api_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
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,
Expand Down Expand Up @@ -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):
Expand Down
86 changes: 86 additions & 0 deletions src/khoj/routers/api_phone.py
Original file line number Diff line number Diff line change
@@ -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"}
5 changes: 5 additions & 0 deletions src/khoj/routers/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ def __call__(self, request: Request):
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).",
Expand Down
4 changes: 2 additions & 2 deletions src/khoj/utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
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


Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/khoj/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 0 additions & 2 deletions src/khoj/utils/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import torch
from tqdm import trange

from khoj.utils import state


class BaseEncoder(ABC):
@abstractmethod
Expand Down

0 comments on commit d1bfb24

Please sign in to comment.