khoj 1.28.3__py3-none-any.whl → 1.28.4.dev92__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 (134) hide show
  1. khoj/configure.py +10 -14
  2. khoj/database/adapters/__init__.py +128 -44
  3. khoj/database/admin.py +6 -3
  4. khoj/database/management/commands/change_default_model.py +7 -72
  5. khoj/database/migrations/0073_delete_usersearchmodelconfig.py +15 -0
  6. khoj/database/models/__init__.py +4 -6
  7. khoj/interface/compiled/404/index.html +1 -1
  8. khoj/interface/compiled/_next/static/chunks/1603-dc5fd983dbcd070d.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/1970-c78f6acc8e16e30b.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/2261-748f7c327df3c8c1.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/3124-a4cea2eda163128d.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/3803-d74118a2d0182c52.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/5538-36aa824a75519c5b.js +1 -0
  14. khoj/interface/compiled/_next/static/chunks/5961-3c104d9736b7902b.js +3 -0
  15. khoj/interface/compiled/_next/static/chunks/8423-ebfa9bb9e2424ca3.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/9417-32c4db52ca42e681.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/app/agents/layout-e9838b642913a071.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/app/agents/page-4353b1a532795ad1.js +1 -0
  19. khoj/interface/compiled/_next/static/chunks/app/automations/{page-d3edae545a1b5393.js → page-c9f13c865e739607.js} +1 -1
  20. khoj/interface/compiled/_next/static/chunks/app/chat/layout-b0e7ff4baa3b5265.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/app/chat/page-45720e1ed71e3ef5.js +1 -0
  22. khoj/interface/compiled/_next/static/chunks/app/{layout-d0f0a9067427fb20.js → layout-86561d2fac35a91a.js} +1 -1
  23. khoj/interface/compiled/_next/static/chunks/app/{page-ea462e20376b6dce.js → page-ecb8e1c192aa8834.js} +1 -1
  24. khoj/interface/compiled/_next/static/chunks/app/search/layout-ea6b73fdaf9b24ca.js +1 -0
  25. khoj/interface/compiled/_next/static/chunks/app/search/{page-a5c277eff207959e.js → page-8e28deacb61f75aa.js} +1 -1
  26. khoj/interface/compiled/_next/static/chunks/app/settings/{layout-a8f33dfe92f997fb.js → layout-254eaaf916449a60.js} +1 -1
  27. khoj/interface/compiled/_next/static/chunks/app/settings/page-2fab613a557d3cc5.js +1 -0
  28. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-cf7445cf0326bda3.js +1 -0
  29. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-30376aa7e9cfa342.js +1 -0
  30. khoj/interface/compiled/_next/static/chunks/{main-f84cd3c1873cd842.js → main-1ea5c2e0fdef4626.js} +1 -1
  31. khoj/interface/compiled/_next/static/chunks/{webpack-8beec5b51cabb39a.js → webpack-27cf153c35b1338d.js} +1 -1
  32. khoj/interface/compiled/_next/static/css/{467a524c75e7d7c0.css → 0e9d53dcd7f11342.css} +1 -1
  33. khoj/interface/compiled/_next/static/css/{26c1c33d0423a7d8.css → 1f293605f2871853.css} +1 -1
  34. khoj/interface/compiled/_next/static/css/2d097a35da6bfe8d.css +1 -0
  35. khoj/interface/compiled/_next/static/css/80bd6301fc657983.css +1 -0
  36. khoj/interface/compiled/_next/static/css/ed437164d77aa600.css +25 -0
  37. khoj/interface/compiled/_next/static/media/5455839c73f146e7-s.p.woff2 +0 -0
  38. khoj/interface/compiled/_next/static/media/5984b96ba4822821-s.woff2 +0 -0
  39. khoj/interface/compiled/_next/static/media/684adc3dde1b03f1-s.woff2 +0 -0
  40. khoj/interface/compiled/_next/static/media/82e3b9a1bdaf0c26-s.woff2 +0 -0
  41. khoj/interface/compiled/_next/static/media/8d1ea331386a0db8-s.woff2 +0 -0
  42. khoj/interface/compiled/_next/static/media/91475f6526542a4f-s.woff2 +0 -0
  43. khoj/interface/compiled/_next/static/media/b98b13dbc1c3b59c-s.woff2 +0 -0
  44. khoj/interface/compiled/_next/static/media/c824d7a20139e39d-s.woff2 +0 -0
  45. khoj/interface/compiled/agents/index.html +1 -1
  46. khoj/interface/compiled/agents/index.txt +2 -2
  47. khoj/interface/compiled/automations/index.html +1 -1
  48. khoj/interface/compiled/automations/index.txt +2 -2
  49. khoj/interface/compiled/chat/index.html +1 -1
  50. khoj/interface/compiled/chat/index.txt +2 -2
  51. khoj/interface/compiled/index.html +1 -1
  52. khoj/interface/compiled/index.txt +3 -3
  53. khoj/interface/compiled/search/index.html +1 -1
  54. khoj/interface/compiled/search/index.txt +2 -2
  55. khoj/interface/compiled/settings/index.html +1 -1
  56. khoj/interface/compiled/settings/index.txt +3 -3
  57. khoj/interface/compiled/share/chat/index.html +1 -1
  58. khoj/interface/compiled/share/chat/index.txt +3 -3
  59. khoj/processor/content/docx/docx_to_entries.py +27 -21
  60. khoj/processor/content/github/github_to_entries.py +2 -2
  61. khoj/processor/content/images/image_to_entries.py +2 -2
  62. khoj/processor/content/markdown/markdown_to_entries.py +2 -2
  63. khoj/processor/content/notion/notion_to_entries.py +2 -2
  64. khoj/processor/content/org_mode/org_to_entries.py +2 -2
  65. khoj/processor/content/org_mode/orgnode.py +1 -1
  66. khoj/processor/content/pdf/pdf_to_entries.py +37 -29
  67. khoj/processor/content/plaintext/plaintext_to_entries.py +2 -2
  68. khoj/processor/content/text_to_entries.py +3 -4
  69. khoj/processor/conversation/anthropic/anthropic_chat.py +9 -1
  70. khoj/processor/conversation/google/gemini_chat.py +15 -2
  71. khoj/processor/conversation/google/utils.py +3 -1
  72. khoj/processor/conversation/offline/chat_model.py +4 -0
  73. khoj/processor/conversation/openai/gpt.py +6 -1
  74. khoj/processor/conversation/prompts.py +72 -13
  75. khoj/processor/conversation/utils.py +80 -13
  76. khoj/processor/image/generate.py +2 -0
  77. khoj/processor/tools/online_search.py +68 -18
  78. khoj/processor/tools/run_code.py +54 -20
  79. khoj/routers/api.py +10 -4
  80. khoj/routers/api_agents.py +8 -10
  81. khoj/routers/api_chat.py +89 -24
  82. khoj/routers/api_content.py +80 -8
  83. khoj/routers/helpers.py +176 -60
  84. khoj/routers/notion.py +1 -1
  85. khoj/routers/research.py +73 -31
  86. khoj/routers/web_client.py +0 -10
  87. khoj/search_type/text_search.py +3 -7
  88. khoj/utils/cli.py +2 -2
  89. khoj/utils/fs_syncer.py +2 -1
  90. khoj/utils/helpers.py +6 -3
  91. khoj/utils/rawconfig.py +32 -0
  92. khoj/utils/state.py +2 -1
  93. {khoj-1.28.3.dist-info → khoj-1.28.4.dev92.dist-info}/METADATA +3 -3
  94. {khoj-1.28.3.dist-info → khoj-1.28.4.dev92.dist-info}/RECORD +99 -105
  95. {khoj-1.28.3.dist-info → khoj-1.28.4.dev92.dist-info}/WHEEL +1 -1
  96. khoj/interface/compiled/_next/static/chunks/1034-da58b679fcbb79c1.js +0 -1
  97. khoj/interface/compiled/_next/static/chunks/1467-b331e469fe411347.js +0 -1
  98. khoj/interface/compiled/_next/static/chunks/1603-c1568f45947e9f2c.js +0 -1
  99. khoj/interface/compiled/_next/static/chunks/1970-d44050bf658ae5cc.js +0 -1
  100. khoj/interface/compiled/_next/static/chunks/3110-ef2cacd1b8d79ad8.js +0 -1
  101. khoj/interface/compiled/_next/static/chunks/3423-f4b7df2f6f3362f7.js +0 -1
  102. khoj/interface/compiled/_next/static/chunks/394-6bcb8c429f168f21.js +0 -3
  103. khoj/interface/compiled/_next/static/chunks/7113-f2e114d7034a0835.js +0 -1
  104. khoj/interface/compiled/_next/static/chunks/8423-da57554315eebcbe.js +0 -1
  105. khoj/interface/compiled/_next/static/chunks/8840-b8d7b9f0923c6651.js +0 -1
  106. khoj/interface/compiled/_next/static/chunks/9417-0d0fc7eb49a86abb.js +0 -1
  107. khoj/interface/compiled/_next/static/chunks/app/agents/layout-75636ab3a413fa8e.js +0 -1
  108. khoj/interface/compiled/_next/static/chunks/app/agents/page-adbf3cd470da248f.js +0 -1
  109. khoj/interface/compiled/_next/static/chunks/app/chat/layout-96fcf62857bf8f30.js +0 -1
  110. khoj/interface/compiled/_next/static/chunks/app/chat/page-222d348681b848a5.js +0 -1
  111. khoj/interface/compiled/_next/static/chunks/app/factchecker/layout-7b30c541c05fb904.js +0 -1
  112. khoj/interface/compiled/_next/static/chunks/app/factchecker/page-bded0868a08ac4ba.js +0 -1
  113. khoj/interface/compiled/_next/static/chunks/app/search/layout-3720f1362310bebb.js +0 -1
  114. khoj/interface/compiled/_next/static/chunks/app/settings/page-210bd54db4841333.js +0 -1
  115. khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-2df56074e42adaa0.js +0 -1
  116. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-a21b7e8890ed1209.js +0 -1
  117. khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +0 -1
  118. khoj/interface/compiled/_next/static/css/553f9cdcc7a2bcd6.css +0 -1
  119. khoj/interface/compiled/_next/static/css/a795ee88875f4853.css +0 -25
  120. khoj/interface/compiled/_next/static/css/afd3d45cc65d55d8.css +0 -1
  121. khoj/interface/compiled/_next/static/media/0e790e04fd40ad16-s.p.woff2 +0 -0
  122. khoj/interface/compiled/_next/static/media/4221e1667cd19c7d-s.woff2 +0 -0
  123. khoj/interface/compiled/_next/static/media/6c276159aa0eb14b-s.woff2 +0 -0
  124. khoj/interface/compiled/_next/static/media/6cc0b9500e4f9168-s.woff2 +0 -0
  125. khoj/interface/compiled/_next/static/media/9d9319a7a2ac39c6-s.woff2 +0 -0
  126. khoj/interface/compiled/_next/static/media/a75c8ea86756d52d-s.woff2 +0 -0
  127. khoj/interface/compiled/_next/static/media/abce7c400ca31a51-s.woff2 +0 -0
  128. khoj/interface/compiled/_next/static/media/f759c939737fb668-s.woff2 +0 -0
  129. khoj/interface/compiled/factchecker/index.html +0 -1
  130. khoj/interface/compiled/factchecker/index.txt +0 -7
  131. /khoj/interface/compiled/_next/static/{EfnEiWDle86AUcxEdEFgO → t_2jovvUVve0Gvc3FqpT9}/_buildManifest.js +0 -0
  132. /khoj/interface/compiled/_next/static/{EfnEiWDle86AUcxEdEFgO → t_2jovvUVve0Gvc3FqpT9}/_ssgManifest.js +0 -0
  133. {khoj-1.28.3.dist-info → khoj-1.28.4.dev92.dist-info}/entry_points.txt +0 -0
  134. {khoj-1.28.3.dist-info → khoj-1.28.4.dev92.dist-info}/licenses/LICENSE +0 -0
khoj/configure.py CHANGED
@@ -168,12 +168,6 @@ class UserAuthenticationBackend(AuthenticationBackend):
168
168
  if create_if_not_exists:
169
169
  user, is_new = await aget_or_create_user_by_phone_number(phone_number)
170
170
  if user and is_new:
171
- update_telemetry_state(
172
- request=request,
173
- telemetry_type="api",
174
- api="create_user",
175
- metadata={"server_id": str(user.uuid)},
176
- )
177
171
  logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}")
178
172
  else:
179
173
  user = await aget_user_by_phone_number(phone_number)
@@ -255,11 +249,15 @@ def configure_server(
255
249
  state.search_models = configure_search(state.search_models, state.config.search_type)
256
250
  setup_default_agent(user)
257
251
 
258
- message = "📡 Telemetry disabled" if telemetry_disabled(state.config.app) else "📡 Telemetry enabled"
252
+ message = (
253
+ "📡 Telemetry disabled"
254
+ if telemetry_disabled(state.config.app, state.telemetry_disabled)
255
+ else "📡 Telemetry enabled"
256
+ )
259
257
  logger.info(message)
260
258
 
261
259
  if not init:
262
- initialize_content(regenerate, search_type, user)
260
+ initialize_content(user, regenerate, search_type)
263
261
 
264
262
  except Exception as e:
265
263
  logger.error(f"Failed to load some search models: {e}", exc_info=True)
@@ -269,17 +267,17 @@ def setup_default_agent(user: KhojUser):
269
267
  AgentAdapters.create_default_agent(user)
270
268
 
271
269
 
272
- def initialize_content(regenerate: bool, search_type: Optional[SearchType] = None, user: KhojUser = None):
270
+ def initialize_content(user: KhojUser, regenerate: bool, search_type: Optional[SearchType] = None):
273
271
  # Initialize Content from Config
274
272
  if state.search_models:
275
273
  try:
276
274
  logger.info("📬 Updating content index...")
277
275
  all_files = collect_files(user=user)
278
276
  status = configure_content(
277
+ user,
279
278
  all_files,
280
279
  regenerate,
281
280
  search_type,
282
- user=user,
283
281
  )
284
282
  if not status:
285
283
  raise RuntimeError("Failed to update content index")
@@ -344,9 +342,7 @@ def configure_middleware(app):
344
342
  def update_content_index():
345
343
  for user in get_all_users():
346
344
  all_files = collect_files(user=user)
347
- success = configure_content(all_files, user=user)
348
- all_files = collect_files(user=None)
349
- success = configure_content(all_files, user=None)
345
+ success = configure_content(user, all_files)
350
346
  if not success:
351
347
  raise RuntimeError("Failed to update content index")
352
348
  logger.info("📪 Content index updated via Scheduler")
@@ -369,7 +365,7 @@ def configure_search_types():
369
365
 
370
366
  @schedule.repeat(schedule.every(2).minutes)
371
367
  def upload_telemetry():
372
- if telemetry_disabled(state.config.app) or not state.telemetry:
368
+ if telemetry_disabled(state.config.app, state.telemetry_disabled) or not state.telemetry:
373
369
  return
374
370
 
375
371
  try:
@@ -8,13 +8,22 @@ import secrets
8
8
  import sys
9
9
  from datetime import date, datetime, timedelta, timezone
10
10
  from enum import Enum
11
- from typing import Callable, Iterable, List, Optional, Type
11
+ from functools import wraps
12
+ from typing import (
13
+ Any,
14
+ Callable,
15
+ Coroutine,
16
+ Iterable,
17
+ List,
18
+ Optional,
19
+ ParamSpec,
20
+ TypeVar,
21
+ )
12
22
 
13
23
  import cron_descriptor
14
24
  from apscheduler.job import Job
15
25
  from asgiref.sync import sync_to_async
16
26
  from django.contrib.sessions.backends.db import SessionStore
17
- from django.db import models
18
27
  from django.db.models import Prefetch, Q
19
28
  from django.db.models.manager import BaseManager
20
29
  from django.db.utils import IntegrityError
@@ -28,7 +37,6 @@ from khoj.database.models import (
28
37
  ChatModelOptions,
29
38
  ClientApplication,
30
39
  Conversation,
31
- DataStore,
32
40
  Entry,
33
41
  FileObject,
34
42
  GithubConfig,
@@ -48,7 +56,6 @@ from khoj.database.models import (
48
56
  TextToImageModelConfig,
49
57
  UserConversationConfig,
50
58
  UserRequests,
51
- UserSearchModelConfig,
52
59
  UserTextToImageModelConfig,
53
60
  UserVoiceModelConfig,
54
61
  VoiceModelOption,
@@ -81,6 +88,45 @@ class SubscriptionState(Enum):
81
88
  INVALID = "invalid"
82
89
 
83
90
 
91
+ P = ParamSpec("P")
92
+ T = TypeVar("T")
93
+
94
+
95
+ def require_valid_user(func: Callable[P, T]) -> Callable[P, T]:
96
+ @wraps(func)
97
+ def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
98
+ # Extract user from args/kwargs
99
+ user = next((arg for arg in args if isinstance(arg, KhojUser)), None)
100
+ if not user:
101
+ user = next((val for val in kwargs.values() if isinstance(val, KhojUser)), None)
102
+
103
+ # Throw error if user is not found
104
+ if not user:
105
+ raise ValueError("Khoj user argument required but not provided.")
106
+
107
+ return func(*args, **kwargs)
108
+
109
+ return sync_wrapper
110
+
111
+
112
+ def arequire_valid_user(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, Coroutine[Any, Any, T]]:
113
+ @wraps(func)
114
+ async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
115
+ # Extract user from args/kwargs
116
+ user = next((arg for arg in args if isinstance(arg, KhojUser)), None)
117
+ if not user:
118
+ user = next((v for v in kwargs.values() if isinstance(v, KhojUser)), None)
119
+
120
+ # Throw error if user is not found
121
+ if not user:
122
+ raise ValueError("Khoj user argument required but not provided.")
123
+
124
+ return await func(*args, **kwargs)
125
+
126
+ return async_wrapper
127
+
128
+
129
+ @arequire_valid_user
84
130
  async def set_notion_config(token: str, user: KhojUser):
85
131
  notion_config = await NotionConfig.objects.filter(user=user).afirst()
86
132
  if not notion_config:
@@ -91,6 +137,7 @@ async def set_notion_config(token: str, user: KhojUser):
91
137
  return notion_config
92
138
 
93
139
 
140
+ @require_valid_user
94
141
  def create_khoj_token(user: KhojUser, name=None):
95
142
  "Create Khoj API key for user"
96
143
  token = f"kk-{secrets.token_urlsafe(32)}"
@@ -98,6 +145,7 @@ def create_khoj_token(user: KhojUser, name=None):
98
145
  return KhojApiUser.objects.create(token=token, user=user, name=name)
99
146
 
100
147
 
148
+ @arequire_valid_user
101
149
  async def acreate_khoj_token(user: KhojUser, name=None):
102
150
  "Create Khoj API key for user"
103
151
  token = f"kk-{secrets.token_urlsafe(32)}"
@@ -105,11 +153,13 @@ async def acreate_khoj_token(user: KhojUser, name=None):
105
153
  return await KhojApiUser.objects.acreate(token=token, user=user, name=name)
106
154
 
107
155
 
156
+ @require_valid_user
108
157
  def get_khoj_tokens(user: KhojUser):
109
158
  "Get all Khoj API keys for user"
110
159
  return list(KhojApiUser.objects.filter(user=user))
111
160
 
112
161
 
162
+ @arequire_valid_user
113
163
  async def delete_khoj_token(user: KhojUser, token: str):
114
164
  "Delete Khoj API Key for user"
115
165
  await KhojApiUser.objects.filter(token=token, user=user).adelete()
@@ -133,6 +183,7 @@ async def aget_or_create_user_by_phone_number(phone_number: str) -> tuple[KhojUs
133
183
  return user, is_new
134
184
 
135
185
 
186
+ @arequire_valid_user
136
187
  async def aset_user_phone_number(user: KhojUser, phone_number: str) -> KhojUser:
137
188
  if is_none_or_empty(phone_number):
138
189
  return None
@@ -156,6 +207,7 @@ async def aset_user_phone_number(user: KhojUser, phone_number: str) -> KhojUser:
156
207
  return user
157
208
 
158
209
 
210
+ @arequire_valid_user
159
211
  async def aremove_phone_number(user: KhojUser) -> KhojUser:
160
212
  user.phone_number = None
161
213
  user.verified_phone_number = False
@@ -193,6 +245,7 @@ async def aget_or_create_user_by_email(email: str) -> tuple[KhojUser, bool]:
193
245
  return user, is_new
194
246
 
195
247
 
248
+ @arequire_valid_user
196
249
  async def astart_trial_subscription(user: KhojUser) -> Subscription:
197
250
  subscription = await Subscription.objects.filter(user=user).afirst()
198
251
  if not subscription:
@@ -247,6 +300,7 @@ async def create_user_by_google_token(token: dict) -> KhojUser:
247
300
  return user
248
301
 
249
302
 
303
+ @require_valid_user
250
304
  def set_user_name(user: KhojUser, first_name: str, last_name: str) -> KhojUser:
251
305
  user.first_name = first_name
252
306
  user.last_name = last_name
@@ -254,6 +308,7 @@ def set_user_name(user: KhojUser, first_name: str, last_name: str) -> KhojUser:
254
308
  return user
255
309
 
256
310
 
311
+ @require_valid_user
257
312
  def get_user_name(user: KhojUser):
258
313
  full_name = user.get_full_name()
259
314
  if not is_none_or_empty(full_name):
@@ -265,6 +320,7 @@ def get_user_name(user: KhojUser):
265
320
  return None
266
321
 
267
322
 
323
+ @require_valid_user
268
324
  def get_user_photo(user: KhojUser):
269
325
  google_profile: GoogleUser = GoogleUser.objects.filter(user=user).first()
270
326
  if google_profile:
@@ -328,6 +384,7 @@ def get_user_subscription_state(email: str) -> str:
328
384
  return subscription_to_state(user_subscription)
329
385
 
330
386
 
387
+ @arequire_valid_user
331
388
  async def aget_user_subscription_state(user: KhojUser) -> str:
332
389
  """Get subscription state of user
333
390
  Valid state transitions: trial -> subscribed <-> unsubscribed OR expired
@@ -336,6 +393,7 @@ async def aget_user_subscription_state(user: KhojUser) -> str:
336
393
  return await sync_to_async(subscription_to_state)(user_subscription)
337
394
 
338
395
 
396
+ @arequire_valid_user
339
397
  async def ais_user_subscribed(user: KhojUser) -> bool:
340
398
  """
341
399
  Get whether the user is subscribed
@@ -352,6 +410,7 @@ async def ais_user_subscribed(user: KhojUser) -> bool:
352
410
  return subscribed
353
411
 
354
412
 
413
+ @require_valid_user
355
414
  def is_user_subscribed(user: KhojUser) -> bool:
356
415
  """
357
416
  Get whether the user is subscribed
@@ -417,11 +476,13 @@ def get_all_users() -> BaseManager[KhojUser]:
417
476
  return KhojUser.objects.all()
418
477
 
419
478
 
479
+ @require_valid_user
420
480
  def get_user_github_config(user: KhojUser):
421
481
  config = GithubConfig.objects.filter(user=user).prefetch_related("githubrepoconfig").first()
422
482
  return config
423
483
 
424
484
 
485
+ @require_valid_user
425
486
  def get_user_notion_config(user: KhojUser):
426
487
  config = NotionConfig.objects.filter(user=user).first()
427
488
  return config
@@ -431,6 +492,7 @@ def delete_user_requests(window: timedelta = timedelta(days=1)):
431
492
  return UserRequests.objects.filter(created_at__lte=datetime.now(tz=timezone.utc) - window).delete()
432
493
 
433
494
 
495
+ @arequire_valid_user
434
496
  async def aget_user_name(user: KhojUser):
435
497
  full_name = user.get_full_name()
436
498
  if not is_none_or_empty(full_name):
@@ -442,18 +504,7 @@ async def aget_user_name(user: KhojUser):
442
504
  return None
443
505
 
444
506
 
445
- async def set_text_content_config(user: KhojUser, object: Type[models.Model], updated_config):
446
- deduped_files = list(set(updated_config.input_files)) if updated_config.input_files else None
447
- deduped_filters = list(set(updated_config.input_filter)) if updated_config.input_filter else None
448
- await object.objects.filter(user=user).adelete()
449
- await object.objects.acreate(
450
- input_files=deduped_files,
451
- input_filter=deduped_filters,
452
- index_heading_entries=updated_config.index_heading_entries,
453
- user=user,
454
- )
455
-
456
-
507
+ @arequire_valid_user
457
508
  async def set_user_github_config(user: KhojUser, pat_token: str, repos: list):
458
509
  config = await GithubConfig.objects.filter(user=user).afirst()
459
510
 
@@ -481,15 +532,6 @@ def get_default_search_model() -> SearchModelConfig:
481
532
  return SearchModelConfig.objects.first()
482
533
 
483
534
 
484
- def get_user_default_search_model(user: KhojUser = None) -> SearchModelConfig:
485
- if user:
486
- user_search_model = UserSearchModelConfig.objects.filter(user=user).first()
487
- if user_search_model:
488
- return user_search_model.setting
489
-
490
- return get_default_search_model()
491
-
492
-
493
535
  def get_or_create_search_models():
494
536
  search_models = SearchModelConfig.objects.all()
495
537
  if search_models.count() == 0:
@@ -597,8 +639,11 @@ class AgentAdapters:
597
639
  )
598
640
 
599
641
  @staticmethod
642
+ @arequire_valid_user
600
643
  async def adelete_agent_by_slug(agent_slug: str, user: KhojUser):
601
644
  agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
645
+ if agent.creator != user:
646
+ return False
602
647
 
603
648
  async for entry in Entry.objects.filter(agent=agent).aiterator():
604
649
  await entry.adelete()
@@ -722,6 +767,7 @@ class AgentAdapters:
722
767
  return await Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).afirst()
723
768
 
724
769
  @staticmethod
770
+ @arequire_valid_user
725
771
  async def aupdate_agent(
726
772
  user: KhojUser,
727
773
  name: str,
@@ -797,19 +843,6 @@ class PublicConversationAdapters:
797
843
  return f"/share/chat/{public_conversation.slug}/"
798
844
 
799
845
 
800
- class DataStoreAdapters:
801
- @staticmethod
802
- async def astore_data(data: dict, key: str, user: KhojUser, private: bool = True):
803
- if await DataStore.objects.filter(key=key).aexists():
804
- return key
805
- await DataStore.objects.acreate(value=data, key=key, owner=user, private=private)
806
- return key
807
-
808
- @staticmethod
809
- async def aretrieve_public_data(key: str):
810
- return await DataStore.objects.filter(key=key, private=False).afirst()
811
-
812
-
813
846
  class ConversationAdapters:
814
847
  @staticmethod
815
848
  def make_public_conversation_copy(conversation: Conversation):
@@ -822,6 +855,7 @@ class ConversationAdapters:
822
855
  )
823
856
 
824
857
  @staticmethod
858
+ @require_valid_user
825
859
  def get_conversation_by_user(
826
860
  user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
827
861
  ) -> Optional[Conversation]:
@@ -840,6 +874,7 @@ class ConversationAdapters:
840
874
  return conversation
841
875
 
842
876
  @staticmethod
877
+ @require_valid_user
843
878
  def get_conversation_sessions(user: KhojUser, client_application: ClientApplication = None):
844
879
  return (
845
880
  Conversation.objects.filter(user=user, client=client_application)
@@ -848,6 +883,7 @@ class ConversationAdapters:
848
883
  )
849
884
 
850
885
  @staticmethod
886
+ @arequire_valid_user
851
887
  async def aset_conversation_title(
852
888
  user: KhojUser, client_application: ClientApplication, conversation_id: str, title: str
853
889
  ):
@@ -865,6 +901,7 @@ class ConversationAdapters:
865
901
  return Conversation.objects.filter(id=conversation_id).first()
866
902
 
867
903
  @staticmethod
904
+ @arequire_valid_user
868
905
  async def acreate_conversation_session(
869
906
  user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None, title: str = None
870
907
  ):
@@ -881,6 +918,7 @@ class ConversationAdapters:
881
918
  )
882
919
 
883
920
  @staticmethod
921
+ @require_valid_user
884
922
  def create_conversation_session(
885
923
  user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None, title: str = None
886
924
  ):
@@ -893,6 +931,7 @@ class ConversationAdapters:
893
931
  return Conversation.objects.create(user=user, client=client_application, agent=agent, title=title)
894
932
 
895
933
  @staticmethod
934
+ @arequire_valid_user
896
935
  async def aget_conversation_by_user(
897
936
  user: KhojUser,
898
937
  client_application: ClientApplication = None,
@@ -917,6 +956,7 @@ class ConversationAdapters:
917
956
  )
918
957
 
919
958
  @staticmethod
959
+ @arequire_valid_user
920
960
  async def adelete_conversation_by_user(
921
961
  user: KhojUser, client_application: ClientApplication = None, conversation_id: str = None
922
962
  ):
@@ -925,6 +965,7 @@ class ConversationAdapters:
925
965
  return await Conversation.objects.filter(user=user, client=client_application).adelete()
926
966
 
927
967
  @staticmethod
968
+ @require_valid_user
928
969
  def has_any_conversation_config(user: KhojUser):
929
970
  return ChatModelOptions.objects.filter(user=user).exists()
930
971
 
@@ -961,6 +1002,7 @@ class ConversationAdapters:
961
1002
  return OpenAIProcessorConversationConfig.objects.filter().exists()
962
1003
 
963
1004
  @staticmethod
1005
+ @arequire_valid_user
964
1006
  async def aset_user_conversation_processor(user: KhojUser, conversation_processor_config_id: int):
965
1007
  config = await ChatModelOptions.objects.filter(id=conversation_processor_config_id).afirst()
966
1008
  if not config:
@@ -969,6 +1011,7 @@ class ConversationAdapters:
969
1011
  return new_config
970
1012
 
971
1013
  @staticmethod
1014
+ @arequire_valid_user
972
1015
  async def aset_user_voice_model(user: KhojUser, model_id: str):
973
1016
  config = await VoiceModelOption.objects.filter(model_id=model_id).afirst()
974
1017
  if not config:
@@ -1153,6 +1196,7 @@ class ConversationAdapters:
1153
1196
  return enabled_scrapers
1154
1197
 
1155
1198
  @staticmethod
1199
+ @require_valid_user
1156
1200
  def create_conversation_from_public_conversation(
1157
1201
  user: KhojUser, public_conversation: PublicConversation, client_app: ClientApplication
1158
1202
  ):
@@ -1169,6 +1213,7 @@ class ConversationAdapters:
1169
1213
  )
1170
1214
 
1171
1215
  @staticmethod
1216
+ @require_valid_user
1172
1217
  def save_conversation(
1173
1218
  user: KhojUser,
1174
1219
  conversation_log: dict,
@@ -1218,6 +1263,7 @@ class ConversationAdapters:
1218
1263
  return await SpeechToTextModelOptions.objects.filter().afirst()
1219
1264
 
1220
1265
  @staticmethod
1266
+ @arequire_valid_user
1221
1267
  async def aget_conversation_starters(user: KhojUser, max_results=3):
1222
1268
  all_questions = []
1223
1269
  if await ReflectiveQuestion.objects.filter(user=user).aexists():
@@ -1347,6 +1393,7 @@ class ConversationAdapters:
1347
1393
  return conversation.file_filters
1348
1394
 
1349
1395
  @staticmethod
1396
+ @require_valid_user
1350
1397
  def delete_message_by_turn_id(user: KhojUser, conversation_id: str, turn_id: str):
1351
1398
  conversation = ConversationAdapters.get_conversation_by_user(user, conversation_id=conversation_id)
1352
1399
  if not conversation or not conversation.conversation_log or not conversation.conversation_log.get("chat"):
@@ -1365,48 +1412,63 @@ class FileObjectAdapters:
1365
1412
  file_object.save()
1366
1413
 
1367
1414
  @staticmethod
1415
+ @require_valid_user
1368
1416
  def create_file_object(user: KhojUser, file_name: str, raw_text: str):
1369
1417
  return FileObject.objects.create(user=user, file_name=file_name, raw_text=raw_text)
1370
1418
 
1371
1419
  @staticmethod
1420
+ @require_valid_user
1372
1421
  def get_file_object_by_name(user: KhojUser, file_name: str):
1373
1422
  return FileObject.objects.filter(user=user, file_name=file_name).first()
1374
1423
 
1375
1424
  @staticmethod
1425
+ @require_valid_user
1376
1426
  def get_all_file_objects(user: KhojUser):
1377
1427
  return FileObject.objects.filter(user=user).all()
1378
1428
 
1379
1429
  @staticmethod
1430
+ @require_valid_user
1380
1431
  def delete_file_object_by_name(user: KhojUser, file_name: str):
1381
1432
  return FileObject.objects.filter(user=user, file_name=file_name).delete()
1382
1433
 
1383
1434
  @staticmethod
1435
+ @require_valid_user
1384
1436
  def delete_all_file_objects(user: KhojUser):
1385
1437
  return FileObject.objects.filter(user=user).delete()
1386
1438
 
1387
1439
  @staticmethod
1388
- async def async_update_raw_text(file_object: FileObject, new_raw_text: str):
1440
+ async def aupdate_raw_text(file_object: FileObject, new_raw_text: str):
1389
1441
  file_object.raw_text = new_raw_text
1390
1442
  await file_object.asave()
1391
1443
 
1392
1444
  @staticmethod
1393
- async def async_create_file_object(user: KhojUser, file_name: str, raw_text: str):
1445
+ @arequire_valid_user
1446
+ async def acreate_file_object(user: KhojUser, file_name: str, raw_text: str):
1394
1447
  return await FileObject.objects.acreate(user=user, file_name=file_name, raw_text=raw_text)
1395
1448
 
1396
1449
  @staticmethod
1397
- async def async_get_file_objects_by_name(user: KhojUser, file_name: str, agent: Agent = None):
1450
+ @arequire_valid_user
1451
+ async def aget_file_objects_by_name(user: KhojUser, file_name: str, agent: Agent = None):
1398
1452
  return await sync_to_async(list)(FileObject.objects.filter(user=user, file_name=file_name, agent=agent))
1399
1453
 
1400
1454
  @staticmethod
1401
- async def async_get_all_file_objects(user: KhojUser):
1455
+ @arequire_valid_user
1456
+ async def aget_file_objects_by_names(user: KhojUser, file_names: List[str]):
1457
+ return await sync_to_async(list)(FileObject.objects.filter(user=user, file_name__in=file_names))
1458
+
1459
+ @staticmethod
1460
+ @arequire_valid_user
1461
+ async def aget_all_file_objects(user: KhojUser):
1402
1462
  return await sync_to_async(list)(FileObject.objects.filter(user=user))
1403
1463
 
1404
1464
  @staticmethod
1405
- async def async_delete_file_object_by_name(user: KhojUser, file_name: str):
1465
+ @arequire_valid_user
1466
+ async def adelete_file_object_by_name(user: KhojUser, file_name: str):
1406
1467
  return await FileObject.objects.filter(user=user, file_name=file_name).adelete()
1407
1468
 
1408
1469
  @staticmethod
1409
- async def async_delete_all_file_objects(user: KhojUser):
1470
+ @arequire_valid_user
1471
+ async def adelete_all_file_objects(user: KhojUser):
1410
1472
  return await FileObject.objects.filter(user=user).adelete()
1411
1473
 
1412
1474
 
@@ -1416,15 +1478,18 @@ class EntryAdapters:
1416
1478
  date_filter = DateFilter()
1417
1479
 
1418
1480
  @staticmethod
1481
+ @require_valid_user
1419
1482
  def does_entry_exist(user: KhojUser, hashed_value: str) -> bool:
1420
1483
  return Entry.objects.filter(user=user, hashed_value=hashed_value).exists()
1421
1484
 
1422
1485
  @staticmethod
1486
+ @require_valid_user
1423
1487
  def delete_entry_by_file(user: KhojUser, file_path: str):
1424
1488
  deleted_count, _ = Entry.objects.filter(user=user, file_path=file_path).delete()
1425
1489
  return deleted_count
1426
1490
 
1427
1491
  @staticmethod
1492
+ @require_valid_user
1428
1493
  def get_filtered_entries(user: KhojUser, file_type: str = None, file_source: str = None):
1429
1494
  queryset = Entry.objects.filter(user=user)
1430
1495
 
@@ -1437,6 +1502,7 @@ class EntryAdapters:
1437
1502
  return queryset
1438
1503
 
1439
1504
  @staticmethod
1505
+ @require_valid_user
1440
1506
  def delete_all_entries(user: KhojUser, file_type: str = None, file_source: str = None, batch_size=1000):
1441
1507
  deleted_count = 0
1442
1508
  queryset = EntryAdapters.get_filtered_entries(user, file_type, file_source)
@@ -1448,6 +1514,7 @@ class EntryAdapters:
1448
1514
  return deleted_count
1449
1515
 
1450
1516
  @staticmethod
1517
+ @arequire_valid_user
1451
1518
  async def adelete_all_entries(user: KhojUser, file_type: str = None, file_source: str = None, batch_size=1000):
1452
1519
  deleted_count = 0
1453
1520
  queryset = EntryAdapters.get_filtered_entries(user, file_type, file_source)
@@ -1459,10 +1526,12 @@ class EntryAdapters:
1459
1526
  return deleted_count
1460
1527
 
1461
1528
  @staticmethod
1529
+ @require_valid_user
1462
1530
  def get_existing_entry_hashes_by_file(user: KhojUser, file_path: str):
1463
1531
  return Entry.objects.filter(user=user, file_path=file_path).values_list("hashed_value", flat=True)
1464
1532
 
1465
1533
  @staticmethod
1534
+ @require_valid_user
1466
1535
  def delete_entry_by_hash(user: KhojUser, hashed_values: List[str]):
1467
1536
  Entry.objects.filter(user=user, hashed_value__in=hashed_values).delete()
1468
1537
 
@@ -1474,6 +1543,7 @@ class EntryAdapters:
1474
1543
  )
1475
1544
 
1476
1545
  @staticmethod
1546
+ @require_valid_user
1477
1547
  def user_has_entries(user: KhojUser):
1478
1548
  return Entry.objects.filter(user=user).exists()
1479
1549
 
@@ -1482,18 +1552,23 @@ class EntryAdapters:
1482
1552
  return Entry.objects.filter(agent=agent).exists()
1483
1553
 
1484
1554
  @staticmethod
1555
+ @arequire_valid_user
1485
1556
  async def auser_has_entries(user: KhojUser):
1486
1557
  return await Entry.objects.filter(user=user).aexists()
1487
1558
 
1488
1559
  @staticmethod
1489
1560
  async def aagent_has_entries(agent: Agent):
1561
+ if agent is None:
1562
+ return False
1490
1563
  return await Entry.objects.filter(agent=agent).aexists()
1491
1564
 
1492
1565
  @staticmethod
1566
+ @arequire_valid_user
1493
1567
  async def adelete_entry_by_file(user: KhojUser, file_path: str):
1494
1568
  return await Entry.objects.filter(user=user, file_path=file_path).adelete()
1495
1569
 
1496
1570
  @staticmethod
1571
+ @arequire_valid_user
1497
1572
  async def adelete_entries_by_filenames(user: KhojUser, filenames: List[str], batch_size=1000):
1498
1573
  deleted_count = 0
1499
1574
  for i in range(0, len(filenames), batch_size):
@@ -1505,11 +1580,14 @@ class EntryAdapters:
1505
1580
 
1506
1581
  @staticmethod
1507
1582
  async def aget_agent_entry_filepaths(agent: Agent):
1583
+ if agent is None:
1584
+ return []
1508
1585
  return await sync_to_async(set)(
1509
1586
  Entry.objects.filter(agent=agent).distinct("file_path").values_list("file_path", flat=True)
1510
1587
  )
1511
1588
 
1512
1589
  @staticmethod
1590
+ @require_valid_user
1513
1591
  def get_all_filenames_by_source(user: KhojUser, file_source: str):
1514
1592
  return (
1515
1593
  Entry.objects.filter(user=user, file_source=file_source)
@@ -1518,6 +1596,7 @@ class EntryAdapters:
1518
1596
  )
1519
1597
 
1520
1598
  @staticmethod
1599
+ @require_valid_user
1521
1600
  def get_size_of_indexed_data_in_mb(user: KhojUser):
1522
1601
  entries = Entry.objects.filter(user=user).iterator()
1523
1602
  total_size = sum(sys.getsizeof(entry.compiled) for entry in entries)
@@ -1538,6 +1617,9 @@ class EntryAdapters:
1538
1617
  if agent != None:
1539
1618
  owner_filter |= Q(agent=agent)
1540
1619
 
1620
+ if owner_filter == Q():
1621
+ return Entry.objects.none()
1622
+
1541
1623
  if len(word_filters) == 0 and len(file_filters) == 0 and len(date_filters) == 0:
1542
1624
  return Entry.objects.filter(owner_filter)
1543
1625
 
@@ -1612,10 +1694,12 @@ class EntryAdapters:
1612
1694
  return relevant_entries[:max_results]
1613
1695
 
1614
1696
  @staticmethod
1697
+ @require_valid_user
1615
1698
  def get_unique_file_types(user: KhojUser):
1616
1699
  return Entry.objects.filter(user=user).values_list("file_type", flat=True).distinct()
1617
1700
 
1618
1701
  @staticmethod
1702
+ @require_valid_user
1619
1703
  def get_unique_file_sources(user: KhojUser):
1620
1704
  return Entry.objects.filter(user=user).values_list("file_source", flat=True).distinct().all()
1621
1705
 
khoj/database/admin.py CHANGED
@@ -28,7 +28,6 @@ from khoj.database.models import (
28
28
  TextToImageModelConfig,
29
29
  UserConversationConfig,
30
30
  UserRequests,
31
- UserSearchModelConfig,
32
31
  UserVoiceModelConfig,
33
32
  VoiceModelOption,
34
33
  WebScraper,
@@ -79,7 +78,12 @@ class KhojUserAdmin(UserAdmin):
79
78
  search_fields = ("email", "username", "phone_number", "uuid")
80
79
  filter_horizontal = ("groups", "user_permissions")
81
80
 
82
- fieldsets = (("Personal info", {"fields": ("phone_number", "email_verification_code")}),) + UserAdmin.fieldsets
81
+ fieldsets = (
82
+ (
83
+ "Personal info",
84
+ {"fields": ("phone_number", "email_verification_code", "verified_phone_number", "verified_email")},
85
+ ),
86
+ ) + UserAdmin.fieldsets
83
87
 
84
88
  actions = ["get_email_login_url"]
85
89
 
@@ -99,7 +103,6 @@ admin.site.register(KhojUser, KhojUserAdmin)
99
103
  admin.site.register(ProcessLock)
100
104
  admin.site.register(SpeechToTextModelOptions)
101
105
  admin.site.register(ReflectiveQuestion)
102
- admin.site.register(UserSearchModelConfig)
103
106
  admin.site.register(ClientApplication)
104
107
  admin.site.register(GithubConfig)
105
108
  admin.site.register(NotionConfig)