khoj 1.30.11.dev64__py3-none-any.whl → 1.32.3.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 +4 -2
- khoj/database/adapters/__init__.py +67 -58
- khoj/database/admin.py +9 -9
- khoj/database/migrations/0077_chatmodel_alter_agent_chat_model_and_more.py +62 -0
- khoj/database/migrations/0078_khojuser_email_verification_code_expiry.py +17 -0
- khoj/database/models/__init__.py +9 -8
- khoj/interface/compiled/404/index.html +1 -1
- khoj/interface/compiled/_next/static/chunks/182-8cd8b17d40e6e989.js +20 -0
- khoj/interface/compiled/_next/static/chunks/1915-605f698f2573cfd4.js +1 -0
- khoj/interface/compiled/_next/static/chunks/2117-9886e6a0232dc093.js +2 -0
- khoj/interface/compiled/_next/static/chunks/2581-455000f8aeb08fc3.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3175-b2e522f8ca392f7e.js +3 -0
- khoj/interface/compiled/_next/static/chunks/3727.dcea8f2193111552.js +1 -0
- khoj/interface/compiled/_next/static/chunks/3789-a09e37a819171a9d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/4124-0baa32400521e909.js +1 -0
- khoj/interface/compiled/_next/static/chunks/4357-03ea130575287c27.js +1 -0
- khoj/interface/compiled/_next/static/chunks/5243-f7f0a2a6e1ac5d28.js +1 -0
- khoj/interface/compiled/_next/static/chunks/5427-3e7360c8e6ac9728.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{1279-4cb23143aa2c0228.js → 5473-b1cf56dedac6577a.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/5477-c5d7eabee28a789a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/6065-64db9ad305ba0bcd.js +1 -0
- khoj/interface/compiled/_next/static/chunks/8667-d3e5bc726e4ff4e3.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9259-27d1ff42af9a43e0.js +1 -0
- khoj/interface/compiled/_next/static/chunks/94ca1967.1d9b42d929a1ee8c.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{1210.ef7a0f9a7e43da1d.js → 9597.83583248dfbf6e73.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/964ecbae.51d6faf8801d15e6.js +1 -0
- khoj/interface/compiled/_next/static/chunks/9665-1ab5c8c667b74dca.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/_not-found/{page-cfba071f5a657256.js → page-a834eddae3e235df.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/page-ab5ebe4efba9b582.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/automations/{layout-7f1b79a2c67af0b4.js → layout-1fe1537449f43496.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-37d56a7bbfd307df.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/page-a0b61f10b0bf6dd5.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/layout-30e7fda7262713ce.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/page-33a3375b1414d1bd.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/page-bbbfda90fa03c5be.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/layout-d09d6510a45cd4bd.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/settings/page-430db6215e48aea2.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-e8e5db7830bf3f47.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-02dc1f2e2a41e522.js +1 -0
- khoj/interface/compiled/_next/static/chunks/d3ac728e-44ebd2a0c99b12a0.js +1 -0
- khoj/interface/compiled/_next/static/chunks/{fd9d1056-2e6c8140e79afc3b.js → fd9d1056-4482b99a36fd1673.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/main-app-de1f09df97a3cfc7.js +1 -0
- khoj/interface/compiled/_next/static/chunks/main-db4bfac6b0a8d00b.js +1 -0
- khoj/interface/compiled/_next/static/chunks/pages/{_app-f870474a17b7f2fd.js → _app-3c9ca398d360b709.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/pages/{_error-c66a4e8afc46f17b.js → _error-cf5ca766ac8f493f.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- khoj/interface/compiled/_next/static/chunks/webpack-b0a1b08bb62bdc15.js +1 -0
- khoj/interface/compiled/_next/static/css/0f04760e76bba6c1.css +25 -0
- khoj/interface/compiled/_next/static/css/37a73b87f02df402.css +1 -0
- khoj/interface/compiled/_next/static/css/8e6a3ca11a60b189.css +1 -0
- khoj/interface/compiled/_next/static/css/9c164d9727dd8092.css +1 -0
- khoj/interface/compiled/_next/static/css/c3acbadc30537d04.css +1 -0
- khoj/interface/compiled/_next/static/css/dac88c17aaee5fcf.css +1 -0
- khoj/interface/compiled/_next/static/css/df4b47a2d0d85eae.css +1 -0
- khoj/interface/compiled/_next/static/css/e546bf5cc4914244.css +1 -0
- khoj/interface/compiled/_next/static/mqcIHpVqVWkmBuN0npYHA/_buildManifest.js +1 -0
- khoj/interface/compiled/agents/index.html +1 -1
- khoj/interface/compiled/agents/index.txt +6 -6
- khoj/interface/compiled/automations/index.html +1 -1
- khoj/interface/compiled/automations/index.txt +7 -7
- khoj/interface/compiled/chat/index.html +1 -1
- khoj/interface/compiled/chat/index.txt +6 -6
- khoj/interface/compiled/index.html +1 -1
- khoj/interface/compiled/index.txt +6 -6
- khoj/interface/compiled/search/index.html +1 -1
- khoj/interface/compiled/search/index.txt +6 -6
- khoj/interface/compiled/settings/index.html +1 -1
- khoj/interface/compiled/settings/index.txt +8 -8
- khoj/interface/compiled/share/chat/index.html +1 -1
- khoj/interface/compiled/share/chat/index.txt +6 -6
- khoj/interface/email/magic_link.html +36 -13
- khoj/main.py +1 -1
- khoj/migrations/migrate_server_pg.py +7 -7
- khoj/processor/conversation/anthropic/anthropic_chat.py +5 -7
- khoj/processor/conversation/google/gemini_chat.py +5 -7
- khoj/processor/conversation/google/utils.py +0 -1
- khoj/processor/conversation/offline/chat_model.py +15 -14
- khoj/processor/conversation/openai/gpt.py +7 -9
- khoj/processor/conversation/openai/utils.py +31 -17
- khoj/processor/conversation/prompts.py +65 -49
- khoj/processor/conversation/utils.py +46 -44
- khoj/processor/tools/online_search.py +49 -2
- khoj/routers/api.py +22 -27
- khoj/routers/api_agents.py +4 -4
- khoj/routers/api_chat.py +33 -13
- khoj/routers/api_model.py +4 -4
- khoj/routers/auth.py +108 -7
- khoj/routers/email.py +10 -14
- khoj/routers/helpers.py +187 -143
- khoj/routers/web_client.py +1 -1
- khoj/utils/constants.py +1 -1
- khoj/utils/helpers.py +5 -3
- khoj/utils/initialization.py +28 -26
- {khoj-1.30.11.dev64.dist-info → khoj-1.32.3.dev34.dist-info}/METADATA +7 -7
- {khoj-1.30.11.dev64.dist-info → khoj-1.32.3.dev34.dist-info}/RECORD +102 -99
- {khoj-1.30.11.dev64.dist-info → khoj-1.32.3.dev34.dist-info}/WHEEL +1 -1
- khoj/interface/compiled/_next/static/67DcUiU9MqkM1fhksWunh/_buildManifest.js +0 -1
- khoj/interface/compiled/_next/static/chunks/1459.690bf20e7d7b7090.js +0 -1
- khoj/interface/compiled/_next/static/chunks/1603-13cef426e0e650ec.js +0 -1
- khoj/interface/compiled/_next/static/chunks/1970-1b63ac1497b03a10.js +0 -1
- khoj/interface/compiled/_next/static/chunks/2646-92ba433951d02d52.js +0 -20
- khoj/interface/compiled/_next/static/chunks/3072-be830e4f8412b9d2.js +0 -1
- khoj/interface/compiled/_next/static/chunks/3463-081c031e873b7966.js +0 -3
- khoj/interface/compiled/_next/static/chunks/3690-51312931ba1eae30.js +0 -1
- khoj/interface/compiled/_next/static/chunks/3717-b46079dbe9f55694.js +0 -1
- khoj/interface/compiled/_next/static/chunks/4504-62ac13e7d94c52f9.js +0 -1
- khoj/interface/compiled/_next/static/chunks/4602-460621c3241e0d13.js +0 -1
- khoj/interface/compiled/_next/static/chunks/4752-554a3db270186ce3.js +0 -1
- khoj/interface/compiled/_next/static/chunks/5512-7cc62049bbe60e11.js +0 -1
- khoj/interface/compiled/_next/static/chunks/5538-0ea2d3944ca051e1.js +0 -1
- khoj/interface/compiled/_next/static/chunks/7023-e8de2bded4df6539.js +0 -2
- khoj/interface/compiled/_next/static/chunks/7592-a09c39a38e60634b.js +0 -1
- khoj/interface/compiled/_next/static/chunks/8423-1dda16bc56236523.js +0 -1
- khoj/interface/compiled/_next/static/chunks/94ca1967.5584df65931cfe83.js +0 -1
- khoj/interface/compiled/_next/static/chunks/964ecbae.ea4eab2a3a835ffe.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-1878cc328ea380bd.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/agents/page-8eead7920b0ff92a.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/page-b5800b5286306140.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-9219a85f3477e722.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-d7d2ab93e519f0b2.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/layout-6310c57b674dd6f5.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/page-3c32ad5472f75965.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/search/layout-2ca475462c0b2176.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/search/page-faa998c71eb7ca8e.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/layout-f285795bc3154b8c.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/page-cbe7f56b1f87d77a.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-592e8c470f2c2084.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/page-cd5757199539bbf2.js +0 -1
- khoj/interface/compiled/_next/static/chunks/d3ac728e-a9e3522eef9b6b28.js +0 -1
- khoj/interface/compiled/_next/static/chunks/main-1ea5c2e0fdef4626.js +0 -1
- khoj/interface/compiled/_next/static/chunks/main-app-6d6ee3495efe03d4.js +0 -1
- khoj/interface/compiled/_next/static/chunks/polyfills-78c92fac7aa8fdd8.js +0 -1
- khoj/interface/compiled/_next/static/chunks/webpack-616f0694bfe6f6c1.js +0 -1
- khoj/interface/compiled/_next/static/css/1f293605f2871853.css +0 -1
- khoj/interface/compiled/_next/static/css/3c34171b174cc381.css +0 -25
- khoj/interface/compiled/_next/static/css/3cf13271869a4aeb.css +0 -1
- khoj/interface/compiled/_next/static/css/592ca99f5122e75a.css +0 -1
- khoj/interface/compiled/_next/static/css/5a400c87d295e68a.css +0 -1
- khoj/interface/compiled/_next/static/css/80bd6301fc657983.css +0 -1
- khoj/interface/compiled/_next/static/css/9c4221ae0779cc04.css +0 -1
- /khoj/interface/compiled/_next/static/{67DcUiU9MqkM1fhksWunh → mqcIHpVqVWkmBuN0npYHA}/_ssgManifest.js +0 -0
- {khoj-1.30.11.dev64.dist-info → khoj-1.32.3.dev34.dist-info}/entry_points.txt +0 -0
- {khoj-1.30.11.dev64.dist-info → khoj-1.32.3.dev34.dist-info}/licenses/LICENSE +0 -0
@@ -102,8 +102,14 @@ async def search_online(
|
|
102
102
|
async for event in send_status_func(f"**Searching the Internet for**: {subqueries_str}"):
|
103
103
|
yield {ChatEvent.STATUS: event}
|
104
104
|
|
105
|
+
if SERPER_DEV_API_KEY:
|
106
|
+
search_func = search_with_serper
|
107
|
+
elif JINA_API_KEY:
|
108
|
+
search_func = search_with_jina
|
109
|
+
else:
|
110
|
+
search_func = search_with_searxng
|
111
|
+
|
105
112
|
with timer(f"Internet searches for {subqueries} took", logger):
|
106
|
-
search_func = search_with_google if SERPER_DEV_API_KEY else search_with_jina
|
107
113
|
search_tasks = [search_func(subquery, location) for subquery in subqueries]
|
108
114
|
search_results = await asyncio.gather(*search_tasks)
|
109
115
|
response_dict = {subquery: search_result for subquery, search_result in search_results}
|
@@ -148,7 +154,48 @@ async def search_online(
|
|
148
154
|
yield response_dict
|
149
155
|
|
150
156
|
|
151
|
-
async def
|
157
|
+
async def search_with_searxng(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]:
|
158
|
+
"""Search using local SearXNG instance."""
|
159
|
+
# Use environment variable or default to localhost
|
160
|
+
searxng_url = os.getenv("KHOJ_SEARXNG_URL", "http://localhost:42113")
|
161
|
+
search_url = f"{searxng_url}/search"
|
162
|
+
country_code = location.country_code.lower() if location and location.country_code else "us"
|
163
|
+
|
164
|
+
params = {"q": query, "format": "html", "language": "en", "country": country_code, "categories": "general"}
|
165
|
+
|
166
|
+
async with aiohttp.ClientSession() as session:
|
167
|
+
try:
|
168
|
+
async with session.get(search_url, params=params) as response:
|
169
|
+
if response.status != 200:
|
170
|
+
logger.error(f"SearXNG search failed to call {searxng_url}: {await response.text()}")
|
171
|
+
return query, {}
|
172
|
+
|
173
|
+
html_content = await response.text()
|
174
|
+
|
175
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
176
|
+
organic_results = []
|
177
|
+
|
178
|
+
for result in soup.find_all("article", class_="result"):
|
179
|
+
title_elem = result.find("a", rel="noreferrer")
|
180
|
+
if title_elem:
|
181
|
+
title = title_elem.text.strip()
|
182
|
+
link = title_elem["href"]
|
183
|
+
|
184
|
+
description_elem = result.find("p", class_="content")
|
185
|
+
description = description_elem.text.strip() if description_elem else None
|
186
|
+
|
187
|
+
organic_results.append({"title": title, "link": link, "description": description})
|
188
|
+
|
189
|
+
extracted_search_result = {"organic": organic_results}
|
190
|
+
|
191
|
+
return query, extracted_search_result
|
192
|
+
|
193
|
+
except Exception as e:
|
194
|
+
logger.error(f"Error searching with SearXNG: {str(e)}")
|
195
|
+
return query, {}
|
196
|
+
|
197
|
+
|
198
|
+
async def search_with_serper(query: str, location: LocationData) -> Tuple[str, Dict[str, List[Dict]]]:
|
152
199
|
country_code = location.country_code.lower() if location and location.country_code else "us"
|
153
200
|
payload = json.dumps({"q": query, "gl": country_code})
|
154
201
|
headers = {"X-API-KEY": SERPER_DEV_API_KEY, "Content-Type": "application/json"}
|
khoj/routers/api.py
CHANGED
@@ -28,12 +28,7 @@ from khoj.database.adapters import (
|
|
28
28
|
get_default_search_model,
|
29
29
|
get_user_photo,
|
30
30
|
)
|
31
|
-
from khoj.database.models import
|
32
|
-
Agent,
|
33
|
-
ChatModelOptions,
|
34
|
-
KhojUser,
|
35
|
-
SpeechToTextModelOptions,
|
36
|
-
)
|
31
|
+
from khoj.database.models import Agent, ChatModel, KhojUser, SpeechToTextModelOptions
|
37
32
|
from khoj.processor.conversation import prompts
|
38
33
|
from khoj.processor.conversation.anthropic.anthropic_chat import (
|
39
34
|
extract_questions_anthropic,
|
@@ -404,15 +399,15 @@ async def extract_references_and_questions(
|
|
404
399
|
# Infer search queries from user message
|
405
400
|
with timer("Extracting search queries took", logger):
|
406
401
|
# If we've reached here, either the user has enabled offline chat or the openai model is enabled.
|
407
|
-
|
408
|
-
vision_enabled =
|
402
|
+
chat_model = await ConversationAdapters.aget_default_chat_model(user)
|
403
|
+
vision_enabled = chat_model.vision_enabled
|
409
404
|
|
410
|
-
if
|
405
|
+
if chat_model.model_type == ChatModel.ModelType.OFFLINE:
|
411
406
|
using_offline_chat = True
|
412
|
-
|
413
|
-
max_tokens =
|
407
|
+
chat_model_name = chat_model.name
|
408
|
+
max_tokens = chat_model.max_prompt_size
|
414
409
|
if state.offline_chat_processor_config is None:
|
415
|
-
state.offline_chat_processor_config = OfflineChatProcessorModel(
|
410
|
+
state.offline_chat_processor_config = OfflineChatProcessorModel(chat_model_name, max_tokens)
|
416
411
|
|
417
412
|
loaded_model = state.offline_chat_processor_config.loaded_model
|
418
413
|
|
@@ -424,18 +419,18 @@ async def extract_references_and_questions(
|
|
424
419
|
should_extract_questions=True,
|
425
420
|
location_data=location_data,
|
426
421
|
user=user,
|
427
|
-
max_prompt_size=
|
422
|
+
max_prompt_size=chat_model.max_prompt_size,
|
428
423
|
personality_context=personality_context,
|
429
424
|
query_files=query_files,
|
430
425
|
tracer=tracer,
|
431
426
|
)
|
432
|
-
elif
|
433
|
-
api_key =
|
434
|
-
base_url =
|
435
|
-
|
427
|
+
elif chat_model.model_type == ChatModel.ModelType.OPENAI:
|
428
|
+
api_key = chat_model.ai_model_api.api_key
|
429
|
+
base_url = chat_model.ai_model_api.api_base_url
|
430
|
+
chat_model_name = chat_model.name
|
436
431
|
inferred_queries = extract_questions(
|
437
432
|
defiltered_query,
|
438
|
-
model=
|
433
|
+
model=chat_model_name,
|
439
434
|
api_key=api_key,
|
440
435
|
api_base_url=base_url,
|
441
436
|
conversation_log=meta_log,
|
@@ -447,13 +442,13 @@ async def extract_references_and_questions(
|
|
447
442
|
query_files=query_files,
|
448
443
|
tracer=tracer,
|
449
444
|
)
|
450
|
-
elif
|
451
|
-
api_key =
|
452
|
-
|
445
|
+
elif chat_model.model_type == ChatModel.ModelType.ANTHROPIC:
|
446
|
+
api_key = chat_model.ai_model_api.api_key
|
447
|
+
chat_model_name = chat_model.name
|
453
448
|
inferred_queries = extract_questions_anthropic(
|
454
449
|
defiltered_query,
|
455
450
|
query_images=query_images,
|
456
|
-
model=
|
451
|
+
model=chat_model_name,
|
457
452
|
api_key=api_key,
|
458
453
|
conversation_log=meta_log,
|
459
454
|
location_data=location_data,
|
@@ -463,17 +458,17 @@ async def extract_references_and_questions(
|
|
463
458
|
query_files=query_files,
|
464
459
|
tracer=tracer,
|
465
460
|
)
|
466
|
-
elif
|
467
|
-
api_key =
|
468
|
-
|
461
|
+
elif chat_model.model_type == ChatModel.ModelType.GOOGLE:
|
462
|
+
api_key = chat_model.ai_model_api.api_key
|
463
|
+
chat_model_name = chat_model.name
|
469
464
|
inferred_queries = extract_questions_gemini(
|
470
465
|
defiltered_query,
|
471
466
|
query_images=query_images,
|
472
|
-
model=
|
467
|
+
model=chat_model_name,
|
473
468
|
api_key=api_key,
|
474
469
|
conversation_log=meta_log,
|
475
470
|
location_data=location_data,
|
476
|
-
max_tokens=
|
471
|
+
max_tokens=chat_model.max_prompt_size,
|
477
472
|
user=user,
|
478
473
|
vision_enabled=vision_enabled,
|
479
474
|
personality_context=personality_context,
|
khoj/routers/api_agents.py
CHANGED
@@ -62,7 +62,7 @@ async def all_agents(
|
|
62
62
|
"color": agent.style_color,
|
63
63
|
"icon": agent.style_icon,
|
64
64
|
"privacy_level": agent.privacy_level,
|
65
|
-
"chat_model": agent.chat_model.
|
65
|
+
"chat_model": agent.chat_model.name,
|
66
66
|
"files": file_names,
|
67
67
|
"input_tools": agent.input_tools,
|
68
68
|
"output_modes": agent.output_modes,
|
@@ -150,7 +150,7 @@ async def get_agent(
|
|
150
150
|
"color": agent.style_color,
|
151
151
|
"icon": agent.style_icon,
|
152
152
|
"privacy_level": agent.privacy_level,
|
153
|
-
"chat_model": agent.chat_model.
|
153
|
+
"chat_model": agent.chat_model.name,
|
154
154
|
"files": file_names,
|
155
155
|
"input_tools": agent.input_tools,
|
156
156
|
"output_modes": agent.output_modes,
|
@@ -225,7 +225,7 @@ async def create_agent(
|
|
225
225
|
"color": agent.style_color,
|
226
226
|
"icon": agent.style_icon,
|
227
227
|
"privacy_level": agent.privacy_level,
|
228
|
-
"chat_model": agent.chat_model.
|
228
|
+
"chat_model": agent.chat_model.name,
|
229
229
|
"files": body.files,
|
230
230
|
"input_tools": agent.input_tools,
|
231
231
|
"output_modes": agent.output_modes,
|
@@ -286,7 +286,7 @@ async def update_agent(
|
|
286
286
|
"color": agent.style_color,
|
287
287
|
"icon": agent.style_icon,
|
288
288
|
"privacy_level": agent.privacy_level,
|
289
|
-
"chat_model": agent.chat_model.
|
289
|
+
"chat_model": agent.chat_model.name,
|
290
290
|
"files": body.files,
|
291
291
|
"input_tools": agent.input_tools,
|
292
292
|
"output_modes": agent.output_modes,
|
khoj/routers/api_chat.py
CHANGED
@@ -23,6 +23,7 @@ from khoj.database.adapters import (
|
|
23
23
|
aget_user_name,
|
24
24
|
)
|
25
25
|
from khoj.database.models import Agent, KhojUser
|
26
|
+
from khoj.processor.conversation import prompts
|
26
27
|
from khoj.processor.conversation.prompts import help_message, no_entries_found
|
27
28
|
from khoj.processor.conversation.utils import defilter_query, save_to_conversation_log
|
28
29
|
from khoj.processor.image.generate import text_to_image
|
@@ -57,7 +58,7 @@ from khoj.routers.helpers import (
|
|
57
58
|
is_ready_to_chat,
|
58
59
|
read_chat_stream,
|
59
60
|
update_telemetry_state,
|
60
|
-
|
61
|
+
validate_chat_model,
|
61
62
|
)
|
62
63
|
from khoj.routers.research import (
|
63
64
|
InformationCollectionIteration,
|
@@ -204,7 +205,7 @@ def chat_history(
|
|
204
205
|
n: Optional[int] = None,
|
205
206
|
):
|
206
207
|
user = request.user.object
|
207
|
-
|
208
|
+
validate_chat_model(user)
|
208
209
|
|
209
210
|
# Load Conversation History
|
210
211
|
conversation = ConversationAdapters.get_conversation_by_user(
|
@@ -723,7 +724,16 @@ async def chat(
|
|
723
724
|
yield result
|
724
725
|
return
|
725
726
|
|
726
|
-
|
727
|
+
# Automated tasks are handled before to allow mixing them with other conversation commands
|
728
|
+
cmds_to_rate_limit = []
|
729
|
+
is_automated_task = False
|
730
|
+
if q.startswith("/automated_task"):
|
731
|
+
is_automated_task = True
|
732
|
+
q = q.replace("/automated_task", "").lstrip()
|
733
|
+
cmds_to_rate_limit += [ConversationCommand.AutomatedTask]
|
734
|
+
|
735
|
+
# Extract conversation command from query
|
736
|
+
conversation_commands = [get_conversation_command(query=q)]
|
727
737
|
|
728
738
|
conversation = await ConversationAdapters.aget_conversation_by_user(
|
729
739
|
user,
|
@@ -756,15 +766,13 @@ async def chat(
|
|
756
766
|
location = None
|
757
767
|
if city or region or country or country_code:
|
758
768
|
location = LocationData(city=city, region=region, country=country, country_code=country_code)
|
759
|
-
|
760
769
|
user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
761
|
-
|
762
770
|
meta_log = conversation.conversation_log
|
763
|
-
is_automated_task = conversation_commands == [ConversationCommand.AutomatedTask]
|
764
771
|
|
765
772
|
researched_results = ""
|
766
773
|
online_results: Dict = dict()
|
767
774
|
code_results: Dict = dict()
|
775
|
+
generated_asset_results: Dict = dict()
|
768
776
|
## Extract Document References
|
769
777
|
compiled_references: List[Any] = []
|
770
778
|
inferred_queries: List[Any] = []
|
@@ -776,7 +784,7 @@ async def chat(
|
|
776
784
|
generated_excalidraw_diagram: str = None
|
777
785
|
program_execution_context: List[str] = []
|
778
786
|
|
779
|
-
if conversation_commands == [ConversationCommand.Default]
|
787
|
+
if conversation_commands == [ConversationCommand.Default]:
|
780
788
|
chosen_io = await aget_data_sources_and_output_format(
|
781
789
|
q,
|
782
790
|
meta_log,
|
@@ -797,7 +805,8 @@ async def chat(
|
|
797
805
|
async for result in send_event(ChatEvent.STATUS, f"**Selected Tools:** {conversation_commands_str}"):
|
798
806
|
yield result
|
799
807
|
|
800
|
-
|
808
|
+
cmds_to_rate_limit += conversation_commands
|
809
|
+
for cmd in cmds_to_rate_limit:
|
801
810
|
try:
|
802
811
|
await conversation_command_rate_limiter.update_and_check_if_valid(request, cmd)
|
803
812
|
q = q.replace(f"/{cmd.value}", "").strip()
|
@@ -896,10 +905,10 @@ async def chat(
|
|
896
905
|
custom_filters = []
|
897
906
|
if conversation_commands == [ConversationCommand.Help]:
|
898
907
|
if not q:
|
899
|
-
|
900
|
-
if
|
901
|
-
|
902
|
-
model_type =
|
908
|
+
chat_model = await ConversationAdapters.aget_user_chat_model(user)
|
909
|
+
if chat_model == None:
|
910
|
+
chat_model = await ConversationAdapters.aget_default_chat_model(user)
|
911
|
+
model_type = chat_model.model_type
|
903
912
|
formatted_help = help_message.format(model=model_type, version=state.khoj_version, device=get_device())
|
904
913
|
async for result in send_llm_response(formatted_help, tracer.get("usage")):
|
905
914
|
yield result
|
@@ -1128,6 +1137,10 @@ async def chat(
|
|
1128
1137
|
else:
|
1129
1138
|
generated_images.append(generated_image)
|
1130
1139
|
|
1140
|
+
generated_asset_results["images"] = {
|
1141
|
+
"query": improved_image_prompt,
|
1142
|
+
}
|
1143
|
+
|
1131
1144
|
async for result in send_event(
|
1132
1145
|
ChatEvent.GENERATED_ASSETS,
|
1133
1146
|
{
|
@@ -1166,6 +1179,10 @@ async def chat(
|
|
1166
1179
|
|
1167
1180
|
generated_excalidraw_diagram = diagram_description
|
1168
1181
|
|
1182
|
+
generated_asset_results["diagrams"] = {
|
1183
|
+
"query": better_diagram_description_prompt,
|
1184
|
+
}
|
1185
|
+
|
1169
1186
|
async for result in send_event(
|
1170
1187
|
ChatEvent.GENERATED_ASSETS,
|
1171
1188
|
{
|
@@ -1176,7 +1193,9 @@ async def chat(
|
|
1176
1193
|
else:
|
1177
1194
|
error_message = "Failed to generate diagram. Please try again later."
|
1178
1195
|
program_execution_context.append(
|
1179
|
-
|
1196
|
+
prompts.failed_diagram_generation.format(
|
1197
|
+
attempted_diagram=better_diagram_description_prompt
|
1198
|
+
)
|
1180
1199
|
)
|
1181
1200
|
|
1182
1201
|
async for result in send_event(ChatEvent.STATUS, error_message):
|
@@ -1209,6 +1228,7 @@ async def chat(
|
|
1209
1228
|
generated_files,
|
1210
1229
|
generated_excalidraw_diagram,
|
1211
1230
|
program_execution_context,
|
1231
|
+
generated_asset_results,
|
1212
1232
|
tracer,
|
1213
1233
|
)
|
1214
1234
|
|
khoj/routers/api_model.py
CHANGED
@@ -24,7 +24,7 @@ def get_chat_model_options(
|
|
24
24
|
|
25
25
|
all_conversation_options = list()
|
26
26
|
for conversation_option in conversation_options:
|
27
|
-
all_conversation_options.append({"chat_model": conversation_option.
|
27
|
+
all_conversation_options.append({"chat_model": conversation_option.name, "id": conversation_option.id})
|
28
28
|
|
29
29
|
return Response(content=json.dumps(all_conversation_options), media_type="application/json", status_code=200)
|
30
30
|
|
@@ -37,12 +37,12 @@ def get_user_chat_model(
|
|
37
37
|
):
|
38
38
|
user = request.user.object
|
39
39
|
|
40
|
-
chat_model = ConversationAdapters.
|
40
|
+
chat_model = ConversationAdapters.get_chat_model(user)
|
41
41
|
|
42
42
|
if chat_model is None:
|
43
|
-
chat_model = ConversationAdapters.
|
43
|
+
chat_model = ConversationAdapters.get_default_chat_model(user)
|
44
44
|
|
45
|
-
return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.
|
45
|
+
return Response(status_code=200, content=json.dumps({"id": chat_model.id, "chat_model": chat_model.name}))
|
46
46
|
|
47
47
|
|
48
48
|
@api_model.post("/chat", status_code=200)
|
khoj/routers/auth.py
CHANGED
@@ -4,7 +4,8 @@ import logging
|
|
4
4
|
import os
|
5
5
|
from typing import Optional
|
6
6
|
|
7
|
-
|
7
|
+
import requests
|
8
|
+
from fastapi import APIRouter, Depends
|
8
9
|
from pydantic import BaseModel, EmailStr
|
9
10
|
from starlette.authentication import requires
|
10
11
|
from starlette.config import Config
|
@@ -21,8 +22,13 @@ from khoj.database.adapters import (
|
|
21
22
|
get_or_create_user,
|
22
23
|
)
|
23
24
|
from khoj.routers.email import send_magic_link_email, send_welcome_email
|
24
|
-
from khoj.routers.helpers import
|
25
|
+
from khoj.routers.helpers import (
|
26
|
+
EmailVerificationApiRateLimiter,
|
27
|
+
get_next_url,
|
28
|
+
update_telemetry_state,
|
29
|
+
)
|
25
30
|
from khoj.utils import state
|
31
|
+
from khoj.utils.helpers import in_debug_mode
|
26
32
|
|
27
33
|
logger = logging.getLogger(__name__)
|
28
34
|
|
@@ -98,16 +104,28 @@ async def login_magic_link(request: Request, form: MagicLinkForm):
|
|
98
104
|
|
99
105
|
|
100
106
|
@auth_router.get("/magic")
|
101
|
-
async def sign_in_with_magic_link(
|
102
|
-
|
107
|
+
async def sign_in_with_magic_link(
|
108
|
+
request: Request,
|
109
|
+
code: str,
|
110
|
+
email: str,
|
111
|
+
rate_limiter=Depends(
|
112
|
+
EmailVerificationApiRateLimiter(requests=10, window=60 * 60 * 24, slug="magic_link_verification")
|
113
|
+
),
|
114
|
+
):
|
115
|
+
user, code_is_expired = await aget_user_validated_by_email_verification_code(code, email)
|
116
|
+
|
103
117
|
if user:
|
118
|
+
if code_is_expired:
|
119
|
+
request.session["user"] = {}
|
120
|
+
return Response(status_code=403)
|
121
|
+
|
104
122
|
id_info = {
|
105
123
|
"email": user.email,
|
106
124
|
}
|
107
125
|
|
108
126
|
request.session["user"] = dict(id_info)
|
109
127
|
return RedirectResponse(url="/")
|
110
|
-
return
|
128
|
+
return Response(status_code=401)
|
111
129
|
|
112
130
|
|
113
131
|
@auth_router.post("/token")
|
@@ -140,11 +158,12 @@ async def delete_token(request: Request, token: str):
|
|
140
158
|
|
141
159
|
|
142
160
|
@auth_router.post("/redirect")
|
143
|
-
async def
|
161
|
+
async def auth_post(request: Request):
|
162
|
+
# This is maintained for compatibility with the /login endpoint
|
144
163
|
form = await request.form()
|
145
164
|
next_url = get_next_url(request)
|
146
165
|
for q in request.query_params:
|
147
|
-
if
|
166
|
+
if q != "next":
|
148
167
|
next_url += f"&{q}={request.query_params[q]}"
|
149
168
|
|
150
169
|
credential = form.get("credential")
|
@@ -183,7 +202,89 @@ async def auth(request: Request):
|
|
183
202
|
return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
|
184
203
|
|
185
204
|
|
205
|
+
@auth_router.get("/redirect")
|
206
|
+
async def auth(request: Request):
|
207
|
+
next_url = get_next_url(request)
|
208
|
+
for q in request.query_params:
|
209
|
+
if q in ["code", "state", "scope", "authuser", "prompt", "session_state", "access_type"]:
|
210
|
+
continue
|
211
|
+
if q != "next":
|
212
|
+
next_url += f"&{q}={request.query_params[q]}"
|
213
|
+
|
214
|
+
code = request.query_params.get("code")
|
215
|
+
|
216
|
+
# 1. Construct the full redirect URI including domain
|
217
|
+
base_url = str(request.base_url).rstrip("/")
|
218
|
+
|
219
|
+
if not in_debug_mode():
|
220
|
+
base_url = base_url.replace("http://", "https://")
|
221
|
+
|
222
|
+
redirect_uri = f"{base_url}{request.app.url_path_for('auth')}"
|
223
|
+
|
224
|
+
payload = {
|
225
|
+
"code": code,
|
226
|
+
"client_id": os.environ["GOOGLE_CLIENT_ID"],
|
227
|
+
"client_secret": os.environ["GOOGLE_CLIENT_SECRET"],
|
228
|
+
"redirect_uri": redirect_uri,
|
229
|
+
"grant_type": "authorization_code",
|
230
|
+
}
|
231
|
+
|
232
|
+
verified_data = requests.post(
|
233
|
+
"https://oauth2.googleapis.com/token",
|
234
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
235
|
+
data=payload,
|
236
|
+
)
|
237
|
+
|
238
|
+
if verified_data.status_code != 200:
|
239
|
+
logger.error(f"Token request failed: {verified_data.text}")
|
240
|
+
try:
|
241
|
+
error_json = verified_data.json()
|
242
|
+
logger.error(f"Error response JSON for Google verification: {error_json}")
|
243
|
+
except ValueError:
|
244
|
+
logger.error("Response content is not valid JSON")
|
245
|
+
verified_data.raise_for_status()
|
246
|
+
|
247
|
+
credential = verified_data.json().get("id_token")
|
248
|
+
|
249
|
+
if not credential:
|
250
|
+
logger.error("Missing id_token in OAuth response")
|
251
|
+
return RedirectResponse(url="/login?error=invalid_token", status_code=HTTP_302_FOUND)
|
252
|
+
|
253
|
+
try:
|
254
|
+
idinfo = id_token.verify_oauth2_token(credential, google_requests.Request(), os.environ["GOOGLE_CLIENT_ID"])
|
255
|
+
except OAuthError as error:
|
256
|
+
return HTMLResponse(f"<h1>{error.error}</h1>")
|
257
|
+
khoj_user = await get_or_create_user(idinfo)
|
258
|
+
|
259
|
+
if khoj_user:
|
260
|
+
request.session["user"] = dict(idinfo)
|
261
|
+
|
262
|
+
if datetime.timedelta(minutes=3) > (datetime.datetime.now(datetime.timezone.utc) - khoj_user.date_joined):
|
263
|
+
asyncio.create_task(send_welcome_email(idinfo["name"], idinfo["email"]))
|
264
|
+
update_telemetry_state(
|
265
|
+
request=request,
|
266
|
+
telemetry_type="api",
|
267
|
+
api="create_user__google",
|
268
|
+
metadata={"server_id": str(khoj_user.uuid)},
|
269
|
+
)
|
270
|
+
logger.log(logging.INFO, f"🥳 New User Created: {khoj_user.uuid}")
|
271
|
+
|
272
|
+
return RedirectResponse(url=next_url, status_code=HTTP_302_FOUND)
|
273
|
+
|
274
|
+
|
186
275
|
@auth_router.get("/logout")
|
187
276
|
async def logout(request: Request):
|
188
277
|
request.session.pop("user", None)
|
189
278
|
return RedirectResponse(url="/")
|
279
|
+
|
280
|
+
|
281
|
+
@auth_router.get("/oauth/metadata")
|
282
|
+
async def oauth_metadata(request: Request):
|
283
|
+
redirect_uri = str(request.app.url_path_for("auth"))
|
284
|
+
|
285
|
+
return {
|
286
|
+
"google": {
|
287
|
+
"client_id": os.environ.get("GOOGLE_CLIENT_ID"),
|
288
|
+
"redirect_uri": f"{redirect_uri}",
|
289
|
+
}
|
290
|
+
}
|
khoj/routers/email.py
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
|
4
|
-
try:
|
5
|
-
import resend
|
6
|
-
except ImportError:
|
7
|
-
pass
|
8
|
-
|
9
4
|
import markdown_it
|
5
|
+
import resend
|
10
6
|
from django.conf import settings
|
11
7
|
from jinja2 import Environment, FileSystemLoader
|
12
8
|
|
@@ -23,7 +19,7 @@ static_files = os.path.join(settings.BASE_DIR, "static")
|
|
23
19
|
env = Environment(loader=FileSystemLoader(static_files))
|
24
20
|
|
25
21
|
if not RESEND_API_KEY:
|
26
|
-
logger.
|
22
|
+
logger.warning("RESEND_API_KEY not set - email sending disabled")
|
27
23
|
else:
|
28
24
|
resend.api_key = RESEND_API_KEY
|
29
25
|
|
@@ -33,7 +29,7 @@ def is_resend_enabled():
|
|
33
29
|
|
34
30
|
|
35
31
|
async def send_magic_link_email(email, unique_id, host):
|
36
|
-
sign_in_link = f"{host}auth/magic?code={unique_id}"
|
32
|
+
sign_in_link = f"{host}auth/magic?code={unique_id}&email={email}"
|
37
33
|
|
38
34
|
if not is_resend_enabled():
|
39
35
|
logger.debug(f"Email sending disabled. Share this sign-in link with the user: {sign_in_link}")
|
@@ -41,13 +37,13 @@ async def send_magic_link_email(email, unique_id, host):
|
|
41
37
|
|
42
38
|
template = env.get_template("magic_link.html")
|
43
39
|
|
44
|
-
html_content = template.render(link=f"{host}auth/magic?code={unique_id}")
|
40
|
+
html_content = template.render(link=f"{host}auth/magic?code={unique_id}", code=unique_id)
|
45
41
|
|
46
42
|
resend.Emails.send(
|
47
43
|
{
|
48
44
|
"sender": os.environ.get("RESEND_EMAIL", "noreply@khoj.dev"),
|
49
45
|
"to": email,
|
50
|
-
"subject": "Your
|
46
|
+
"subject": f"Your login code to Khoj",
|
51
47
|
"html": html_content,
|
52
48
|
}
|
53
49
|
)
|
@@ -64,7 +60,7 @@ async def send_welcome_email(name, email):
|
|
64
60
|
|
65
61
|
resend.Emails.send(
|
66
62
|
{
|
67
|
-
"sender": "team@khoj.dev",
|
63
|
+
"sender": os.environ.get("RESEND_EMAIL", "team@khoj.dev"),
|
68
64
|
"to": email,
|
69
65
|
"subject": f"{name}, four ways to use Khoj" if name else "Four ways to use Khoj",
|
70
66
|
"html": html_content,
|
@@ -92,7 +88,7 @@ async def send_query_feedback(uquery, kquery, sentiment, user_email):
|
|
92
88
|
|
93
89
|
logger.info(f"Sending feedback email for query {uquery}")
|
94
90
|
|
95
|
-
#
|
91
|
+
# render feedback email using feedback.html as template
|
96
92
|
template = env.get_template("feedback.html")
|
97
93
|
html_content = template.render(
|
98
94
|
uquery=uquery if not is_none_or_empty(uquery) else "N/A",
|
@@ -100,10 +96,10 @@ async def send_query_feedback(uquery, kquery, sentiment, user_email):
|
|
100
96
|
sentiment=sentiment if not is_none_or_empty(sentiment) else "N/A",
|
101
97
|
user_email=user_email if not is_none_or_empty(user_email) else "N/A",
|
102
98
|
)
|
103
|
-
# send feedback
|
99
|
+
# send feedback to fixed account
|
104
100
|
r = resend.Emails.send(
|
105
101
|
{
|
106
|
-
"sender": "
|
102
|
+
"sender": os.environ.get("RESEND_EMAIL", "noreply@khoj.dev"),
|
107
103
|
"to": "team@khoj.dev",
|
108
104
|
"subject": f"User Feedback",
|
109
105
|
"html": html_content,
|
@@ -130,7 +126,7 @@ def send_task_email(name, email, query, result, subject, is_image=False):
|
|
130
126
|
|
131
127
|
r = resend.Emails.send(
|
132
128
|
{
|
133
|
-
"sender":
|
129
|
+
"sender": f'Khoj <{os.environ.get("RESEND_EMAIL", "khoj@khoj.dev")}>',
|
134
130
|
"to": email,
|
135
131
|
"subject": f"✨ {subject}",
|
136
132
|
"html": html_content,
|