mirror of
https://github.com/khoj-ai/khoj.git
synced 2025-02-17 08:04:21 +00:00
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:
parent
9ad44f0e77
commit
d1bfb245df
13 changed files with 134 additions and 90 deletions
|
@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||||
name = "khoj-assistant"
|
name = "khoj-assistant"
|
||||||
description = "An AI copilot for your Second Brain"
|
description = "An AI copilot for your Second Brain"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Debanjum Singh Solanky, Saba Imran" },
|
{ name = "Debanjum Singh Solanky, Saba Imran" },
|
||||||
|
|
|
@ -892,6 +892,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
/* color chat bubble by khoj blue */
|
/* color chat bubble by khoj blue */
|
||||||
.chat-message-text.khoj {
|
.chat-message-text.khoj {
|
||||||
|
|
|
@ -13,6 +13,8 @@ https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from khoj.utils.helpers import in_debug_mode
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
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")
|
SECRET_KEY = os.getenv("KHOJ_DJANGO_SECRET_KEY", "!secret")
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# 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
|
# All Subdomains of KHOJ_DOMAIN are trusted
|
||||||
KHOJ_DOMAIN = os.getenv("KHOJ_DOMAIN", "khoj.dev")
|
KHOJ_DOMAIN = os.getenv("KHOJ_DOMAIN", "khoj.dev")
|
||||||
|
|
|
@ -34,6 +34,7 @@ from khoj.database.adapters import (
|
||||||
from khoj.database.models import ClientApplication, KhojUser, Subscription
|
from khoj.database.models import ClientApplication, KhojUser, Subscription
|
||||||
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
|
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
|
||||||
from khoj.routers.indexer import configure_content, configure_search, load_content
|
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 import constants, state
|
||||||
from khoj.utils.config import SearchType
|
from khoj.utils.config import SearchType
|
||||||
from khoj.utils.fs_syncer import collect_files
|
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.api_config import api_config
|
||||||
from khoj.routers.auth import auth_router
|
from khoj.routers.auth import auth_router
|
||||||
from khoj.routers.indexer import indexer
|
from khoj.routers.indexer import indexer
|
||||||
from khoj.routers.subscription import subscription_router
|
|
||||||
from khoj.routers.web_client import web_client
|
from khoj.routers.web_client import web_client
|
||||||
|
|
||||||
app.include_router(api, prefix="/api")
|
app.include_router(api, prefix="/api")
|
||||||
app.include_router(api_config, prefix="/api/config")
|
app.include_router(api_config, prefix="/api/config")
|
||||||
app.include_router(indexer, prefix="/api/v1/index")
|
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(web_client)
|
||||||
app.include_router(auth_router, prefix="/auth")
|
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):
|
def configure_middleware(app):
|
||||||
app.add_middleware(AuthenticationMiddleware, backend=UserAuthenticationBackend())
|
app.add_middleware(AuthenticationMiddleware, backend=UserAuthenticationBackend())
|
||||||
|
|
|
@ -965,6 +965,7 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
/* color chat bubble by khoj blue */
|
/* color chat bubble by khoj blue */
|
||||||
.chat-message-text.khoj {
|
.chat-message-text.khoj {
|
||||||
|
|
|
@ -194,7 +194,9 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="card-title-row">
|
||||||
<img class="card-icon" src="/static/assets/icons/whatsapp.svg" alt="WhatsApp icon">
|
<img class="card-icon" src="/static/assets/icons/whatsapp.svg" alt="WhatsApp icon">
|
||||||
<h3 class="card-title">WhatsApp</h3>
|
<h3 class="card-title">WhatsApp</h3>
|
||||||
|
@ -610,15 +612,21 @@
|
||||||
const phonenumberVerifiedText = document.getElementById("api-settings-card-description-verified");
|
const phonenumberVerifiedText = document.getElementById("api-settings-card-description-verified");
|
||||||
const phonenumberUnverifiedText = document.getElementById("api-settings-card-description-unverified");
|
const phonenumberUnverifiedText = document.getElementById("api-settings-card-description-unverified");
|
||||||
|
|
||||||
let preExistingPhoneNumber = "{{ phone_number }}";
|
const preExistingPhoneNumber = "{{ phone_number }}";
|
||||||
let isPhoneNumberVerified = "{{ is_phone_number_verified }}";
|
let isPhoneNumberVerified = "{{ is_phone_number_verified }}";
|
||||||
let isTwilioEnabled = "{{ is_twilio_enabled }}";
|
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";
|
phonenumberVerifyButton.style.display = "none";
|
||||||
phonenumberRemoveButton.style.display = "";
|
phonenumberRemoveButton.style.display = "";
|
||||||
} else if (preExistingPhoneNumber != "None" && (isPhoneNumberVerified == "False" || isPhoneNumberVerified == "false")) {
|
} else if (preExistingPhoneNumber != "None" && isPhoneNumberVerified === "false") {
|
||||||
if (isTwilioEnabled == "True" || isTwilioEnabled == "true") {
|
if (isTwilioEnabled == "true") {
|
||||||
phonenumberVerifyButton.style.display = "";
|
phonenumberVerifyButton.style.display = "";
|
||||||
phonenumberRemoveButton.style.display = "none";
|
phonenumberRemoveButton.style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
|
@ -759,7 +767,5 @@
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import threading
|
||||||
import warnings
|
import warnings
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
|
|
||||||
|
from khoj.utils.helpers import in_debug_mode
|
||||||
|
|
||||||
# Ignore non-actionable warnings
|
# Ignore non-actionable warnings
|
||||||
warnings.filterwarnings("ignore", message=r"snapshot_download.py has been made private", category=FutureWarning)
|
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)
|
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")
|
call_command("collectstatic", "--noinput")
|
||||||
|
|
||||||
# Initialize the Application Server
|
# Initialize the Application Server
|
||||||
if os.getenv("KHOJ_DEBUG", "false").lower() == "true":
|
if in_debug_mode():
|
||||||
app = FastAPI(debug=True)
|
app = FastAPI(debug=True)
|
||||||
else:
|
else:
|
||||||
app = FastAPI(docs_url=None) # Disable Swagger UI in production
|
app = FastAPI(docs_url=None) # Disable Swagger UI in production
|
||||||
|
|
|
@ -22,7 +22,6 @@ from khoj.database.models import (
|
||||||
NotionConfig,
|
NotionConfig,
|
||||||
)
|
)
|
||||||
from khoj.routers.helpers import CommonQueryParams, update_telemetry_state
|
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 import constants, state
|
||||||
from khoj.utils.rawconfig import (
|
from khoj.utils.rawconfig import (
|
||||||
FullConfig,
|
FullConfig,
|
||||||
|
@ -279,77 +278,6 @@ async def update_search_model(
|
||||||
return {"status": "ok"}
|
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])
|
@api_config.get("/index/size", response_model=Dict[str, int])
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
async def get_indexed_data_size(request: Request, common: CommonQueryParams):
|
async def get_indexed_data_size(request: Request, common: CommonQueryParams):
|
||||||
|
|
86
src/khoj/routers/api_phone.py
Normal file
86
src/khoj/routers/api_phone.py
Normal 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"}
|
|
@ -360,6 +360,11 @@ class ApiUserRateLimiter:
|
||||||
if subscribed and count_requests >= self.subscribed_requests:
|
if subscribed and count_requests >= self.subscribed_requests:
|
||||||
raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
|
raise HTTPException(status_code=429, detail="Slow down! Too Many Requests")
|
||||||
if not subscribed and count_requests >= self.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(
|
raise HTTPException(
|
||||||
status_code=429,
|
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).",
|
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).",
|
||||||
|
|
|
@ -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_server_pg import migrate_server_pg
|
||||||
from khoj.migrations.migrate_version import migrate_config_to_version
|
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
|
from khoj.utils.yaml import parse_config_from_file
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ def cli(args=None):
|
||||||
else:
|
else:
|
||||||
args = run_migrations(args)
|
args = run_migrations(args)
|
||||||
args.config = parse_config_from_file(args.config_file)
|
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
|
args.config.app.should_log_telemetry = False
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
|
@ -317,3 +317,9 @@ def batcher(iterable, max_n):
|
||||||
if not chunk:
|
if not chunk:
|
||||||
return
|
return
|
||||||
yield (x for x in chunk if x is not None)
|
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"
|
||||||
|
|
|
@ -5,8 +5,6 @@ import openai
|
||||||
import torch
|
import torch
|
||||||
from tqdm import trange
|
from tqdm import trange
|
||||||
|
|
||||||
from khoj.utils import state
|
|
||||||
|
|
||||||
|
|
||||||
class BaseEncoder(ABC):
|
class BaseEncoder(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
Loading…
Add table
Reference in a new issue