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.
- khoj/configure.py +13 -4
- khoj/database/adapters/__init__.py +289 -52
- khoj/database/admin.py +20 -1
- khoj/database/migrations/0065_remove_agent_avatar_remove_agent_public_and_more.py +49 -0
- khoj/database/migrations/0066_remove_agent_tools_agent_input_tools_and_more.py +69 -0
- khoj/database/migrations/0067_alter_agent_style_icon.py +50 -0
- khoj/database/migrations/0068_alter_agent_output_modes.py +24 -0
- khoj/database/migrations/0069_webscraper_serverchatsettings_web_scraper.py +89 -0
- khoj/database/models/__init__.py +136 -18
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/chunks/1603-fa3ee48860b9dc5c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/2697-a38d01981ad3bdf8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3110-ef2cacd1b8d79ad8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/4086-2c74808ba38a5a0f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/477-ec86e93db10571c1.js +1 -0
- khoj/interface/compiled/_next/static/chunks/51-e8f5bdb69b5ea421.js +1 -0
- khoj/interface/compiled/_next/static/chunks/7762-79f2205740622b5c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9178-899fe9a6b754ecfe.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9417-29502e39c3e7d60c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9479-7eed36fc954ef804.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/{layout-e71c8e913cccf792.js → layout-75636ab3a413fa8e.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-fa282831808ee536.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/page-5480731341f34450.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/{layout-8102549127db3067.js → layout-96fcf62857bf8f30.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-702057ccbcf27881.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/factchecker/page-e7b34316ec6f44de.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/{layout-f3e40d346da53112.js → layout-d0f0a9067427fb20.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/page-10a5aad6e04f3cf8.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/page-d56541c746fded7d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/{layout-6f9314b0d7a26046.js → layout-a8f33dfe92f997fb.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-e044a999468a7c5d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{layout-39f03f9e32399f0f.js → layout-2df56074e42adaa0.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-fbbd66a4d4633438.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{webpack-d4781cada9b58e75.js → webpack-c0cd5a6afb1f0798.js} +1 -1
- khoj/interface/compiled/_next/static/css/2de69f0be774c768.css +1 -0
- khoj/interface/compiled/_next/static/css/467a524c75e7d7c0.css +1 -0
- khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +1 -0
- khoj/interface/compiled/_next/static/css/b9a6bf04305d98d7.css +25 -0
- khoj/interface/compiled/agents/index.html +1 -1
- khoj/interface/compiled/agents/index.txt +2 -2
- khoj/interface/compiled/automations/index.html +1 -1
- khoj/interface/compiled/automations/index.txt +2 -2
- khoj/interface/compiled/chat/index.html +1 -1
- khoj/interface/compiled/chat/index.txt +2 -2
- khoj/interface/compiled/factchecker/index.html +1 -1
- khoj/interface/compiled/factchecker/index.txt +2 -2
- khoj/interface/compiled/index.html +1 -1
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +1 -1
- khoj/interface/compiled/search/index.txt +2 -2
- khoj/interface/compiled/settings/index.html +1 -1
- khoj/interface/compiled/settings/index.txt +3 -3
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/interface/web/assets/icons/agents.svg +1 -0
- khoj/interface/web/assets/icons/automation.svg +1 -0
- khoj/interface/web/assets/icons/chat.svg +24 -0
- khoj/interface/web/login.html +11 -22
- khoj/processor/content/notion/notion_to_entries.py +2 -1
- khoj/processor/conversation/anthropic/anthropic_chat.py +2 -0
- khoj/processor/conversation/google/gemini_chat.py +6 -19
- khoj/processor/conversation/google/utils.py +33 -15
- khoj/processor/conversation/offline/chat_model.py +3 -1
- khoj/processor/conversation/openai/gpt.py +2 -0
- khoj/processor/conversation/prompts.py +67 -5
- khoj/processor/conversation/utils.py +3 -7
- khoj/processor/embeddings.py +6 -3
- khoj/processor/image/generate.py +4 -3
- khoj/processor/tools/online_search.py +139 -44
- khoj/routers/api.py +35 -6
- khoj/routers/api_agents.py +235 -4
- khoj/routers/api_chat.py +102 -530
- khoj/routers/api_content.py +14 -0
- khoj/routers/api_model.py +1 -1
- khoj/routers/auth.py +9 -1
- khoj/routers/helpers.py +181 -68
- khoj/routers/subscription.py +18 -4
- khoj/search_type/text_search.py +11 -3
- khoj/utils/helpers.py +64 -8
- khoj/utils/initialization.py +0 -3
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/METADATA +19 -21
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/RECORD +87 -81
- khoj/interface/compiled/_next/static/chunks/1603-3e2e1528e3b6ea1d.js +0 -1
- khoj/interface/compiled/_next/static/chunks/2697-a29cb9191a9e339c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/6648-ee109f4ea33a74e2.js +0 -1
- khoj/interface/compiled/_next/static/chunks/7071-b4711cecca6619a8.js +0 -1
- khoj/interface/compiled/_next/static/chunks/743-1a64254447cda71f.js +0 -1
- khoj/interface/compiled/_next/static/chunks/8423-62ac6c832be2461b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9162-0be016519a18568b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9178-7e815211edcb3657.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9417-5d14ac74aaab2c66.js +0 -1
- khoj/interface/compiled/_next/static/chunks/9984-e410179c6fac7cf1.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-d302911777a3e027.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-0a5de8c254c29a1c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-d96bf6a84bb05290.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/factchecker/page-32e61af29e6b431d.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/page-96cab08c985716f4.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/search/page-b3193d46c65571c5.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-0db9b708366606ec.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-f06ac16cfe5b5a16.js +0 -1
- khoj/interface/compiled/_next/static/css/1538cedb321e3a97.css +0 -1
- khoj/interface/compiled/_next/static/css/24f141a6e37cd204.css +0 -25
- khoj/interface/compiled/_next/static/css/4cae6c0e5c72fb2d.css +0 -1
- khoj/interface/compiled/_next/static/css/f768dddada62459d.css +0 -1
- /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → Jid9q6Qg851ioDaaO_fth}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{_29ceahp81LhuIHo5QgOD → Jid9q6Qg851ioDaaO_fth}/_ssgManifest.js +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/WHEEL +0 -0
- {khoj-1.24.2.dev3.dist-info → khoj-1.25.1.dev34.dist-info}/entry_points.txt +0 -0
- {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
|
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
|
-
|
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,
|
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
|
560
|
-
|
561
|
-
|
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(
|
567
|
-
|
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
|
573
|
-
|
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
|
-
|
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.
|
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.
|
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
|
857
|
-
return
|
858
|
-
|
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
|
868
|
-
return
|
869
|
-
|
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
|
875
|
-
return
|
876
|
-
return
|
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
|
886
|
-
return
|
887
|
-
return
|
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(
|
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(
|
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
|
-
|
1291
|
-
|
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)
|