khoj 1.31.1.dev13__py3-none-any.whl → 1.32.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- khoj/configure.py +4 -2
- khoj/database/adapters/__init__.py +15 -6
- khoj/database/admin.py +1 -1
- khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py +17 -0
- khoj/database/models/__init__.py +1 -0
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/EXN7llilwwtkW67UhumHd/_buildManifest.js +1 -0
- khoj/interface/compiled/_next/static/chunks/1201-aac5b5f9a28edf09.js +1 -0
- khoj/interface/compiled/_next/static/chunks/1662-adf4c615bef2fdc2.js +1 -0
- khoj/interface/compiled/_next/static/chunks/1915-878efdc6db697d8f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/2117-9886e6a0232dc093.js +2 -0
- khoj/interface/compiled/_next/static/chunks/{5538-0ea2d3944ca051e1.js → 2264-23b2c33cd8c74d07.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/2781-4f022b6e9eb6df6e.js +3 -0
- khoj/interface/compiled/_next/static/chunks/2813-f842b08bce4c61a0.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3091-e0ff2288e8a29dd7.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3727.dcea8f2193111552.js +1 -0
- khoj/interface/compiled/_next/static/chunks/5401-980a4f512c81232e.js +20 -0
- khoj/interface/compiled/_next/static/chunks/{1279-4cb23143aa2c0228.js → 5473-b1cf56dedac6577a.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/5477-8d032883aed8a2d2.js +1 -0
- khoj/interface/compiled/_next/static/chunks/6589-f806113de469d684.js +1 -0
- khoj/interface/compiled/_next/static/chunks/8117-2e1697b782c5f185.js +1 -0
- khoj/interface/compiled/_next/static/chunks/8407-af326f8c200e619b.js +1 -0
- khoj/interface/compiled/_next/static/chunks/8667-d3e5bc726e4ff4e3.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9058-25ef3344805f06ea.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9262-21c17de77aafdce8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/94ca1967.1d9b42d929a1ee8c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{1210.ef7a0f9a7e43da1d.js → 9597.83583248dfbf6e73.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/964ecbae.51d6faf8801d15e6.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/_not-found/{page-cfba071f5a657256.js → page-a834eddae3e235df.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/page-6f4ff1d32a66ed71.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/{layout-7f1b79a2c67af0b4.js → layout-dce809da279a4a8a.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-148a48ddfb2ff90d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/page-be00870a40de3a25.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/layout-30e7fda7262713ce.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/page-765292332c31523e.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/{page-845fe099f1f4375e.js → page-7af2cab294dccd81.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/layout-b3f6bc6f1aa118e0.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/page-6b600bf11fa89194.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-e8e5db7830bf3f47.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-f625859c1a122441.js → page-6054e88b56708f44.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/d3ac728e-44ebd2a0c99b12a0.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{fd9d1056-2e6c8140e79afc3b.js → fd9d1056-4482b99a36fd1673.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/main-app-de1f09df97a3cfc7.js +1 -0
- khoj/interface/compiled/_next/static/chunks/main-db4bfac6b0a8d00b.js +1 -0
- khoj/interface/compiled/_next/static/chunks/pages/{_app-f870474a17b7f2fd.js → _app-3c9ca398d360b709.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/pages/{_error-c66a4e8afc46f17b.js → _error-cf5ca766ac8f493f.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/webpack-fe0d01dd1cc44c15.js +1 -0
- khoj/interface/compiled/_next/static/css/3f27c3cf45375eb5.css +1 -0
- khoj/interface/compiled/_next/static/css/65ac59e147eb2057.css +25 -0
- khoj/interface/compiled/_next/static/css/9504108437df6804.css +1 -0
- khoj/interface/compiled/_next/static/css/c8837bdb7d5f13de.css +1 -0
- khoj/interface/compiled/agents/index.html +1 -1
- khoj/interface/compiled/agents/index.txt +6 -6
- khoj/interface/compiled/automations/index.html +1 -1
- khoj/interface/compiled/automations/index.txt +7 -7
- khoj/interface/compiled/chat/index.html +1 -1
- khoj/interface/compiled/chat/index.txt +6 -6
- khoj/interface/compiled/index.html +1 -1
- khoj/interface/compiled/index.txt +6 -6
- khoj/interface/compiled/search/index.html +1 -1
- khoj/interface/compiled/search/index.txt +6 -6
- khoj/interface/compiled/settings/index.html +1 -1
- khoj/interface/compiled/settings/index.txt +8 -8
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +6 -6
- khoj/interface/email/magic_link.html +36 -13
- khoj/main.py +1 -1
- khoj/processor/tools/online_search.py +49 -2
- khoj/routers/api_chat.py +13 -6
- khoj/routers/auth.py +94 -7
- khoj/routers/email.py +10 -14
- khoj/routers/helpers.py +70 -32
- khoj/routers/web_client.py +1 -1
- {khoj-1.31.1.dev13.dist-info → khoj-1.32.0.dist-info}/METADATA +5 -5
- {khoj-1.31.1.dev13.dist-info → khoj-1.32.0.dist-info}/RECORD +83 -81
- {khoj-1.31.1.dev13.dist-info → khoj-1.32.0.dist-info}/WHEEL +1 -1
- khoj/interface/compiled/_next/static/7K5OLB23CafMhZBFHh4NG/_buildManifest.js +0 -1
- khoj/interface/compiled/_next/static/chunks/1459.690bf20e7d7b7090.js +0 -1
- khoj/interface/compiled/_next/static/chunks/1603-f8ef9930c1f4eaef.js +0 -1
- khoj/interface/compiled/_next/static/chunks/1970-1b63ac1497b03a10.js +0 -1
- khoj/interface/compiled/_next/static/chunks/2646-92ba433951d02d52.js +0 -20
- khoj/interface/compiled/_next/static/chunks/3072-be830e4f8412b9d2.js +0 -1
- khoj/interface/compiled/_next/static/chunks/3463-081c031e873b7966.js +0 -3
- khoj/interface/compiled/_next/static/chunks/3690-51312931ba1eae30.js +0 -1
- khoj/interface/compiled/_next/static/chunks/3717-b46079dbe9f55694.js +0 -1
- khoj/interface/compiled/_next/static/chunks/4200-ea75740bb3c6ae60.js +0 -1
- khoj/interface/compiled/_next/static/chunks/4504-62ac13e7d94c52f9.js +0 -1
- khoj/interface/compiled/_next/static/chunks/4602-460621c3241e0d13.js +0 -1
- khoj/interface/compiled/_next/static/chunks/5512-7cc62049bbe60e11.js +0 -1
- khoj/interface/compiled/_next/static/chunks/7023-e8de2bded4df6539.js +0 -2
- khoj/interface/compiled/_next/static/chunks/7592-a09c39a38e60634b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/8423-1dda16bc56236523.js +0 -1
- khoj/interface/compiled/_next/static/chunks/94ca1967.5584df65931cfe83.js +0 -1
- khoj/interface/compiled/_next/static/chunks/964ecbae.ea4eab2a3a835ffe.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-1878cc328ea380bd.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-2a0b821cf69bdf06.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-ffa30be1dda97643.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-9219a85f3477e722.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-c2c62ae6f013443c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/layout-6310c57b674dd6f5.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/page-083f798a7562cda5.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/search/layout-2ca475462c0b2176.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/layout-f285795bc3154b8c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-3257ef0146ab18da.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-592e8c470f2c2084.js +0 -1
- khoj/interface/compiled/_next/static/chunks/d3ac728e-a9e3522eef9b6b28.js +0 -1
- khoj/interface/compiled/_next/static/chunks/main-1ea5c2e0fdef4626.js +0 -1
- khoj/interface/compiled/_next/static/chunks/main-app-6d6ee3495efe03d4.js +0 -1
- khoj/interface/compiled/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +0 -1
- khoj/interface/compiled/_next/static/chunks/webpack-062298330010d2aa.js +0 -1
- khoj/interface/compiled/_next/static/css/1f293605f2871853.css +0 -1
- khoj/interface/compiled/_next/static/css/80bd6301fc657983.css +0 -1
- khoj/interface/compiled/_next/static/css/f172a0fb3eb177e1.css +0 -1
- khoj/interface/compiled/_next/static/css/fd628f01a581ec3c.css +0 -25
- /khoj/interface/compiled/_next/static/{7K5OLB23CafMhZBFHh4NG → EXN7llilwwtkW67UhumHd}/_ssgManifest.js +0 -0
- {khoj-1.31.1.dev13.dist-info → khoj-1.32.0.dist-info}/entry_points.txt +0 -0
- {khoj-1.31.1.dev13.dist-info → khoj-1.32.0.dist-info}/licenses/LICENSE +0 -0
@@ -102,8 +102,14 @@ async def search_online(
|
|
102
102
|
async for event in send_status_func(f"**Searching the Internet for**: {subqueries_str}"):
|
103
103
|
yield {ChatEvent.STATUS: event}
|
104
104
|
|
105
|
+
if SERPER_DEV_API_KEY:
|
106
|
+
search_func = search_with_serper
|
107
|
+
elif JINA_API_KEY:
|
108
|
+
search_func = search_with_jina
|
109
|
+
else:
|
110
|
+
search_func = search_with_searxng
|
111
|
+
|
105
112
|
with timer(f"Internet searches for {subqueries} took", logger):
|
106
|
-
search_func = search_with_google if SERPER_DEV_API_KEY else search_with_jina
|
107
113
|
search_tasks = [search_func(subquery, location) for subquery in subqueries]
|
108
114
|
search_results = await asyncio.gather(*search_tasks)
|
109
115
|
response_dict = {subquery: search_result for subquery, search_result in search_results}
|
@@ -148,7 +154,48 @@ async def search_online(
|
|
148
154
|
yield response_dict
|
149
155
|
|
150
156
|
|
151
|
-
async def
|
157
|
+
async def search_with_searxng(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]:
|
158
|
+
"""Search using local SearXNG instance."""
|
159
|
+
# Use environment variable or default to localhost
|
160
|
+
searxng_url = os.getenv("KHOJ_SEARXNG_URL", "http://localhost:42113")
|
161
|
+
search_url = f"{searxng_url}/search"
|
162
|
+
country_code = location.country_code.lower() if location and location.country_code else "us"
|
163
|
+
|
164
|
+
params = {"q": query, "format": "html", "language": "en", "country": country_code, "categories": "general"}
|
165
|
+
|
166
|
+
async with aiohttp.ClientSession() as session:
|
167
|
+
try:
|
168
|
+
async with session.get(search_url, params=params) as response:
|
169
|
+
if response.status != 200:
|
170
|
+
logger.error(f"SearXNG search failed to call {searxng_url}: {await response.text()}")
|
171
|
+
return query, {}
|
172
|
+
|
173
|
+
html_content = await response.text()
|
174
|
+
|
175
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
176
|
+
organic_results = []
|
177
|
+
|
178
|
+
for result in soup.find_all("article", class_="result"):
|
179
|
+
title_elem = result.find("a", rel="noreferrer")
|
180
|
+
if title_elem:
|
181
|
+
title = title_elem.text.strip()
|
182
|
+
link = title_elem["href"]
|
183
|
+
|
184
|
+
description_elem = result.find("p", class_="content")
|
185
|
+
description = description_elem.text.strip() if description_elem else None
|
186
|
+
|
187
|
+
organic_results.append({"title": title, "link": link, "description": description})
|
188
|
+
|
189
|
+
extracted_search_result = {"organic": organic_results}
|
190
|
+
|
191
|
+
return query, extracted_search_result
|
192
|
+
|
193
|
+
except Exception as e:
|
194
|
+
logger.error(f"Error searching with SearXNG: {str(e)}")
|
195
|
+
return query, {}
|
196
|
+
|
197
|
+
|
198
|
+
async def search_with_serper(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]:
|
152
199
|
country_code = location.country_code.lower() if location and location.country_code else "us"
|
153
200
|
payload = json.dumps({"q": query, "gl": country_code})
|
154
201
|
headers = {"X-API-KEY": SERPER_DEV_API_KEY, "Content-Type": "application/json"}
|
khoj/routers/api_chat.py
CHANGED
@@ -724,7 +724,16 @@ async def chat(
|
|
724
724
|
yield result
|
725
725
|
return
|
726
726
|
|
727
|
-
|
727
|
+
# Automated tasks are handled before to allow mixing them with other conversation commands
|
728
|
+
cmds_to_rate_limit = []
|
729
|
+
is_automated_task = False
|
730
|
+
if q.startswith("/automated_task"):
|
731
|
+
is_automated_task = True
|
732
|
+
q = q.replace("/automated_task", "").lstrip()
|
733
|
+
cmds_to_rate_limit += [ConversationCommand.AutomatedTask]
|
734
|
+
|
735
|
+
# Extract conversation command from query
|
736
|
+
conversation_commands = [get_conversation_command(query=q)]
|
728
737
|
|
729
738
|
conversation = await ConversationAdapters.aget_conversation_by_user(
|
730
739
|
user,
|
@@ -757,11 +766,8 @@ async def chat(
|
|
757
766
|
location = None
|
758
767
|
if city or region or country or country_code:
|
759
768
|
location = LocationData(city=city, region=region, country=country, country_code=country_code)
|
760
|
-
|
761
769
|
user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
762
|
-
|
763
770
|
meta_log = conversation.conversation_log
|
764
|
-
is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask]
|
765
771
|
|
766
772
|
researched_results = ""
|
767
773
|
online_results: Dict = dict()
|
@@ -778,7 +784,7 @@ async def chat(
|
|
778
784
|
generated_excalidraw_diagram: str = None
|
779
785
|
program_execution_context: List[str] = []
|
780
786
|
|
781
|
-
if conversation_commands == [ConversationCommand.Default]
|
787
|
+
if conversation_commands == [ConversationCommand.Default]:
|
782
788
|
chosen_io = await aget_data_sources_and_output_format(
|
783
789
|
q,
|
784
790
|
meta_log,
|
@@ -799,7 +805,8 @@ async def chat(
|
|
799
805
|
async for result in send_event(ChatEvent.STATUS, f"**Selected Tools:** {conversation_commands_str}"):
|
800
806
|
yield result
|
801
807
|
|
802
|
-
|
808
|
+
cmds_to_rate_limit += conversation_commands
|
809
|
+
for cmd in cmds_to_rate_limit:
|
803
810
|
try:
|
804
811
|
await conversation_command_rate_limiter.update_and_check_if_valid(request, cmd)
|
805
812
|
q = q.replace(f"/{cmd.value}", "").strip()
|
khoj/routers/auth.py
CHANGED
@@ -4,7 +4,8 @@ import logging
|
|
4
4
|
import os
|
5
5
|
from typing import Optional
|
6
6
|
|
7
|
-
|
7
|
+
import requests
|
8
|
+
from fastapi import APIRouter, Depends
|
8
9
|
from pydantic import BaseModel, EmailStr
|
9
10
|
from starlette.authentication import requires
|
10
11
|
from starlette.config import Config
|
@@ -21,7 +22,11 @@ from khoj.database.adapters import (
|
|
21
22
|
get_or_create_user,
|
22
23
|
)
|
23
24
|
from khoj.routers.email import send_magic_link_email, send_welcome_email
|
24
|
-
from khoj.routers.helpers import
|
25
|
+
from khoj.routers.helpers import (
|
26
|
+
EmailVerificationApiRateLimiter,
|
27
|
+
get_next_url,
|
28
|
+
update_telemetry_state,
|
29
|
+
)
|
25
30
|
from khoj.utils import state
|
26
31
|
|
27
32
|
logger = logging.getLogger(__name__)
|
@@ -98,16 +103,28 @@ async def login_magic_link(request: Request, form: MagicLinkForm):
|
|
98
103
|
|
99
104
|
|
100
105
|
@auth_router.get("/magic")
|
101
|
-
async def sign_in_with_magic_link(
|
102
|
-
|
106
|
+
async def sign_in_with_magic_link(
|
107
|
+
request: Request,
|
108
|
+
code: str,
|
109
|
+
email: str,
|
110
|
+
rate_limiter=Depends(
|
111
|
+
EmailVerificationApiRateLimiter(requests=10, window=60 * 60 * 24, slug="magic_link_verification")
|
112
|
+
),
|
113
|
+
):
|
114
|
+
user, code_is_expired = await aget_user_validated_by_email_verification_code(code, email)
|
115
|
+
|
103
116
|
if user:
|
117
|
+
if code_is_expired:
|
118
|
+
request.session["user"] = {}
|
119
|
+
return Response(status_code=403)
|
120
|
+
|
104
121
|
id_info = {
|
105
122
|
"email": user.email,
|
106
123
|
}
|
107
124
|
|
108
125
|
request.session["user"] = dict(id_info)
|
109
126
|
return RedirectResponse(url="/")
|
110
|
-
return
|
127
|
+
return Response(status_code=401)
|
111
128
|
|
112
129
|
|
113
130
|
@auth_router.post("/token")
|
@@ -140,11 +157,12 @@ async def delete_token(request: Request, token: str):
|
|
140
157
|
|
141
158
|
|
142
159
|
@auth_router.post("/redirect")
|
143
|
-
async def
|
160
|
+
async def auth_post(request: Request):
|
161
|
+
# This is maintained for compatibility with the /login endpoint
|
144
162
|
form = await request.form()
|
145
163
|
next_url = get_next_url(request)
|
146
164
|
for q in request.query_params:
|
147
|
-
if
|
165
|
+
if q != "next":
|
148
166
|
next_url += f"&{q}={request.query_params[q]}"
|
149
167
|
|
150
168
|
credential = form.get("credential")
|
@@ -183,7 +201,76 @@ async def auth(request: Request):
|
|
183
201
|
return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
|
184
202
|
|
185
203
|
|
204
|
+
@auth_router.get("/redirect")
|
205
|
+
async def auth(request: Request):
|
206
|
+
next_url = get_next_url(request)
|
207
|
+
for q in request.query_params:
|
208
|
+
if q in ["code", "state", "scope", "authuser", "prompt", "session_state", "access_type"]:
|
209
|
+
continue
|
210
|
+
if q != "next":
|
211
|
+
next_url += f"&{q}={request.query_params[q]}"
|
212
|
+
|
213
|
+
code = request.query_params.get("code")
|
214
|
+
|
215
|
+
# 1. Construct the full redirect URI including domain
|
216
|
+
base_url = str(request.base_url).rstrip("/")
|
217
|
+
redirect_uri = f"{base_url}{request.app.url_path_for('auth')}"
|
218
|
+
|
219
|
+
verified_data = requests.post(
|
220
|
+
"https://oauth2.googleapis.com/token",
|
221
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
222
|
+
data={
|
223
|
+
"code": code,
|
224
|
+
"client_id": os.environ["GOOGLE_CLIENT_ID"],
|
225
|
+
"client_secret": os.environ["GOOGLE_CLIENT_SECRET"],
|
226
|
+
"redirect_uri": redirect_uri,
|
227
|
+
"grant_type": "authorization_code",
|
228
|
+
},
|
229
|
+
)
|
230
|
+
|
231
|
+
verified_data.raise_for_status()
|
232
|
+
|
233
|
+
credential = verified_data.json().get("id_token")
|
234
|
+
|
235
|
+
if not credential:
|
236
|
+
logger.error("Missing id_token in OAuth response")
|
237
|
+
return RedirectResponse(url="/login?error=invalid_token", status_code=HTTP_302_FOUND)
|
238
|
+
|
239
|
+
try:
|
240
|
+
idinfo = id_token.verify_oauth2_token(credential, google_requests.Request(), os.environ["GOOGLE_CLIENT_ID"])
|
241
|
+
except OAuthError as error:
|
242
|
+
return HTMLResponse(f"<h1>{error.error}</h1>")
|
243
|
+
khoj_user = await get_or_create_user(idinfo)
|
244
|
+
|
245
|
+
if khoj_user:
|
246
|
+
request.session["user"] = dict(idinfo)
|
247
|
+
|
248
|
+
if datetime.timedelta(minutes=3) > (datetime.datetime.now(datetime.timezone.utc) - khoj_user.date_joined):
|
249
|
+
asyncio.create_task(send_welcome_email(idinfo["name"], idinfo["email"]))
|
250
|
+
update_telemetry_state(
|
251
|
+
request=request,
|
252
|
+
telemetry_type="api",
|
253
|
+
api="create_user__google",
|
254
|
+
metadata={"server_id": str(khoj_user.uuid)},
|
255
|
+
)
|
256
|
+
logger.log(logging.INFO, f"🥳 New User Created: {khoj_user.uuid}")
|
257
|
+
|
258
|
+
return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
|
259
|
+
|
260
|
+
|
186
261
|
@auth_router.get("/logout")
|
187
262
|
async def logout(request: Request):
|
188
263
|
request.session.pop("user", None)
|
189
264
|
return RedirectResponse(url="/")
|
265
|
+
|
266
|
+
|
267
|
+
@auth_router.get("/oauth/metadata")
|
268
|
+
async def oauth_metadata(request: Request):
|
269
|
+
redirect_uri = str(request.app.url_path_for("auth"))
|
270
|
+
|
271
|
+
return {
|
272
|
+
"google": {
|
273
|
+
"client_id": os.environ.get("GOOGLE_CLIENT_ID"),
|
274
|
+
"redirect_uri": f"{redirect_uri}",
|
275
|
+
}
|
276
|
+
}
|
khoj/routers/email.py
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
|
4
|
-
try:
|
5
|
-
import resend
|
6
|
-
except ImportError:
|
7
|
-
pass
|
8
|
-
|
9
4
|
import markdown_it
|
5
|
+
import resend
|
10
6
|
from django.conf import settings
|
11
7
|
from jinja2 import Environment, FileSystemLoader
|
12
8
|
|
@@ -23,7 +19,7 @@ static_files = os.path.join(settings.BASE_DIR, "static")
|
|
23
19
|
env = Environment(loader=FileSystemLoader(static_files))
|
24
20
|
|
25
21
|
if not RESEND_API_KEY:
|
26
|
-
logger.
|
22
|
+
logger.warning("RESEND_API_KEY not set - email sending disabled")
|
27
23
|
else:
|
28
24
|
resend.api_key = RESEND_API_KEY
|
29
25
|
|
@@ -33,7 +29,7 @@ def is_resend_enabled():
|
|
33
29
|
|
34
30
|
|
35
31
|
async def send_magic_link_email(email, unique_id, host):
|
36
|
-
sign_in_link = f"{host}auth/magic?code={unique_id}"
|
32
|
+
sign_in_link = f"{host}auth/magic?code={unique_id}&email={email}"
|
37
33
|
|
38
34
|
if not is_resend_enabled():
|
39
35
|
logger.debug(f"Email sending disabled. Share this sign-in link with the user: {sign_in_link}")
|
@@ -41,13 +37,13 @@ async def send_magic_link_email(email, unique_id, host):
|
|
41
37
|
|
42
38
|
template = env.get_template("magic_link.html")
|
43
39
|
|
44
|
-
html_content = template.render(link=f"{host}auth/magic?code={unique_id}")
|
40
|
+
html_content = template.render(link=f"{host}auth/magic?code={unique_id}", code=unique_id)
|
45
41
|
|
46
42
|
resend.Emails.send(
|
47
43
|
{
|
48
44
|
"sender": os.environ.get("RESEND_EMAIL", "noreply@khoj.dev"),
|
49
45
|
"to": email,
|
50
|
-
"subject": "Your
|
46
|
+
"subject": f"Your login code to Khoj",
|
51
47
|
"html": html_content,
|
52
48
|
}
|
53
49
|
)
|
@@ -64,7 +60,7 @@ async def send_welcome_email(name, email):
|
|
64
60
|
|
65
61
|
resend.Emails.send(
|
66
62
|
{
|
67
|
-
"sender": "team@khoj.dev",
|
63
|
+
"sender": os.environ.get("RESEND_EMAIL", "team@khoj.dev"),
|
68
64
|
"to": email,
|
69
65
|
"subject": f"{name}, four ways to use Khoj" if name else "Four ways to use Khoj",
|
70
66
|
"html": html_content,
|
@@ -92,7 +88,7 @@ async def send_query_feedback(uquery, kquery, sentiment, user_email):
|
|
92
88
|
|
93
89
|
logger.info(f"Sending feedback email for query {uquery}")
|
94
90
|
|
95
|
-
#
|
91
|
+
# render feedback email using feedback.html as template
|
96
92
|
template = env.get_template("feedback.html")
|
97
93
|
html_content = template.render(
|
98
94
|
uquery=uquery if not is_none_or_empty(uquery) else "N/A",
|
@@ -100,10 +96,10 @@ async def send_query_feedback(uquery, kquery, sentiment, user_email):
|
|
100
96
|
sentiment=sentiment if not is_none_or_empty(sentiment) else "N/A",
|
101
97
|
user_email=user_email if not is_none_or_empty(user_email) else "N/A",
|
102
98
|
)
|
103
|
-
# send feedback
|
99
|
+
# send feedback to fixed account
|
104
100
|
r = resend.Emails.send(
|
105
101
|
{
|
106
|
-
"sender": "
|
102
|
+
"sender": os.environ.get("RESEND_EMAIL", "noreply@khoj.dev"),
|
107
103
|
"to": "team@khoj.dev",
|
108
104
|
"subject": f"User Feedback",
|
109
105
|
"html": html_content,
|
@@ -130,7 +126,7 @@ def send_task_email(name, email, query, result, subject, is_image=False):
|
|
130
126
|
|
131
127
|
r = resend.Emails.send(
|
132
128
|
{
|
133
|
-
"sender":
|
129
|
+
"sender": f'Khoj <{os.environ.get("RESEND_EMAIL", "khoj@khoj.dev")}>',
|
134
130
|
"to": email,
|
135
131
|
"subject": f"✨ {subject}",
|
136
132
|
"html": html_content,
|
khoj/routers/helpers.py
CHANGED
@@ -49,6 +49,7 @@ from khoj.database.adapters import (
|
|
49
49
|
ais_user_subscribed,
|
50
50
|
create_khoj_token,
|
51
51
|
get_khoj_tokens,
|
52
|
+
get_user_by_email,
|
52
53
|
get_user_name,
|
53
54
|
get_user_notion_config,
|
54
55
|
get_user_subscription_state,
|
@@ -230,7 +231,7 @@ def get_next_url(request: Request) -> str:
|
|
230
231
|
return urljoin(str(request.base_url).rstrip("/"), next_path)
|
231
232
|
|
232
233
|
|
233
|
-
def get_conversation_command(query: str
|
234
|
+
def get_conversation_command(query: str) -> ConversationCommand:
|
234
235
|
if query.startswith("/notes"):
|
235
236
|
return ConversationCommand.Notes
|
236
237
|
elif query.startswith("/help"):
|
@@ -253,9 +254,6 @@ def get_conversation_command(query: str, any_references: bool = False) -> Conver
|
|
253
254
|
return ConversationCommand.Code
|
254
255
|
elif query.startswith("/research"):
|
255
256
|
return ConversationCommand.Research
|
256
|
-
# If no relevant notes found for the given query
|
257
|
-
elif not any_references:
|
258
|
-
return ConversationCommand.General
|
259
257
|
else:
|
260
258
|
return ConversationCommand.Default
|
261
259
|
|
@@ -407,42 +405,39 @@ async def aget_data_sources_and_output_format(
|
|
407
405
|
response = clean_json(response)
|
408
406
|
response = json.loads(response)
|
409
407
|
|
410
|
-
|
411
|
-
|
408
|
+
chosen_sources = [s.strip() for s in response.get("source", []) if s.strip()]
|
409
|
+
chosen_output = response.get("output", "text").strip() # Default to text output
|
412
410
|
|
413
|
-
if
|
411
|
+
if is_none_or_empty(chosen_sources) or not isinstance(chosen_sources, list):
|
414
412
|
raise ValueError(
|
415
|
-
f"Invalid response for determining relevant tools: {
|
413
|
+
f"Invalid response for determining relevant tools: {chosen_sources}. Raw Response: {response}"
|
416
414
|
)
|
417
415
|
|
418
|
-
|
419
|
-
for
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
if is_none_or_empty(result):
|
416
|
+
output_mode = ConversationCommand.Text
|
417
|
+
# Verify selected output mode is enabled for the agent, as the LLM can sometimes get confused by the tool options.
|
418
|
+
if chosen_output in output_options.keys() and (len(agent_outputs) == 0 or chosen_output in agent_outputs):
|
419
|
+
# Ensure that the chosen output mode exists as a valid ConversationCommand
|
420
|
+
output_mode = ConversationCommand(chosen_output)
|
421
|
+
|
422
|
+
data_sources = []
|
423
|
+
# Verify selected data sources are enabled for the agent, as the LLM can sometimes get confused by the tool options.
|
424
|
+
for chosen_source in chosen_sources:
|
425
|
+
# Ensure that the chosen data source exists as a valid ConversationCommand
|
426
|
+
if chosen_source in source_options.keys() and (len(agent_sources) == 0 or chosen_source in agent_sources):
|
427
|
+
data_sources.append(ConversationCommand(chosen_source))
|
428
|
+
|
429
|
+
# Fallback to default sources if the inferred data sources are unset or invalid
|
430
|
+
if is_none_or_empty(data_sources):
|
435
431
|
if len(agent_sources) == 0:
|
436
|
-
|
432
|
+
data_sources = [ConversationCommand.Default]
|
437
433
|
else:
|
438
|
-
|
434
|
+
data_sources = [ConversationCommand.General]
|
439
435
|
except Exception as e:
|
440
436
|
logger.error(f"Invalid response for determining relevant tools: {response}. Error: {e}", exc_info=True)
|
441
|
-
|
442
|
-
|
443
|
-
result = {"sources": sources, "output": output}
|
437
|
+
data_sources = agent_sources if len(agent_sources) > 0 else [ConversationCommand.Default]
|
438
|
+
output_mode = agent_outputs[0] if len(agent_outputs) > 0 else ConversationCommand.Text
|
444
439
|
|
445
|
-
return
|
440
|
+
return {"sources": data_sources, "output": output_mode}
|
446
441
|
|
447
442
|
|
448
443
|
async def infer_webpage_urls(
|
@@ -1363,6 +1358,49 @@ class FeedbackData(BaseModel):
|
|
1363
1358
|
sentiment: str
|
1364
1359
|
|
1365
1360
|
|
1361
|
+
class EmailVerificationApiRateLimiter:
|
1362
|
+
def __init__(self, requests: int, window: int, slug: str):
|
1363
|
+
self.requests = requests
|
1364
|
+
self.window = window
|
1365
|
+
self.slug = slug
|
1366
|
+
|
1367
|
+
def __call__(self, request: Request):
|
1368
|
+
# Rate limiting disabled if billing is disabled
|
1369
|
+
if state.billing_enabled is False:
|
1370
|
+
return
|
1371
|
+
|
1372
|
+
# Extract the email query parameter
|
1373
|
+
email = request.query_params.get("email")
|
1374
|
+
|
1375
|
+
if email:
|
1376
|
+
logger.info(f"Email query parameter: {email}")
|
1377
|
+
|
1378
|
+
user: KhojUser = get_user_by_email(email)
|
1379
|
+
|
1380
|
+
if not user:
|
1381
|
+
raise HTTPException(
|
1382
|
+
status_code=404,
|
1383
|
+
detail="User not found.",
|
1384
|
+
)
|
1385
|
+
|
1386
|
+
# Remove requests outside of the time window
|
1387
|
+
cutoff = datetime.now(tz=timezone.utc) - timedelta(seconds=self.window)
|
1388
|
+
count_requests = UserRequests.objects.filter(user=user, created_at__gte=cutoff, slug=self.slug).count()
|
1389
|
+
|
1390
|
+
# Check if the user has exceeded the rate limit
|
1391
|
+
if count_requests >= self.requests:
|
1392
|
+
logger.info(
|
1393
|
+
f"Rate limit: {count_requests}/{self.requests} requests not allowed in {self.window} seconds for email: {email}."
|
1394
|
+
)
|
1395
|
+
raise HTTPException(
|
1396
|
+
status_code=429,
|
1397
|
+
detail="Ran out of login attempts",
|
1398
|
+
)
|
1399
|
+
|
1400
|
+
# Add the current request to the db
|
1401
|
+
UserRequests.objects.create(user=user, slug=self.slug)
|
1402
|
+
|
1403
|
+
|
1366
1404
|
class ApiUserRateLimiter:
|
1367
1405
|
def __init__(self, requests: int, subscribed_requests: int, window: int, slug: str):
|
1368
1406
|
self.requests = requests
|
@@ -1642,7 +1680,7 @@ def scheduled_chat(
|
|
1642
1680
|
last_run_time = datetime.strptime(last_run_time, "%Y-%m-%d %I:%M %p %Z").replace(tzinfo=timezone.utc)
|
1643
1681
|
|
1644
1682
|
# If the last run time was within the last 6 hours, don't run it again. This helps avoid multithreading issues and rate limits.
|
1645
|
-
if (datetime.now(timezone.utc) - last_run_time).total_seconds() <
|
1683
|
+
if (datetime.now(timezone.utc) - last_run_time).total_seconds() < 6 * 60 * 60:
|
1646
1684
|
logger.info(f"Skipping scheduled chat {job_id} as the next run time is in the future.")
|
1647
1685
|
return
|
1648
1686
|
|
khoj/routers/web_client.py
CHANGED
@@ -57,7 +57,7 @@ def login_page(request: Request):
|
|
57
57
|
if request.user.is_authenticated:
|
58
58
|
return RedirectResponse(url=next_url)
|
59
59
|
google_client_id = os.environ.get("GOOGLE_CLIENT_ID")
|
60
|
-
redirect_uri = str(request.app.url_path_for("
|
60
|
+
redirect_uri = str(request.app.url_path_for("auth_post"))
|
61
61
|
return templates.TemplateResponse(
|
62
62
|
"login.html",
|
63
63
|
context={
|
@@ -1,12 +1,13 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: khoj
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.32.0
|
4
4
|
Summary: Your Second Brain
|
5
5
|
Project-URL: Homepage, https://khoj.dev
|
6
6
|
Project-URL: Documentation, https://docs.khoj.dev
|
7
7
|
Project-URL: Code, https://github.com/khoj-ai/khoj
|
8
8
|
Author: Debanjum Singh Solanky, Saba Imran
|
9
|
-
License: AGPL-3.0-or-later
|
9
|
+
License-Expression: AGPL-3.0-or-later
|
10
|
+
License-File: LICENSE
|
10
11
|
Keywords: AI,NLP,images,markdown,org-mode,pdf,productivity,search,semantic-search
|
11
12
|
Classifier: Development Status :: 5 - Production/Stable
|
12
13
|
Classifier: Intended Audience :: Information Technology
|
@@ -63,6 +64,7 @@ Requires-Dist: pytz~=2024.1
|
|
63
64
|
Requires-Dist: pyyaml~=6.0
|
64
65
|
Requires-Dist: rapidocr-onnxruntime==1.3.24
|
65
66
|
Requires-Dist: requests>=2.26.0
|
67
|
+
Requires-Dist: resend==1.0.1
|
66
68
|
Requires-Dist: rich>=13.3.1
|
67
69
|
Requires-Dist: schedule==1.1.0
|
68
70
|
Requires-Dist: sentence-transformers==3.0.1
|
@@ -90,14 +92,12 @@ Requires-Dist: pytest-asyncio==0.21.1; extra == 'dev'
|
|
90
92
|
Requires-Dist: pytest-django==4.5.2; extra == 'dev'
|
91
93
|
Requires-Dist: pytest-xdist[psutil]; extra == 'dev'
|
92
94
|
Requires-Dist: pytest>=7.1.2; extra == 'dev'
|
93
|
-
Requires-Dist: resend==1.0.1; extra == 'dev'
|
94
95
|
Requires-Dist: stripe==7.3.0; extra == 'dev'
|
95
96
|
Requires-Dist: twilio==8.11; extra == 'dev'
|
96
97
|
Provides-Extra: prod
|
97
98
|
Requires-Dist: boto3>=1.34.57; extra == 'prod'
|
98
99
|
Requires-Dist: google-auth==2.23.3; extra == 'prod'
|
99
100
|
Requires-Dist: gunicorn==22.0.0; extra == 'prod'
|
100
|
-
Requires-Dist: resend==1.0.1; extra == 'prod'
|
101
101
|
Requires-Dist: stripe==7.3.0; extra == 'prod'
|
102
102
|
Requires-Dist: twilio==8.11; extra == 'prod'
|
103
103
|
Description-Content-Type: text/markdown
|