khoj 1.31.0__py3-none-any.whl → 1.31.1.dev62__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.
Files changed (133) hide show
  1. khoj/configure.py +4 -2
  2. khoj/database/adapters/__init__.py +66 -57
  3. khoj/database/admin.py +9 -9
  4. khoj/database/migrations/0077_chatmodel_alter_agent_chat_model_and_more.py +62 -0
  5. khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py +17 -0
  6. khoj/database/models/__init__.py +8 -7
  7. khoj/interface/compiled/404/index.html +1 -1
  8. khoj/interface/compiled/_next/static/9O0zlbSu9rZ459NKSv2aS/_buildManifest.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/1201-aac5b5f9a28edf09.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/1662-adf4c615bef2fdc2.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/1915-878efdc6db697d8f.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/2117-9886e6a0232dc093.js +2 -0
  13. khoj/interface/compiled/_next/static/chunks/{5538-0ea2d3944ca051e1.js → 2264-23b2c33cd8c74d07.js} +1 -1
  14. khoj/interface/compiled/_next/static/chunks/2781-4f022b6e9eb6df6e.js +3 -0
  15. khoj/interface/compiled/_next/static/chunks/2813-f842b08bce4c61a0.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/3091-e0ff2288e8a29dd7.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/3727.dcea8f2193111552.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/5401-980a4f512c81232e.js +20 -0
  19. khoj/interface/compiled/_next/static/chunks/{1279-4cb23143aa2c0228.js → 5473-b1cf56dedac6577a.js} +1 -1
  20. khoj/interface/compiled/_next/static/chunks/5477-8d032883aed8a2d2.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/6589-f806113de469d684.js +1 -0
  22. khoj/interface/compiled/_next/static/chunks/8117-2e1697b782c5f185.js +1 -0
  23. khoj/interface/compiled/_next/static/chunks/8407-af326f8c200e619b.js +1 -0
  24. khoj/interface/compiled/_next/static/chunks/8667-d3e5bc726e4ff4e3.js +1 -0
  25. khoj/interface/compiled/_next/static/chunks/9058-25ef3344805f06ea.js +1 -0
  26. khoj/interface/compiled/_next/static/chunks/9262-21c17de77aafdce8.js +1 -0
  27. khoj/interface/compiled/_next/static/chunks/94ca1967.1d9b42d929a1ee8c.js +1 -0
  28. khoj/interface/compiled/_next/static/chunks/{1210.ef7a0f9a7e43da1d.js → 9597.83583248dfbf6e73.js} +1 -1
  29. khoj/interface/compiled/_next/static/chunks/964ecbae.51d6faf8801d15e6.js +1 -0
  30. khoj/interface/compiled/_next/static/chunks/app/_not-found/{page-cfba071f5a657256.js → page-a834eddae3e235df.js} +1 -1
  31. khoj/interface/compiled/_next/static/chunks/app/agents/layout-e49165209d2e406c.js +1 -0
  32. khoj/interface/compiled/_next/static/chunks/app/agents/page-6f4ff1d32a66ed71.js +1 -0
  33. khoj/interface/compiled/_next/static/chunks/app/automations/{layout-7f1b79a2c67af0b4.js → layout-dce809da279a4a8a.js} +1 -1
  34. khoj/interface/compiled/_next/static/chunks/app/automations/page-148a48ddfb2ff90d.js +1 -0
  35. khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +1 -0
  36. khoj/interface/compiled/_next/static/chunks/app/chat/page-be00870a40de3a25.js +1 -0
  37. khoj/interface/compiled/_next/static/chunks/app/layout-30e7fda7262713ce.js +1 -0
  38. khoj/interface/compiled/_next/static/chunks/app/page-765292332c31523e.js +1 -0
  39. khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +1 -0
  40. khoj/interface/compiled/_next/static/chunks/app/search/{page-bd47c769b7700d1d.js → page-7af2cab294dccd81.js} +1 -1
  41. khoj/interface/compiled/_next/static/chunks/app/settings/layout-b3f6bc6f1aa118e0.js +1 -0
  42. khoj/interface/compiled/_next/static/chunks/app/settings/page-6b600bf11fa89194.js +1 -0
  43. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-6fb51c5c80f8ec67.js +1 -0
  44. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-751695d28116e626.js → page-6054e88b56708f44.js} +1 -1
  45. khoj/interface/compiled/_next/static/chunks/d3ac728e-44ebd2a0c99b12a0.js +1 -0
  46. khoj/interface/compiled/_next/static/chunks/{fd9d1056-2e6c8140e79afc3b.js → fd9d1056-4482b99a36fd1673.js} +1 -1
  47. khoj/interface/compiled/_next/static/chunks/main-app-de1f09df97a3cfc7.js +1 -0
  48. khoj/interface/compiled/_next/static/chunks/main-db4bfac6b0a8d00b.js +1 -0
  49. khoj/interface/compiled/_next/static/chunks/pages/{_app-f870474a17b7f2fd.js → _app-3c9ca398d360b709.js} +1 -1
  50. khoj/interface/compiled/_next/static/chunks/pages/{_error-c66a4e8afc46f17b.js → _error-cf5ca766ac8f493f.js} +1 -1
  51. khoj/interface/compiled/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  52. khoj/interface/compiled/_next/static/chunks/webpack-afd772b9f7c34b3f.js +1 -0
  53. khoj/interface/compiled/_next/static/css/3f27c3cf45375eb5.css +1 -0
  54. khoj/interface/compiled/_next/static/css/65ac59e147eb2057.css +25 -0
  55. khoj/interface/compiled/_next/static/css/8a00c3799ec0c9f8.css +1 -0
  56. khoj/interface/compiled/_next/static/css/{e8fb39147bff7bb4.css → 9504108437df6804.css} +1 -1
  57. khoj/interface/compiled/agents/index.html +1 -1
  58. khoj/interface/compiled/agents/index.txt +6 -6
  59. khoj/interface/compiled/automations/index.html +1 -1
  60. khoj/interface/compiled/automations/index.txt +7 -7
  61. khoj/interface/compiled/chat/index.html +1 -1
  62. khoj/interface/compiled/chat/index.txt +6 -6
  63. khoj/interface/compiled/index.html +1 -1
  64. khoj/interface/compiled/index.txt +6 -6
  65. khoj/interface/compiled/search/index.html +1 -1
  66. khoj/interface/compiled/search/index.txt +6 -6
  67. khoj/interface/compiled/settings/index.html +1 -1
  68. khoj/interface/compiled/settings/index.txt +8 -8
  69. khoj/interface/compiled/share/chat/index.html +1 -1
  70. khoj/interface/compiled/share/chat/index.txt +6 -6
  71. khoj/interface/email/magic_link.html +36 -13
  72. khoj/main.py +1 -1
  73. khoj/migrations/migrate_server_pg.py +7 -7
  74. khoj/processor/conversation/anthropic/anthropic_chat.py +3 -3
  75. khoj/processor/conversation/google/gemini_chat.py +3 -3
  76. khoj/processor/conversation/offline/chat_model.py +12 -12
  77. khoj/processor/conversation/openai/gpt.py +4 -4
  78. khoj/processor/conversation/openai/utils.py +18 -10
  79. khoj/processor/conversation/utils.py +4 -4
  80. khoj/processor/tools/online_search.py +49 -2
  81. khoj/routers/api.py +22 -27
  82. khoj/routers/api_agents.py +4 -4
  83. khoj/routers/api_chat.py +19 -12
  84. khoj/routers/api_model.py +4 -4
  85. khoj/routers/auth.py +94 -7
  86. khoj/routers/email.py +10 -14
  87. khoj/routers/helpers.py +176 -134
  88. khoj/routers/web_client.py +1 -1
  89. khoj/utils/helpers.py +5 -3
  90. khoj/utils/initialization.py +28 -26
  91. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/METADATA +5 -5
  92. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/RECORD +96 -93
  93. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/WHEEL +1 -1
  94. khoj/interface/compiled/_next/static/SHDrv3iet5TKNwccvVt6m/_buildManifest.js +0 -1
  95. khoj/interface/compiled/_next/static/chunks/1459.690bf20e7d7b7090.js +0 -1
  96. khoj/interface/compiled/_next/static/chunks/1603-f8ef9930c1f4eaef.js +0 -1
  97. khoj/interface/compiled/_next/static/chunks/1970-1b63ac1497b03a10.js +0 -1
  98. khoj/interface/compiled/_next/static/chunks/2646-92ba433951d02d52.js +0 -20
  99. khoj/interface/compiled/_next/static/chunks/3072-be830e4f8412b9d2.js +0 -1
  100. khoj/interface/compiled/_next/static/chunks/3463-081c031e873b7966.js +0 -3
  101. khoj/interface/compiled/_next/static/chunks/3690-51312931ba1eae30.js +0 -1
  102. khoj/interface/compiled/_next/static/chunks/3717-b46079dbe9f55694.js +0 -1
  103. khoj/interface/compiled/_next/static/chunks/4200-ea75740bb3c6ae60.js +0 -1
  104. khoj/interface/compiled/_next/static/chunks/4504-62ac13e7d94c52f9.js +0 -1
  105. khoj/interface/compiled/_next/static/chunks/4602-460621c3241e0d13.js +0 -1
  106. khoj/interface/compiled/_next/static/chunks/5512-7cc62049bbe60e11.js +0 -1
  107. khoj/interface/compiled/_next/static/chunks/7023-e8de2bded4df6539.js +0 -2
  108. khoj/interface/compiled/_next/static/chunks/7592-a09c39a38e60634b.js +0 -1
  109. khoj/interface/compiled/_next/static/chunks/8423-1dda16bc56236523.js +0 -1
  110. khoj/interface/compiled/_next/static/chunks/94ca1967.5584df65931cfe83.js +0 -1
  111. khoj/interface/compiled/_next/static/chunks/964ecbae.ea4eab2a3a835ffe.js +0 -1
  112. khoj/interface/compiled/_next/static/chunks/app/agents/layout-1878cc328ea380bd.js +0 -1
  113. khoj/interface/compiled/_next/static/chunks/app/agents/page-379949e11f084cf5.js +0 -1
  114. khoj/interface/compiled/_next/static/chunks/app/automations/page-ca10c1cf79ae54bb.js +0 -1
  115. khoj/interface/compiled/_next/static/chunks/app/chat/layout-1072c3b0ab136e74.js +0 -1
  116. khoj/interface/compiled/_next/static/chunks/app/chat/page-8a87c5de878f4f44.js +0 -1
  117. khoj/interface/compiled/_next/static/chunks/app/layout-6310c57b674dd6f5.js +0 -1
  118. khoj/interface/compiled/_next/static/chunks/app/page-b09139cb91859cd7.js +0 -1
  119. khoj/interface/compiled/_next/static/chunks/app/search/layout-cae84c87073877f0.js +0 -1
  120. khoj/interface/compiled/_next/static/chunks/app/settings/layout-f285795bc3154b8c.js +0 -1
  121. khoj/interface/compiled/_next/static/chunks/app/settings/page-2a2679b6e10dbac1.js +0 -1
  122. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-3b0c60bc13a963db.js +0 -1
  123. khoj/interface/compiled/_next/static/chunks/d3ac728e-a9e3522eef9b6b28.js +0 -1
  124. khoj/interface/compiled/_next/static/chunks/main-1ea5c2e0fdef4626.js +0 -1
  125. khoj/interface/compiled/_next/static/chunks/main-app-6d6ee3495efe03d4.js +0 -1
  126. khoj/interface/compiled/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +0 -1
  127. khoj/interface/compiled/_next/static/chunks/webpack-4bd818a6399690ae.js +0 -1
  128. khoj/interface/compiled/_next/static/css/1f293605f2871853.css +0 -1
  129. khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +0 -1
  130. khoj/interface/compiled/_next/static/css/fd628f01a581ec3c.css +0 -25
  131. /khoj/interface/compiled/_next/static/{SHDrv3iet5TKNwccvVt6m → 9O0zlbSu9rZ459NKSv2aS}/_ssgManifest.js +0 -0
  132. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/entry_points.txt +0 -0
  133. {khoj-1.31.0.dist-info → khoj-1.31.1.dev62.dist-info}/licenses/LICENSE +0 -0
khoj/routers/auth.py CHANGED
@@ -4,7 +4,8 @@ import logging
4
4
  import os
5
5
  from typing import Optional
6
6
 
7
- from fastapi import APIRouter
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 get_next_url, update_telemetry_state
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(request: Request, code: str):
102
- user = await aget_user_validated_by_email_verification_code(code)
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 RedirectResponse(request.app.url_path_for("login_page"))
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 auth(request: Request):
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 not q == "next":
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.warn("RESEND_API_KEY not set - email sending disabled")
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 Sign-In Link for Khoj 🚀",
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
- # rendering feedback email using feedback.html as template
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 from two fixed accounts
99
+ # send feedback to fixed account
104
100
  r = resend.Emails.send(
105
101
  {
106
- "sender": "saba@khoj.dev",
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": "Khoj <khoj@khoj.dev>",
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,