khoj 1.24.2.dev3__py3-none-any.whl → 1.25.1.dev34__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 (109) hide show
  1. khoj/configure.py +13 -4
  2. khoj/database/adapters/__init__.py +289 -52
  3. khoj/database/admin.py +20 -1
  4. khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py +49 -0
  5. khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py +69 -0
  6. khoj/database/migrations/0067_alter_agent_style_icon.py +50 -0
  7. khoj/database/migrations/0068_alter_agent_output_modes.py +24 -0
  8. khoj/database/migrations/0069_webscraper_serverchatsettings_web_scraper.py +89 -0
  9. khoj/database/models/__init__.py +136 -18
  10. khoj/interface/compiled/404/index.html +1 -1
  11. khoj/interface/compiled/_next/static/chunks/1603-fa3ee48860b9dc5c.js +1 -0
  12. khoj/interface/compiled/_next/static/chunks/2697-a38d01981ad3bdf8.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/3110-ef2cacd1b8d79ad8.js +1 -0
  14. khoj/interface/compiled/_next/static/chunks/4086-2c74808ba38a5a0f.js +1 -0
  15. khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +1 -0
  16. khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +1 -0
  17. khoj/interface/compiled/_next/static/chunks/7762-79f2205740622b5c.js +1 -0
  18. khoj/interface/compiled/_next/static/chunks/9178-899fe9a6b754ecfe.js +1 -0
  19. khoj/interface/compiled/_next/static/chunks/9417-29502e39c3e7d60c.js +1 -0
  20. khoj/interface/compiled/_next/static/chunks/9479-7eed36fc954ef804.js +1 -0
  21. khoj/interface/compiled/_next/static/chunks/app/agents/{layout-e71c8e913cccf792.js → layout-75636ab3a413fa8e.js} +1 -1
  22. khoj/interface/compiled/_next/static/chunks/app/agents/page-fa282831808ee536.js +1 -0
  23. khoj/interface/compiled/_next/static/chunks/app/automations/page-5480731341f34450.js +1 -0
  24. khoj/interface/compiled/_next/static/chunks/app/chat/{layout-8102549127db3067.js → layout-96fcf62857bf8f30.js} +1 -1
  25. khoj/interface/compiled/_next/static/chunks/app/chat/page-702057ccbcf27881.js +1 -0
  26. khoj/interface/compiled/_next/static/chunks/app/factchecker/page-e7b34316ec6f44de.js +1 -0
  27. khoj/interface/compiled/_next/static/chunks/app/{layout-f3e40d346da53112.js → layout-d0f0a9067427fb20.js} +1 -1
  28. khoj/interface/compiled/_next/static/chunks/app/page-10a5aad6e04f3cf8.js +1 -0
  29. khoj/interface/compiled/_next/static/chunks/app/search/page-d56541c746fded7d.js +1 -0
  30. khoj/interface/compiled/_next/static/chunks/app/settings/{layout-6f9314b0d7a26046.js → layout-a8f33dfe92f997fb.js} +1 -1
  31. khoj/interface/compiled/_next/static/chunks/app/settings/page-e044a999468a7c5d.js +1 -0
  32. khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-39f03f9e32399f0f.js → layout-2df56074e42adaa0.js} +1 -1
  33. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-fbbd66a4d4633438.js +1 -0
  34. khoj/interface/compiled/_next/static/chunks/{webpack-d4781cada9b58e75.js → webpack-c0cd5a6afb1f0798.js} +1 -1
  35. khoj/interface/compiled/_next/static/css/2de69f0be774c768.css +1 -0
  36. khoj/interface/compiled/_next/static/css/467a524c75e7d7c0.css +1 -0
  37. khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +1 -0
  38. khoj/interface/compiled/_next/static/css/b9a6bf04305d98d7.css +25 -0
  39. khoj/interface/compiled/agents/index.html +1 -1
  40. khoj/interface/compiled/agents/index.txt +2 -2
  41. khoj/interface/compiled/automations/index.html +1 -1
  42. khoj/interface/compiled/automations/index.txt +2 -2
  43. khoj/interface/compiled/chat/index.html +1 -1
  44. khoj/interface/compiled/chat/index.txt +2 -2
  45. khoj/interface/compiled/factchecker/index.html +1 -1
  46. khoj/interface/compiled/factchecker/index.txt +2 -2
  47. khoj/interface/compiled/index.html +1 -1
  48. khoj/interface/compiled/index.txt +2 -2
  49. khoj/interface/compiled/search/index.html +1 -1
  50. khoj/interface/compiled/search/index.txt +2 -2
  51. khoj/interface/compiled/settings/index.html +1 -1
  52. khoj/interface/compiled/settings/index.txt +3 -3
  53. khoj/interface/compiled/share/chat/index.html +1 -1
  54. khoj/interface/compiled/share/chat/index.txt +2 -2
  55. khoj/interface/web/assets/icons/agents.svg +1 -0
  56. khoj/interface/web/assets/icons/automation.svg +1 -0
  57. khoj/interface/web/assets/icons/chat.svg +24 -0
  58. khoj/interface/web/login.html +11 -22
  59. khoj/processor/content/notion/notion_to_entries.py +2 -1
  60. khoj/processor/conversation/anthropic/anthropic_chat.py +2 -0
  61. khoj/processor/conversation/google/gemini_chat.py +6 -19
  62. khoj/processor/conversation/google/utils.py +33 -15
  63. khoj/processor/conversation/offline/chat_model.py +3 -1
  64. khoj/processor/conversation/openai/gpt.py +2 -0
  65. khoj/processor/conversation/prompts.py +67 -5
  66. khoj/processor/conversation/utils.py +3 -7
  67. khoj/processor/embeddings.py +6 -3
  68. khoj/processor/image/generate.py +4 -3
  69. khoj/processor/tools/online_search.py +139 -44
  70. khoj/routers/api.py +35 -6
  71. khoj/routers/api_agents.py +235 -4
  72. khoj/routers/api_chat.py +102 -530
  73. khoj/routers/api_content.py +14 -0
  74. khoj/routers/api_model.py +1 -1
  75. khoj/routers/auth.py +9 -1
  76. khoj/routers/helpers.py +181 -68
  77. khoj/routers/subscription.py +18 -4
  78. khoj/search_type/text_search.py +11 -3
  79. khoj/utils/helpers.py +64 -8
  80. khoj/utils/initialization.py +0 -3
  81. {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/METADATA +19 -21
  82. {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/RECORD +87 -81
  83. khoj/interface/compiled/_next/static/chunks/1603-3e2e1528e3b6ea1d.js +0 -1
  84. khoj/interface/compiled/_next/static/chunks/2697-a29cb9191a9e339c.js +0 -1
  85. khoj/interface/compiled/_next/static/chunks/6648-ee109f4ea33a74e2.js +0 -1
  86. khoj/interface/compiled/_next/static/chunks/7071-b4711cecca6619a8.js +0 -1
  87. khoj/interface/compiled/_next/static/chunks/743-1a64254447cda71f.js +0 -1
  88. khoj/interface/compiled/_next/static/chunks/8423-62ac6c832be2461b.js +0 -1
  89. khoj/interface/compiled/_next/static/chunks/9162-0be016519a18568b.js +0 -1
  90. khoj/interface/compiled/_next/static/chunks/9178-7e815211edcb3657.js +0 -1
  91. khoj/interface/compiled/_next/static/chunks/9417-5d14ac74aaab2c66.js +0 -1
  92. khoj/interface/compiled/_next/static/chunks/9984-e410179c6fac7cf1.js +0 -1
  93. khoj/interface/compiled/_next/static/chunks/app/agents/page-d302911777a3e027.js +0 -1
  94. khoj/interface/compiled/_next/static/chunks/app/automations/page-0a5de8c254c29a1c.js +0 -1
  95. khoj/interface/compiled/_next/static/chunks/app/chat/page-d96bf6a84bb05290.js +0 -1
  96. khoj/interface/compiled/_next/static/chunks/app/factchecker/page-32e61af29e6b431d.js +0 -1
  97. khoj/interface/compiled/_next/static/chunks/app/page-96cab08c985716f4.js +0 -1
  98. khoj/interface/compiled/_next/static/chunks/app/search/page-b3193d46c65571c5.js +0 -1
  99. khoj/interface/compiled/_next/static/chunks/app/settings/page-0db9b708366606ec.js +0 -1
  100. khoj/interface/compiled/_next/static/chunks/app/share/chat/page-f06ac16cfe5b5a16.js +0 -1
  101. khoj/interface/compiled/_next/static/css/1538cedb321e3a97.css +0 -1
  102. khoj/interface/compiled/_next/static/css/24f141a6e37cd204.css +0 -25
  103. khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +0 -1
  104. khoj/interface/compiled/_next/static/css/f768dddada62459d.css +0 -1
  105. /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → Jid9q6Qg851ioDaaO_fth}/_buildManifest.js +0 -0
  106. /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → Jid9q6Qg851ioDaaO_fth}/_ssgManifest.js +0 -0
  107. {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/WHEEL +0 -0
  108. {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/entry_points.txt +0 -0
  109. {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/licenses/LICENSE +0 -0
khoj/configure.py CHANGED
@@ -42,6 +42,7 @@ from khoj.database.adapters import (
42
42
  from khoj.database.models import ClientApplication, KhojUser, ProcessLock, Subscription
43
43
  from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
44
44
  from khoj.routers.api_content import configure_content, configure_search
45
+ from khoj.routers.helpers import update_telemetry_state
45
46
  from khoj.routers.twilio import is_twilio_enabled
46
47
  from khoj.utils import constants, state
47
48
  from khoj.utils.config import SearchType
@@ -165,7 +166,15 @@ class UserAuthenticationBackend(AuthenticationBackend):
165
166
 
166
167
  create_if_not_exists = request.query_params.get("create_if_not_exists")
167
168
  if create_if_not_exists:
168
- user = await aget_or_create_user_by_phone_number(phone_number)
169
+ user, is_new = await aget_or_create_user_by_phone_number(phone_number)
170
+ if user and is_new:
171
+ update_telemetry_state(
172
+ request=request,
173
+ telemetry_type="api",
174
+ api="create_user",
175
+ metadata={"user_id": str(user.uuid)},
176
+ )
177
+ logger.log(logging.INFO, f"🥳 New User Created: {user.uuid}")
169
178
  else:
170
179
  user = await aget_user_by_phone_number(phone_number)
171
180
 
@@ -244,7 +253,7 @@ def configure_server(
244
253
 
245
254
  state.SearchType = configure_search_types()
246
255
  state.search_models = configure_search(state.search_models, state.config.search_type)
247
- setup_default_agent()
256
+ setup_default_agent(user)
248
257
 
249
258
  message = "📡 Telemetry disabled" if telemetry_disabled(state.config.app) else "📡 Telemetry enabled"
250
259
  logger.info(message)
@@ -256,8 +265,8 @@ def configure_server(
256
265
  raise e
257
266
 
258
267
 
259
- def setup_default_agent():
260
- AgentAdapters.create_default_agent()
268
+ def setup_default_agent(user: KhojUser):
269
+ AgentAdapters.create_default_agent(user)
261
270
 
262
271
 
263
272
  def initialize_content(regenerate: bool, search_type: Optional[SearchType] = None, user: KhojUser = None):
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  import math
4
+ import os
4
5
  import random
5
6
  import re
6
7
  import secrets
@@ -51,6 +52,7 @@ from khoj.database.models import (
51
52
  UserTextToImageModelConfig,
52
53
  UserVoiceModelConfig,
53
54
  VoiceModelOption,
55
+ WebScraper,
54
56
  )
55
57
  from khoj.processor.conversation import prompts
56
58
  from khoj.search_filter.date_filter import DateFilter
@@ -58,7 +60,12 @@ from khoj.search_filter.file_filter import FileFilter
58
60
  from khoj.search_filter.word_filter import WordFilter
59
61
  from khoj.utils import state
60
62
  from khoj.utils.config import OfflineChatProcessorModel
61
- from khoj.utils.helpers import generate_random_name, is_none_or_empty, timer
63
+ from khoj.utils.helpers import (
64
+ generate_random_name,
65
+ in_debug_mode,
66
+ is_none_or_empty,
67
+ timer,
68
+ )
62
69
 
63
70
  logger = logging.getLogger(__name__)
64
71
 
@@ -112,13 +119,15 @@ async def get_or_create_user(token: dict) -> KhojUser:
112
119
  return user
113
120
 
114
121
 
115
- async def aget_or_create_user_by_phone_number(phone_number: str) -> KhojUser:
122
+ async def aget_or_create_user_by_phone_number(phone_number: str) -> tuple[KhojUser, bool]:
123
+ is_new = False
116
124
  if is_none_or_empty(phone_number):
117
- return None
125
+ return None, is_new
118
126
  user = await aget_user_by_phone_number(phone_number)
119
127
  if not user:
120
128
  user = await acreate_user_by_phone_number(phone_number)
121
- return user
129
+ is_new = True
130
+ return user, is_new
122
131
 
123
132
 
124
133
  async def aset_user_phone_number(user: KhojUser, phone_number: str) -> KhojUser:
@@ -164,8 +173,10 @@ async def acreate_user_by_phone_number(phone_number: str) -> KhojUser:
164
173
  return user
165
174
 
166
175
 
167
- async def aget_or_create_user_by_email(email: str) -> KhojUser:
168
- user, _ = await KhojUser.objects.filter(email=email).aupdate_or_create(defaults={"username": email, "email": email})
176
+ async def aget_or_create_user_by_email(email: str) -> tuple[KhojUser, bool]:
177
+ user, is_new = await KhojUser.objects.filter(email=email).aupdate_or_create(
178
+ defaults={"username": email, "email": email}
179
+ )
169
180
  await user.asave()
170
181
 
171
182
  if user:
@@ -176,7 +187,7 @@ async def aget_or_create_user_by_email(email: str) -> KhojUser:
176
187
  if not user_subscription:
177
188
  await Subscription.objects.acreate(user=user, type="trial")
178
189
 
179
- return user
190
+ return user, is_new
180
191
 
181
192
 
182
193
  async def aget_user_validated_by_email_verification_code(code: str) -> KhojUser:
@@ -247,9 +258,9 @@ def get_user_subscription(email: str) -> Optional[Subscription]:
247
258
 
248
259
  async def set_user_subscription(
249
260
  email: str, is_recurring=None, renewal_date=None, type="standard"
250
- ) -> Optional[Subscription]:
261
+ ) -> tuple[Optional[Subscription], bool]:
251
262
  # Get or create the user object and their subscription
252
- user = await aget_or_create_user_by_email(email)
263
+ user, is_new = await aget_or_create_user_by_email(email)
253
264
  user_subscription = await Subscription.objects.filter(user=user).afirst()
254
265
 
255
266
  # Update the user subscription state
@@ -261,7 +272,7 @@ async def set_user_subscription(
261
272
  elif renewal_date is not None:
262
273
  user_subscription.renewal_date = renewal_date
263
274
  await user_subscription.asave()
264
- return user_subscription
275
+ return user_subscription, is_new
265
276
 
266
277
 
267
278
  def subscription_to_state(subscription: Subscription) -> str:
@@ -551,26 +562,78 @@ class ClientApplicationAdapters:
551
562
 
552
563
  class AgentAdapters:
553
564
  DEFAULT_AGENT_NAME = "Khoj"
554
- DEFAULT_AGENT_AVATAR = "https://assets.khoj.dev/lamp-128.png"
555
565
  DEFAULT_AGENT_SLUG = "khoj"
556
566
 
567
+ @staticmethod
568
+ async def aget_readonly_agent_by_slug(agent_slug: str, user: KhojUser):
569
+ return (
570
+ await Agent.objects.filter(
571
+ (Q(slug__iexact=agent_slug.lower()))
572
+ & (
573
+ Q(privacy_level=Agent.PrivacyLevel.PUBLIC)
574
+ | Q(privacy_level=Agent.PrivacyLevel.PROTECTED)
575
+ | Q(creator=user)
576
+ )
577
+ )
578
+ .prefetch_related("creator", "chat_model", "fileobject_set")
579
+ .afirst()
580
+ )
581
+
582
+ @staticmethod
583
+ async def adelete_agent_by_slug(agent_slug: str, user: KhojUser):
584
+ agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
585
+
586
+ async for entry in Entry.objects.filter(agent=agent).aiterator():
587
+ await entry.adelete()
588
+
589
+ if agent:
590
+ await agent.adelete()
591
+ return True
592
+ return False
593
+
557
594
  @staticmethod
558
595
  async def aget_agent_by_slug(agent_slug: str, user: KhojUser):
559
- return await Agent.objects.filter(
560
- (Q(slug__iexact=agent_slug.lower())) & (Q(public=True) | Q(creator=user))
561
- ).afirst()
596
+ return (
597
+ await Agent.objects.filter(
598
+ (Q(slug__iexact=agent_slug.lower())) & (Q(privacy_level=Agent.PrivacyLevel.PUBLIC) | Q(creator=user))
599
+ )
600
+ .prefetch_related("creator", "chat_model", "fileobject_set")
601
+ .afirst()
602
+ )
603
+
604
+ @staticmethod
605
+ async def aget_agent_by_name(agent_name: str, user: KhojUser):
606
+ return (
607
+ await Agent.objects.filter(
608
+ (Q(name__iexact=agent_name.lower())) & (Q(privacy_level=Agent.PrivacyLevel.PUBLIC) | Q(creator=user))
609
+ )
610
+ .prefetch_related("creator", "chat_model", "fileobject_set")
611
+ .afirst()
612
+ )
562
613
 
563
614
  @staticmethod
564
615
  def get_agent_by_slug(slug: str, user: KhojUser = None):
565
616
  if user:
566
- return Agent.objects.filter((Q(slug__iexact=slug.lower())) & (Q(public=True) | Q(creator=user))).first()
567
- return Agent.objects.filter(slug__iexact=slug.lower(), public=True).first()
617
+ return Agent.objects.filter(
618
+ (Q(slug__iexact=slug.lower())) & (Q(privacy_level=Agent.PrivacyLevel.PUBLIC) | Q(creator=user))
619
+ ).first()
620
+ return Agent.objects.filter(slug__iexact=slug.lower(), privacy_level=Agent.PrivacyLevel.PUBLIC).first()
568
621
 
569
622
  @staticmethod
570
623
  def get_all_accessible_agents(user: KhojUser = None):
624
+ public_query = Q(privacy_level=Agent.PrivacyLevel.PUBLIC)
571
625
  if user:
572
- return Agent.objects.filter(Q(public=True) | Q(creator=user)).distinct().order_by("created_at")
573
- return Agent.objects.filter(public=True).order_by("created_at")
626
+ return (
627
+ Agent.objects.filter(public_query | Q(creator=user))
628
+ .distinct()
629
+ .order_by("created_at")
630
+ .prefetch_related("creator", "chat_model", "fileobject_set")
631
+ )
632
+ return (
633
+ Agent.objects.filter(public_query)
634
+ .order_by("created_at")
635
+ .prefetch_related("creator", "chat_model", "fileobject_set")
636
+ )
574
637
 
575
638
  @staticmethod
576
639
  async def aget_all_accessible_agents(user: KhojUser = None) -> List[Agent]:
@@ -590,8 +653,8 @@ class AgentAdapters:
590
653
  return Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).first()
591
654
 
592
655
  @staticmethod
593
- def create_default_agent():
594
- default_conversation_config = ConversationAdapters.get_default_conversation_config()
656
+ def create_default_agent(user: KhojUser):
657
+ default_conversation_config = ConversationAdapters.get_default_conversation_config(user)
595
658
  if default_conversation_config is None:
596
659
  logger.info("No default conversation config found, skipping default agent creation")
597
660
  return None
@@ -604,17 +667,19 @@ class AgentAdapters:
604
667
  agent.chat_model = default_conversation_config
605
668
  agent.slug = AgentAdapters.DEFAULT_AGENT_SLUG
606
669
  agent.name = AgentAdapters.DEFAULT_AGENT_NAME
670
+ agent.privacy_level = Agent.PrivacyLevel.PUBLIC
671
+ agent.managed_by_admin = True
672
+ agent.input_tools = []
673
+ agent.output_modes = []
607
674
  agent.save()
608
675
  else:
609
676
  # The default agent is public and managed by the admin. It's handled a little differently than other agents.
610
677
  agent = Agent.objects.create(
611
678
  name=AgentAdapters.DEFAULT_AGENT_NAME,
612
- public=True,
679
+ privacy_level=Agent.PrivacyLevel.PUBLIC,
613
680
  managed_by_admin=True,
614
681
  chat_model=default_conversation_config,
615
682
  personality=default_personality,
616
- tools=["*"],
617
- avatar=AgentAdapters.DEFAULT_AGENT_AVATAR,
618
683
  slug=AgentAdapters.DEFAULT_AGENT_SLUG,
619
684
  )
620
685
  Conversation.objects.filter(agent=None).update(agent=agent)
@@ -625,6 +690,70 @@ class AgentAdapters:
625
690
  async def aget_default_agent():
626
691
  return await Agent.objects.filter(name=AgentAdapters.DEFAULT_AGENT_NAME).afirst()
627
692
 
693
+ @staticmethod
694
+ async def aupdate_agent(
695
+ user: KhojUser,
696
+ name: str,
697
+ personality: str,
698
+ privacy_level: str,
699
+ icon: str,
700
+ color: str,
701
+ chat_model: str,
702
+ files: List[str],
703
+ input_tools: List[str],
704
+ output_modes: List[str],
705
+ slug: Optional[str] = None,
706
+ ):
707
+ chat_model_option = await ChatModelOptions.objects.filter(chat_model=chat_model).afirst()
708
+
709
+ # Slug will be None for new agents, which will trigger a new agent creation with a generated, immutable slug
710
+ agent, created = await Agent.objects.filter(slug=slug, creator=user).aupdate_or_create(
711
+ defaults={
712
+ "name": name,
713
+ "creator": user,
714
+ "personality": personality,
715
+ "privacy_level": privacy_level,
716
+ "style_icon": icon,
717
+ "style_color": color,
718
+ "chat_model": chat_model_option,
719
+ "input_tools": input_tools,
720
+ "output_modes": output_modes,
721
+ }
722
+ )
723
+
724
+ # Delete all existing files and entries
725
+ await FileObject.objects.filter(agent=agent).adelete()
726
+ await Entry.objects.filter(agent=agent).adelete()
727
+
728
+ for file in files:
729
+ reference_file = await FileObject.objects.filter(file_name=file, user=agent.creator).afirst()
730
+ if reference_file:
731
+ await FileObject.objects.acreate(file_name=file, agent=agent, raw_text=reference_file.raw_text)
732
+
733
+ # Duplicate all entries associated with the file
734
+ entries: List[Entry] = []
735
+ async for entry in Entry.objects.filter(file_path=file, user=agent.creator).aiterator():
736
+ entries.append(
737
+ Entry(
738
+ agent=agent,
739
+ embeddings=entry.embeddings,
740
+ raw=entry.raw,
741
+ compiled=entry.compiled,
742
+ heading=entry.heading,
743
+ file_source=entry.file_source,
744
+ file_type=entry.file_type,
745
+ file_path=entry.file_path,
746
+ file_name=entry.file_name,
747
+ url=entry.url,
748
+ hashed_value=entry.hashed_value,
749
+ )
750
+ )
751
+
752
+ # Bulk create entries
753
+ await Entry.objects.abulk_create(entries)
754
+
755
+ return agent
756
+
628
757
 
629
758
  class PublicConversationAdapters:
630
759
  @staticmethod
@@ -709,7 +838,7 @@ class ConversationAdapters:
709
838
  user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None, title: str = None
710
839
  ):
711
840
  if agent_slug:
712
- agent = await AgentAdapters.aget_agent_by_slug(agent_slug, user)
841
+ agent = await AgentAdapters.aget_readonly_agent_by_slug(agent_slug, user)
713
842
  if agent is None:
714
843
  raise HTTPException(status_code=400, detail="No such agent currently exists.")
715
844
  return await Conversation.objects.acreate(user=user, client=client_application, agent=agent, title=title)
@@ -721,7 +850,7 @@ class ConversationAdapters:
721
850
  user: KhojUser, client_application: ClientApplication = None, agent_slug: str = None, title: str = None
722
851
  ):
723
852
  if agent_slug:
724
- agent = AgentAdapters.get_agent_by_slug(agent_slug, user)
853
+ agent = AgentAdapters.aget_readonly_agent_by_slug(agent_slug, user)
725
854
  if agent is None:
726
855
  raise HTTPException(status_code=400, detail="No such agent currently exists.")
727
856
  return Conversation.objects.create(user=user, client=client_application, agent=agent, title=title)
@@ -816,21 +945,21 @@ class ConversationAdapters:
816
945
  def get_conversation_config(user: KhojUser):
817
946
  subscribed = is_user_subscribed(user)
818
947
  if not subscribed:
819
- return ConversationAdapters.get_default_conversation_config()
948
+ return ConversationAdapters.get_default_conversation_config(user)
820
949
  config = UserConversationConfig.objects.filter(user=user).first()
821
950
  if config:
822
951
  return config.setting
823
- return ConversationAdapters.get_advanced_conversation_config()
952
+ return ConversationAdapters.get_advanced_conversation_config(user)
824
953
 
825
954
  @staticmethod
826
955
  async def aget_conversation_config(user: KhojUser):
827
956
  subscribed = await ais_user_subscribed(user)
828
957
  if not subscribed:
829
- return await ConversationAdapters.aget_default_conversation_config()
958
+ return await ConversationAdapters.aget_default_conversation_config(user)
830
959
  config = await UserConversationConfig.objects.filter(user=user).prefetch_related("setting").afirst()
831
960
  if config:
832
961
  return config.setting
833
- return ConversationAdapters.aget_advanced_conversation_config()
962
+ return ConversationAdapters.aget_advanced_conversation_config(user)
834
963
 
835
964
  @staticmethod
836
965
  async def aget_voice_model_config(user: KhojUser) -> Optional[VoiceModelOption]:
@@ -851,40 +980,126 @@ class ConversationAdapters:
851
980
  return VoiceModelOption.objects.first()
852
981
 
853
982
  @staticmethod
854
- def get_default_conversation_config():
983
+ def get_default_conversation_config(user: KhojUser = None):
984
+ """Get default conversation config. Prefer chat model by server admin > user > first created chat model"""
985
+ # Get the server chat settings
855
986
  server_chat_settings = ServerChatSettings.objects.first()
856
- if server_chat_settings is None or server_chat_settings.chat_default is None:
857
- return ChatModelOptions.objects.filter().first()
858
- return server_chat_settings.chat_default
987
+ if server_chat_settings is not None and server_chat_settings.chat_default is not None:
988
+ return server_chat_settings.chat_default
989
+
990
+ # Get the user's chat settings, if the server chat settings are not set
991
+ user_chat_settings = UserConversationConfig.objects.filter(user=user).first() if user else None
992
+ if user_chat_settings is not None and user_chat_settings.setting is not None:
993
+ return user_chat_settings.setting
994
+
995
+ # Get the first chat model if even the user chat settings are not set
996
+ return ChatModelOptions.objects.filter().first()
859
997
 
860
998
  @staticmethod
861
- async def aget_default_conversation_config():
999
+ async def aget_default_conversation_config(user: KhojUser = None):
1000
+ """Get default conversation config. Prefer chat model by server admin > user > first created chat model"""
1001
+ # Get the server chat settings
862
1002
  server_chat_settings: ServerChatSettings = (
863
1003
  await ServerChatSettings.objects.filter()
864
1004
  .prefetch_related("chat_default", "chat_default__openai_config")
865
1005
  .afirst()
866
1006
  )
867
- if server_chat_settings is None or server_chat_settings.chat_default is None:
868
- return await ChatModelOptions.objects.filter().prefetch_related("openai_config").afirst()
869
- return server_chat_settings.chat_default
1007
+ if server_chat_settings is not None and server_chat_settings.chat_default is not None:
1008
+ return server_chat_settings.chat_default
1009
+
1010
+ # Get the user's chat settings, if the server chat settings are not set
1011
+ user_chat_settings = (
1012
+ (await UserConversationConfig.objects.filter(user=user).prefetch_related("setting__openai_config").afirst())
1013
+ if user
1014
+ else None
1015
+ )
1016
+ if user_chat_settings is not None and user_chat_settings.setting is not None:
1017
+ return user_chat_settings.setting
1018
+
1019
+ # Get the first chat model if even the user chat settings are not set
1020
+ return await ChatModelOptions.objects.filter().prefetch_related("openai_config").afirst()
870
1021
 
871
1022
  @staticmethod
872
- def get_advanced_conversation_config():
1023
+ def get_advanced_conversation_config(user: KhojUser):
873
1024
  server_chat_settings = ServerChatSettings.objects.first()
874
- if server_chat_settings is None or server_chat_settings.chat_advanced is None:
875
- return ConversationAdapters.get_default_conversation_config()
876
- return server_chat_settings.chat_advanced
1025
+ if server_chat_settings is not None and server_chat_settings.chat_advanced is not None:
1026
+ return server_chat_settings.chat_advanced
1027
+ return ConversationAdapters.get_default_conversation_config(user)
877
1028
 
878
1029
  @staticmethod
879
- async def aget_advanced_conversation_config():
1030
+ async def aget_advanced_conversation_config(user: KhojUser = None):
880
1031
  server_chat_settings: ServerChatSettings = (
881
1032
  await ServerChatSettings.objects.filter()
882
1033
  .prefetch_related("chat_advanced", "chat_advanced__openai_config")
883
1034
  .afirst()
884
1035
  )
885
- if server_chat_settings is None or server_chat_settings.chat_advanced is None:
886
- return await ConversationAdapters.aget_default_conversation_config()
887
- return server_chat_settings.chat_advanced
1036
+ if server_chat_settings is not None and server_chat_settings.chat_advanced is not None:
1037
+ return server_chat_settings.chat_advanced
1038
+ return await ConversationAdapters.aget_default_conversation_config(user)
1039
+
1040
+ @staticmethod
1041
+ async def aget_server_webscraper():
1042
+ server_chat_settings = await ServerChatSettings.objects.filter().prefetch_related("web_scraper").afirst()
1043
+ if server_chat_settings is not None and server_chat_settings.web_scraper is not None:
1044
+ return server_chat_settings.web_scraper
1045
+ return None
1046
+
1047
+ @staticmethod
1048
+ async def aget_enabled_webscrapers() -> list[WebScraper]:
1049
+ enabled_scrapers: list[WebScraper] = []
1050
+ server_webscraper = await ConversationAdapters.aget_server_webscraper()
1051
+ if server_webscraper:
1052
+ # Only use the webscraper set in the server chat settings
1053
+ enabled_scrapers = [server_webscraper]
1054
+ if not enabled_scrapers:
1055
+ # Use the enabled web scrapers, ordered by priority, until get web page content
1056
+ enabled_scrapers = [scraper async for scraper in WebScraper.objects.all().order_by("priority").aiterator()]
1057
+ if not enabled_scrapers:
1058
+ # Use scrapers enabled via environment variables
1059
+ if os.getenv("FIRECRAWL_API_KEY"):
1060
+ api_url = os.getenv("FIRECRAWL_API_URL", "https://api.firecrawl.dev")
1061
+ enabled_scrapers.append(
1062
+ WebScraper(
1063
+ type=WebScraper.WebScraperType.FIRECRAWL,
1064
+ name=WebScraper.WebScraperType.FIRECRAWL.capitalize(),
1065
+ api_key=os.getenv("FIRECRAWL_API_KEY"),
1066
+ api_url=api_url,
1067
+ )
1068
+ )
1069
+ if os.getenv("OLOSTEP_API_KEY"):
1070
+ api_url = os.getenv("OLOSTEP_API_URL", "https://agent.olostep.com/olostep-p2p-incomingAPI")
1071
+ enabled_scrapers.append(
1072
+ WebScraper(
1073
+ type=WebScraper.WebScraperType.OLOSTEP,
1074
+ name=WebScraper.WebScraperType.OLOSTEP.capitalize(),
1075
+ api_key=os.getenv("OLOSTEP_API_KEY"),
1076
+ api_url=api_url,
1077
+ )
1078
+ )
1079
+ # Jina is the default fallback scrapers to use as it does not require an API key
1080
+ api_url = os.getenv("JINA_READER_API_URL", "https://r.jina.ai/")
1081
+ enabled_scrapers.append(
1082
+ WebScraper(
1083
+ type=WebScraper.WebScraperType.JINA,
1084
+ name=WebScraper.WebScraperType.JINA.capitalize(),
1085
+ api_key=os.getenv("JINA_API_KEY"),
1086
+ api_url=api_url,
1087
+ )
1088
+ )
1089
+
1090
+ # Only enable the direct web page scraper by default in self-hosted single user setups.
1091
+ # Useful for reading webpages on your intranet.
1092
+ if state.anonymous_mode or in_debug_mode():
1093
+ enabled_scrapers.append(
1094
+ WebScraper(
1095
+ type=WebScraper.WebScraperType.DIRECT,
1096
+ name=WebScraper.WebScraperType.DIRECT.capitalize(),
1097
+ api_key=None,
1098
+ api_url=None,
1099
+ )
1100
+ )
1101
+
1102
+ return enabled_scrapers
888
1103
 
889
1104
  @staticmethod
890
1105
  def create_conversation_from_public_conversation(
@@ -1113,8 +1328,8 @@ class FileObjectAdapters:
1113
1328
  return await FileObject.objects.acreate(user=user, file_name=file_name, raw_text=raw_text)
1114
1329
 
1115
1330
  @staticmethod
1116
- async def async_get_file_objects_by_name(user: KhojUser, file_name: str):
1117
- return await sync_to_async(list)(FileObject.objects.filter(user=user, file_name=file_name))
1331
+ async def async_get_file_objects_by_name(user: KhojUser, file_name: str, agent: Agent = None):
1332
+ return await sync_to_async(list)(FileObject.objects.filter(user=user, file_name=file_name, agent=agent))
1118
1333
 
1119
1334
  @staticmethod
1120
1335
  async def async_get_all_file_objects(user: KhojUser):
@@ -1196,10 +1411,18 @@ class EntryAdapters:
1196
1411
  def user_has_entries(user: KhojUser):
1197
1412
  return Entry.objects.filter(user=user).exists()
1198
1413
 
1414
+ @staticmethod
1415
+ def agent_has_entries(agent: Agent):
1416
+ return Entry.objects.filter(agent=agent).exists()
1417
+
1199
1418
  @staticmethod
1200
1419
  async def auser_has_entries(user: KhojUser):
1201
1420
  return await Entry.objects.filter(user=user).aexists()
1202
1421
 
1422
+ @staticmethod
1423
+ async def aagent_has_entries(agent: Agent):
1424
+ return await Entry.objects.filter(agent=agent).aexists()
1425
+
1203
1426
  @staticmethod
1204
1427
  async def adelete_entry_by_file(user: KhojUser, file_path: str):
1205
1428
  return await Entry.objects.filter(user=user, file_path=file_path).adelete()
@@ -1214,6 +1437,10 @@ class EntryAdapters:
1214
1437
 
1215
1438
  return deleted_count
1216
1439
 
1440
+ @staticmethod
1441
+ async def aget_agent_entry_filepaths(agent: Agent):
1442
+ return await sync_to_async(list)(Entry.objects.filter(agent=agent).values_list("file_path", flat=True))
1443
+
1217
1444
  @staticmethod
1218
1445
  def get_all_filenames_by_source(user: KhojUser, file_source: str):
1219
1446
  return (
@@ -1229,15 +1456,19 @@ class EntryAdapters:
1229
1456
  return total_size / 1024 / 1024
1230
1457
 
1231
1458
  @staticmethod
1232
- def apply_filters(user: KhojUser, query: str, file_type_filter: str = None):
1459
+ def apply_filters(user: KhojUser, query: str, file_type_filter: str = None, agent: Agent = None):
1233
1460
  q_filter_terms = Q()
1234
1461
 
1235
1462
  word_filters = EntryAdapters.word_filter.get_filter_terms(query)
1236
1463
  file_filters = EntryAdapters.file_filter.get_filter_terms(query)
1237
1464
  date_filters = EntryAdapters.date_filter.get_query_date_range(query)
1238
1465
 
1466
+ user_or_agent = Q(user=user)
1467
+ if agent != None:
1468
+ user_or_agent |= Q(agent=agent)
1469
+
1239
1470
  if len(word_filters) == 0 and len(file_filters) == 0 and len(date_filters) == 0:
1240
- return Entry.objects.filter(user=user)
1471
+ return Entry.objects.filter(user_or_agent)
1241
1472
 
1242
1473
  for term in word_filters:
1243
1474
  if term.startswith("+"):
@@ -1273,7 +1504,7 @@ class EntryAdapters:
1273
1504
  formatted_max_date = date.fromtimestamp(max_date).strftime("%Y-%m-%d")
1274
1505
  q_filter_terms &= Q(embeddings_dates__date__lte=formatted_max_date)
1275
1506
 
1276
- relevant_entries = Entry.objects.filter(user=user).filter(q_filter_terms)
1507
+ relevant_entries = Entry.objects.filter(user_or_agent).filter(q_filter_terms)
1277
1508
  if file_type_filter:
1278
1509
  relevant_entries = relevant_entries.filter(file_type=file_type_filter)
1279
1510
  return relevant_entries
@@ -1286,9 +1517,15 @@ class EntryAdapters:
1286
1517
  file_type_filter: str = None,
1287
1518
  raw_query: str = None,
1288
1519
  max_distance: float = math.inf,
1520
+ agent: Agent = None,
1289
1521
  ):
1290
- relevant_entries = EntryAdapters.apply_filters(user, raw_query, file_type_filter)
1291
- relevant_entries = relevant_entries.filter(user=user).annotate(
1522
+ user_or_agent = Q(user=user)
1523
+
1524
+ if agent != None:
1525
+ user_or_agent |= Q(agent=agent)
1526
+
1527
+ relevant_entries = EntryAdapters.apply_filters(user, raw_query, file_type_filter, agent)
1528
+ relevant_entries = relevant_entries.filter(user_or_agent).annotate(
1292
1529
  distance=CosineDistance("embeddings", embeddings)
1293
1530
  )
1294
1531
  relevant_entries = relevant_entries.filter(distance__lte=max_distance)
khoj/database/admin.py CHANGED
@@ -27,9 +27,11 @@ from khoj.database.models import (
27
27
  Subscription,
28
28
  TextToImageModelConfig,
29
29
  UserConversationConfig,
30
+ UserRequests,
30
31
  UserSearchModelConfig,
31
32
  UserVoiceModelConfig,
32
33
  VoiceModelOption,
34
+ WebScraper,
33
35
  )
34
36
  from khoj.utils.helpers import ImageIntentType
35
37
 
@@ -68,10 +70,11 @@ class KhojUserAdmin(UserAdmin):
68
70
  "id",
69
71
  "email",
70
72
  "username",
73
+ "phone_number",
71
74
  "is_active",
75
+ "uuid",
72
76
  "is_staff",
73
77
  "is_superuser",
74
- "phone_number",
75
78
  )
76
79
  search_fields = ("email", "username", "phone_number", "uuid")
77
80
  filter_horizontal = ("groups", "user_permissions")
@@ -103,6 +106,7 @@ admin.site.register(NotionConfig)
103
106
  admin.site.register(UserVoiceModelConfig)
104
107
  admin.site.register(VoiceModelOption)
105
108
  admin.site.register(UserConversationConfig)
109
+ admin.site.register(UserRequests)
106
110
 
107
111
 
108
112
  @admin.register(Agent)
@@ -195,7 +199,22 @@ class ServerChatSettingsAdmin(admin.ModelAdmin):
195
199
  list_display = (
196
200
  "chat_default",
197
201
  "chat_advanced",
202
+ "web_scraper",
203
+ )
204
+
205
+
206
+ @admin.register(WebScraper)
207
+ class WebScraperAdmin(admin.ModelAdmin):
208
+ list_display = (
209
+ "priority",
210
+ "name",
211
+ "type",
212
+ "api_key",
213
+ "api_url",
214
+ "created_at",
198
215
  )
216
+ search_fields = ("name", "api_key", "api_url", "type")
217
+ ordering = ("priority",)
199
218
 
200
219
 
201
220
  @admin.register(Conversation)